From 42f1c1f9cdacd3c50f5c651e8238830a70b462b2 Mon Sep 17 00:00:00 2001
From: Jakub Fidler <31575114+RisingOrange@users.noreply.github.com>
Date: Thu, 19 Oct 2023 11:05:55 +0200
Subject: [PATCH] ref: Move `create_collaborative_deck` to
`gui.operations.deck_creation`, Remove `SubscribeDialog` (#778)
* ref: Extract create_collaborative_deck operation
* Remove unused SubscribeDialog
* Add test
* Increase coverage
---
ankihub/gui/decks_dialog.py | 111 +-----------
ankihub/gui/menu.py | 160 +----------------
.../gui/operations/db_check/ah_db_check.py | 2 +-
ankihub/gui/operations/deck_creation.py | 167 ++++++++++++++++++
tests/addon/test_integration.py | 3 +-
tests/addon/test_unit.py | 91 +++++++++-
tests/fixtures.py | 15 ++
7 files changed, 276 insertions(+), 273 deletions(-)
create mode 100644 ankihub/gui/operations/deck_creation.py
diff --git a/ankihub/gui/decks_dialog.py b/ankihub/gui/decks_dialog.py
index aa523fb26..3bd3d312b 100644
--- a/ankihub/gui/decks_dialog.py
+++ b/ankihub/gui/decks_dialog.py
@@ -1,6 +1,5 @@
"""Dialog for managing subscriptions to AnkiHub decks and deck-specific settings."""
import uuid
-from concurrent.futures import Future
from typing import Optional
from uuid import UUID
@@ -11,12 +10,9 @@
QDialog,
QDialogButtonBox,
QHBoxLayout,
- QLabel,
- QLineEdit,
QListWidget,
QListWidgetItem,
QPushButton,
- QSizePolicy,
Qt,
QVBoxLayout,
qconnect,
@@ -27,8 +23,7 @@
from ..addon_ankihub_client import AddonAnkiHubClient as AnkiHubClient
from ..main.deck_unsubscribtion import unsubscribe_from_deck_and_uninstall
from ..main.subdecks import SUBDECK_TAG
-from ..settings import config, url_deck_base, url_decks, url_help
-from .operations.deck_installation import download_and_install_decks
+from ..settings import config, url_deck_base, url_decks
from .operations.subdecks import confirm_and_toggle_subdecks
from .utils import ask_user, set_tooltip_icon
@@ -114,12 +109,6 @@ def _refresh_anki(self) -> None:
op.study_queues = True
gui_hooks.operation_did_execute(op, handler=None)
- def _on_add(self) -> None:
- SubscribeDialog().exec()
-
- self._refresh_decks_list()
- self._refresh_anki()
-
def _select_deck(self, ah_did: uuid.UUID):
deck_item = next(
(
@@ -270,101 +259,3 @@ def __init__(self, *args, **kwargs) -> None:
self.form.buttonBox.removeButton(
self.form.buttonBox.button(QDialogButtonBox.StandardButton.Help)
)
-
-
-class SubscribeDialog(QDialog):
- silentlyClose = True
-
- def __init__(self):
- super(SubscribeDialog, self).__init__()
-
- self.thread = None # type: ignore
- self.box_top = QVBoxLayout()
- self.box_mid = QHBoxLayout()
- self.box_left = QVBoxLayout()
- self.box_right = QVBoxLayout()
-
- self.deck_id_box = QHBoxLayout()
- self.deck_id_box_label = QLabel("Deck ID:")
- self.deck_id_box_text = QLineEdit("", self)
- self.deck_id_box_text.setMinimumWidth(300)
- self.deck_id_box.addWidget(self.deck_id_box_label)
- self.deck_id_box.addWidget(self.deck_id_box_text)
- self.box_left.addLayout(self.deck_id_box)
-
- self.box_mid.addLayout(self.box_left)
- self.box_mid.addSpacing(20)
- self.box_mid.addLayout(self.box_right)
-
- self.buttonbox = QDialogButtonBox(
- QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel # type: ignore
- )
- self.buttonbox.button(QDialogButtonBox.StandardButton.Ok).setText("Subscribe")
- self.browse_btn = self.buttonbox.addButton(
- "Browse Decks", QDialogButtonBox.ButtonRole.ActionRole
- )
- qconnect(self.browse_btn.clicked, self._on_browse_deck)
- qconnect(self.buttonbox.accepted, self._subscribe)
- self.buttonbox.rejected.connect(self.close)
-
- self.instructions_label = QLabel(
- "
Copy/Paste a Deck ID from AnkiHub.net/decks to subscribe."
- )
- # Add all widgets to top layout.
- self.box_top.addWidget(self.instructions_label)
- self.box_top.addSpacing(10)
- self.box_top.addLayout(self.box_mid)
- self.box_top.addStretch(1)
- self.box_top.addWidget(self.buttonbox)
- self.setLayout(self.box_top)
-
- self.setMinimumWidth(500)
- self.setSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Minimum)
- self.setWindowTitle("Subscribe to AnkiHub Deck")
-
- self.client = AnkiHubClient()
- if not config.is_logged_in():
- showText("Oops! Please make sure you are logged into AnkiHub!")
- self.close()
- else:
- self.show()
-
- def _subscribe(self) -> None:
- ah_did_str = self.deck_id_box_text.text().strip()
-
- try:
- ah_did = uuid.UUID(ah_did_str)
- except ValueError:
- showInfo(
- "The format of the Deck ID is invalid. Please make sure you copied the Deck ID correctly."
- )
- return
-
- if ah_did in config.deck_ids():
- showText(
- f"You've already subscribed to deck {ah_did}. "
- "Syncing with AnkiHub will happen automatically everytime you "
- "restart Anki. You can manually sync with AnkiHub from the AnkiHub "
- f"menu. See {url_help()} for more details."
- )
- self.close()
- return
-
- confirmed = ask_user(
- f"Would you like to proceed with downloading and installing the deck? "
- f"Your personal collection will be modified.
"
- f"See {url_help()} for details.",
- title="Please confirm to proceed.",
- )
- if not confirmed:
- return
-
- def on_done(future: Future) -> None:
- future.result()
-
- self.accept()
-
- download_and_install_decks([ah_did], on_done=on_done)
-
- def _on_browse_deck(self) -> None:
- openLink(url_decks())
diff --git a/ankihub/gui/menu.py b/ankihub/gui/menu.py
index eaa454380..789f16942 100644
--- a/ankihub/gui/menu.py
+++ b/ankihub/gui/menu.py
@@ -2,42 +2,35 @@
import re
from concurrent.futures import Future
from dataclasses import dataclass
-from datetime import datetime, timezone
from pathlib import Path
from typing import Optional
import aqt
from aqt import (
AnkiApp,
- QCheckBox,
QHBoxLayout,
QLabel,
QLineEdit,
- QMessageBox,
QPushButton,
QSizePolicy,
QVBoxLayout,
QWidget,
)
-from aqt.operations import QueryOp
from aqt.qt import QAction, QDialog, QKeySequence, QMenu, Qt, qconnect
-from aqt.studydeck import StudyDeck
from aqt.utils import openLink, showInfo, tooltip
from .. import LOGGER
from ..addon_ankihub_client import AddonAnkiHubClient as AnkiHubClient
-from ..ankihub_client import AnkiHubHTTPError, get_media_names_from_notes_data
-from ..ankihub_client.models import UserDeckRelation
+from ..ankihub_client import AnkiHubHTTPError
from ..db import ankihub_db
-from ..main.deck_creation import DeckCreationResult, create_ankihub_deck
-from ..main.subdecks import SUBDECK_TAG
from ..media_import.ui import open_import_dialog
-from ..settings import ADDON_VERSION, config, url_view_deck
+from ..settings import ADDON_VERSION, config
from .config_dialog import get_config_dialog_manager
from .decks_dialog import SubscribedDecksDialog
from .errors import upload_logs_and_data_in_background, upload_logs_in_background
from .media_sync import media_sync
from .operations.ankihub_sync import sync_with_ankihub
+from .operations.deck_creation import create_collaborative_deck
from .utils import (
ask_user,
check_and_prompt_for_updates_on_main_window,
@@ -192,154 +185,9 @@ def display_login(cls):
return cls._window
-class DeckCreationConfirmationDialog(QMessageBox):
- def __init__(self):
- super().__init__(parent=aqt.mw)
-
- self.setWindowTitle("Confirm AnkiHub Deck Creation")
- self.setIcon(QMessageBox.Icon.Question)
- self.setText(
- "Are you sure you want to create a new collaborative deck?
"
- 'Terms of use: https://www.ankihub.net/terms
'
- 'Privacy Policy: https://www.ankihub.net/privacy
',
- )
- self.setStandardButtons(
- QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.Cancel # type: ignore
- )
- self.confirmation_cb = QCheckBox(
- text=" by checking this checkbox you agree to the terms of use",
- parent=self,
- )
- self.setCheckBox(self.confirmation_cb)
-
- def run(self) -> bool:
- clicked_ok = self.exec() == QMessageBox.StandardButton.Yes
- if not clicked_ok:
- return False
-
- if not self.confirmation_cb.isChecked():
- tooltip("You didn't agree to the terms of use.")
- return False
-
- return True
-
-
-def _create_collaborative_deck_action() -> None:
-
- confirm = DeckCreationConfirmationDialog().run()
- if not confirm:
- return
-
- LOGGER.info("Asking user to choose a deck to upload...")
- deck_chooser = StudyDeck(
- aqt.mw,
- title="AnkiHub",
- accept="Upload",
- # Removes the "Add" button
- buttons=[],
- names=lambda: [
- d.name
- for d in aqt.mw.col.decks.all_names_and_ids(include_filtered=False)
- if "::" not in d.name and d.id != 1
- ],
- parent=aqt.mw,
- )
- LOGGER.info(f"Closed deck chooser dialog: {deck_chooser}")
- LOGGER.info(f"Chosen deck name: {deck_chooser.name}")
- deck_name = deck_chooser.name
- if not deck_name:
- return
-
- if len(aqt.mw.col.find_cards(f'deck:"{deck_name}"')) == 0:
- showInfo("You can't upload an empty deck.")
- return
-
- public = ask_user(
- "Would you like to make this deck public?
"
- 'If you chose "No" it will be private and only people with a link '
- "will be able to see it on the AnkiHub website."
- )
- if public is None:
- return
-
- private = public is False
-
- add_subdeck_tags = False
- if aqt.mw.col.decks.children(aqt.mw.col.decks.id_for_name(deck_name)):
- add_subdeck_tags = ask_user(
- "Would you like to add a tag to each note in the deck that indicates which subdeck it belongs to?
"
- "For example, if you have a deck named My Deck with a subdeck named My Deck::Subdeck, "
- "each note in My Deck::Subdeck will have a tag "
- f"{SUBDECK_TAG}::Subdeck added to it.
"
- "This allows subscribers to have the same subdeck structure as you have."
- )
- if add_subdeck_tags is None:
- return
-
- confirm = ask_user(
- "Uploading the deck to AnkiHub requires modifying notes and note types in "
- f"{deck_name} and will require a full sync afterwards. Would you like to "
- "continue?",
- )
- if not confirm:
- return
-
- should_upload_media = ask_user(
- "Do you want to upload media for this deck as well? "
- "This will take some extra time but it is required to display images "
- "on AnkiHub and this way subscribers will be able to download media files "
- "when installing the deck. "
- )
-
- def on_success(deck_creation_result: DeckCreationResult) -> None:
-
- # Upload all existing local media for this deck
- # (media files that are referenced on Deck's notes)
- if should_upload_media:
- media_names = get_media_names_from_notes_data(
- deck_creation_result.notes_data
- )
- media_sync.start_media_upload(media_names, deck_creation_result.ankihub_did)
-
- # Add the deck to the list of decks the user owns
- anki_did = aqt.mw.col.decks.id_for_name(deck_name)
- creation_time = datetime.now(tz=timezone.utc)
- config.add_deck(
- deck_name,
- deck_creation_result.ankihub_did,
- anki_did,
- user_relation=UserDeckRelation.OWNER,
- latest_udpate=creation_time,
- )
-
- # Show a message to the user with a link to the deck on AnkiHub
- deck_url = f"{url_view_deck()}{deck_creation_result.ankihub_did}"
- showInfo(
- "🎉 Deck upload successful!
"
- "Link to the deck on AnkiHub:
"
- f"{deck_url}"
- )
-
- def on_failure(exc: Exception):
- aqt.mw.progress.finish()
- raise exc
-
- op = QueryOp(
- parent=aqt.mw,
- op=lambda col: create_ankihub_deck(
- deck_name,
- private=private,
- add_subdeck_tags=add_subdeck_tags,
- ),
- success=on_success,
- ).failure(on_failure)
- LOGGER.info("Instantiated QueryOp for creating collaborative deck")
- op.with_progress(label="Creating collaborative deck").run_in_background()
-
-
def _create_collaborative_deck_setup(parent: QMenu):
q_action = QAction("🛠️ Create Collaborative Deck", parent=parent)
- qconnect(q_action.triggered, _create_collaborative_deck_action)
+ qconnect(q_action.triggered, create_collaborative_deck)
parent.addAction(q_action)
diff --git a/ankihub/gui/operations/db_check/ah_db_check.py b/ankihub/gui/operations/db_check/ah_db_check.py
index 53976298e..91740a7a9 100644
--- a/ankihub/gui/operations/db_check/ah_db_check.py
+++ b/ankihub/gui/operations/db_check/ah_db_check.py
@@ -7,8 +7,8 @@
from ....db import ankihub_db
from ....main.deck_unsubscribtion import uninstall_deck
from ....settings import config
-from ...decks_dialog import download_and_install_decks
from ...exceptions import DeckDownloadAndInstallError, RemoteDeckNotFoundError
+from ...operations.deck_installation import download_and_install_decks
from ...utils import ask_user
diff --git a/ankihub/gui/operations/deck_creation.py b/ankihub/gui/operations/deck_creation.py
new file mode 100644
index 000000000..03585e927
--- /dev/null
+++ b/ankihub/gui/operations/deck_creation.py
@@ -0,0 +1,167 @@
+from datetime import datetime, timezone
+
+import aqt
+from aqt import QCheckBox, QMessageBox
+from aqt.operations import QueryOp
+from aqt.studydeck import StudyDeck
+from aqt.utils import showInfo, tooltip
+
+from ... import LOGGER
+from ...ankihub_client import get_media_names_from_notes_data
+from ...ankihub_client.models import UserDeckRelation
+from ...main.deck_creation import DeckCreationResult, create_ankihub_deck
+from ...main.subdecks import SUBDECK_TAG
+from ...settings import config, url_view_deck
+from ..media_sync import media_sync
+from ..utils import ask_user
+
+
+def create_collaborative_deck() -> None:
+ """Creates a new collaborative deck and uploads it to AnkiHub.
+
+ Asks the user to confirm, choose a deck to upload and for some additional options,
+ and then uploads the deck to AnkiHub.
+ When the upload is complete, shows a message to the user with a link to the deck on AnkiHub.
+ """
+
+ confirm = DeckCreationConfirmationDialog().run()
+ if not confirm:
+ return
+
+ LOGGER.info("Asking user to choose a deck to upload...")
+ deck_chooser = StudyDeck(
+ aqt.mw,
+ title="AnkiHub",
+ accept="Upload",
+ # Removes the "Add" button
+ buttons=[],
+ names=lambda: [
+ d.name
+ for d in aqt.mw.col.decks.all_names_and_ids(include_filtered=False)
+ if "::" not in d.name and d.id != 1
+ ],
+ parent=aqt.mw,
+ )
+ LOGGER.info(f"Closed deck chooser dialog: {deck_chooser}")
+ LOGGER.info(f"Chosen deck name: {deck_chooser.name}")
+ deck_name = deck_chooser.name
+ if not deck_name:
+ return
+
+ if len(aqt.mw.col.find_cards(f'deck:"{deck_name}"')) == 0:
+ showInfo("You can't upload an empty deck.")
+ return
+
+ public = ask_user(
+ "Would you like to make this deck public?
"
+ 'If you chose "No" it will be private and only people with a link '
+ "will be able to see it on the AnkiHub website."
+ )
+ if public is None:
+ return
+
+ private = public is False
+
+ add_subdeck_tags = False
+ if aqt.mw.col.decks.children(aqt.mw.col.decks.id_for_name(deck_name)):
+ add_subdeck_tags = ask_user(
+ "Would you like to add a tag to each note in the deck that indicates which subdeck it belongs to?
"
+ "For example, if you have a deck named My Deck with a subdeck named My Deck::Subdeck, "
+ "each note in My Deck::Subdeck will have a tag "
+ f"{SUBDECK_TAG}::Subdeck added to it.
"
+ "This allows subscribers to have the same subdeck structure as you have."
+ )
+ if add_subdeck_tags is None:
+ return
+
+ confirm = ask_user(
+ "Uploading the deck to AnkiHub requires modifying notes and note types in "
+ f"{deck_name} and will require a full sync afterwards. Would you like to "
+ "continue?",
+ )
+ if not confirm:
+ return
+
+ should_upload_media = ask_user(
+ "Do you want to upload media for this deck as well? "
+ "This will take some extra time but it is required to display images "
+ "on AnkiHub and this way subscribers will be able to download media files "
+ "when installing the deck. "
+ )
+
+ def on_success(deck_creation_result: DeckCreationResult) -> None:
+
+ # Upload all existing local media for this deck
+ # (media files that are referenced on Deck's notes)
+ if should_upload_media:
+ media_names = get_media_names_from_notes_data(
+ deck_creation_result.notes_data
+ )
+ media_sync.start_media_upload(media_names, deck_creation_result.ankihub_did)
+
+ # Add the deck to the list of decks the user owns
+ anki_did = aqt.mw.col.decks.id_for_name(deck_name)
+ creation_time = datetime.now(tz=timezone.utc)
+ config.add_deck(
+ deck_name,
+ deck_creation_result.ankihub_did,
+ anki_did,
+ user_relation=UserDeckRelation.OWNER,
+ latest_udpate=creation_time,
+ )
+
+ # Show a message to the user with a link to the deck on AnkiHub
+ deck_url = f"{url_view_deck()}{deck_creation_result.ankihub_did}"
+ showInfo(
+ "🎉 Deck upload successful!
"
+ "Link to the deck on AnkiHub:
"
+ f"{deck_url}"
+ )
+
+ def on_failure(exc: Exception):
+ aqt.mw.progress.finish()
+ raise exc
+
+ op = QueryOp(
+ parent=aqt.mw,
+ op=lambda col: create_ankihub_deck(
+ deck_name,
+ private=private,
+ add_subdeck_tags=add_subdeck_tags,
+ ),
+ success=on_success,
+ ).failure(on_failure)
+ LOGGER.info("Instantiated QueryOp for creating collaborative deck")
+ op.with_progress(label="Creating collaborative deck").run_in_background()
+
+
+class DeckCreationConfirmationDialog(QMessageBox):
+ def __init__(self):
+ super().__init__(parent=aqt.mw)
+
+ self.setWindowTitle("Confirm AnkiHub Deck Creation")
+ self.setIcon(QMessageBox.Icon.Question)
+ self.setText(
+ "Are you sure you want to create a new collaborative deck?
"
+ 'Terms of use: https://www.ankihub.net/terms
'
+ 'Privacy Policy: https://www.ankihub.net/privacy
',
+ )
+ self.setStandardButtons(
+ QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.Cancel # type: ignore
+ )
+ self.confirmation_cb = QCheckBox(
+ text=" by checking this checkbox you agree to the terms of use",
+ parent=self,
+ )
+ self.setCheckBox(self.confirmation_cb)
+
+ def run(self) -> bool:
+ clicked_ok = self.exec() == QMessageBox.StandardButton.Yes
+ if not clicked_ok:
+ return False
+
+ if not self.confirmation_cb.isChecked():
+ tooltip("You didn't agree to the terms of use.")
+ return False
+
+ return True
diff --git a/tests/addon/test_integration.py b/tests/addon/test_integration.py
index c68ec0a21..d0d7e4b09 100644
--- a/tests/addon/test_integration.py
+++ b/tests/addon/test_integration.py
@@ -115,12 +115,13 @@
setup_config_dialog_manager,
)
from ankihub.gui.deck_updater import _AnkiHubDeckUpdater, ah_deck_updater
-from ankihub.gui.decks_dialog import SubscribedDecksDialog, download_and_install_decks
+from ankihub.gui.decks_dialog import SubscribedDecksDialog
from ankihub.gui.editor import _on_suggestion_button_press, _refresh_buttons
from ankihub.gui.errors import upload_logs_and_data_in_background
from ankihub.gui.media_sync import media_sync
from ankihub.gui.menu import menu_state
from ankihub.gui.operations import ankihub_sync
+from ankihub.gui.operations.deck_installation import download_and_install_decks
from ankihub.gui.operations.new_deck_subscriptions import (
check_and_install_new_deck_subscriptions,
)
diff --git a/tests/addon/test_unit.py b/tests/addon/test_unit.py
index 4f50a0790..c9413c12b 100644
--- a/tests/addon/test_unit.py
+++ b/tests/addon/test_unit.py
@@ -21,15 +21,14 @@
from pytestqt.qtbot import QtBot # type: ignore
from requests import Response # type: ignore
-from ankihub.gui import errors
-from ankihub.gui.operations.utils import future_with_exception, future_with_result
-
from ..factories import DeckMediaFactory, NoteInfoFactory
from ..fixtures import ( # type: ignore
ImportAHNoteType,
MockFunction,
NewNoteWithNoteType,
SetFeatureFlagState,
+ add_basic_anki_note_to_deck,
+ create_anki_deck,
)
from .test_integration import ImportAHNote
@@ -41,7 +40,7 @@
from ankihub.db.db import _AnkiHubDB
from ankihub.db.exceptions import IntegrityError
from ankihub.feature_flags import _FeatureFlags, feature_flags
-from ankihub.gui import suggestion_dialog
+from ankihub.gui import errors, suggestion_dialog
from ankihub.gui.error_dialog import ErrorDialog
from ankihub.gui.errors import (
OUTDATED_CLIENT_ERROR_REASON,
@@ -50,6 +49,12 @@
_try_handle_exception,
)
from ankihub.gui.menu import AnkiHubLogin
+from ankihub.gui.operations import deck_creation
+from ankihub.gui.operations.deck_creation import (
+ DeckCreationConfirmationDialog,
+ create_collaborative_deck,
+)
+from ankihub.gui.operations.utils import future_with_exception, future_with_result
from ankihub.gui.suggestion_dialog import (
SourceType,
SuggestionDialog,
@@ -62,7 +67,10 @@
)
from ankihub.gui.threading_utils import rate_limited
from ankihub.main import suggestions
-from ankihub.main.deck_creation import _note_type_name_without_ankihub_modifications
+from ankihub.main.deck_creation import (
+ DeckCreationResult,
+ _note_type_name_without_ankihub_modifications,
+)
from ankihub.main.exporting import _prepared_field_html
from ankihub.main.importing import _updated_tags
from ankihub.main.note_conversion import (
@@ -1493,3 +1501,76 @@ def test_combined(
nids = [nid_1, nid_2, nid_3]
assert retain_nids_with_ah_note_type(nids) == [nid_1, nid_2]
+
+
+@pytest.mark.parametrize(
+ "creating_deck_fails",
+ [True, False],
+)
+class TestCreateCollaborativeDeck:
+ @pytest.mark.qt_no_exception_capture
+ def test_basic(
+ self,
+ anki_session_with_addon_data: AnkiSession,
+ mock_function: MockFunction,
+ next_deterministic_uuid: Callable[[], uuid.UUID],
+ qtbot: QtBot,
+ creating_deck_fails: bool,
+ ) -> None:
+ with anki_session_with_addon_data.profile_loaded():
+ # Setup Anki deck with a note.
+ deck_name = "test"
+ anki_did = create_anki_deck(deck_name=deck_name)
+ add_basic_anki_note_to_deck(anki_did)
+
+ # Mock all the UI interactions.
+ mock_function(DeckCreationConfirmationDialog, "run", return_value=True)
+
+ study_deck_mock = Mock
+ study_deck_mock.name = deck_name
+ mock_function(deck_creation, "StudyDeck", return_value=study_deck_mock)
+
+ mock_function(deck_creation, "ask_user", return_value=True)
+
+ def raise_exception(*args, **kwargs) -> None:
+ raise Exception("test")
+
+ ah_did = next_deterministic_uuid()
+ notes_data = [NoteInfoFactory.create()]
+ create_ankihub_deck_mock = mock_function(
+ deck_creation,
+ "create_ankihub_deck",
+ return_value=DeckCreationResult(
+ ankihub_did=ah_did,
+ notes_data=notes_data,
+ ),
+ side_effect=raise_exception if creating_deck_fails else None,
+ )
+
+ get_media_names_from_notes_data_mock = mock_function(
+ deck_creation,
+ "get_media_names_from_notes_data",
+ return_value=[],
+ )
+ start_media_upload_mock = mock_function(
+ deck_creation.media_sync, "start_media_upload"
+ )
+ showInfo_mock = mock_function(deck_creation, "showInfo")
+
+ # Create the collaborative deck.
+ if creating_deck_fails:
+ create_collaborative_deck()
+ qtbot.wait(500)
+ showInfo_mock.assert_not_called()
+ else:
+ create_collaborative_deck()
+
+ qtbot.wait_until(lambda: showInfo_mock.called)
+
+ # Assert that the correct functions were called.
+ create_ankihub_deck_mock.assert_called_once_with(
+ deck_name, private=False, add_subdeck_tags=False
+ )
+
+ get_media_names_from_notes_data_mock.assert_called_once_with(notes_data)
+ start_media_upload_mock.assert_called_once()
diff --git a/tests/fixtures.py b/tests/fixtures.py
index d524ba8fe..9a52f7062 100644
--- a/tests/fixtures.py
+++ b/tests/fixtures.py
@@ -374,3 +374,18 @@ def add_mock(object, func_name: str, return_value: Any = None):
return mocks
return mock_install_deck_dependencies
+
+
+def create_anki_deck(deck_name: str) -> DeckId:
+ """Creates an Anki deck with the given name and returns the id."""
+ deck = aqt.mw.col.decks.new_deck()
+ deck.name = deck_name
+ changes = aqt.mw.col.decks.add_deck(deck)
+ return DeckId(changes.id)
+
+
+def add_basic_anki_note_to_deck(anki_did: DeckId) -> None:
+ """Adds a basic Anki note to the given deck."""
+ note = aqt.mw.col.new_note(aqt.mw.col.models.by_name("Basic"))
+ note["Front"] = "some text"
+ aqt.mw.col.add_note(note, anki_did)