Skip to content

Commit 1d93f6b

Browse files
authored
Merge pull request #255 from fabiomanz/fix/import-dialog-subdeck-counts
Fix import dialog showing 0 notes/media for subdeck-only decks
2 parents 10cfbb0 + 2622309 commit 1d93f6b

6 files changed

Lines changed: 114 additions & 14 deletions

File tree

crowd_anki/importer/anki_importer.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,15 @@ def load_deck(self, directory_path) -> bool:
2727
:param directory_path: Path
2828
"""
2929
deck_json = self.read_deck(self.get_deck_path(directory_path))
30+
deck = deck_initializer.from_json(deck_json)
3031

31-
import_config = self.read_import_config(directory_path, deck_json)
32+
import_config = self.read_import_config(directory_path, deck)
3233
if import_config is None:
3334
return False
3435

3536
if aqt.mw:
3637
aqt.mw.create_backup_now()
3738
try:
38-
deck = deck_initializer.from_json(deck_json)
3939
deck.save_to_collection(self.collection, import_config=import_config)
4040

4141
if import_config.use_media:
@@ -84,7 +84,7 @@ def read_deck(file_path: Path):
8484
return json.load(deck_file)
8585

8686
@staticmethod
87-
def read_import_config(directory_path, deck_json):
87+
def read_import_config(directory_path, deck):
8888
file_path = directory_path.joinpath(IMPORT_CONFIG_NAME)
8989

9090
if not file_path.exists():
@@ -93,7 +93,7 @@ def read_import_config(directory_path, deck_json):
9393
with file_path.open(encoding='utf8') as meta_file:
9494
import_dict = yaml.full_load(meta_file)
9595

96-
import_dialog = ImportDialog(deck_json, import_dict)
96+
import_dialog = ImportDialog(deck, import_dict)
9797
if import_dialog.exec() == QDialog.DialogCode.Rejected:
9898
return None
9999

crowd_anki/importer/import_dialog.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -66,13 +66,13 @@ class ImportConfig(PersonalFieldsHolder):
6666

6767

6868
class ImportDialog(QDialog):
69-
def __init__(self, deck_json, config, parent=None):
69+
def __init__(self, deck, config, parent=None):
7070
super().__init__(None)
7171
self.parent = parent
7272
self.form = ConfigUI()
7373
self.form.setupUi(self)
7474
self.userConfig = ConfigSettings.get_instance()
75-
self.deck_json = deck_json
75+
self.deck = deck
7676
self.import_defaults = ImportDefaults.from_dict(config)
7777
self.personal_field_ui_dict = defaultdict(dict)
7878
self.ui_initial_setup()
@@ -120,12 +120,12 @@ def on_click(item):
120120
item.setCheckState(Qt.CheckState.Checked)
121121
self.form.list_personal_fields.itemClicked.connect(on_click)
122122

123-
for model in self.deck_json["note_models"]:
124-
model_name = model["name"]
125-
model_id = model[UUID_FIELD_NAME]
123+
for model in self.deck.metadata.models.values():
124+
model_name = model.anki_dict["name"]
125+
model_id = model.anki_dict[UUID_FIELD_NAME]
126126
add_header(model_name)
127127

128-
for field in model["flds"]:
128+
for field in model.anki_dict["flds"]:
129129
field_name = field["name"]
130130
field_ui = add_field(field_name, self.import_defaults.is_personal_field(model_name, field_name))
131131
self.personal_field_ui_dict[model_name].setdefault(field_name, field_ui)
@@ -149,10 +149,10 @@ def set_checked_and_text(checkbox, text, count, checked: bool = True):
149149
text = f"{text}: {'{:,}'.format(count)}"
150150
checkbox.setText(text)
151151

152-
set_checked_and_text(self.form.cb_notes, "Notes", len(self.deck_json['notes']))
153-
set_checked_and_text(self.form.cb_media, "Media Files", len(self.deck_json['media_files']))
152+
set_checked_and_text(self.form.cb_notes, "Notes", self.deck.get_note_count())
153+
set_checked_and_text(self.form.cb_media, "Media Files", self.deck.get_media_file_count())
154154

155-
# TODO: Deck Parts to Use, check which are actually in the deck_json
155+
# TODO: Deck Parts to Use, check which are actually in the deck
156156

157157
def read_import_config(self):
158158
config = ImportConfig(

crowd_anki/representation/deck.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ def __init__(self,
5151

5252
self.collection = None
5353
self.notes = []
54+
self.media_files = []
5455
self.children = []
5556
self.metadata = None
5657
self.deck_config_uuid = None
@@ -69,6 +70,21 @@ def flatten(self):
6970
def get_note_count(self):
7071
return len(self.notes) + sum(child.get_note_count() for child in self.children)
7172

73+
def get_media_file_count(self):
74+
return len(self._collect_media_files())
75+
76+
def _collect_media_files(self):
77+
"""Unique media file names in this deck and all its subdecks.
78+
79+
Media files are deduplicated by name, since the same file (e.g.
80+
re-used media or shared `_xxx` media) can appear in multiple
81+
decks and must only be counted once.
82+
"""
83+
media = set(self.media_files)
84+
for child in self.children:
85+
media |= child._collect_media_files()
86+
return media
87+
7288
def _update_db(self):
7389
# Introduce uuid field for unique identification of entities
7490
utils.add_column(self.collection.db, "notes", UUID_FIELD_NAME)

crowd_anki/representation/deck_initializer.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ def from_json(json_dict, deck_metadata=None) -> Deck:
4545

4646
deck.deck_config_uuid = json_dict["deck_config_uuid"]
4747
deck.notes = [Note.from_json(json_note) for json_note in json_dict["notes"]]
48+
deck.media_files = json_dict.get("media_files", [])
4849
deck.children = [from_json(child, deck_metadata=deck.metadata) for child in json_dict["children"]]
4950

5051
deck.post_import_filter()

test/representation/deck_initializer_spec.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from expects import expect, be
1+
from expects import expect, be, equal
22
from mamba import description, it
33
from unittest.mock import MagicMock
44

@@ -8,10 +8,31 @@
88

99
TEST_DECK = "test deck"
1010

11+
12+
def _deck_json(media_files=None, children=None):
13+
return {
14+
"deck_config_uuid": "config-uuid",
15+
"notes": [],
16+
"media_files": media_files or [],
17+
"children": children or [],
18+
}
19+
20+
1121
with description("Initializer from deck") as self:
1222
with it("should return None when trying to export dynamic deck"):
1323
collection = MagicMock()
1424

1525
collection.decks.byName.return_value = DYNAMIC_DECK
1626

1727
expect(deck_initializer.from_collection(collection, TEST_DECK)).to(be(None))
28+
29+
with description("from_json") as self:
30+
with it("populates media_files from JSON, including subdecks"):
31+
json_dict = _deck_json(
32+
media_files=["a.png", "b.png"],
33+
children=[_deck_json(media_files=["c.png"])],
34+
)
35+
36+
deck = deck_initializer.from_json(json_dict)
37+
38+
expect(deck.get_media_file_count()).to(equal(3))

test/representation/deck_spec.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
from mamba import description, it
2+
from expects import expect, equal
3+
4+
from crowd_anki.representation.deck import Deck
5+
6+
7+
def _make_deck(notes=0, media_files=None, children=None):
8+
deck = Deck(file_provider_supplier=None)
9+
deck.notes = [None] * notes
10+
deck.media_files = list(media_files or [])
11+
deck.children = children or []
12+
return deck
13+
14+
15+
with description(Deck) as self:
16+
with description(".get_note_count") as self:
17+
with it("returns 0 for an empty deck"):
18+
expect(_make_deck().get_note_count()).to(equal(0))
19+
20+
with it("counts notes in a flat deck"):
21+
expect(_make_deck(notes=3).get_note_count()).to(equal(3))
22+
23+
with it("counts notes recursively across subdecks"):
24+
deck = _make_deck(notes=1, children=[
25+
_make_deck(notes=2, children=[
26+
_make_deck(notes=3),
27+
]),
28+
])
29+
expect(deck.get_note_count()).to(equal(6))
30+
31+
with it("counts notes in subdecks even when the parent has none"):
32+
deck = _make_deck(notes=0, children=[_make_deck(notes=5)])
33+
expect(deck.get_note_count()).to(equal(5))
34+
35+
with description(".get_media_file_count") as self:
36+
with it("returns 0 for an empty deck"):
37+
expect(_make_deck().get_media_file_count()).to(equal(0))
38+
39+
with it("counts media files in a flat deck"):
40+
expect(_make_deck(media_files=['a.jpg', 'b.jpg', 'c.jpg']).get_media_file_count()).to(equal(3))
41+
42+
with it("counts media files recursively across subdecks"):
43+
deck = _make_deck(media_files=['a.jpg'], children=[
44+
_make_deck(media_files=['b.jpg', 'c.jpg'], children=[
45+
_make_deck(media_files=['d.jpg', 'e.jpg', 'f.jpg']),
46+
]),
47+
])
48+
expect(deck.get_media_file_count()).to(equal(6))
49+
50+
with it("counts media files in subdecks even when the parent has none"):
51+
deck = _make_deck(children=[_make_deck(media_files=['a.jpg', 'b.jpg', 'c.jpg', 'd.jpg'])])
52+
expect(deck.get_media_file_count()).to(equal(4))
53+
54+
with it("counts media files shared across subdecks only once"):
55+
deck = _make_deck(media_files=['shared.jpg'], children=[
56+
_make_deck(media_files=['shared.jpg', 'a.jpg']),
57+
_make_deck(media_files=['shared.jpg', 'b.jpg']),
58+
])
59+
expect(deck.get_media_file_count()).to(equal(3))
60+
61+
with it("counts media files duplicated within a single deck only once"):
62+
expect(_make_deck(media_files=['a.jpg', 'a.jpg']).get_media_file_count()).to(equal(1))

0 commit comments

Comments
 (0)