Anki/anki/notes.py
Damien Elmes b5c0b1f2c7 drop required/unique field properties
Instead of having required and unique flags for every field, enforce both
requirements on the first field, and neither on the rest. This mirrors the
subject/body format people are used to in note-taking apps. The subject
defines the object being learnt, and the remaining fields represent properties
of that object.

In the past, duplicate checking served two purposes: it quickly notified the
user that they're entering the same fact twice, and it notified the user if
they'd accidentally mistyped a secondary field. The former behaviour is
important for avoiding wasted effort, and so it should be done in real time.
The latter behaviour is not essential however - a typo is not wasted effort,
and it could be fixed in a periodic 'find duplicates' function. Given that
some users ended up with sluggish decks due to the overhead a large number of
facts * a large number of unique fields caused, this seems like a change for
the better.

This also means Anki will let you add notes as long as as the first field has
been filled out. Again, this is not a big deal: Anki is still checking to make
sure one or more cards will be generated, and the user can easily add any
missing fields later.

As a bonus, this change simplifies field configuration somewhat. As the card
layout and field dialogs are a popular point of confusion, the more they can
be simplified, the better.
2011-11-24 22:16:03 +09:00

168 lines
5.2 KiB
Python

# -*- coding: utf-8 -*-
# Copyright: Damien Elmes <anki@ichi2.net>
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import time
from anki.errors import AnkiError
from anki.utils import fieldChecksum, intTime, \
joinFields, splitFields, ids2str, stripHTML, timestampID, guid64
class Note(object):
def __init__(self, col, model=None, id=None):
assert not (model and id)
self.col = col
if id:
self.id = id
self.load()
else:
self.id = timestampID(col.db, "notes")
self.guid = guid64()
self._model = model
self.did = model['did']
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)
def load(self):
(self.guid,
self.mid,
self.did,
self.mod,
self.usn,
self.tags,
self.fields,
self.flags,
self.data) = self.col.db.first("""
select guid, mid, did, mod, usn, tags, flds, flags, data
from notes where id = ?""", self.id)
self.fields = splitFields(self.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)
def flush(self, mod=None):
if self.model()['cloze']:
self._clozePreFlush()
self.mod = mod if mod else intTime()
self.usn = self.col.usn()
sfld = stripHTML(self.fields[self.col.models.sortIdx(self._model)])
tags = self.stringTags()
csum = fieldChecksum(self.fields[0])
res = self.col.db.execute("""
insert or replace into notes values (?,?,?,?,?,?,?,?,?,?,?,?)""",
self.id, self.guid, self.mid, self.did,
self.mod, self.usn, tags,
self.joinedFields(), sfld, csum, self.flags,
self.data)
self.id = res.lastrowid
self.col.tags.register(self.tags)
if self.model()['cloze']:
self._clozePostFlush()
def joinedFields(self):
return joinFields(self.fields)
def cards(self):
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):
return self._model
def updateCardDids(self):
for c in self.cards():
if c.did != self.did and not c.template()['did']:
c.did = self.did
c.flush()
# Dict interface
##################################################
def keys(self):
return self._fmap.keys()
def values(self):
return self.fields
def items(self):
return [(f['name'], self.fields[ord])
for ord, f in sorted(self._fmap.values())]
def _fieldOrd(self, key):
try:
return self._fmap[key][0]
except:
raise KeyError(key)
def __getitem__(self, key):
return self.fields[self._fieldOrd(key)]
def __setitem__(self, key, value):
self.fields[self._fieldOrd(key)] = value
# Tags
##################################################
def hasTag(self, tag):
return self.col.tags.inList(tag, self.tags)
def stringTags(self):
return self.col.tags.join(self.col.tags.canonify(self.tags))
def setTagsFromStr(self, str):
self.tags = self.col.tags.split(str)
def delTag(self, tag):
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):
# duplicates will be stripped on save
self.tags.append(tag)
# Unique/duplicate check
##################################################
def dupeOrEmpty(self):
"True if first field is a duplicate or is empty."
val = self.fields[0]
if not val.strip():
return True
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 splitFields(flds)[0] == self.fields[0]:
return True
return False
# Flushing cloze notes
##################################################
def _clozePreFlush(self):
self.newlyAdded = not self.col.db.scalar(
"select 1 from cards where nid = ?", self.id)
tmpls = self.col.findTemplates(self)
ok = []
for t in tmpls:
ok.append(t['ord'])
# check if there are cards referencing a deleted cloze
if self.col.db.scalar(
"select 1 from cards where nid = ? and ord not in %s" %
ids2str(ok), self.id):
# there are; abort, as the UI should have handled this
raise Exception("UI should have deleted cloze")
def _clozePostFlush(self):
# generate missing cards
if not self.newlyAdded:
self.col.genCards([self.id])