-
Notifications
You must be signed in to change notification settings - Fork 146
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
Add deck and note options. #12
base: main
Are you sure you want to change the base?
Changes from 11 commits
28766a3
eab7246
685d962
ebacc3c
cd261da
6eb44fa
96606ec
0fa1870
a5a8d00
4d0449a
61b18b5
bda7097
8286fbc
0a13191
d9fd190
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,8 @@ | ||
from cached_property import cached_property | ||
from copy import copy | ||
import json | ||
from datetime import datetime | ||
import hashlib | ||
import json | ||
import os | ||
import pystache | ||
import sqlite3 | ||
|
@@ -155,25 +156,31 @@ class Card: | |
def __init__(self, ord_): | ||
self.ord = ord_ | ||
|
||
def write_to_db(self, cursor, now_ts, deck_id, note_id): | ||
self.interval = 0 | ||
|
||
def write_to_db(self, cursor, now_ts, deck_id, note_id, | ||
stage, queue, due, interval, ease, reps_til_grad): | ||
cursor.execute('INSERT INTO cards VALUES(null,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);', ( | ||
note_id, # nid | ||
deck_id, # did | ||
self.ord, # ord | ||
now_ts, # mod | ||
-1, # usn | ||
0, # type (=0 for non-Cloze) | ||
0, # queue | ||
0, # due | ||
0, # ivl | ||
0, # factor | ||
0, # reps | ||
0, # lapses | ||
0, # left | ||
0, # odue | ||
0, # odid | ||
0, # flags | ||
"", # data | ||
note_id, # nid - note ID | ||
deck_id, # did - deck ID | ||
self.ord, # ord - which card template it corresponds to | ||
now_ts, # mod - modification time as seconds since Unix epoch | ||
-1, # usn - value of -1 indicates need to push to server | ||
stage, # type - 0=new, 1=learning, 2=review | ||
queue, # queue - same as type, but | ||
# -1=suspended, -2=user buried, -3=sched buried | ||
due, # due - new: unused | ||
# learning: due time as integer seconds since Unix epoch | ||
# review: integer days relative to deck creation | ||
interval, # ivl - positive days, negative seconds | ||
ease, # factor - integer ease factor used by SRS, 2500 = 250% | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think all these properties should be attributes of the So the user would do something like this: my_note = Note(...)
...
my_note.cards[0].queue = -1 If you have a use case for setting attributes on all cards, you can add a helper function like this:
Note: you can skip adding There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Okydoke! I actually planned for and forgot to do this, heh. |
||
0, # reps - number of reviews | ||
0, # lapses - # times card went from "answered correctly" to "answered incorrectly" | ||
reps_til_grad, # left - reps left until graduation | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ditto here. This should be called |
||
0, # odue - only used when card is in filtered deck | ||
0, # odid - only used when card is in filtered deck | ||
0, # flags - currently unused | ||
"", # data - currently unused | ||
)) | ||
|
||
|
||
|
@@ -189,6 +196,29 @@ def __init__(self, model=None, fields=None, sort_field=None, tags=None, guid=Non | |
# guid was defined as a property | ||
pass | ||
|
||
## Options ## | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Per above comment, please move this out of the |
||
|
||
"""SRS learning stage. | ||
0 = new, 1 = learning, 2 = review.""" | ||
self.stage = 0 | ||
"""SRS queue status modifiers. | ||
0 = normal, 1 = suspended, 2 = user buried, 3 = scheduler buried""" | ||
self.status = 0 | ||
"""Behavior depends on learning stage of note. | ||
new: unused. | ||
learning: due time as integer seconds since Unix epoch. | ||
review: integer days relative to deck creation timestamp.""" | ||
self.due = 0 | ||
"""Time between next review and the one following. | ||
Positive values are in days, negative in seconds.""" | ||
self.interval = 0 | ||
"""Integer 'ease' factor used by SRS algorithm. | ||
Example: 2500 = 250%.""" | ||
self.ease = 0 | ||
"""Repititions remaining until graduation from the learning stage. | ||
Unused during other SRS stages.""" | ||
self.reps_til_grad = 0 | ||
|
||
@property | ||
def sort_field(self): | ||
return self._sort_field or self.fields[0] | ||
|
@@ -233,8 +263,11 @@ def write_to_db(self, cursor, now_ts, deck_id): | |
)) | ||
|
||
note_id = cursor.lastrowid | ||
queue = -self.status if self.status else self.stage | ||
for card in self.cards: | ||
card.write_to_db(cursor, now_ts, deck_id, note_id) | ||
card.write_to_db(cursor, now_ts, deck_id, note_id, | ||
self.stage, queue, self.due, self.interval, | ||
self.ease, self.reps_til_grad) | ||
|
||
def _format_fields(self): | ||
return '\x1f'.join(self.fields) | ||
|
@@ -243,12 +276,67 @@ def _format_tags(self): | |
return ' ' + ' '.join(self.tags) + ' ' | ||
|
||
|
||
class OptionsGroup: | ||
def __init__(self, options_id=None, name=None): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd like to follow the pattern of allowing all arguments to be set via keyword arguments to So you should write something like def __init__(
self,
options_id=None,
options_group_name=None,
max_time_per_answer=60,
...
misc=0):
self.options_id = options_id
self.options_group_name = options_group_name
... I realize this is extremely verbose, so you could also go this alternate route: class OptionsGroup:
ATTRIBUTES_WITH_DEFAULTS = {
'options_id': None,
'options_group_name': None,
'max_time_per_answer': 60,
...
}
def __init__(self, **kwargs):
for attr, default in self.ATTRIBUTES_WITH_DEFAULTS.items():
setattr(self, attr, kwargs.get(attr, default)) Either style is fine. However, the first style is advantageous in that it's easier for tools like IDEs to introspect. And it's pretty easy to automatically generate the code so it ends up being about the same amount of work. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will do! |
||
self.options_id = options_id | ||
self.options_group_name = name | ||
# Organized according to options window tabs in Anki. | ||
## General ## | ||
self.max_time_per_answer = 60 # minutes | ||
self.show_timer = False | ||
self.autoplay_audio = True | ||
self.replay_audio_for_answer = True | ||
## New Cards ## | ||
self.new_steps = [1, 10] # list of minute intervals per learning stage | ||
self.order = 1 # option selected in dropdown (0 = first, 1 = second) | ||
self.new_cards_per_day = 20 # days | ||
self.graduating_interval = 1 # days | ||
self.easy_interval = 4 # days | ||
self.starting_ease = 2500 # 2500 = 250% | ||
self.bury_related_new_cards = True | ||
## Reviews ## | ||
self.max_reviews_per_day = 100 | ||
self.easy_bonus = 1.3 | ||
self.interval_modifier = 1.0 | ||
self.max_interval = 36500 # days | ||
self.bury_related_review_cards = True | ||
## Lapses ## | ||
self.lapse_steps = [10] | ||
self.leech_interval_multiplier = 0 | ||
self.lapse_min_interval = 1 | ||
self.leech_threshold = 8 | ||
self.leech_action = 0 | ||
|
||
# Used for adding arbitrary options via JSON string. Useful for | ||
# addons. | ||
self.misc = '' | ||
|
||
def validate(self): | ||
if self.misc and self.misc[-1] != ',': | ||
self.misc += ',' | ||
|
||
def _format_fields(self): | ||
self.validate() | ||
fields = {} | ||
for key, value in self.__dict__.items(): | ||
if key.startswith('__') or callable(key): | ||
continue | ||
if type(value) is bool: | ||
fields[key] = str(value).lower() | ||
else: | ||
fields[key] = str(value) | ||
return fields | ||
|
||
|
||
class Deck: | ||
def __init__(self, deck_id=None, name=None): | ||
def __init__(self, deck_id=None, name=None, options=None): | ||
self.deck_id = deck_id | ||
self.name = name | ||
self.description = '' | ||
self.creation_time = datetime.now() | ||
self.notes = [] | ||
self.models = {} # map of model id to model | ||
self.options = options or OptionsGroup() | ||
|
||
def add_note(self, note): | ||
self.notes.append(note) | ||
|
@@ -261,7 +349,18 @@ def write_to_db(self, cursor, now_ts): | |
self.add_model(note.model) | ||
models = {model.model_id: model.to_json(now_ts, self.deck_id) for model in self.models.values()} | ||
|
||
cursor.execute(APKG_COL, [self.name, self.deck_id, json.dumps(models)]) | ||
params = self.options._format_fields() | ||
|
||
params.update({ | ||
'creation_time': int(self.creation_time.timestamp()), | ||
'modification_time': int(self.creation_time.timestamp()) * 1000, | ||
'name': self.name, | ||
'deck_id': self.deck_id, | ||
'models': json.dumps(models), | ||
'description': self.description, | ||
}) | ||
|
||
cursor.execute(APKG_COL, params) | ||
|
||
for note in self.notes: | ||
note.write_to_db(cursor, now_ts, self.deck_id) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be called
type
. In general, we should always use the same variable naming that Anki does. If Anki's naming is confusing (which it often is), detailed comments will help make the field's purpose clear to the user.Ditto for all the other properties here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you mean just within this function, or in the user-facing API as well?
If the latter, I think of this as 'spreading the disease', where one unintuitive interface gives rise to another, but I can understand wanting a close correspondence for debugging. I doubt many users will be familiar with the Anki DB column names, so I don't believe it will make the lib easier to learn. So...I'll make the change, but I'll feel dirty, lol.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, I remember now why I didn't use
type
for a variable name: it's a Python reserved keyword. Perhaps we should make an exception in this case?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's better to closely match Anki's naming. I want
genanki
to be a lower-level library that more-or-less directly maps to the way Anki represents data. Plus, the user will probably have to end up learning the Anki names anyway, for example if they're digging into thecollections.anki2
SQLite file to debug something. Abstractions are leaky more often than people expect.I's OK to use
type
as the name. If you need to access the realtype()
function, you can import thebuiltins
module and callbuiltins.type
. (Previously I also had an instance of this, calling a paramord_
instead oford
. But I changed it because it's silly to convolute your external API for the convenience of internal code, and I wasn't usingord()
anyway).