From 026ced4d7467d47a0e9761479698bf8ec067f822 Mon Sep 17 00:00:00 2001 From: Divyansh Singh Date: Tue, 20 Jun 2023 02:25:02 +0530 Subject: [PATCH 01/10] feat: inline edit for archive renaming --- src/vorta/assets/UI/archivetab.ui | 19 -------- src/vorta/views/archive_tab.py | 77 +++++++++++++++---------------- 2 files changed, 37 insertions(+), 59 deletions(-) diff --git a/src/vorta/assets/UI/archivetab.ui b/src/vorta/assets/UI/archivetab.ui index 3e3b0b4c5..bcec957b5 100644 --- a/src/vorta/assets/UI/archivetab.ui +++ b/src/vorta/assets/UI/archivetab.ui @@ -255,25 +255,6 @@ - - - - - 0 - 0 - - - - Rename selected archive - - - Rename… - - - Qt::ToolButtonTextBesideIcon - - - diff --git a/src/vorta/views/archive_tab.py b/src/vorta/views/archive_tab.py index f5b92c8ee..9ba188625 100644 --- a/src/vorta/views/archive_tab.py +++ b/src/vorta/views/archive_tab.py @@ -10,7 +10,6 @@ QAbstractItemView, QApplication, QHeaderView, - QInputDialog, QLayout, QMenu, QMessageBox, @@ -70,6 +69,7 @@ def __init__(self, parent=None, app=None): self.app = app self.toolBox.setCurrentIndex(0) self.repoactions_enabled = True + self.renamed_archive_orginal_name = None #: Tooltip dict to save the tooltips set in the designer self.tooltip_dict: Dict[QWidget, str] = {} @@ -94,6 +94,7 @@ def __init__(self, parent=None, app=None): self.archiveTable.setTextElideMode(QtCore.Qt.TextElideMode.ElideLeft) self.archiveTable.setAlternatingRowColors(True) self.archiveTable.cellDoubleClicked.connect(self.cell_double_clicked) + self.archiveTable.cellChanged.connect(self.cell_changed) self.archiveTable.setSortingEnabled(True) self.archiveTable.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) self.archiveTable.customContextMenuRequested.connect(self.archiveitem_contextmenu) @@ -109,7 +110,6 @@ def __init__(self, parent=None, app=None): # connect archive actions self.bMountArchive.clicked.connect(self.bmountarchive_clicked) self.bRefreshArchive.clicked.connect(self.refresh_archive_info) - self.bRename.clicked.connect(self.rename_action) self.bDelete.clicked.connect(self.delete_action) self.bExtract.clicked.connect(self.extract_action) self.compactButton.clicked.connect(self.compact_action) @@ -145,7 +145,6 @@ def set_icons(self): self.toolBox.setItemIcon(0, get_colored_icon('tasks')) self.toolBox.setItemIcon(1, get_colored_icon('cut')) self.bRefreshArchive.setIcon(get_colored_icon('refresh')) - self.bRename.setIcon(get_colored_icon('edit')) self.bDelete.setIcon(get_colored_icon('trash')) self.bExtract.setIcon(get_colored_icon('cloud-download')) @@ -189,7 +188,6 @@ def archiveitem_contextmenu(self, pos: QPoint): ) ) archive_actions.append(menu.addAction(self.bExtract.icon(), self.bExtract.text(), self.extract_action)) - archive_actions.append(menu.addAction(self.bRename.icon(), self.bRename.text(), self.rename_action)) # deletion possible with one but also multiple archives menu.addAction(self.bDelete.icon(), self.bDelete.text(), self.delete_action) @@ -778,6 +776,40 @@ def cell_double_clicked(self, row, column): if mount_point is not None: QDesktopServices.openUrl(QtCore.QUrl(f'file:///{mount_point}')) + if column == 4: + item = self.archiveTable.item(row, column) + self.renamed_archive_orginal_name = item.text() + item.setFlags(item.flags() | QtCore.Qt.ItemFlag.ItemIsEditable) + self.archiveTable.editItem(item) + + def cell_changed(self, row, column): + item = self.archiveTable.item(row, column) + new_name = item.text() + profile = self.profile() + + # if the name hasn't changed or if this slot is called when first repopulating the table, do nothing. + if new_name == self.renamed_archive_orginal_name or not self.renamed_archive_orginal_name: + return + + if not new_name: + self._set_status(self.tr('Archive name cannot be blank.')) + return + + new_name_exists = ArchiveModel.get_or_none(name=new_name, repo=profile.repo) + if new_name_exists is not None: + self._set_status(self.tr('An archive with this name already exists.')) + return + + params = BorgRenameJob.prepare(profile, self.renamed_archive_orginal_name, new_name) + if not params['ok']: + self._set_status(params['message']) + + job = BorgRenameJob(params['cmd'], params, self.profile().repo.id) + job.updated.connect(self._set_status) + job.result.connect(self.rename_result) + self._toggle_all_buttons(False) + self.app.jobs_manager.add_job(job) + def row_of_archive(self, archive_name): items = self.archiveTable.findItems(archive_name, QtCore.Qt.MatchFlag.MatchExactly) rows = [item.row() for item in items if item.column() == 4] @@ -912,45 +944,10 @@ def show_diff_result(self, archive_newer, archive_older, model): self._resultwindow = window # for testing window.show() - def rename_action(self): - profile = self.profile() - - archive_name = self.selected_archive_name() - if archive_name is not None: - new_name, finished = QInputDialog.getText( - self, - self.tr("Change name"), - self.tr("New archive name:"), - text=archive_name, - ) - - if not finished: - return - - if not new_name: - self._set_status(self.tr('Archive name cannot be blank.')) - return - - new_name_exists = ArchiveModel.get_or_none(name=new_name, repo=profile.repo) - if new_name_exists is not None: - self._set_status(self.tr('An archive with this name already exists.')) - return - - params = BorgRenameJob.prepare(profile, archive_name, new_name) - if not params['ok']: - self._set_status(params['message']) - - job = BorgRenameJob(params['cmd'], params, self.profile().repo.id) - job.updated.connect(self._set_status) - job.result.connect(self.rename_result) - self._toggle_all_buttons(False) - self.app.jobs_manager.add_job(job) - else: - self._set_status(self.tr("No archive selected")) - def rename_result(self, result): if result['returncode'] == 0: self._set_status(self.tr('Archive renamed.')) + self.renamed_archive_orginal_name = None self.populate_from_profile() else: self._toggle_all_buttons(True) From eded8be51eb09e282050c0e07f3845dafaa8a2fd Mon Sep 17 00:00:00 2001 From: Divyansh Singh Date: Tue, 20 Jun 2023 02:48:12 +0530 Subject: [PATCH 02/10] remove old renaming test --- tests/test_archives.py | 28 ---------------------------- 1 file changed, 28 deletions(-) diff --git a/tests/test_archives.py b/tests/test_archives.py index a7627af16..5e26e3bfd 100644 --- a/tests/test_archives.py +++ b/tests/test_archives.py @@ -168,31 +168,3 @@ def test_archive_delete(qapp, qtbot, mocker, borg_json_output): qtbot.waitUntil(lambda: 'Archive deleted.' in main.progressText.text(), **pytest._wait_defaults) assert ArchiveModel.select().count() == 1 assert tab.archiveTable.rowCount() == 1 - - -def test_archive_rename(qapp, qtbot, mocker, borg_json_output): - main = qapp.main_window - tab = main.archiveTab - main.tabWidget.setCurrentIndex(3) - - tab.populate_from_profile() - qtbot.waitUntil(lambda: tab.archiveTable.rowCount() == 2) - - tab.archiveTable.selectRow(0) - new_archive_name = 'idf89d8f9d8fd98' - stdout, stderr = borg_json_output('rename') - popen_result = mocker.MagicMock(stdout=stdout, stderr=stderr, returncode=0) - mocker.patch.object(vorta.borg.borg_job, 'Popen', return_value=popen_result) - mocker.patch.object(vorta.views.archive_tab.QInputDialog, 'getText', return_value=(new_archive_name, True)) - tab.rename_action() - - # Successful rename case - qtbot.waitUntil(lambda: tab.mountErrors.text() == 'Archive renamed.', **pytest._wait_defaults) - assert ArchiveModel.select().filter(name=new_archive_name).count() == 1 - - # Duplicate name case - tab.archiveTable.selectRow(0) - exp_text = 'An archive with this name already exists.' - mocker.patch.object(vorta.views.archive_tab.QInputDialog, 'getText', return_value=(new_archive_name, True)) - tab.rename_action() - qtbot.waitUntil(lambda: tab.mountErrors.text() == exp_text, **pytest._wait_defaults) From cd51daeca071b59a1b7e4f5b3ea23756e5db5453 Mon Sep 17 00:00:00 2001 From: Divyansh Singh Date: Tue, 27 Jun 2023 21:03:38 +0530 Subject: [PATCH 03/10] bring back context menu and rename button --- src/vorta/assets/UI/archivetab.ui | 19 +++++++++++++++++++ src/vorta/views/archive_tab.py | 9 ++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/vorta/assets/UI/archivetab.ui b/src/vorta/assets/UI/archivetab.ui index 9ca3feeda..1e0c1e662 100644 --- a/src/vorta/assets/UI/archivetab.ui +++ b/src/vorta/assets/UI/archivetab.ui @@ -257,6 +257,25 @@ + + + + + 0 + 0 + + + + Rename selected archive + + + Rename… + + + Qt::ToolButtonTextBesideIcon + + + diff --git a/src/vorta/views/archive_tab.py b/src/vorta/views/archive_tab.py index 04c9ca0fc..50abaa8b8 100644 --- a/src/vorta/views/archive_tab.py +++ b/src/vorta/views/archive_tab.py @@ -121,6 +121,7 @@ def __init__(self, parent=None, app=None): # connect archive actions self.bMountArchive.clicked.connect(self.bmountarchive_clicked) self.bRefreshArchive.clicked.connect(self.refresh_archive_info) + self.bRename.clicked.connect(self.cell_double_clicked) self.bDelete.clicked.connect(self.delete_action) self.bExtract.clicked.connect(self.extract_action) self.compactButton.clicked.connect(self.compact_action) @@ -156,6 +157,7 @@ def set_icons(self): self.toolBox.setItemIcon(0, get_colored_icon('tasks')) self.toolBox.setItemIcon(1, get_colored_icon('cut')) self.bRefreshArchive.setIcon(get_colored_icon('refresh')) + self.bRename.setIcon(get_colored_icon('edit')) self.bDelete.setIcon(get_colored_icon('trash')) self.bExtract.setIcon(get_colored_icon('cloud-download')) @@ -199,6 +201,7 @@ def archiveitem_contextmenu(self, pos: QPoint): ) ) archive_actions.append(menu.addAction(self.bExtract.icon(), self.bExtract.text(), self.extract_action)) + archive_actions.append(menu.addAction(self.bRename.icon(), self.bRename.text(), self.cell_double_clicked)) # deletion possible with one but also multiple archives menu.addAction(self.bDelete.icon(), self.bDelete.text(), self.delete_action) @@ -792,7 +795,11 @@ def extract_archive_result(self, result): """Finished extraction.""" self._toggle_all_buttons(True) - def cell_double_clicked(self, row, column): + def cell_double_clicked(self, row=None, column=None): + if not row or not column: + row = self.archiveTable.currentRow() + column = self.archiveTable.currentColumn() + if column == 3: archive_name = self.selected_archive_name() if not archive_name: From e0e3efa687f2d36deae21f9ce2fda3325eaeacc9 Mon Sep 17 00:00:00 2001 From: Divyansh Singh Date: Wed, 28 Jun 2023 02:50:52 +0530 Subject: [PATCH 04/10] add rename test --- tests/test_archives.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/test_archives.py b/tests/test_archives.py index 5e26e3bfd..a37dcfd8c 100644 --- a/tests/test_archives.py +++ b/tests/test_archives.py @@ -168,3 +168,27 @@ def test_archive_delete(qapp, qtbot, mocker, borg_json_output): qtbot.waitUntil(lambda: 'Archive deleted.' in main.progressText.text(), **pytest._wait_defaults) assert ArchiveModel.select().count() == 1 assert tab.archiveTable.rowCount() == 1 + + +def test_archive_rename(qapp, qtbot, mocker, borg_json_output): + main = qapp.main_window + tab = main.archiveTab + main.tabWidget.setCurrentIndex(3) + + tab.populate_from_profile() + qtbot.waitUntil(lambda: tab.archiveTable.rowCount() == 2) + + tab.archiveTable.selectRow(0) + new_archive_name = 'idf89d8f9d8fd98' + stdout, stderr = borg_json_output('rename') + popen_result = mocker.MagicMock(stdout=stdout, stderr=stderr, returncode=0) + mocker.patch.object(vorta.borg.borg_job, 'Popen', return_value=popen_result) + + pos = tab.archiveTable.visualRect(tab.archiveTable.model().index(0, 4)).center() + qtbot.mouseClick(tab.archiveTable.viewport(), QtCore.Qt.MouseButton.LeftButton, pos=pos) + qtbot.mouseDClick(tab.archiveTable.viewport(), QtCore.Qt.MouseButton.LeftButton, pos=pos) + qtbot.keyClicks(tab.archiveTable.viewport().focusWidget(), new_archive_name) + qtbot.keyClick(tab.archiveTable.viewport().focusWidget(), QtCore.Qt.Key.Key_Return) + + # Successful rename case + qtbot.waitUntil(lambda: tab.archiveTable.model().index(0, 4).data() == new_archive_name, **pytest._wait_defaults) From 2de4b3f5c972b092b866bff2b444b67deccf0ec9 Mon Sep 17 00:00:00 2001 From: Divyansh Singh Date: Tue, 11 Jul 2023 19:33:46 +0530 Subject: [PATCH 05/10] undo archive rename if blank. fix typo. early return when changed cell isnt 4 --- src/vorta/views/archive_tab.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/vorta/views/archive_tab.py b/src/vorta/views/archive_tab.py index 50abaa8b8..83d388ea6 100644 --- a/src/vorta/views/archive_tab.py +++ b/src/vorta/views/archive_tab.py @@ -77,7 +77,7 @@ def __init__(self, parent=None, app=None): self.app = app self.toolBox.setCurrentIndex(0) self.repoactions_enabled = True - self.renamed_archive_orginal_name = None + self.renamed_archive_original_name = None #: Tooltip dict to save the tooltips set in the designer self.tooltip_dict: Dict[QWidget, str] = {} @@ -812,20 +812,25 @@ def cell_double_clicked(self, row=None, column=None): if column == 4: item = self.archiveTable.item(row, column) - self.renamed_archive_orginal_name = item.text() + self.renamed_archive_original_name = item.text() item.setFlags(item.flags() | QtCore.Qt.ItemFlag.ItemIsEditable) self.archiveTable.editItem(item) def cell_changed(self, row, column): + # return if this is not a name change + if column != 4: + return + item = self.archiveTable.item(row, column) new_name = item.text() profile = self.profile() # if the name hasn't changed or if this slot is called when first repopulating the table, do nothing. - if new_name == self.renamed_archive_orginal_name or not self.renamed_archive_orginal_name: + if new_name == self.renamed_archive_original_name or not self.renamed_archive_original_name: return if not new_name: + item.setText(self.renamed_archive_original_name) self._set_status(self.tr('Archive name cannot be blank.')) return @@ -834,7 +839,7 @@ def cell_changed(self, row, column): self._set_status(self.tr('An archive with this name already exists.')) return - params = BorgRenameJob.prepare(profile, self.renamed_archive_orginal_name, new_name) + params = BorgRenameJob.prepare(profile, self.renamed_archive_original_name, new_name) if not params['ok']: self._set_status(params['message']) @@ -981,7 +986,7 @@ def show_diff_result(self, archive_newer, archive_older, model): def rename_result(self, result): if result['returncode'] == 0: self._set_status(self.tr('Archive renamed.')) - self.renamed_archive_orginal_name = None + self.renamed_archive_original_name = None self.populate_from_profile() else: self._toggle_all_buttons(True) From 48204f8bfd087b0206421c515d3f11bdc803ddaa Mon Sep 17 00:00:00 2001 From: Divyansh Singh Date: Tue, 11 Jul 2023 19:54:14 +0530 Subject: [PATCH 06/10] typo --- src/vorta/views/archive_tab.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vorta/views/archive_tab.py b/src/vorta/views/archive_tab.py index 1b0b539a4..42c70c344 100644 --- a/src/vorta/views/archive_tab.py +++ b/src/vorta/views/archive_tab.py @@ -78,7 +78,7 @@ def __init__(self, parent=None, app=None): self.app = app self.toolBox.setCurrentIndex(0) self.repoactions_enabled = True - self.renamed_archive_orginal_name = None + self.renamed_archive_original_name = None self.remaining_refresh_archives = ( 0 # number of archives that are left to refresh before action buttons are enabled again ) From 52adf31ad2e2628d25bf65bef5d6dd4558cfaafa Mon Sep 17 00:00:00 2001 From: Divyansh Singh Date: Wed, 2 Aug 2023 02:51:01 +0530 Subject: [PATCH 07/10] fix archive rename id change bug --- src/vorta/borg/info_archive.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/vorta/borg/info_archive.py b/src/vorta/borg/info_archive.py index 72caf06c3..d0b4daa33 100644 --- a/src/vorta/borg/info_archive.py +++ b/src/vorta/borg/info_archive.py @@ -41,6 +41,10 @@ def process_result(self, result): # Update remote archives. for remote_archive in remote_archives: archive = ArchiveModel.get_or_none(snapshot_id=remote_archive['id'], repo=repo_id) + if archive is None: + # archive id was changed during rename, so we need to find it by name + archive = ArchiveModel.get_or_none(name=remote_archive['name'], repo=repo_id) + archive.name = remote_archive['name'] # incase name changed # archive.time = parser.parse(remote_archive['time']) archive.duration = remote_archive['duration'] From 1ac424061eef2774754f2e0992c238001677ba85 Mon Sep 17 00:00:00 2001 From: Divyansh Singh Date: Wed, 2 Aug 2023 15:16:23 +0530 Subject: [PATCH 08/10] use original name when new name already exists --- src/vorta/borg/info_archive.py | 1 + src/vorta/views/archive_tab.py | 1 + 2 files changed, 2 insertions(+) diff --git a/src/vorta/borg/info_archive.py b/src/vorta/borg/info_archive.py index d0b4daa33..afb94b2f3 100644 --- a/src/vorta/borg/info_archive.py +++ b/src/vorta/borg/info_archive.py @@ -44,6 +44,7 @@ def process_result(self, result): if archive is None: # archive id was changed during rename, so we need to find it by name archive = ArchiveModel.get_or_none(name=remote_archive['name'], repo=repo_id) + archive.snapshot_id = remote_archive['id'] archive.name = remote_archive['name'] # incase name changed # archive.time = parser.parse(remote_archive['time']) diff --git a/src/vorta/views/archive_tab.py b/src/vorta/views/archive_tab.py index 42c70c344..8a55feda4 100644 --- a/src/vorta/views/archive_tab.py +++ b/src/vorta/views/archive_tab.py @@ -863,6 +863,7 @@ def cell_changed(self, row, column): new_name_exists = ArchiveModel.get_or_none(name=new_name, repo=profile.repo) if new_name_exists is not None: self._set_status(self.tr('An archive with this name already exists.')) + item.setText(self.renamed_archive_original_name) return params = BorgRenameJob.prepare(profile, self.renamed_archive_original_name, new_name) From b25957ff31a5229eb8b1b32dbceeb67f6066d535 Mon Sep 17 00:00:00 2001 From: Divyansh Singh Date: Sat, 5 Aug 2023 20:31:38 +0530 Subject: [PATCH 09/10] refresh archive info on rename --- src/vorta/views/archive_tab.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vorta/views/archive_tab.py b/src/vorta/views/archive_tab.py index 8a55feda4..2383c5f55 100644 --- a/src/vorta/views/archive_tab.py +++ b/src/vorta/views/archive_tab.py @@ -1012,6 +1012,7 @@ def show_diff_result(self, archive_newer, archive_older, model): def rename_result(self, result): if result['returncode'] == 0: + self.refresh_archive_info() self._set_status(self.tr('Archive renamed.')) self.renamed_archive_original_name = None self.populate_from_profile() From 01a4f62a1ee96aeb8abe30864bd9a1289e149022 Mon Sep 17 00:00:00 2001 From: Divyansh Singh Date: Sat, 5 Aug 2023 21:33:10 +0530 Subject: [PATCH 10/10] fix tests --- tests/integration/test_archives.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/tests/integration/test_archives.py b/tests/integration/test_archives.py index 98d653fbc..e74f9bb33 100644 --- a/tests/integration/test_archives.py +++ b/tests/integration/test_archives.py @@ -171,15 +171,11 @@ def test_archive_rename(qapp, qtbot, mocker): tab.archiveTable.selectRow(0) new_archive_name = 'idf89d8f9d8fd98' - mocker.patch.object(vorta.views.archive_tab.QInputDialog, 'getText', return_value=(new_archive_name, True)) - tab.rename_action() + pos = tab.archiveTable.visualRect(tab.archiveTable.model().index(0, 4)).center() + qtbot.mouseClick(tab.archiveTable.viewport(), QtCore.Qt.MouseButton.LeftButton, pos=pos) + qtbot.mouseDClick(tab.archiveTable.viewport(), QtCore.Qt.MouseButton.LeftButton, pos=pos) + qtbot.keyClicks(tab.archiveTable.viewport().focusWidget(), new_archive_name) + qtbot.keyClick(tab.archiveTable.viewport().focusWidget(), QtCore.Qt.Key.Key_Return) # Successful rename case - qtbot.waitUntil(lambda: tab.mountErrors.text() == 'Archive renamed.', **pytest._wait_defaults) - assert ArchiveModel.select().filter(name=new_archive_name).count() == 1 - - # Duplicate name case - tab.archiveTable.selectRow(0) - exp_text = 'An archive with this name already exists.' - tab.rename_action() - qtbot.waitUntil(lambda: tab.mountErrors.text() == exp_text, **pytest._wait_defaults) + qtbot.waitUntil(lambda: tab.archiveTable.model().index(0, 4).data() == new_archive_name, **pytest._wait_defaults)