Skip to content

Commit

Permalink
ref: Move create_collaborative_deck to `gui.operations.deck_creatio…
Browse files Browse the repository at this point in the history
…n`, Remove `SubscribeDialog` (#778)

* ref: Extract create_collaborative_deck operation

* Remove unused SubscribeDialog

* Add test

* Increase coverage
  • Loading branch information
RisingOrange authored Oct 19, 2023
1 parent c2452c5 commit 42f1c1f
Show file tree
Hide file tree
Showing 7 changed files with 276 additions and 273 deletions.
111 changes: 1 addition & 110 deletions ankihub/gui/decks_dialog.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -11,12 +10,9 @@
QDialog,
QDialogButtonBox,
QHBoxLayout,
QLabel,
QLineEdit,
QListWidget,
QListWidgetItem,
QPushButton,
QSizePolicy,
Qt,
QVBoxLayout,
qconnect,
Expand All @@ -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

Expand Down Expand Up @@ -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(
(
Expand Down Expand Up @@ -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(
"<center>Copy/Paste a Deck ID from AnkiHub.net/decks to subscribe.</center>"
)
# 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.<br><br>"
f"See <a href='{url_help()}'>{url_help()}</a> 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())
160 changes: 4 additions & 156 deletions ankihub/gui/menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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?<br><br><br>"
'Terms of use: <a href="https://www.ankihub.net/terms">https://www.ankihub.net/terms</a><br>'
'Privacy Policy: <a href="https://www.ankihub.net/privacy">https://www.ankihub.net/privacy</a><br>',
)
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?<br><br>"
'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?<br><br>"
"For example, if you have a deck named <b>My Deck</b> with a subdeck named <b>My Deck::Subdeck</b>, "
"each note in <b>My Deck::Subdeck</b> will have a tag "
f"<b>{SUBDECK_TAG}::Subdeck</b> added to it.<br><br>"
"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"<b>{deck_name}</b> 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!<br><br>"
"Link to the deck on AnkiHub:<br>"
f"<a href={deck_url}>{deck_url}</a>"
)

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)


Expand Down
2 changes: 1 addition & 1 deletion ankihub/gui/operations/db_check/ah_db_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
Loading

0 comments on commit 42f1c1f

Please sign in to comment.