mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 14:02:21 -04:00
202 lines
6 KiB
Python
202 lines
6 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 NewType, Sequence
|
|
|
|
import anki # pylint: disable=unused-import
|
|
import anki.cards
|
|
import anki.collection
|
|
import anki.decks
|
|
import anki.template
|
|
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.get_card(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) -> str:
|
|
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
|
|
)
|