-
Notifications
You must be signed in to change notification settings - Fork 158
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?
Conversation
* Switch to named placeholders in SQL commands for sqlite3 to maintain readability as number of deck options increases. * Add lots of deck options which are maintained through new OptionsGroup class. The OptionsGroup encapsulates the corresponding concept in Anki so multiple decks can share the same set of options.
* A correct deck creation time is necessary since card due dates are defined relative to it.
* Anki does not use a consistent nomenclature for the SRS stages. 'Type' cannot be used like it is in the database because that is a reserved word in Python. 'Level' could be taken as a generic measure, like 'interval' is for due days. It is hard(er) to mistake 'stage' for any of the other parameters.
Thanks for the PR! I don't have time to look at it right now but will try to get to it in the next two days. |
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.
Looks pretty good overall, thanks for taking this on!
genanki/__init__.py
Outdated
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 |
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 the collections.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 real type()
function, you can import the builtins
module and call builtins.type
. (Previously I also had an instance of this, calling a param ord_
instead of ord
. But I changed it because it's silly to convolute your external API for the convenience of internal code, and I wasn't using ord()
anyway).
genanki/__init__.py
Outdated
# 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 comment
The reason will be displayed to describe this comment to others. Learn more.
I think all these properties should be attributes of the Card
object, not the Note
object, since you can set them per-card.
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:
def set_attributes_on_call_cards(self, **kwargs):
for card in self.cards:
for k, v in kwargs.items():
setattr(card, k, v)
Note: you can skip adding ease
, ivl
, etc as keyword arguments to Card.__init__
. Unlike Note
and Deck
, the user generally doesn't directly initialize Card
instances; instead, they're implicitly initialized when the user creates the Note
. So it doesn't make sense to set Card
attributes via keyword arguments.
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.
Okydoke! I actually planned for and forgot to do this, heh.
genanki/__init__.py
Outdated
ease, # factor - integer ease factor used by SRS, 2500 = 250% | ||
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 comment
The reason will be displayed to describe this comment to others. Learn more.
Ditto here. This should be called left
. If you want to, you can add a @property
that aliases reps_til_grad
to left
. But the main name of the property should be left
.
@@ -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 comment
The reason will be displayed to describe this comment to others. Learn more.
Per above comment, please move this out of the Note
class and into the Card
class.
genanki/__init__.py
Outdated
@@ -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 comment
The 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 __init__
.
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 comment
The reason will be displayed to describe this comment to others. Learn more.
Will do!
I think I made all the requested changes. I moved the card options to a new I also added |
genanki/__init__.py
Outdated
class Card: | ||
def __init__(self, ord): | ||
def __init__(self, ord, options=None): |
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.
With this options
object, you can't directly set properties on the Card
. For example, you can't do my_card.type = 1
, you have to do my_card.options.type = 1
, which seems like an unnecessary level of indirection and doesn't mesh with how other objects (Note
s and Deck
s) work. Can you make these direct properties of Card
and get rid of the CardOptions
class?
To achieve what you're trying to do with CardOptions
, you can write a helper function (in your own code) like
def set_card_attributes(card, attrs):
for a in attr:
setattr(card, a, attrs[a])
and then write code like
my_attrs = {'type': 2, 'due': 1234}
for note in notes:
for card in note.cards:
set_card_attributes(card, my_attrs)
d41d693
to
dfb7cdd
Compare
Okydoke, removed |
dfb7cdd
to
0a13191
Compare
Very old bump, but is this something that can be done nowadays even without these changes? |
Adds options to decks and notes. This fulfills #11 and then some.
OptionsGroup
class which allows multiple decks to share options, reflecting Anki's very similar option groups.OptionsGroup
is closely modeled after the deck options window in Anki.description
andcreation_time
. The latter is important since card due dates are defined relative to deck creation time.akpg_col.py
now has field substitution by dict rather than list. This was necessary due to the large number of options being substituted. I didn't useformat
though!I tried to stick to the coding style that already existed. Hopefully my blasphemies were kept at a minimum. X-D
Edit: Currently investigating test failures. I have succesfully created and imported decks into Anki 2.0.47 and 2.1 beta 12, so curious why it is failing in test...
Edit 2: Found the problem.
OptionsGroup
requires a unique ID, which means the OptionsGroup ID must be manually set same as for the deck ID. Could generate an ID from the name of the OptionsGroup, but if users create generic option group names this could lead to conflicts. I updated tests accordingly by adding options groups to test decks. Now all tests pass.