Anki/anki/facts.py
Damien Elmes 0c9672e7b8 rewrite media support
- media is no longer hashed, and instead stored in the db using its original
  name
- when adding media, its checksum is calculated and used to look for
  duplicates
- duplicate filenames will result in a number tacked on the file
- the size column is used to count card references to media. If media is
  referenced in a fact but not the question or answer, the count will be zero.
- there is no guarantee media will be listed in the media db if it is unused
  on the question & answer
- if rebuildMediaDir(delete=True), then entries with zero references are
  deleted, along with any unused files in the media dir.
- rebuildMediaDir() will update the internal checksums, and set the checksum
  to "" if a file can't be found
- rebuildMediaDir() is a lot less destructive now, and will leave alone
  directories it finds in the media folder (but not look in them either)
- rebuildMediaDir() returns more information about the state of media now
- the online and mobile clients will need to to make sure that when
  downloading media, entries with no checksum are non-fatal and should not
  abort the download process.
- the ref count is updated every time the q/a is updated - so the db should be
  up to date after every add/edit/import
- since we look for media on the q/a now, card templates like '<img
  src="{{{field}}}">' will work now
- export original files as gone as it is not needed anymore
- move from per-model media URL to deckVar. downloadMissingMedia() uses this
  now. Deck subscriptions will have to be updated to share media another way.
- pass deck in formatQA, as latex support is going to change
2010-12-11 01:19:31 +09:00

149 lines
4.7 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
"""\
Facts
========
"""
__docformat__ = 'restructuredtext'
import time
from anki.db import *
from anki.errors import *
from anki.models import Model, FieldModel, fieldModelsTable, formatQA
from anki.utils import genID, stripHTMLMedia
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))
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
##########################################################################
# mapped in cards.py
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""),
# spaceUntil is reused as a html-stripped cache of the fields
Column('spaceUntil', UnicodeText, nullable=False, default=u""),
# obsolete
Column('lastCardId', Integer, ForeignKey(
"cards.id", use_alter=True, name="lastCardIdfk")))
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:
[f for f in self.fields if f.name == key][0].value = value
except IndexError:
raise KeyError
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")
if field.id:
req += " and id != %s" % field.id
return not s.scalar(req, val=field.value, fmid=field.fieldModel.id)
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:
assert deck
self.spaceUntil = stripHTMLMedia(u" ".join(
self.values()))
for card in self.cards:
card.rebuildQA(deck)
# Fact deletions
##########################################################################
factsDeletedTable = Table(
'factsDeleted', metadata,
Column('factId', Integer, ForeignKey("facts.id"),
nullable=False),
Column('deletedTime', Float, nullable=False))