anki/qt/aqt/browser/table/state.py
RumovZ c521753057
Refactor error handling (#2136)
* Add crate snafu

* Replace all inline structs in AnkiError

* Derive Snafu on AnkiError

* Use snafu for card type errors

* Use snafu whatever error for InvalidInput

* Use snafu for NotFoundError and improve message

* Use snafu for FileIoError to attach context

Remove IoError.
Add some context-attaching helpers to replace code returning bare
io::Errors.

* Add more context-attaching io helpers

* Add message, context and backtrace to new snafus

* Utilize error context and backtrace on frontend

* Rename LocalizedError -> BackendError.
* Remove DocumentedError.
* Have all backend exceptions inherit BackendError.

* Rename localized(_description) -> message

* Remove accidentally committed experimental trait

* invalid_input_context -> ok_or_invalid

* ensure_valid_input! -> require!

* Always return `Err` from `invalid_input!`

Instead of a Result to unwrap, the macro accepts a source error now.

* new_tempfile_in_parent -> new_tempfile_in_parent_of

* ok_or_not_found -> or_not_found

* ok_or_invalid -> or_invalid

* Add crate convert_case

* Use unqualified lowercase type name

* Remove uses of snafu::ensure

* Allow public construction of InvalidInputErrors (dae)

Needed to port the AnkiDroid changes.

* Make into_protobuf() public (dae)

Also required for AnkiDroid. Not sure why it worked previously - possible
bug in older Rust version?
2022-10-21 18:02:12 +10:00

221 lines
7.3 KiB
Python

# Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from __future__ import annotations
from abc import ABC, abstractmethod, abstractproperty
from typing import Sequence, cast
from anki.browser import BrowserConfig
from anki.cards import Card, CardId
from anki.collection import Collection
from anki.errors import NotFoundError
from anki.notes import Note, NoteId
from anki.utils import ids2str
from aqt.browser.table import Column, ItemId, ItemList
class ItemState(ABC):
GEOMETRY_KEY_PREFIX: str
SORT_COLUMN_KEY: str
SORT_BACKWARDS_KEY: str
_active_columns: list[str]
def __init__(self, col: Collection) -> None:
self.col = col
self._sort_column = self.col.get_config(self.SORT_COLUMN_KEY)
self._sort_backwards = self.col.get_config(self.SORT_BACKWARDS_KEY, False)
def is_notes_mode(self) -> bool:
"""Return True if the state is a NoteState."""
return isinstance(self, NoteState)
# Stateless Helpers
def note_ids_from_card_ids(self, items: Sequence[ItemId]) -> Sequence[NoteId]:
return self.col.db.list(
f"select distinct nid from cards where id in {ids2str(items)}"
)
def card_ids_from_note_ids(self, items: Sequence[ItemId]) -> Sequence[CardId]:
return self.col.db.list(f"select id from cards where nid in {ids2str(items)}")
def column_key_at(self, index: int) -> str:
return self._active_columns[index]
def column_label(self, column: Column) -> str:
return (
column.notes_mode_label if self.is_notes_mode() else column.cards_mode_label
)
def column_tooltip(self, column: Column) -> str:
if self.is_notes_mode():
return column.notes_mode_tooltip
return column.cards_mode_tooltip
# Columns and sorting
# abstractproperty is deprecated but used due to mypy limitations
# (https://github.com/python/mypy/issues/1362)
@abstractproperty # pylint: disable=deprecated-decorator
def active_columns(self) -> list[str]:
"""Return the saved or default columns for the state."""
@abstractmethod
def toggle_active_column(self, column: str) -> None:
"""Add or remove an active column."""
@property
def sort_column(self) -> str:
return self._sort_column
@sort_column.setter
def sort_column(self, column: str) -> None:
self.col.set_config(self.SORT_COLUMN_KEY, column)
self._sort_column = column
@property
def sort_backwards(self) -> bool:
"If true, descending order."
return self._sort_backwards
@sort_backwards.setter
def sort_backwards(self, order: bool) -> None:
self.col.set_config(self.SORT_BACKWARDS_KEY, order)
self._sort_backwards = order
# Get objects
@abstractmethod
def get_card(self, item: ItemId) -> Card:
"""Return the item if it's a card or its first card if it's a note."""
@abstractmethod
def get_note(self, item: ItemId) -> Note:
"""Return the item if it's a note or its note if it's a card."""
# Get ids
@abstractmethod
def find_items(
self, search: str, order: bool | str | Column, reverse: bool
) -> Sequence[ItemId]:
"""Return the item ids fitting the given search and order."""
@abstractmethod
def get_item_from_card_id(self, card: CardId) -> ItemId:
"""Return the appropriate item id for a card id."""
@abstractmethod
def get_card_ids(self, items: Sequence[ItemId]) -> Sequence[CardId]:
"""Return the card ids for the given item ids."""
@abstractmethod
def get_note_ids(self, items: Sequence[ItemId]) -> Sequence[NoteId]:
"""Return the note ids for the given item ids."""
# Toggle
@abstractmethod
def toggle_state(self) -> ItemState:
"""Return an instance of the other state."""
@abstractmethod
def get_new_items(self, old_items: Sequence[ItemId]) -> ItemList:
"""Given a list of ids from the other state, return the corresponding ids for this state."""
class CardState(ItemState):
GEOMETRY_KEY_PREFIX = "editor"
SORT_COLUMN_KEY = BrowserConfig.CARDS_SORT_COLUMN_KEY
SORT_BACKWARDS_KEY = BrowserConfig.CARDS_SORT_BACKWARDS_KEY
def __init__(self, col: Collection) -> None:
super().__init__(col)
self._active_columns = self.col.load_browser_card_columns()
@property
def active_columns(self) -> list[str]:
return self._active_columns
def toggle_active_column(self, column: str) -> None:
if column in self._active_columns:
self._active_columns.remove(column)
else:
self._active_columns.append(column)
self.col.set_browser_card_columns(self._active_columns)
def get_card(self, item: ItemId) -> Card:
return self.col.get_card(CardId(item))
def get_note(self, item: ItemId) -> Note:
return self.get_card(item).note()
def find_items(
self, search: str, order: bool | str | Column, reverse: bool
) -> Sequence[ItemId]:
return self.col.find_cards(search, order, reverse)
def get_item_from_card_id(self, card: CardId) -> ItemId:
return card
def get_card_ids(self, items: Sequence[ItemId]) -> Sequence[CardId]:
return cast(Sequence[CardId], items)
def get_note_ids(self, items: Sequence[ItemId]) -> Sequence[NoteId]:
return super().note_ids_from_card_ids(items)
def toggle_state(self) -> NoteState:
return NoteState(self.col)
def get_new_items(self, old_items: Sequence[ItemId]) -> Sequence[CardId]:
return super().card_ids_from_note_ids(old_items)
class NoteState(ItemState):
GEOMETRY_KEY_PREFIX = "editorNotesMode"
SORT_COLUMN_KEY = BrowserConfig.NOTES_SORT_COLUMN_KEY
SORT_BACKWARDS_KEY = BrowserConfig.NOTES_SORT_BACKWARDS_KEY
def __init__(self, col: Collection) -> None:
super().__init__(col)
self._active_columns = self.col.load_browser_note_columns()
@property
def active_columns(self) -> list[str]:
return self._active_columns
def toggle_active_column(self, column: str) -> None:
if column in self._active_columns:
self._active_columns.remove(column)
else:
self._active_columns.append(column)
self.col.set_browser_note_columns(self._active_columns)
def get_card(self, item: ItemId) -> Card:
if cards := self.get_note(item).cards():
return cards[0]
raise NotFoundError("card not found", None, None, None)
def get_note(self, item: ItemId) -> Note:
return self.col.get_note(NoteId(item))
def find_items(
self, search: str, order: bool | str | Column, reverse: bool
) -> Sequence[ItemId]:
return self.col.find_notes(search, order, reverse)
def get_item_from_card_id(self, card: CardId) -> ItemId:
return self.col.get_card(card).note().id
def get_card_ids(self, items: Sequence[ItemId]) -> Sequence[CardId]:
return super().card_ids_from_note_ids(items)
def get_note_ids(self, items: Sequence[ItemId]) -> Sequence[NoteId]:
return cast(Sequence[NoteId], items)
def toggle_state(self) -> CardState:
return CardState(self.col)
def get_new_items(self, old_items: Sequence[ItemId]) -> Sequence[NoteId]:
return super().note_ids_from_card_ids(old_items)