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

Add deck and note options. #12

Open
wants to merge 15 commits into
base: main
Choose a base branch
from

Conversation

holocronweaver
Copy link
Contributor

@holocronweaver holocronweaver commented Aug 24, 2017

Adds options to decks and notes. This fulfills #11 and then some.

  • Most deck options are handled in a new 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.
  • Decks now have a description and creation_time. The latter is important since card due dates are defined relative to deck creation time.
  • Notes now have a few option attributes, all related to SRS in some way (due date, interval, etc.).
  • Add some initial README documentation.
  • Add more internal documentation about database fields.
  • Add internal documentation about options.
  • No tests yet.
  • 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 use format 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.

* 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.
@kerrickstaley
Copy link
Owner

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.

Copy link
Owner

@kerrickstaley kerrickstaley left a 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!

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
Copy link
Owner

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.

Copy link
Contributor Author

@holocronweaver holocronweaver Aug 29, 2017

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.

Copy link
Contributor Author

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?

Copy link
Owner

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).

# 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%
Copy link
Owner

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.

Copy link
Contributor Author

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.

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
Copy link
Owner

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 ##
Copy link
Owner

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.

@@ -243,12 +276,67 @@ def _format_tags(self):
return ' ' + ' '.join(self.tags) + ' '


class OptionsGroup:
def __init__(self, options_id=None, name=None):
Copy link
Owner

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do!

@holocronweaver
Copy link
Contributor Author

I think I made all the requested changes.

I moved the card options to a new CardOptions class. This simplifies sharing the same options among multiple cards, similar to what OptionsGroup does for decks, so it's handy on several levels.

I also added @property for the aliases as you suggested, while leaving the 'real' internal names consistent with the Anki DB field names.

class Card:
def __init__(self, ord):
def __init__(self, ord, options=None):
Copy link
Owner

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 (Notes and Decks) 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)

@holocronweaver
Copy link
Contributor Author

Okydoke, removed CardOptions and now Card has all the attributes. I left a utility function in Note to make the common case of setting all card options at once more concise.

@jontelang
Copy link

Very old bump, but is this something that can be done nowadays even without these changes?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants