mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 22:12:21 -04:00

The packaged builds of 2.1.50 use python -OO, which means our assertion statements won't be run. This is not an issue for unit tests (as we don't run them from a packaged build), or for type assertions (which are added for mypy's benefit), but we do need to ensure that invariant checks are still run.
198 lines
5.9 KiB
Python
198 lines
5.9 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
|
|
|
|
import copy
|
|
from typing import Any, NewType, Sequence
|
|
|
|
import anki # pylint: disable=unused-import
|
|
from anki import hooks, notes_pb2
|
|
from anki._legacy import DeprecatedNamesMixin
|
|
from anki.consts import MODEL_STD
|
|
from anki.models import NotetypeDict, NotetypeId, TemplateDict
|
|
from anki.utils import join_fields
|
|
|
|
DuplicateOrEmptyResult = notes_pb2.NoteFieldsCheckResponse.State
|
|
NoteFieldsCheckResult = notes_pb2.NoteFieldsCheckResponse.State
|
|
DefaultsForAdding = notes_pb2.DeckAndNotetype
|
|
|
|
# types
|
|
NoteId = NewType("NoteId", int)
|
|
|
|
|
|
class Note(DeprecatedNamesMixin):
|
|
# not currently exposed
|
|
flags = 0
|
|
data = ""
|
|
id: NoteId
|
|
mid: NotetypeId
|
|
|
|
def __init__(
|
|
self,
|
|
col: anki.collection.Collection,
|
|
model: NotetypeDict | NotetypeId | None = None,
|
|
id: NoteId | None = None,
|
|
) -> None:
|
|
if model and id:
|
|
raise Exception("only model or id should be provided")
|
|
notetype_id = model["id"] if isinstance(model, dict) else model
|
|
self.col = col.weakref()
|
|
|
|
if id:
|
|
# existing note
|
|
self.id = id
|
|
self.load()
|
|
else:
|
|
# new note for provided notetype
|
|
self._load_from_backend_note(self.col._backend.new_note(notetype_id))
|
|
|
|
def load(self) -> None:
|
|
note = self.col._backend.get_note(self.id)
|
|
assert note
|
|
self._load_from_backend_note(note)
|
|
|
|
def _load_from_backend_note(self, note: notes_pb2.Note) -> None:
|
|
self.id = NoteId(note.id)
|
|
self.guid = note.guid
|
|
self.mid = NotetypeId(note.notetype_id)
|
|
self.mod = note.mtime_secs
|
|
self.usn = note.usn
|
|
self.tags = list(note.tags)
|
|
self.fields = list(note.fields)
|
|
self._fmap = self.col.models.field_map(self.note_type())
|
|
|
|
def _to_backend_note(self) -> notes_pb2.Note:
|
|
hooks.note_will_flush(self)
|
|
return notes_pb2.Note(
|
|
id=self.id,
|
|
guid=self.guid,
|
|
notetype_id=self.mid,
|
|
mtime_secs=self.mod,
|
|
usn=self.usn,
|
|
tags=self.tags,
|
|
fields=self.fields,
|
|
)
|
|
|
|
def flush(self) -> None:
|
|
"""This preserves any current checkpoint.
|
|
For an undo entry, use col.update_note() instead."""
|
|
if self.id == 0:
|
|
raise Exception("can't flush a new note")
|
|
self.col._backend.update_notes(
|
|
notes=[self._to_backend_note()], skip_undo_entry=True
|
|
)
|
|
|
|
def joined_fields(self) -> str:
|
|
return join_fields(self.fields)
|
|
|
|
def ephemeral_card(
|
|
self,
|
|
ord: int = 0,
|
|
*,
|
|
custom_note_type: NotetypeDict = None,
|
|
custom_template: TemplateDict = None,
|
|
fill_empty: bool = False,
|
|
) -> anki.cards.Card:
|
|
card = anki.cards.Card(self.col)
|
|
card.ord = ord
|
|
card.did = anki.decks.DEFAULT_DECK_ID
|
|
|
|
model = custom_note_type or self.note_type()
|
|
template = copy.copy(
|
|
custom_template
|
|
or (
|
|
model["tmpls"][ord] if model["type"] == MODEL_STD else model["tmpls"][0]
|
|
)
|
|
)
|
|
# may differ in cloze case
|
|
template["ord"] = card.ord
|
|
|
|
output = anki.template.TemplateRenderContext.from_card_layout(
|
|
self,
|
|
card,
|
|
notetype=model,
|
|
template=template,
|
|
fill_empty=fill_empty,
|
|
).render()
|
|
card.set_render_output(output)
|
|
card._note = self
|
|
return card
|
|
|
|
def cards(self) -> list[anki.cards.Card]:
|
|
return [self.col.getCard(id) for id in self.card_ids()]
|
|
|
|
def card_ids(self) -> Sequence[anki.cards.CardId]:
|
|
return self.col.card_ids_of_note(self.id)
|
|
|
|
def note_type(self) -> NotetypeDict | None:
|
|
return self.col.models.get(self.mid)
|
|
|
|
_note_type = property(note_type)
|
|
|
|
def cloze_numbers_in_fields(self) -> Sequence[int]:
|
|
return self.col._backend.cloze_numbers_in_note(self._to_backend_note())
|
|
|
|
# Dict interface
|
|
##################################################
|
|
|
|
def keys(self) -> list[str]:
|
|
return list(self._fmap.keys())
|
|
|
|
def values(self) -> list[str]:
|
|
return self.fields
|
|
|
|
def items(self) -> list[tuple[str, str]]:
|
|
return [(f["name"], self.fields[ord]) for ord, f in sorted(self._fmap.values())]
|
|
|
|
def _field_index(self, key: str) -> int:
|
|
try:
|
|
return self._fmap[key][0]
|
|
except Exception as exc:
|
|
raise KeyError(key) from exc
|
|
|
|
def __getitem__(self, key: str) -> str:
|
|
return self.fields[self._field_index(key)]
|
|
|
|
def __setitem__(self, key: str, value: str) -> None:
|
|
self.fields[self._field_index(key)] = value
|
|
|
|
def __contains__(self, key: str) -> bool:
|
|
return key in self._fmap
|
|
|
|
# Tags
|
|
##################################################
|
|
|
|
def has_tag(self, tag: str) -> bool:
|
|
return self.col.tags.in_list(tag, self.tags)
|
|
|
|
def remove_tag(self, tag: str) -> None:
|
|
rem = []
|
|
for tag_ in self.tags:
|
|
if tag_.lower() == tag.lower():
|
|
rem.append(tag_)
|
|
for tag_ in rem:
|
|
self.tags.remove(tag_)
|
|
|
|
def add_tag(self, tag: str) -> None:
|
|
"Add tag. Duplicates will be stripped on save."
|
|
self.tags.append(tag)
|
|
|
|
def string_tags(self) -> Any:
|
|
return self.col.tags.join(self.tags)
|
|
|
|
def set_tags_from_str(self, tags: str) -> None:
|
|
self.tags = self.col.tags.split(tags)
|
|
|
|
# Unique/duplicate/cloze check
|
|
##################################################
|
|
|
|
def fields_check(self) -> NoteFieldsCheckResult.V:
|
|
return self.col._backend.note_fields_check(self._to_backend_note()).state
|
|
|
|
dupeOrEmpty = duplicate_or_empty = fields_check
|
|
|
|
|
|
Note.register_deprecated_aliases(
|
|
delTag=Note.remove_tag, _fieldOrd=Note._field_index, model=Note.note_type
|
|
)
|