Skip to content

add ArchiveTableModel (QAbstractTableModel) spike#2517

Open
ebuzerdrmz44 wants to merge 4 commits into
borgbase:masterfrom
ebuzerdrmz44:refactor/archive-table-model
Open

add ArchiveTableModel (QAbstractTableModel) spike#2517
ebuzerdrmz44 wants to merge 4 commits into
borgbase:masterfrom
ebuzerdrmz44:refactor/archive-table-model

Conversation

@ebuzerdrmz44

Copy link
Copy Markdown
Contributor

A small spike of the Archive table as a QAbstractTableModel, opened as a draft to confirm the shape before going deep on the view rewiring (as offered in the Phase 5 #2361 thread).

Adds, and nothing else (the live Archive tab is untouched):

  • src/vorta/views/partials/archive_table_model.py ,ArchiveTableModel, mirroring the existing EventLogTableModel. Owns the per-row presentation currently inline in ArchiveTab.populate_from_profile: time/size/duration formatting, trigger icon + tooltip, raw sort-role keys, an editable name column, plus set_rows(...) and archive_at(row).
  • tests/unit/test_archive_table_model.py 15 isolated unit tests (content + ordering, sort role, fixed/dynamic units, editable flag, setData/dataChanged, icons/tooltips,headers).

Deliberately deferred to the wiring PR:

  • Swap QTableWidgetQTableView and call model.set_rows(...) from populate_from_profile.
  • Sorting: an ArchiveSortProxyModel(QSortFilterProxyModel) overriding lessThan() (matching treemodel.py), reading the model's SortRole.
  • Re-routing the ~10 archiveTable.item(row, col) reads to model.archive_at(row).
  • Migrating in-place rename + selection/double-click/delete handling.

Conventions: no DB reads in the model (the enable_fixed_units setting is injected by the view via set_rows(..., use_fixed_units=...)); translation contexts reuse existing catalog entries (Form headers, ArchiveTab tooltips).

Related Issue

Phase 5 of the views refactor (#2361) the redirect to QAbstractTableModel + views/partials/.

Motivation and Context

Per the Phase 5 redirect, tabular views (Archive, Log) should become QAbstractTableModel + QTableView rather than ViewModels. This spike extracts the Archive table's presentation logic into a self-contained, idiomatic model so the shape can be reviewed before the larger archive_tab.py migration.

How Has This Been Tested?

uv run nox -r -- tests/unit/test_archive_table_model.py . The model is exercised directly in isolation (no widget). No behavior change to the live app, since the model isn't wired in yet.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist:

  • I have read the CONTRIBUTING guide.
  • My code follows the code style of this project.
  • My change requires a change to the documentation.
  • I have updated the documentation accordingly.
  • I have added tests to cover my changes.
  • All new and existing tests passed.

I provide my contribution under the terms of the license of this repository and I affirm the Developer Certificate of Origin.

Self-contained table model for archive rows: time/size/duration
formatting, trigger icons, sort-role keys, and an editable name column,
with isolated unit tests. Not yet wired into ArchiveTab.
@ebuzerdrmz44 ebuzerdrmz44 marked this pull request as ready for review June 6, 2026 21:25
@ebuzerdrmz44

Copy link
Copy Markdown
Contributor Author

Quick question on sorting before I wire this in: custom SortRole (current approach) or override lessThan() in a proxy like treemodel.py? Same question as #2515 . whatever you prefer I'll apply across Archive/Source.

@ebuzerdrmz44 ebuzerdrmz44 marked this pull request as draft June 6, 2026 21:33
@ebuzerdrmz44

Copy link
Copy Markdown
Contributor Author

m3nu small heads-up: I've got finals the next few days, back at full pace June 11.After that I will work through the Archive and Source tabs ( and any other tabs that need it) per your reviews here and on the others. #2514 and #2515 are reshaped and ready for re-review whenever you get a chance. No rush , thanks!

@m3nu m3nu left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Review — ArchiveTableModel spike

The shape is right and this is the correct foundation for the wiring PR. I diffed the model against the live archive_tab.py:295-340 formatting and fidelity is good; the SortRole design is exactly what we settled on in #2515 and even fixes a latent bug (durations >24h render as '1 day, …' and sort wrong as text today — raw-seconds sort keys order them correctly). Tests (15, widget-free) are the bar I'd want for the other tables.

Six findings inline, two of which are important to resolve before the wiring is written against this API:

  • 🔴 P1 (data-safety): archive_at(row) is source-indexed; once the planned sort proxy is added it will target the wrong archive for rename/mount/delete. Use a custom data role instead.
  • 🟡 P2 (perf regression): the trigger icon is re-rendered from disk on every paint.
  • 🟡 P3: the "Trigger" column header is silently dropped.
  • 🔵 P4–P6: a duplicated constant, a missed alignment parity, and a typing nit.

Not approving since this is a draft — leaving as a comment. Shape-wise: 👍, address P1/P2 in the wiring PR and the recipe is solid.

self._fixed_unit = None
self.endResetModel()

def archive_at(self, row: int) -> Optional[ArchiveModel]:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 [P1 — data-safety] archive_at(row) takes a source row, but the wiring will feed it proxy rows.

This method is correct in isolation, but the PR's deferred recipe ("re-route the ~10 archiveTable.item(row, col) reads to model.archive_at(row)") will be wrong once ArchiveSortProxyModel is in place: selectionModel().selectedRows() returns proxy indices, and archive_at(proxy_row) indexes self._rows in source order. After any sort, that resolves to the wrong archive for rename (archive_tab.py:466,496,591), delete (701-702), and diff (757-758) — i.e. acting on the wrong backup.

The repo already establishes the rule that proxy indices must be mapped first — see diff_result.py:180 and extract.py:174 (index = self.sortproxy.mapToSource(index)).

Recommended: expose the archive through a custom data role rather than a raw-row accessor, e.g. ArchiveRole = Qt.ItemDataRole.UserRole + 1 returned from data(). Callers then use index.data(ArchiveRole) on the proxy index, and QSortFilterProxyModel auto-forwards through mapToSource — there's no raw row to get wrong, and it's symmetric with the existing SortRole. (If you keep archive_at, every one of the ~10 call sites must proxy.mapToSource() first, and rename it / document it as source-indexed.)

return row.trigger or ''
return None

def _trigger_icon(self, row: ArchiveModel) -> Any:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 [P2 — perf regression] Trigger icon is re-rendered from disk on every paint.

get_colored_icon (views/utils.py:10) does open() + read + QImage.fromData (SVG parse) + scaledToHeight(128) + wrap, with no caching. data() calls this on every DecorationRole request, which Qt fires per repaint/scroll for each visible trigger cell — and it renders a 128px image for a cell-sized icon. This is a regression from the legacy path, which built the icon once per populate and stored it in the QTableWidgetItem.

Recommended: cache the two icons (clock-o, user) on the model, keyed on dark-mode state, and rebuild them on QEvent.PaletteChange (the icons are theme-colored, so a plain permanent cache would go stale on a light/dark switch).

trans_late('Form', 'Duration'),
trans_late('Form', 'Mount Point'),
trans_late('Form', 'Name'),
'', # trigger icon column has no header label

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 [P3 — behavior] The "Trigger" column header is silently dropped. The live .ui defines a Trigger header for column 5 (archive_tab.ui:175); _HEADERS[5] = '' removes it. That's a real UX change, not a no-op, and it diverges from the PR's "reuse existing catalog entries" claim. Either keep trans_late('Form', 'Trigger'), or make the icon-only-no-header decision explicit in the description.

from vorta.views.utils import get_colored_icon

#: Decimal digits shown in the size column.
SIZE_DECIMAL_DIGITS = 1

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 [P4 — maintainability] SIZE_DECIMAL_DIGITS is now defined here and at archive_tab.py:56. On wiring, have one own it and the other import it so they can't drift.

return row.name # pre-fill the rename editor with the current name
if role == self.SortRole:
return self._sort_data(row, column)
if role == Qt.ItemDataRole.DecorationRole and column == self.COL_TRIGGER:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 [P5 — parity] The legacy user-trigger item sets AlignRight (archive_tab.py:339); the model exposes no TextAlignmentRole. Negligible since the cell is icon-only, but worth a line on the wiring parity checklist so it's a conscious drop rather than an accidental one.

'', # trigger icon column has no header label
)

def __init__(self, parent: Optional[Any] = None):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 [P6 — nit] parent: Optional[Any] → prefer Optional[QObject], matching the typed style in treemodel.py (same nit I left on #2515).

@ebuzerdrmz44 ebuzerdrmz44 marked this pull request as ready for review June 12, 2026 15:12
@ebuzerdrmz44

Copy link
Copy Markdown
Contributor Author

Thanks for the review m3nu , all six findings are addressed .

  • P1 : added ArchiveRole = Qt.ItemDataRole.UserRole + 1 returned from data(). Selection is resolved via index.data(ArchiveRole) on proxy indices, so QSortFilterProxyModel auto-forwards through mapToSource
  • P2 : trigger icons cached on the model, keyed on dark-mode, invalidated on theme switch.
  • P3: restored the Trigger header.
  • P6: parent: Optional[QObject].

P4/P5 are handled in the wiring, as you suggested: it single-sources SIZE_DECIMAL_DIGITS from the model (P4), and the AlignRight drop is intentional , the Trigger cell is icon-only and IconDelegate centers it (P5).

The wiring is implemented and green (Phase 4 part 1: QTableView + ArchiveTableModel fully wired into archive_tab.py. I'll open it as a follow-up PR once this spike lands. Phase 4 part 2 (peeling rename/diff/context-menu off the orchestrator) will be a separate slice.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants