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

- removed 'created' column from various tables. We don't care when things like models are created, and card creation time didn't reflect the actual time a card was created - facts were previously ordered by their creation date. The code would manually set the creation time for subsequent facts on import by 0.0001 seconds, and then card due times were set by adding the fact time to the ordinal number*0.000001. This was prone to error, and the number of zeros used was actually different in different parts of the code. Instead of this, we replace it with a 'pos' column on facts, which increments for each new fact. - importing should add new facts with a higher pos, but concurrent updates in a synced deck can have multiple facts with the same pos - due times are completely different now, and depend on the card type - new cards have due=fact.pos or random(0, 10000) - reviews have due set to an integer representing days since deck creation/download - cards in the learn queue use an integer timestamp in seconds - many columns like modified, lastSync, factor, interval, etc have been converted to integer columns. They are cheaper to store (large decks can save 10s of megabytes) and faster to search for. - cards have their group assigned on fact creation. In the future we'll add a per-template option for a default group. - switch to due/random order for the review queue on upgrade. Users can still switch to the old behaviour if they want, but many people don't care what it's set to, and due is considerably faster, which may result in a better user experience
197 lines
7.2 KiB
Python
197 lines
7.2 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, re, simplejson
|
|
from sqlalchemy.ext.orderinglist import ordering_list
|
|
from anki.db import *
|
|
from anki.utils import genID, canonifyTags, intTime
|
|
from anki.fonts import toPlatformFont
|
|
from anki.utils import parseTags, hexifyID, checksum, stripHTML, intTime
|
|
from anki.lang import _
|
|
from anki.hooks import runFilter
|
|
from anki.template import render
|
|
from copy import copy
|
|
|
|
def alignmentLabels():
|
|
return {
|
|
0: _("Center"),
|
|
1: _("Left"),
|
|
2: _("Right"),
|
|
}
|
|
|
|
# Field models
|
|
##########################################################################
|
|
|
|
fieldModelsTable = Table(
|
|
'fieldModels', metadata,
|
|
Column('id', Integer, primary_key=True),
|
|
Column('ordinal', Integer, nullable=False),
|
|
Column('modelId', Integer, ForeignKey('models.id'), nullable=False),
|
|
Column('name', UnicodeText, nullable=False),
|
|
Column('description', UnicodeText, nullable=False, default=u""), # obsolete
|
|
# reused as RTL marker
|
|
Column('features', UnicodeText, nullable=False, default=u""),
|
|
Column('required', Boolean, nullable=False, default=True),
|
|
# if code changes this, it should call deck.updateFieldChecksums()
|
|
Column('unique', Boolean, nullable=False, default=True), # sqlite keyword
|
|
Column('numeric', Boolean, nullable=False, default=False),
|
|
# display
|
|
Column('quizFontFamily', UnicodeText, default=u"Arial"),
|
|
Column('quizFontSize', Integer, default=20),
|
|
Column('quizFontColour', String(7)),
|
|
Column('editFontFamily', UnicodeText, default=u"1"), # reused as <pre> toggle
|
|
Column('editFontSize', Integer, default=20))
|
|
|
|
class FieldModel(object):
|
|
"The definition of one field in a fact."
|
|
|
|
def __init__(self, name=u"", required=True, unique=True):
|
|
self.name = name
|
|
self.required = required
|
|
self.unique = unique
|
|
self.id = genID()
|
|
|
|
def copy(self):
|
|
new = FieldModel()
|
|
for p in class_mapper(FieldModel).iterate_properties:
|
|
setattr(new, p.key, getattr(self, p.key))
|
|
new.id = genID()
|
|
new.model = None
|
|
return new
|
|
|
|
mapper(FieldModel, fieldModelsTable)
|
|
|
|
# Card models
|
|
##########################################################################
|
|
|
|
cardModelsTable = Table(
|
|
'cardModels', metadata,
|
|
Column('id', Integer, primary_key=True),
|
|
Column('ordinal', Integer, nullable=False),
|
|
Column('modelId', Integer, ForeignKey('models.id'), nullable=False),
|
|
Column('name', UnicodeText, nullable=False),
|
|
Column('description', UnicodeText, nullable=False, default=u""), # obsolete
|
|
Column('active', Boolean, nullable=False, default=True),
|
|
# formats: question/answer/last(not used)
|
|
Column('qformat', UnicodeText, nullable=False),
|
|
Column('aformat', UnicodeText, nullable=False),
|
|
Column('lformat', UnicodeText),
|
|
# question/answer editor format (not used yet)
|
|
Column('qedformat', UnicodeText),
|
|
Column('aedformat', UnicodeText),
|
|
Column('questionInAnswer', Boolean, nullable=False, default=False),
|
|
# unused
|
|
Column('questionFontFamily', UnicodeText, default=u"Arial"),
|
|
Column('questionFontSize', Integer, default=20),
|
|
Column('questionFontColour', String(7), default=u"#000000"),
|
|
# used for both question & answer
|
|
Column('questionAlign', Integer, default=0),
|
|
# ununsed
|
|
Column('answerFontFamily', UnicodeText, default=u"Arial"),
|
|
Column('answerFontSize', Integer, default=20),
|
|
Column('answerFontColour', String(7), default=u"#000000"),
|
|
Column('answerAlign', Integer, default=0),
|
|
Column('lastFontFamily', UnicodeText, default=u"Arial"),
|
|
Column('lastFontSize', Integer, default=20),
|
|
# used as background colour
|
|
Column('lastFontColour', String(7), default=u"#FFFFFF"),
|
|
Column('editQuestionFontFamily', UnicodeText, default=None),
|
|
Column('editQuestionFontSize', Integer, default=None),
|
|
Column('editAnswerFontFamily', UnicodeText, default=None),
|
|
Column('editAnswerFontSize', Integer, default=None),
|
|
# empty answer
|
|
Column('allowEmptyAnswer', Boolean, nullable=False, default=True),
|
|
Column('typeAnswer', UnicodeText, nullable=False, default=u""))
|
|
|
|
class CardModel(object):
|
|
"""Represents how to generate the front and back of a card."""
|
|
def __init__(self, name=u"", qformat=u"q", aformat=u"a", active=True):
|
|
self.name = name
|
|
self.qformat = qformat
|
|
self.aformat = aformat
|
|
self.active = active
|
|
self.id = genID()
|
|
|
|
def copy(self):
|
|
new = CardModel()
|
|
for p in class_mapper(CardModel).iterate_properties:
|
|
setattr(new, p.key, getattr(self, p.key))
|
|
new.id = genID()
|
|
new.model = None
|
|
return new
|
|
|
|
mapper(CardModel, cardModelsTable)
|
|
|
|
def formatQA(cid, mid, fact, tags, cm, deck):
|
|
"Return a dict of {id, question, answer}"
|
|
d = {'id': cid}
|
|
fields = {}
|
|
for (k, v) in fact.items():
|
|
fields["text:"+k] = stripHTML(v[1])
|
|
if v[1]:
|
|
fields[k] = '<span class="fm%s">%s</span>' % (
|
|
hexifyID(v[0]), v[1])
|
|
else:
|
|
fields[k] = u""
|
|
fields['tags'] = tags[0]
|
|
fields['Tags'] = tags[0]
|
|
fields['modelTags'] = tags[1]
|
|
fields['cardModel'] = tags[2]
|
|
# render q & a
|
|
ret = []
|
|
for (type, format) in (("question", cm.qformat),
|
|
("answer", cm.aformat)):
|
|
# convert old style
|
|
format = re.sub("%\((.+?)\)s", "{{\\1}}", format)
|
|
# allow custom rendering functions & info
|
|
fields = runFilter("prepareFields", fields, cid, mid, fact, tags, cm, deck)
|
|
html = render(format, fields)
|
|
d[type] = runFilter("formatQA", html, type, cid, mid, fact, tags, cm, deck)
|
|
return d
|
|
|
|
# Model table
|
|
##########################################################################
|
|
|
|
modelsTable = Table(
|
|
'models', metadata,
|
|
Column('id', Integer, primary_key=True),
|
|
Column('modified', Integer, nullable=False, default=intTime),
|
|
Column('name', UnicodeText, nullable=False),
|
|
# currently unused
|
|
Column('config', UnicodeText, nullable=False, default=u"")
|
|
)
|
|
|
|
class Model(object):
|
|
"Defines the way a fact behaves, what fields it can contain, etc."
|
|
def __init__(self, name=u""):
|
|
self.name = name
|
|
self.id = genID()
|
|
|
|
def setModified(self):
|
|
self.modified = intTime()
|
|
|
|
def addFieldModel(self, field):
|
|
"Add a field model. Don't call this directly."
|
|
self.fieldModels.append(field)
|
|
s = object_session(self)
|
|
if s:
|
|
s.flush()
|
|
|
|
def addCardModel(self, card):
|
|
"Add a card model. Don't call this directly."
|
|
self.cardModels.append(card)
|
|
s = object_session(self)
|
|
if s:
|
|
s.flush()
|
|
|
|
mapper(Model, modelsTable, properties={
|
|
'fieldModels': relation(FieldModel, backref='model',
|
|
collection_class=ordering_list('ordinal'),
|
|
order_by=[fieldModelsTable.c.ordinal],
|
|
cascade="all, delete-orphan"),
|
|
'cardModels': relation(CardModel, backref='model',
|
|
collection_class=ordering_list('ordinal'),
|
|
order_by=[cardModelsTable.c.ordinal],
|
|
cascade="all, delete-orphan"),
|
|
})
|