mirror of
https://github.com/ankitects/anki.git
synced 2025-09-23 08:22:24 -04:00

Cards had developed quite a lot of cruft from incremental changes, and a number of important attributes were stored in names that had no bearing to their actual use. Added: - position, which new cards will be sorted on in the future - flags, which is reserved for future use Renamed: - type to queue - relativeDelay to type - noCount to lapses Removed: - all new/young/matureEase counts; the information is in the revlog - firstAnswered, lastDue, lastFactor, averageTime and totalTime for the same reason - isDue, spaceUntil and combinedDue, because they are no longer used. Spaced cards will be implemented differently in a coming commit. - priority - yesCount, because it can be inferred from reps & lapses - tags; they've been stored in facts for a long time now Also compatibility with deck versions less than 65 has been dropped, so decks will need to be upgraded to 1.2 before they can be upgraded by the dev code. All shared decks are on 1.2, so this should hopefully not be a problem.
205 lines
6.5 KiB
Python
205 lines
6.5 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, sys, math, random
|
|
from anki.db import *
|
|
from anki.models import CardModel, Model, FieldModel, formatQA
|
|
from anki.facts import Fact, factsTable, Field
|
|
from anki.utils import parseTags, findTag, stripHTML, genID, hexifyID
|
|
from anki.media import updateMediaCount, mediaFiles
|
|
|
|
MAX_TIMER = 60
|
|
|
|
# Cards
|
|
##########################################################################
|
|
|
|
# Type: 0=lapsed, 1=due, 2=new, 3=drilled
|
|
# Queue: under normal circumstances, same as type.
|
|
# -1=suspended, -2=user buried, -3=sched buried (rev early, etc)
|
|
# Ordinal: card template # for fact
|
|
# Position: sorting position, only for new cards
|
|
# Flags: unused; reserved for future use
|
|
|
|
cardsTable = Table(
|
|
'cards', metadata,
|
|
Column('id', Integer, primary_key=True),
|
|
Column('factId', Integer, ForeignKey("facts.id"), nullable=False),
|
|
Column('cardModelId', Integer, ForeignKey("cardModels.id"), nullable=False),
|
|
# general
|
|
Column('created', Float, nullable=False, default=time.time),
|
|
Column('modified', Float, nullable=False, default=time.time),
|
|
Column('question', UnicodeText, nullable=False, default=u""),
|
|
Column('answer', UnicodeText, nullable=False, default=u""),
|
|
Column('flags', Integer, nullable=False, default=0),
|
|
# ordering
|
|
Column('ordinal', Integer, nullable=False),
|
|
Column('position', Integer, nullable=False),
|
|
# scheduling data
|
|
Column('type', Integer, nullable=False, default=2),
|
|
Column('queue', Integer, nullable=False, default=2),
|
|
Column('lastInterval', Float, nullable=False, default=0),
|
|
Column('interval', Float, nullable=False, default=0),
|
|
Column('due', Float, nullable=False),
|
|
Column('factor', Float, nullable=False, default=2.5),
|
|
# counters
|
|
Column('reps', Integer, nullable=False, default=0),
|
|
Column('successive', Integer, nullable=False, default=0),
|
|
Column('lapses', Integer, nullable=False, default=0))
|
|
|
|
class Card(object):
|
|
|
|
def __init__(self, fact=None, cardModel=None, created=None):
|
|
self.id = genID()
|
|
self.modified = time.time()
|
|
if created:
|
|
self.created = created
|
|
self.due = created
|
|
else:
|
|
self.due = self.modified
|
|
self.position = self.due
|
|
if fact:
|
|
self.fact = fact
|
|
if cardModel:
|
|
self.cardModel = cardModel
|
|
# for non-orm use
|
|
self.cardModelId = cardModel.id
|
|
self.ordinal = cardModel.ordinal
|
|
# timer
|
|
self.timerStarted = None
|
|
|
|
def setModified(self):
|
|
self.modified = time.time()
|
|
|
|
def startTimer(self):
|
|
self.timerStarted = time.time()
|
|
|
|
def userTime(self):
|
|
return min(time.time() - self.timerStarted, MAX_TIMER)
|
|
|
|
# Questions and answers
|
|
##########################################################################
|
|
|
|
def rebuildQA(self, deck, media=True):
|
|
# format qa
|
|
d = {}
|
|
for f in self.fact.model.fieldModels:
|
|
d[f.name] = (f.id, self.fact[f.name])
|
|
qa = formatQA(None, self.fact.modelId, d, self._splitTags(),
|
|
self.cardModel, deck)
|
|
# find old media references
|
|
files = {}
|
|
for type in ("question", "answer"):
|
|
for f in mediaFiles(getattr(self, type) or ""):
|
|
if f in files:
|
|
files[f] -= 1
|
|
else:
|
|
files[f] = -1
|
|
# update q/a
|
|
self.question = qa['question']
|
|
self.answer = qa['answer']
|
|
# determine media delta
|
|
for type in ("question", "answer"):
|
|
for f in mediaFiles(getattr(self, type)):
|
|
if f in files:
|
|
files[f] += 1
|
|
else:
|
|
files[f] = 1
|
|
# update media counts if we're attached to deck
|
|
if media:
|
|
for (f, cnt) in files.items():
|
|
updateMediaCount(deck, f, cnt)
|
|
self.setModified()
|
|
|
|
def htmlQuestion(self, type="question", align=True):
|
|
div = '''<div class="card%s" id="cm%s%s">%s</div>''' % (
|
|
type[0], type[0], hexifyID(self.cardModelId),
|
|
getattr(self, type))
|
|
# add outer div & alignment (with tables due to qt's html handling)
|
|
if not align:
|
|
return div
|
|
attr = type + 'Align'
|
|
if getattr(self.cardModel, attr) == 0:
|
|
align = "center"
|
|
elif getattr(self.cardModel, attr) == 1:
|
|
align = "left"
|
|
else:
|
|
align = "right"
|
|
return (("<center><table width=95%%><tr><td align=%s>" % align) +
|
|
div + "</td></tr></table></center>")
|
|
|
|
def htmlAnswer(self, align=True):
|
|
return self.htmlQuestion(type="answer", align=align)
|
|
|
|
def _splitTags(self):
|
|
return (self.fact.tags, self.fact.model.tags, self.cardModel.name)
|
|
|
|
# Non-ORM
|
|
##########################################################################
|
|
|
|
def fromDB(self, s, id):
|
|
r = s.first("""select * from cards where id = :id""", id=id)
|
|
if not r:
|
|
return
|
|
(self.id,
|
|
self.factId,
|
|
self.cardModelId,
|
|
self.created,
|
|
self.modified,
|
|
self.question,
|
|
self.answer,
|
|
self.flags,
|
|
self.ordinal,
|
|
self.position,
|
|
self.type,
|
|
self.queue,
|
|
self.lastInterval,
|
|
self.interval,
|
|
self.due,
|
|
self.factor,
|
|
self.reps,
|
|
self.successive,
|
|
self.lapses) = r
|
|
return True
|
|
|
|
def toDB(self, s):
|
|
s.execute("""update cards set
|
|
factId=:factId,
|
|
cardModelId=:cardModelId,
|
|
created=:created,
|
|
modified=:modified,
|
|
question=:question,
|
|
answer=:answer,
|
|
flags=:flags,
|
|
ordinal=:ordinal,
|
|
position=:position,
|
|
type=:type,
|
|
queue=:queue,
|
|
lastInterval=:lastInterval,
|
|
interval=:interval,
|
|
due=:due,
|
|
factor=:factor,
|
|
reps=:reps,
|
|
successive=:successive,
|
|
lapses=:lapses
|
|
where id=:id""", self.__dict__)
|
|
|
|
mapper(Card, cardsTable, properties={
|
|
'cardModel': relation(CardModel),
|
|
'fact': relation(Fact, backref="cards", primaryjoin=
|
|
cardsTable.c.factId == factsTable.c.id),
|
|
})
|
|
|
|
mapper(Fact, factsTable, properties={
|
|
'model': relation(Model),
|
|
'fields': relation(Field, backref="fact", order_by=Field.ordinal),
|
|
})
|
|
|
|
# Card deletions
|
|
##########################################################################
|
|
|
|
cardsDeletedTable = Table(
|
|
'cardsDeleted', metadata,
|
|
Column('cardId', Integer, ForeignKey("cards.id"),
|
|
nullable=False),
|
|
Column('deletedTime', Float, nullable=False))
|