Skip to content

Instantly share code, notes, and snippets.

@eshapard
Last active November 30, 2023 19:49
Show Gist options
  • Star 9 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save eshapard/12c14947d31dfcb8f0915761fb649c32 to your computer and use it in GitHub Desktop.
Save eshapard/12c14947d31dfcb8f0915761fb649c32 to your computer and use it in GitHub Desktop.
Anki Deadline
# 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)
@eshapard
Copy link
Author

eshapard commented Jan 9, 2018

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.

@JZL
Copy link

JZL commented May 27, 2019

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)

@BSCrumpton
Copy link

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment