mirror of
https://github.com/ankitects/anki.git
synced 2025-09-20 06:52:21 -04:00

Users who want to study small subsections at one time (eg, "lesson 14") are currently best served by creating lots of little decks. This is because: - selective study is a bit cumbersome to switch between - the graphs and statitics are for the entire deck - selective study can be slow on mobile devices - when the list of cards to hide/show is big, or when there are many due cards, performance can suffer - scheduling can only be configured per deck Groups are intended to address the above problems. All cards start off in the same group, but they can have their group changed. Unlike tags, cards can only be a member of a single group at once time. This allows us to divide the deck up into a non-overlapping set of cards, which will make things like showing due counts for a single category considerably cheaper. The user interface might want to show something like a deck browser for decks that have more than one group, showing due counts and allowing people to study each group individually, or to study all at once. Instead of storing the scheduling config in the deck or the model, we move the scheduling into a separate config table, and link that to the groups table. That way a user can have multiple groups that all share the same scheduling information if they want. And deletion tracking is now in a single table.
147 lines
4.9 KiB
Python
147 lines
4.9 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Copyright: Damien Elmes <anki@ichi2.net>
|
|
# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html
|
|
|
|
import time
|
|
from anki.db import *
|
|
from anki.errors import *
|
|
from anki.models import Model, FieldModel, fieldModelsTable
|
|
from anki.utils import genID, stripHTMLMedia, fieldChecksum
|
|
from anki.hooks import runHook
|
|
|
|
# Fields in a fact
|
|
##########################################################################
|
|
|
|
fieldsTable = Table(
|
|
'fields', metadata,
|
|
Column('id', Integer, primary_key=True),
|
|
Column('factId', Integer, ForeignKey("facts.id"), nullable=False),
|
|
Column('fieldModelId', Integer, ForeignKey("fieldModels.id"),
|
|
nullable=False),
|
|
Column('ordinal', Integer, nullable=False),
|
|
Column('value', UnicodeText, nullable=False),
|
|
Column('chksum', String, nullable=False, default=""))
|
|
|
|
class Field(object):
|
|
"A field in a fact."
|
|
|
|
def __init__(self, fieldModel=None):
|
|
if fieldModel:
|
|
self.fieldModel = fieldModel
|
|
self.ordinal = fieldModel.ordinal
|
|
self.value = u""
|
|
self.id = genID()
|
|
|
|
def getName(self):
|
|
return self.fieldModel.name
|
|
name = property(getName)
|
|
|
|
mapper(Field, fieldsTable, properties={
|
|
'fieldModel': relation(FieldModel)
|
|
})
|
|
|
|
# Facts: a set of fields and a model
|
|
##########################################################################
|
|
|
|
# Cache: a HTML-stripped amalgam of the field contents, so we can perform
|
|
# searches of marked up text in a reasonable time.
|
|
|
|
factsTable = Table(
|
|
'facts', metadata,
|
|
Column('id', Integer, primary_key=True),
|
|
Column('modelId', Integer, ForeignKey("models.id"), nullable=False),
|
|
Column('created', Float, nullable=False, default=time.time),
|
|
Column('modified', Float, nullable=False, default=time.time),
|
|
Column('tags', UnicodeText, nullable=False, default=u""),
|
|
Column('cache', UnicodeText, nullable=False, default=u""))
|
|
|
|
class Fact(object):
|
|
"A single fact. Fields exposed as dict interface."
|
|
|
|
def __init__(self, model=None):
|
|
self.model = model
|
|
self.id = genID()
|
|
if model:
|
|
for fm in model.fieldModels:
|
|
self.fields.append(Field(fm))
|
|
self.new = True
|
|
|
|
def isNew(self):
|
|
return getattr(self, 'new', False)
|
|
|
|
def keys(self):
|
|
return [field.name for field in self.fields]
|
|
|
|
def values(self):
|
|
return [field.value for field in self.fields]
|
|
|
|
def __getitem__(self, key):
|
|
try:
|
|
return [f.value for f in self.fields if f.name == key][0]
|
|
except IndexError:
|
|
raise KeyError(key)
|
|
|
|
def __setitem__(self, key, value):
|
|
try:
|
|
item = [f for f in self.fields if f.name == key][0]
|
|
except IndexError:
|
|
raise KeyError
|
|
item.value = value
|
|
if item.fieldModel.unique:
|
|
item.chksum = fieldChecksum(value)
|
|
else:
|
|
item.chksum = ""
|
|
|
|
def get(self, key, default):
|
|
try:
|
|
return self[key]
|
|
except (IndexError, KeyError):
|
|
return default
|
|
|
|
def assertValid(self):
|
|
"Raise an error if required fields are empty."
|
|
for field in self.fields:
|
|
if not self.fieldValid(field):
|
|
raise FactInvalidError(type="fieldEmpty",
|
|
field=field.name)
|
|
|
|
def fieldValid(self, field):
|
|
return not (field.fieldModel.required and not field.value.strip())
|
|
|
|
def assertUnique(self, s):
|
|
"Raise an error if duplicate fields are found."
|
|
for field in self.fields:
|
|
if not self.fieldUnique(field, s):
|
|
raise FactInvalidError(type="fieldNotUnique",
|
|
field=field.name)
|
|
|
|
def fieldUnique(self, field, s):
|
|
if not field.fieldModel.unique:
|
|
return True
|
|
req = ("select value from fields "
|
|
"where fieldModelId = :fmid and value = :val and chksum = :chk")
|
|
if field.id:
|
|
req += " and id != %s" % field.id
|
|
return not s.scalar(req, val=field.value, fmid=field.fieldModel.id,
|
|
chk=fieldChecksum(field.value))
|
|
|
|
def focusLost(self, field):
|
|
runHook('fact.focusLost', self, field)
|
|
|
|
def setModified(self, textChanged=False, deck=None, media=True):
|
|
"Mark modified and update cards."
|
|
self.modified = time.time()
|
|
if textChanged:
|
|
if not deck:
|
|
# FIXME: compat code
|
|
import ankiqt
|
|
if not getattr(ankiqt, 'setModWarningShown', None):
|
|
import sys; sys.stderr.write(
|
|
"plugin needs to pass deck to fact.setModified()")
|
|
ankiqt.setModWarningShown = True
|
|
deck = ankiqt.mw.deck
|
|
assert deck
|
|
self.cache = stripHTMLMedia(u" ".join(
|
|
self.values()))
|
|
for card in self.cards:
|
|
card.rebuildQA(deck)
|