mirror of
https://github.com/ankitects/anki.git
synced 2025-09-19 06:22:22 -04:00

Earlier today I pushed a change that split this code up into multiple repos, but that has proved to complicate things too much. So we're back to a single repo, except the individual submodules are better separated than they were before. The README files need updating again; I will push them out soon. Aside from splitting out the different modules, the sound code has moved from from anki to aqt.
215 lines
6.1 KiB
Python
215 lines
6.1 KiB
Python
# Copyright: Ankitects Pty Ltd and contributors
|
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|
|
|
from typing import Any, Dict, List, Optional, Tuple
|
|
|
|
import anki # pylint: disable=unused-import
|
|
from anki.types import NoteType
|
|
from anki.utils import (
|
|
fieldChecksum,
|
|
guid64,
|
|
intTime,
|
|
joinFields,
|
|
splitFields,
|
|
stripHTMLMedia,
|
|
timestampID,
|
|
)
|
|
|
|
|
|
class Note:
|
|
col: "anki.storage._Collection"
|
|
newlyAdded: bool
|
|
id: int
|
|
guid: str
|
|
_model: NoteType
|
|
mid: int
|
|
tags: List[str]
|
|
fields: List[str]
|
|
flags: int
|
|
data: str
|
|
_fmap: Dict[str, Tuple[Any, Any]]
|
|
scm: int
|
|
|
|
def __init__(
|
|
self,
|
|
col: "anki.storage._Collection",
|
|
model: Optional[NoteType] = None,
|
|
id: Optional[int] = None,
|
|
) -> None:
|
|
assert not (model and id)
|
|
self.col = col
|
|
self.newlyAdded = False
|
|
if id:
|
|
self.id = id
|
|
self.load()
|
|
else:
|
|
self.id = timestampID(col.db, "notes")
|
|
self.guid = guid64()
|
|
self._model = model
|
|
self.mid = model["id"]
|
|
self.tags = []
|
|
self.fields = [""] * len(self._model["flds"])
|
|
self.flags = 0
|
|
self.data = ""
|
|
self._fmap = self.col.models.fieldMap(self._model)
|
|
self.scm = self.col.scm
|
|
|
|
def load(self) -> None:
|
|
(
|
|
self.guid,
|
|
self.mid,
|
|
self.mod,
|
|
self.usn,
|
|
self.tags,
|
|
fields,
|
|
self.flags,
|
|
self.data,
|
|
) = self.col.db.first(
|
|
"""
|
|
select guid, mid, mod, usn, tags, flds, flags, data
|
|
from notes where id = ?""",
|
|
self.id,
|
|
)
|
|
self.fields = splitFields(fields)
|
|
self.tags = self.col.tags.split(self.tags)
|
|
self._model = self.col.models.get(self.mid)
|
|
self._fmap = self.col.models.fieldMap(self._model)
|
|
self.scm = self.col.scm
|
|
|
|
def flush(self, mod: Optional[int] = None) -> None:
|
|
"If fields or tags have changed, write changes to disk."
|
|
assert self.scm == self.col.scm
|
|
self._preFlush()
|
|
sfld = stripHTMLMedia(self.fields[self.col.models.sortIdx(self._model)])
|
|
tags = self.stringTags()
|
|
fields = self.joinedFields()
|
|
if not mod and self.col.db.scalar(
|
|
"select 1 from notes where id = ? and tags = ? and flds = ?",
|
|
self.id,
|
|
tags,
|
|
fields,
|
|
):
|
|
return
|
|
csum = fieldChecksum(self.fields[0])
|
|
self.mod = mod if mod else intTime()
|
|
self.usn = self.col.usn()
|
|
res = self.col.db.execute(
|
|
"""
|
|
insert or replace into notes values (?,?,?,?,?,?,?,?,?,?,?)""",
|
|
self.id,
|
|
self.guid,
|
|
self.mid,
|
|
self.mod,
|
|
self.usn,
|
|
tags,
|
|
fields,
|
|
sfld,
|
|
csum,
|
|
self.flags,
|
|
self.data,
|
|
)
|
|
self.col.tags.register(self.tags)
|
|
self._postFlush()
|
|
|
|
def joinedFields(self) -> str:
|
|
return joinFields(self.fields)
|
|
|
|
def cards(self) -> List:
|
|
return [
|
|
self.col.getCard(id)
|
|
for id in self.col.db.list(
|
|
"select id from cards where nid = ? order by ord", self.id
|
|
)
|
|
]
|
|
|
|
def model(self) -> Optional[NoteType]:
|
|
return self._model
|
|
|
|
# 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[Any, Any]]:
|
|
return [(f["name"], self.fields[ord]) for ord, f in sorted(self._fmap.values())]
|
|
|
|
def _fieldOrd(self, key: str) -> Any:
|
|
try:
|
|
return self._fmap[key][0]
|
|
except:
|
|
raise KeyError(key)
|
|
|
|
def __getitem__(self, key: str) -> str:
|
|
return self.fields[self._fieldOrd(key)]
|
|
|
|
def __setitem__(self, key: str, value: str) -> None:
|
|
self.fields[self._fieldOrd(key)] = value
|
|
|
|
def __contains__(self, key) -> bool:
|
|
return key in self._fmap
|
|
|
|
# Tags
|
|
##################################################
|
|
|
|
def hasTag(self, tag: str) -> Any:
|
|
return self.col.tags.inList(tag, self.tags)
|
|
|
|
def stringTags(self) -> Any:
|
|
return self.col.tags.join(self.col.tags.canonify(self.tags))
|
|
|
|
def setTagsFromStr(self, tags: str) -> None:
|
|
self.tags = self.col.tags.split(tags)
|
|
|
|
def delTag(self, tag: str) -> None:
|
|
rem = []
|
|
for t in self.tags:
|
|
if t.lower() == tag.lower():
|
|
rem.append(t)
|
|
for r in rem:
|
|
self.tags.remove(r)
|
|
|
|
def addTag(self, tag: str) -> None:
|
|
# duplicates will be stripped on save
|
|
self.tags.append(tag)
|
|
|
|
# Unique/duplicate check
|
|
##################################################
|
|
|
|
def dupeOrEmpty(self) -> int:
|
|
"1 if first is empty; 2 if first is a duplicate, False otherwise."
|
|
val = self.fields[0]
|
|
if not val.strip():
|
|
return 1
|
|
csum = fieldChecksum(val)
|
|
# find any matching csums and compare
|
|
for flds in self.col.db.list(
|
|
"select flds from notes where csum = ? and id != ? and mid = ?",
|
|
csum,
|
|
self.id or 0,
|
|
self.mid,
|
|
):
|
|
if stripHTMLMedia(splitFields(flds)[0]) == stripHTMLMedia(self.fields[0]):
|
|
return 2
|
|
return False
|
|
|
|
# Flushing cloze notes
|
|
##################################################
|
|
|
|
def _preFlush(self) -> None:
|
|
# have we been added yet?
|
|
self.newlyAdded = not self.col.db.scalar(
|
|
"select 1 from cards where nid = ?", self.id
|
|
)
|
|
|
|
def _postFlush(self) -> None:
|
|
# generate missing cards
|
|
if not self.newlyAdded:
|
|
rem = self.col.genCards([self.id])
|
|
# popping up a dialog while editing is confusing; instead we can
|
|
# document that the user should open the templates window to
|
|
# garbage collect empty cards
|
|
# self.col.remEmptyCards(ids)
|