Skip to content

Commit

Permalink
Merge branch 'main' into chore/update-scripts-for-anki-23-10
Browse files Browse the repository at this point in the history
  • Loading branch information
RisingOrange authored Nov 9, 2023
2 parents 652f046 + 5a517a2 commit 09dd1c6
Show file tree
Hide file tree
Showing 5 changed files with 116 additions and 44 deletions.
12 changes: 11 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ on:
jobs:
test-addon:
runs-on: ubuntu-latest
strategy:
matrix:
include:
- replace_anki_version: false
- replace_anki_version: true
anki_package_version: 'aqt[qt5]==2.1.56'
timeout-minutes: 30
steps:
- uses: actions/setup-python@v4
Expand All @@ -30,6 +36,10 @@ jobs:
google_api_key: ${{ secrets.GOOGLE_API_KEY }}
install_qt: true

- name: Replace Anki version
if: ${{ matrix.replace_anki_version }}
run: pip install "${{ matrix.anki_package_version }}"

- name: Run pytest with coverage
run: |
pytest ./tests/addon --cov --cov-report=xml -n 4
Expand All @@ -38,7 +48,7 @@ jobs:
- name: Upload coverage
uses: actions/upload-artifact@v2
with:
name: coverage_addon
name: coverage_addon_${{ matrix.anki_package_version }}
path: .coverage


