Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Assign card dues per individual card and not just note level #123

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
18 changes: 16 additions & 2 deletions genanki/note.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import re
import warnings
from typing import Union, List

from cached_property import cached_property

from .builtin_models import _fix_deprecated_builtin_models_and_warn
Expand Down Expand Up @@ -50,7 +52,7 @@ def insert(self, i, tag):
class Note:
_INVALID_HTML_TAG_RE = re.compile(r'<(?!/?[a-zA-Z0-9]+(?: .*|/?)>|!--|!\[CDATA\[)(?:.|\n)*?>')

def __init__(self, model=None, fields=None, sort_field=None, tags=None, guid=None, due=0):
def __init__(self, model=None, fields=None, sort_field=None, tags=None, guid=None, due: Union[int, List[int]] = 0):
self.model = model
self.fields = fields
self.sort_field = sort_field
Expand Down Expand Up @@ -167,15 +169,27 @@ def write_to_db(self, cursor, timestamp: float, deck_id, id_gen):
))

note_id = cursor.lastrowid
nr_cards = len(self.cards)
card_dues_by_ord = self._get_due_by_ord(nr_cards)
for card in self.cards:
card.write_to_db(cursor, timestamp, deck_id, note_id, id_gen, self.due)
card_due = card_dues_by_ord[card.ord]
card.write_to_db(cursor, timestamp, deck_id, note_id, id_gen, card_due)

def _format_fields(self):
return '\x1f'.join(self.fields)

def _format_tags(self):
return ' ' + ' '.join(self.tags) + ' '

def _get_due_by_ord(self, nr_cards: int) -> List[int]:
if not isinstance(self.due, list):
return [self.due for _ in range(nr_cards)]

if nr_cards != len(self.due):
raise ValueError(f"Expected self.due to have length {nr_cards + 1}")

return self.due

def __repr__(self):
attrs = ['model', 'fields', 'sort_field', 'tags', 'guid']
pieces = ['{}={}'.format(attr, repr(getattr(self, attr))) for attr in attrs]
Expand Down
113 changes: 113 additions & 0 deletions tests/test_genanki.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,34 @@
sort_field_index=CUSTOM_SORT_FIELD_INDEX,
)

TEST_MODEL_WITH_2_TEMPLATES = genanki.Model(
234532, 'foomodel',
fields=[
{
'name': 'AField',
},
{
'name': 'BField',
},
],
templates=[
{
'name': 'card1',
'qfmt': '{{AField}}',
'afmt': '{{FrontSide}}'
'<hr id="answer">'
'{{BField}}',
},
{
'name': 'card2',
'qfmt': '{{BField}}',
'afmt': '{{FrontSide}}'
'<hr id="answer">'
'{{AField}}',
}
],
)

# VALID_MP3 and VALID_JPG courtesy of https://github.com/mathiasbynens/small
VALID_MP3 = (
b'\xff\xe3\x18\xc4\x00\x00\x00\x03H\x00\x00\x00\x00LAME3.98.2\x00\x00\x00'
Expand Down Expand Up @@ -476,3 +504,88 @@ def test_notes_with_due2(self):
# Next card changes to "Capital of Oregon", because it has lower
# due value.
assert next_note.fields == ['Capital of Oregon', 'Salem']

def test_notes_with_default_due(self):
deck = genanki.Deck(4145273928, 'foodeck')
deck.add_note(genanki.Note(
TEST_MODEL,
['Capital of Washington', 'Olympia']))
deck.add_note(genanki.Note(
TEST_MODEL,
['Capital of Oregon', 'Salem']))

self.import_package(genanki.Package(deck))

self.col.decks.select(self.col.decks.id('foodeck'))
self.col.sched.reset()
next_card = self.col.sched.getCard()
next_note = self.col.getNote(next_card.nid)

# Next card is "Capital of Washington", because it was created first.
assert next_note.fields == ['Capital of Washington', 'Olympia']

def test_notes_with_card_and_note_due(self):
deck = genanki.Deck(4145273929, 'foodeck')
deck.add_note(genanki.Note(
TEST_MODEL_WITH_2_TEMPLATES,
['D', 'E'], due=2))
deck.add_note(genanki.Note(
TEST_MODEL_WITH_2_TEMPLATES,
['B', 'C'], due=1))
deck.add_note(genanki.Note(
TEST_MODEL_WITH_2_TEMPLATES,
['A', 'F'], due=[0, 3]))

self.import_package(genanki.Package(deck))

self.col.decks.select(self.col.decks.id('foodeck'))
self.col.sched.reset()

# 6 cards in total
cards = [
self.col.sched.getCard(),
self.col.sched.getCard(),
self.col.sched.getCard(),
self.col.sched.getCard(),
self.col.sched.getCard(),
self.col.sched.getCard(),
]

note0 = self.col.getNote(cards[0].nid)
note1 = self.col.getNote(cards[1].nid)
note2 = self.col.getNote(cards[2].nid)
note3 = self.col.getNote(cards[3].nid)
note4 = self.col.getNote(cards[4].nid)
note5 = self.col.getNote(cards[5].nid)

assert note0.fields == ['A', 'F']
assert note1.fields == ['B', 'C']
assert note2.fields == ['B', 'C']
assert note3.fields == ['D', 'E']
assert note4.fields == ['D', 'E']
assert note5.fields == ['A', 'F']

def test_notes_raises_when_card_due_mismatch1(self):
# Empty.
with pytest.raises(ValueError):
deck = genanki.Deck(4145273929, 'foodeck')
deck.add_note(genanki.Note(
TEST_MODEL_WITH_2_TEMPLATES,
['D', 'E'], due=[]))
self.import_package(genanki.Package(deck))

# Missing one.
with pytest.raises(ValueError):
deck = genanki.Deck(4145273929, 'foodeck')
deck.add_note(genanki.Note(
TEST_MODEL_WITH_2_TEMPLATES,
['D', 'E'], due=[0]))
self.import_package(genanki.Package(deck))

# More than one.
with pytest.raises(ValueError):
deck = genanki.Deck(4145273929, 'foodeck')
deck.add_note(genanki.Note(
TEST_MODEL_WITH_2_TEMPLATES,
['D', 'E'], due=[0, 0, 0]))
self.import_package(genanki.Package(deck))