-
-
Save eshapard/12c14947d31dfcb8f0915761fb649c32 to your computer and use it in GitHub Desktop.
# Anki Deadline | |
# Anki 2 plugin | |
# Author: EJS | |
# Version 0.1 | |
# Description: Adjusts 'New Cards per Day' setting of options group to ensure all cards | |
# are seen by deadline. | |
# License: GNU GPL v3 <www.gnu.org/licenses/gpl.html> | |
from __future__ import division | |
import datetime, time, math | |
from anki.hooks import wrap, addHook | |
from aqt import * | |
from aqt.main import AnkiQt | |
from anki.utils import intTime | |
deadlines = {} | |
# ADD DEADLINES HERE | |
# One definition for each profile (default profile is User 1) | |
# deadlines['User 1'] = ... for User 1 | |
# deadlines['Tom'] = ... for Tom's profile | |
# deadlines['Jerry'] = ... for Jerry's profile | |
# etc. | |
# | |
# Format: ["OGName", "DeadlineDate"] | |
# OGName = "Options Group Name" | |
# DeadlineDate = last day of studying ("YYYY-MM-DD") | |
# Examples: | |
# deadlines['profile name'] = [ | |
# ["Silly Cards", "2017-01-01"], | |
# ["Options Group 2", "2018-01-01"], | |
# etc... (**no comma afer the last pair**) | |
# ] | |
# | |
# Tip: The whole string must be enclosed within square brackets | |
# and each name/date pair must be enclosed within its own | |
# set of square brackets. INCLUDE a COMMA between deadlines, | |
# but not between the last deadline and the final ] bracket. | |
# | |
# *Deadline date is the *last day of new cards*, not the day after | |
# all new card should be seen. | |
# Format: [["OGName", "YYYY-MM-DD"]] | |
deadlines['User 1'] = [ | |
["Options Group 1 Name", "2018-12-31"], | |
["Options Group 2 Name", "2017-12-31"] | |
] | |
# IF YOU FIND THIS ADDON HELPFUL, PLEASE CONSIDER MAKING A $1 DONATION | |
# USING THIS LINK: https://paypal.me/eshapard/1 | |
# ------------Nothing to edit below--------------------------------# | |
DeadlineMenu = QMenu("Deadline", mw) | |
mw.form.menuTools.addMenu(DeadlineMenu) | |
# count new cards in a deck | |
def new_cards_in_deck(deck_id): | |
new_cards = mw.col.db.scalar("""select | |
count() | |
from cards where | |
type = 0 and | |
queue != -1 and | |
did = ?""", deck_id) | |
return new_cards | |
# Find settings group ID | |
def find_settings_group_id(name): | |
dconf = mw.col.decks.dconf | |
for k in dconf: | |
if dconf[k]['name'] == name: | |
return k | |
return False | |
# Find decks in settings group | |
def find_decks_in_settings_group(group_id): | |
members = [] | |
decks = mw.col.decks.decks | |
for d in decks: | |
if 'conf' in decks[d] and int(decks[d]['conf']) == int(group_id): | |
members.append(d) | |
return members | |
# Count new cards in settings group | |
def new_cards_in_settings_group(name): | |
new_cards = 0 | |
new_today = 0 | |
group_id = find_settings_group_id(name) | |
if group_id: | |
# Find decks and cycle through | |
decks = find_decks_in_settings_group(group_id) | |
for d in decks: | |
new_cards += new_cards_in_deck(d) | |
new_today += first_seen_cards_in_deck(d) | |
return new_cards, new_today | |
# Count cards first seen today | |
def first_seen_cards_in_deck(deck_id): | |
#return mw.col.decks.decks[deck_id]["newToday"][1] #unreliable | |
#a new Anki day starts at 04:00 AM (by default); not midnight | |
dayStartTime = datetime.datetime.fromtimestamp(mw.col.crt).time() | |
midnight = datetime.datetime.combine(datetime.date.today(), dayStartTime) | |
midNight = int(time.mktime(midnight.timetuple()) * 1000) | |
query = ("""select count() from | |
(select r.id as review, c.id as card, c.did as deck | |
from revlog as r, cards as c | |
where r.cid = c.id | |
and r.type = 0 | |
order by c.id, r.id DESC) | |
where deck = %s | |
and review >= %s | |
group by card""" % (deck_id, midNight)) | |
ret = mw.col.db.scalar(query) | |
if not ret: | |
ret = 0 | |
return ret | |
# find days until deadline | |
def days_until_deadline(deadline_date, include_today=True): | |
if not deadline_date: | |
# No deadline date | |
return False | |
date_format = "%Y-%m-%d" | |
today = datetime.datetime.today() | |
deadline_date = datetime.datetime.strptime(deadline_date, date_format) | |
delta = deadline_date - today | |
if include_today: | |
days_left = delta.days + 1 # includes today | |
else: | |
days_left = delta.days # today not included | |
if days_left < 1: | |
days_left = 0 | |
return days_left | |
# calculate cards per day | |
def cards_per_day(new_cards, days_left): | |
if new_cards % days_left == 0: | |
per_day = int(new_cards / days_left) | |
else: | |
per_day = int(new_cards / days_left) + 1 | |
#sanity check | |
if per_day < 0: | |
per_day = 0 | |
return per_day | |
# update new cards per day of a settings group | |
def update_new_cards_per_day(name, per_day): | |
group_id = find_settings_group_id(name) | |
if group_id: | |
if group_id in mw.col.decks.dconf: | |
mw.col.decks.dconf[group_id]["new"]["perDay"] = int(per_day) | |
# utils.showInfo("updating deadlines disabled") | |
mw.col.decks.save(mw.col.decks.dconf[group_id]) | |
#mw.col.decks.flush() | |
# Calc new cards per day | |
def calc_new_cards_per_day(name, days_left, silent=True): | |
new_cards, new_today = new_cards_in_settings_group(name) | |
per_day = cards_per_day((new_cards + new_today), days_left) | |
if not silent: | |
utils.showInfo( | |
"%s\n\nNew cards seen today: %s\nNew cards remaining: %s\nDays left: %s\nNew cards per day: %s" % ( | |
name, new_today, new_cards, days_left, per_day) | |
) | |
update_new_cards_per_day(name, per_day) | |
# Main Function | |
def allDeadlines(silent=True): | |
profile = str(aqt.mw.pm.name) | |
include_today = True # include today in the number of days left | |
if profile in deadlines: | |
for d in deadlines[profile]: | |
name = d[0] | |
# new_cards, new_today = new_cards_in_settings_group(name) | |
days_left = days_until_deadline(d[1], include_today) | |
# Change per_day amount if there's still time | |
# before the deadline | |
if days_left: | |
calc_new_cards_per_day(name, days_left, silent) | |
#Manual Version | |
def manualDeadlines(): | |
allDeadlines(False) | |
manualDeadlineAction = QAction("Process Deadlines", mw) | |
mw.connect(manualDeadlineAction, SIGNAL("triggered()"), manualDeadlines) | |
DeadlineMenu.addAction(manualDeadlineAction) | |
# Add hook to adjust Deadlines on load profile | |
addHook("profileLoaded", allDeadlines) |
Hi Eshapard,
I really find this addon helpful for studying so I updated it to Anki 2.1. I used the pyqt4 converter to do it automatically (I put the diff at the bottom, it's 2 small lines). The other change is that you can no longer just drop the .py
file in the addon directory, it needs to be in its own folder with a __init__.py
file (the docs give a nice way to be 2.0/2.1 compatible but I took the simpler approach).
Thanks!
diff ../deadline.py deadline__2_1.py
9a10
> from PyQt5.QtWidgets import *
192c193
< mw.connect(manualDeadlineAction, SIGNAL("triggered()"), manualDeadlines)
---
> manualDeadlineAction.triggered.connect(manualDeadlines)
I just wanted to let folks know that I've extended this project to be GUI configurable at https://github.com/BSCrumpton/Deadline2 . It can also be found at https://ankiweb.net/shared/info/723639202 .
@eshapard , I used your code as the starting base, and am going to see if there is a way to retroactively show a fork from this original code in my repo.
Settings are adjusted on startup, but numbers on deck display screen will not appear to change unless you hit 'd' or click on one of the decks.
There will also be an option in the tools menu to recalculate the new cards per day manually. This manual option will give you a display of the statistics for each deck option group with an active deadline. Passed deadlines are ignored.
Note: This will only ensure that you see new cards by your deadline; make sure that you plan for extra review time if desired.