Expand Down
6 changes: 6 additions & 0 deletions ankihub/ankihub_client/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,13 @@ class DeckExtensionUpdateChunk(DataClassJSONMixinWithConfig):
@dataclass
class CardReviewData(DataClassJSONMixinWithConfig):
ah_did: uuid.UUID = dataclasses.field(metadata=field_options(alias="deck_id"))
total_card_reviews_last_7_days: int
total_card_reviews_last_30_days: int
first_card_review_at: datetime = dataclasses.field(
metadata=field_options(
deserialize=lambda x: datetime.strptime(x, ANKIHUB_DATETIME_FORMAT_STR)
),
)
last_card_review_at: datetime = dataclasses.field(
metadata=field_options(
deserialize=lambda x: datetime.strptime(x, ANKIHUB_DATETIME_FORMAT_STR)
Expand Down
49 changes: 31 additions & 18 deletions ankihub/main/review_data.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import uuid
from datetime import datetime, timedelta
from typing import Optional
from typing import Optional, Tuple

import aqt

Expand All @@ -10,34 +10,40 @@
from ..db import attached_ankihub_db
from ..settings import config

# The server needs the review counts for the last 30 days
REVIEW_PERIOD_DAYS = timedelta(days=30)


def send_review_data() -> None:
"""Send data about card reviews for each installed AnkiHub deck to the server.
Data about decks that have not been reviewed yet will not be included."""
since = datetime.now() - REVIEW_PERIOD_DAYS
now = datetime.now()

with attached_ankihub_db():
card_review_data = []
for ah_did in config.deck_ids():
last_card_review_at = _get_last_review_datetime_for_ah_deck(ah_did)
if last_card_review_at is None:
first_and_last_review_times = (
_get_first_and_last_review_datetime_for_ah_deck(ah_did)
)
if first_and_last_review_times is None:
# This deck has no reviews yet
continue

first_review_at, last_review_at = first_and_last_review_times
total_card_reviews_last_7_days = _get_review_count_for_ah_deck_since(
ah_did, now - timedelta(days=7)
)
total_card_reviews_last_30_days = _get_review_count_for_ah_deck_since(
ah_did, since
ah_did, now - timedelta(days=30)
)
card_review_data.append(
CardReviewData(
ah_did=ah_did,
total_card_reviews_last_7_days=total_card_reviews_last_7_days,
total_card_reviews_last_30_days=total_card_reviews_last_30_days,
last_card_review_at=last_card_review_at,
first_card_review_at=first_review_at,
last_card_review_at=last_review_at,
)
)

LOGGER.info(f"Review counts: {card_review_data}")
LOGGER.info(f"Review data: {card_review_data}")

client = AnkiHubClient()
client.send_card_review_data(card_review_data)
Expand All @@ -61,22 +67,29 @@ def _get_review_count_for_ah_deck_since(ah_did: uuid.UUID, since: datetime) -> i
return result


def _get_last_review_datetime_for_ah_deck(ah_did: uuid.UUID) -> Optional[datetime]:
"""Get the date time of the last review (recorded in Anki's review log table) for an ankihub deck.
def _get_first_and_last_review_datetime_for_ah_deck(
ah_did: uuid.UUID,
) -> Optional[Tuple[datetime, datetime]]:
"""Get the date time of the first and last review (recorded in Anki's review log table) for an ankihub deck.
Requires the ankihub db to be attached to the Anki db."""
timestamp_str = aqt.mw.col.db.scalar(
row = aqt.mw.col.db.first(
"""
SELECT MAX(r.id)
SELECT MIN(r.id), MAX(r.id)
FROM revlog as r
JOIN cards as c ON r.cid = c.id
JOIN ankihub_db.notes as ah_n ON c.nid = ah_n.anki_note_id
WHERE ah_n.ankihub_deck_id = ?
""",
str(ah_did),
)
if timestamp_str is None:
if row[0] is None:
return None

timestamp_ms = int(timestamp_str)
result = datetime.fromtimestamp(timestamp_ms / 1000)
return result
first_timestamp_str, last_timestamp_str = row
first_review_datetime = _ms_timestamp_to_datetime(int(first_timestamp_str))
last_review_datetime = _ms_timestamp_to_datetime(int(last_timestamp_str))
return first_review_datetime, last_review_datetime


def _ms_timestamp_to_datetime(timestamp: int) -> datetime:
return datetime.fromtimestamp(timestamp / 1000)
84 changes: 62 additions & 22 deletions tests/addon/test_unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@
_get_fields_protected_by_tags,
)
from ankihub.main.review_data import (
_get_last_review_datetime_for_ah_deck,
_get_first_and_last_review_datetime_for_ah_deck,
_get_review_count_for_ah_deck_since,
send_review_data,
)
Expand Down Expand Up @@ -1795,13 +1795,16 @@ def test_with_review_for_other_deck(

class TestGetLastReviewTimeForAHDeck:
@pytest.mark.parametrize(
"review_deltas, expected_last_review_delta",
"review_deltas, expected_first_review_delta, expected_last_review_delta",
[
# Reviews in the past, the latest review is returned
([timedelta(days=-3), timedelta(days=-2)], timedelta(days=-2)),
([timedelta(days=-3), timedelta(days=0)], timedelta(days=0)),
# Reviews in the past, the first and last review times are returned
(
[timedelta(days=-3), timedelta(days=-2), timedelta(days=-1)],
timedelta(days=-3),
timedelta(days=-1),
),
# No reviews, None is returned
([], None),
([], None, None),
],
)
def test_review_times_relative_to_since_time(
Expand All @@ -1810,6 +1813,7 @@ def test_review_times_relative_to_since_time(
install_ah_deck: InstallAHDeck,
import_ah_note: ImportAHNote,
review_deltas: List[timedelta],
expected_first_review_delta: Optional[timedelta],
expected_last_review_delta: Optional[timedelta],
) -> None:
with anki_session_with_addon_data.profile_loaded():
Expand All @@ -1823,15 +1827,25 @@ def test_review_times_relative_to_since_time(
)

with attached_ankihub_db():
first_and_last_time = _get_first_and_last_review_datetime_for_ah_deck(
ah_did=ah_did
)

if expected_last_review_delta is not None:
expected_last_review_time = now + expected_last_review_delta
first_review_time, last_review_time = first_and_last_time

expected_first_review_time = now + expected_first_review_delta
assert_datetime_equal_ignore_milliseconds(
first_review_time,
expected_first_review_time,
)

expected_last_review_time = now + expected_last_review_delta
assert_datetime_equal_ignore_milliseconds(
_get_last_review_datetime_for_ah_deck(ah_did=ah_did),
expected_last_review_time,
last_review_time, expected_last_review_time
)
else:
assert _get_last_review_datetime_for_ah_deck(ah_did=ah_did) is None
assert first_and_last_time is None

def test_with_multiple_notes(
self,
Expand All @@ -1844,17 +1858,29 @@ def test_with_multiple_notes(
note_info_1 = import_ah_note(ah_did=ah_did)
note_info_2 = import_ah_note(ah_did=ah_did)

first_review_time = datetime.now()
record_review_for_anki_nid(NoteId(note_info_1.anki_nid), first_review_time)
expected_first_review_time = datetime.now()
record_review_for_anki_nid(
NoteId(note_info_1.anki_nid), expected_first_review_time
)

second_review_time = first_review_time + timedelta(days=1)
record_review_for_anki_nid(NoteId(note_info_2.anki_nid), second_review_time)
expected_last_review_time = expected_first_review_time + timedelta(days=1)
record_review_for_anki_nid(
NoteId(note_info_2.anki_nid), expected_last_review_time
)

with attached_ankihub_db():
assert_datetime_equal_ignore_milliseconds(
_get_last_review_datetime_for_ah_deck(ah_did=ah_did),
second_review_time,
)
(
first_review_time,
last_review_time,
) = _get_first_and_last_review_datetime_for_ah_deck(ah_did=ah_did)

assert_datetime_equal_ignore_milliseconds(
first_review_time,
expected_first_review_time,
)
assert_datetime_equal_ignore_milliseconds(
last_review_time, expected_last_review_time
)

def test_with_review_for_other_deck(
self,
Expand All @@ -1869,16 +1895,26 @@ def test_with_review_for_other_deck(
ah_did_2 = install_ah_deck()
note_info_2 = import_ah_note(ah_did=ah_did_2)

now = datetime.now()
record_review_for_anki_nid(NoteId(note_info_1.anki_nid), now)
expected_review_time = datetime.now()
record_review_for_anki_nid(
NoteId(note_info_2.anki_nid), now + timedelta(seconds=1)
NoteId(note_info_1.anki_nid), expected_review_time
)
record_review_for_anki_nid(
NoteId(note_info_2.anki_nid),
expected_review_time + timedelta(seconds=1),
)

# Only the review for the first deck should be considered.
with attached_ankihub_db():
(
first_review_time,
last_review_time,
) = _get_first_and_last_review_datetime_for_ah_deck(ah_did=ah_did_1)

assert first_review_time == last_review_time
assert_datetime_equal_ignore_milliseconds(
_get_last_review_datetime_for_ah_deck(ah_did=ah_did_1), now
first_review_time,
expected_review_time,
)


Expand Down Expand Up @@ -1914,7 +1950,11 @@ def test_with_two_reviews_for_one_deck(
0
][0]
assert card_review_data.ah_did == ah_did
assert card_review_data.total_card_reviews_last_7_days == 2
assert card_review_data.total_card_reviews_last_30_days == 2
assert_datetime_equal_ignore_milliseconds(
card_review_data.first_card_review_at, first_review_time
)
assert_datetime_equal_ignore_milliseconds(
card_review_data.last_card_review_at, second_review_time
)
9 changes: 6 additions & 3 deletions tests/client/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import uuid
import zipfile
from copy import deepcopy
from datetime import datetime, timezone
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Callable, Generator, List, cast
from unittest.mock import MagicMock, Mock
Expand Down Expand Up @@ -1588,10 +1588,13 @@ def test_basic(
self,
authorized_client_for_user_test1: AnkiHubClient,
) -> None:
now = datetime.now(tz=timezone.utc)
card_review_data = CardReviewData(
ah_did=ID_OF_DECK_OF_USER_TEST1,
total_card_reviews_last_30_days=1,
last_card_review_at=datetime.now(tz=timezone.utc),
total_card_reviews_last_7_days=10,
total_card_reviews_last_30_days=20,
first_card_review_at=now - timedelta(days=30),
last_card_review_at=now,
)

authorized_client_for_user_test1.send_card_review_data([card_review_data])

0 comments on commit 09dd1c6

Please sign in to comment.