mirror of
https://github.com/ankitects/anki.git
synced 2025-09-19 06:22:22 -04:00
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.
This commit is contained in:
parent
daea038aa4
commit
b5c0b1f2c7
11 changed files with 40 additions and 135 deletions
|
@ -263,7 +263,6 @@ crt=?, mod=?, scm=?, dty=?, usn=?, ls=?, conf=?""",
|
|||
# more card templates
|
||||
self._logRem(ids, REM_NOTE)
|
||||
self.db.execute("delete from notes where id in %s" % strids)
|
||||
self.db.execute("delete from nsums where nid in %s" % strids)
|
||||
|
||||
# Card creation
|
||||
##########################################################################
|
||||
|
@ -368,24 +367,18 @@ select id from notes where id in %s and id not in (select nid from cards)""" %
|
|||
return self.db.execute(
|
||||
"select id, mid, flds from notes where id in "+snids)
|
||||
|
||||
def updateFieldCache(self, nids, csum=True):
|
||||
def updateFieldCache(self, nids):
|
||||
"Update field checksums and sort cache, after find&replace, etc."
|
||||
snids = ids2str(nids)
|
||||
r = []
|
||||
r2 = []
|
||||
for (nid, mid, flds) in self._fieldData(snids):
|
||||
fields = splitFields(flds)
|
||||
model = self.models.get(mid)
|
||||
if csum:
|
||||
for f in model['flds']:
|
||||
if f['uniq'] and fields[f['ord']]:
|
||||
r.append((nid, mid, fieldChecksum(fields[f['ord']])))
|
||||
r2.append((stripHTML(fields[self.models.sortIdx(model)]), nid))
|
||||
if csum:
|
||||
self.db.execute("delete from nsums where nid in "+snids)
|
||||
self.db.executemany("insert into nsums values (?,?,?)", r)
|
||||
# rely on calling code to bump usn+mod
|
||||
self.db.executemany("update notes set sfld = ? where id = ?", r2)
|
||||
r.append((stripHTML(fields[self.models.sortIdx(model)]),
|
||||
fieldChecksum(fields[0]),
|
||||
nid))
|
||||
# apply, relying on calling code to bump usn+mod
|
||||
self.db.executemany("update notes set sfld=?, csum=? where id=?", r)
|
||||
|
||||
# Q/A generation
|
||||
##########################################################################
|
||||
|
|
|
@ -87,7 +87,7 @@ class Anki2Importer(Importer):
|
|||
continue #raise Exception("merging notes nyi")
|
||||
# add to col
|
||||
self.dst.db.executemany(
|
||||
"insert or replace into notes values (?,?,?,?,?,?,?,?,?,?,?)",
|
||||
"insert or replace into notes values (?,?,?,?,?,?,?,?,?,?,?,?)",
|
||||
add)
|
||||
self.dst.updateFieldCache(dirty)
|
||||
self.dst.tags.registerNotes(dirty)
|
||||
|
|
|
@ -36,8 +36,6 @@ defaultModel = {
|
|||
defaultField = {
|
||||
'name': "",
|
||||
'ord': None,
|
||||
'req': False,
|
||||
'uniq': False,
|
||||
'sticky': False,
|
||||
# the following alter editing, and are used as defaults for the
|
||||
# template wizard
|
||||
|
|
|
@ -51,13 +51,14 @@ from notes where id = ?""", self.id)
|
|||
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
||||
insert or replace into notes values (?,?,?,?,?,?,?,?,?,?,?,?)""",
|
||||
self.id, self.guid, self.mid, self.did,
|
||||
self.mod, self.usn, tags,
|
||||
self.joinedFields(), sfld, self.flags, self.data)
|
||||
self.joinedFields(), sfld, csum, self.flags,
|
||||
self.data)
|
||||
self.id = res.lastrowid
|
||||
self.updateFieldChecksums()
|
||||
self.col.tags.register(self.tags)
|
||||
if self.model()['cloze']:
|
||||
self._clozePostFlush()
|
||||
|
@ -65,18 +66,6 @@ insert or replace into notes values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
|||
def joinedFields(self):
|
||||
return joinFields(self.fields)
|
||||
|
||||
def updateFieldChecksums(self):
|
||||
self.col.db.execute("delete from nsums where nid = ?", self.id)
|
||||
d = []
|
||||
for (ord, conf) in self._fmap.values():
|
||||
if not conf['uniq']:
|
||||
continue
|
||||
val = self.fields[ord]
|
||||
if not val:
|
||||
continue
|
||||
d.append((self.id, self.mid, fieldChecksum(val)))
|
||||
self.col.db.executemany("insert into nsums values (?, ?, ?)", d)
|
||||
|
||||
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)]
|
||||
|
@ -139,51 +128,22 @@ insert or replace into notes values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
|||
# duplicates will be stripped on save
|
||||
self.tags.append(tag)
|
||||
|
||||
# Unique/duplicate checks
|
||||
# Unique/duplicate check
|
||||
##################################################
|
||||
|
||||
def fieldUnique(self, name):
|
||||
(ord, conf) = self._fmap[name]
|
||||
if not conf['uniq']:
|
||||
return True
|
||||
val = self[name]
|
||||
if not val:
|
||||
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)
|
||||
if self.id:
|
||||
lim = "and nid != :nid"
|
||||
else:
|
||||
lim = ""
|
||||
nids = self.col.db.list(
|
||||
"select nid from nsums where csum = ? and nid != ? and mid = ?",
|
||||
csum, self.id or 0, self.mid)
|
||||
if not nids:
|
||||
# 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
|
||||
# grab notes with the same checksums, and see if they're actually
|
||||
# duplicates
|
||||
for flds in self.col.db.list("select flds from notes where id in "+
|
||||
ids2str(nids)):
|
||||
fields = splitFields(flds)
|
||||
if fields[ord] == val:
|
||||
return False
|
||||
return True
|
||||
|
||||
def fieldComplete(self, name, text=None):
|
||||
(ord, conf) = self._fmap[name]
|
||||
if not conf['req']:
|
||||
return True
|
||||
return self[name]
|
||||
|
||||
def problems(self):
|
||||
d = []
|
||||
for (k, (ord, conf)) in self._fmap.items():
|
||||
if not self.fieldUnique(k):
|
||||
d.append((ord, "unique"))
|
||||
elif not self.fieldComplete(k):
|
||||
d.append((ord, "required"))
|
||||
else:
|
||||
d.append((ord, None))
|
||||
return [x[1] for x in sorted(d)]
|
||||
|
||||
# Flushing cloze notes
|
||||
##################################################
|
||||
|
|
|
@ -13,8 +13,6 @@ def addBasicModel(col):
|
|||
mm = col.models
|
||||
m = mm.new(_("Basic"))
|
||||
fm = mm.newField(_("Front"))
|
||||
fm['req'] = True
|
||||
fm['uniq'] = True
|
||||
mm.addField(m, fm)
|
||||
fm = mm.newField(_("Back"))
|
||||
mm.addField(m, fm)
|
||||
|
@ -34,8 +32,6 @@ def addClozeModel(col):
|
|||
mm = col.models
|
||||
m = mm.new(_("Cloze"))
|
||||
fm = mm.newField(_("Text"))
|
||||
fm['req'] = True
|
||||
fm['uniq'] = True
|
||||
mm.addField(m, fm)
|
||||
fm = mm.newField(_("Notes"))
|
||||
mm.addField(m, fm)
|
||||
|
|
|
@ -90,15 +90,11 @@ create table if not exists notes (
|
|||
tags text not null,
|
||||
flds text not null,
|
||||
sfld integer not null,
|
||||
csum integer not null,
|
||||
flags integer not null,
|
||||
data text not null
|
||||
);
|
||||
|
||||
create table if not exists nsums (
|
||||
nid integer not null,
|
||||
mid integer not null,
|
||||
csum integer not null
|
||||
);
|
||||
create table if not exists cards (
|
||||
id integer primary key,
|
||||
nid integer not null,
|
||||
|
@ -177,7 +173,6 @@ create index if not exists ix_cards_nid on cards (nid);
|
|||
create index if not exists ix_cards_sched on cards (did, queue, due);
|
||||
-- revlog by card
|
||||
create index if not exists ix_revlog_cid on revlog (cid);
|
||||
-- field uniqueness check
|
||||
create index if not exists ix_nsums_nid on nsums (nid);
|
||||
create index if not exists ix_nsums_csum on nsums (csum);
|
||||
-- field uniqueness
|
||||
create index if not exists ix_notes_csum on notes (csum);
|
||||
""")
|
||||
|
|
|
@ -144,7 +144,6 @@ select count() from notes where id not in (select distinct nid from cards)""")
|
|||
self.col.db.scalar("select count() from cards"),
|
||||
self.col.db.scalar("select count() from notes"),
|
||||
self.col.db.scalar("select count() from revlog"),
|
||||
self.col.db.scalar("select count() from nsums"),
|
||||
self.col.db.scalar("select count() from graves"),
|
||||
len(self.col.models.all()),
|
||||
len(self.col.tags.all()),
|
||||
|
@ -188,7 +187,7 @@ select id, nid, did, ord, mod, %d, type, queue, due, ivl, factor, reps,
|
|||
lapses, left, edue, flags, data from cards where %s""" % d)
|
||||
else:
|
||||
return x("""
|
||||
select id, guid, mid, did, mod, %d, tags, flds, '', flags, data
|
||||
select id, guid, mid, did, mod, %d, tags, flds, '', '', flags, data
|
||||
from notes where %s""" % d)
|
||||
|
||||
def chunk(self):
|
||||
|
@ -356,7 +355,7 @@ from notes where %s""" % d)
|
|||
def mergeNotes(self, notes):
|
||||
rows = self.newerRows(notes, "notes", 4)
|
||||
self.col.db.executemany(
|
||||
"insert or replace into notes values (?,?,?,?,?,?,?,?,?,?,?)",
|
||||
"insert or replace into notes values (?,?,?,?,?,?,?,?,?,?,?,?)",
|
||||
rows)
|
||||
self.col.updateFieldCache([f[0] for f in rows])
|
||||
|
||||
|
|
|
@ -177,7 +177,7 @@ select id, id, modelId, 1, cast(created*1000 as int), cast(modified as int),
|
|||
# and put the facts into the new table
|
||||
db.execute("drop table facts")
|
||||
_addSchema(db, False)
|
||||
db.executemany("insert into notes values (?,?,?,?,?,?,?,?,'',0,'')", data)
|
||||
db.executemany("insert into notes values (?,?,?,?,?,?,?,?,'','',0,'')", data)
|
||||
db.execute("drop table fields")
|
||||
|
||||
# cards
|
||||
|
@ -349,15 +349,12 @@ insert or replace into col select id, cast(created as int), :t,
|
|||
flds = []
|
||||
# note: qsize & qcol are used in upgrade then discarded
|
||||
for c, row in enumerate(db.all("""
|
||||
select name, features, required, "unique",
|
||||
quizFontFamily, quizFontSize, quizFontColour, editFontSize from fieldModels
|
||||
where modelId = ?
|
||||
select name, features, quizFontFamily, quizFontSize, quizFontColour,
|
||||
editFontSize from fieldModels where modelId = ?
|
||||
order by ordinal""", mid)):
|
||||
conf = dconf.copy()
|
||||
(conf['name'],
|
||||
conf['rtl'],
|
||||
conf['req'],
|
||||
conf['uniq'],
|
||||
conf['font'],
|
||||
conf['qsize'],
|
||||
conf['qcol'],
|
||||
|
|
|
@ -41,7 +41,6 @@ def test_delete():
|
|||
assert deck.noteCount() == 0
|
||||
assert deck.db.scalar("select count() from notes") == 0
|
||||
assert deck.db.scalar("select count() from cards") == 0
|
||||
assert deck.db.scalar("select count() from nsums") == 0
|
||||
assert deck.db.scalar("select count() from revlog") == 0
|
||||
assert deck.db.scalar("select count() from graves") == 2
|
||||
|
||||
|
|
|
@ -71,27 +71,14 @@ def test_noteAddDelete():
|
|||
c0 = f.cards()[0]
|
||||
assert re.sub("</?.+?>", "", c0.q()) == u"three"
|
||||
# it should not be a duplicate
|
||||
for p in f.problems():
|
||||
assert not p
|
||||
# now let's make a duplicate and test uniqueness
|
||||
assert not f.dupeOrEmpty()
|
||||
# now let's make a duplicate
|
||||
f2 = deck.newNote()
|
||||
f2.model()['flds'][1]['req'] = True
|
||||
f2['Front'] = u"one"; f2['Back'] = u""
|
||||
p = f2.problems()
|
||||
assert p[0] == "unique"
|
||||
assert p[1] == "required"
|
||||
# try delete the first card
|
||||
cards = f.cards()
|
||||
id1 = cards[0].id; id2 = cards[1].id
|
||||
assert deck.cardCount() == 4
|
||||
assert deck.noteCount() == 2
|
||||
deck.remCards([id1])
|
||||
assert deck.cardCount() == 3
|
||||
assert deck.noteCount() == 2
|
||||
# and the second should clear the note
|
||||
deck.remCards([id2])
|
||||
assert deck.cardCount() == 2
|
||||
assert deck.noteCount() == 1
|
||||
assert f2.dupeOrEmpty()
|
||||
# empty first field should not be permitted either
|
||||
f2['Front'] = " "
|
||||
assert f2.dupeOrEmpty()
|
||||
|
||||
def test_fieldChecksum():
|
||||
deck = getEmptyDeck()
|
||||
|
@ -99,31 +86,12 @@ def test_fieldChecksum():
|
|||
f['Front'] = u"new"; f['Back'] = u"new2"
|
||||
deck.addNote(f)
|
||||
assert deck.db.scalar(
|
||||
"select csum from nsums") == int("c2a6b03f", 16)
|
||||
# empty field should have no checksum
|
||||
f['Front'] = u""
|
||||
f.flush()
|
||||
assert deck.db.scalar(
|
||||
"select count() from nsums") == 0
|
||||
"select csum from notes") == int("c2a6b03f", 16)
|
||||
# changing the val should change the checksum
|
||||
f['Front'] = u"newx"
|
||||
f.flush()
|
||||
assert deck.db.scalar(
|
||||
"select csum from nsums") == int("302811ae", 16)
|
||||
# turning off unique and modifying the note should delete the sum
|
||||
m = f.model()
|
||||
m['flds'][0]['uniq'] = False
|
||||
deck.models.save(m)
|
||||
f.flush()
|
||||
assert deck.db.scalar(
|
||||
"select count() from nsums") == 0
|
||||
# and turning on both should ensure two checksums generated
|
||||
m['flds'][0]['uniq'] = True
|
||||
m['flds'][1]['uniq'] = True
|
||||
deck.models.save(m)
|
||||
f.flush()
|
||||
assert deck.db.scalar(
|
||||
"select count() from nsums") == 2
|
||||
"select csum from notes") == int("302811ae", 16)
|
||||
|
||||
def test_selective():
|
||||
deck = getEmptyDeck()
|
||||
|
|
|
@ -62,7 +62,7 @@ def test_changedSchema():
|
|||
def test_sync():
|
||||
def check(num):
|
||||
for d in deck1, deck2:
|
||||
for t in ("revlog", "notes", "cards", "nsums"):
|
||||
for t in ("revlog", "notes", "cards"):
|
||||
assert d.db.scalar("select count() from %s" % t) == num
|
||||
assert len(d.models.all()) == num*2
|
||||
# the default deck and config have an id of 1, so always 1
|
||||
|
|
Loading…
Reference in a new issue