diff --git a/ankihub/gui/decks_dialog.py b/ankihub/gui/decks_dialog.py
index bce476cd5..b235fa24c 100644
--- a/ankihub/gui/decks_dialog.py
+++ b/ankihub/gui/decks_dialog.py
@@ -29,7 +29,7 @@
from .. import LOGGER
from ..addon_ankihub_client import AddonAnkiHubClient as AnkiHubClient
-from ..ankihub_client.models import UserDeckRelation
+from ..ankihub_client.models import UserDeckRelation, get_media_names_from_notetype
from ..db import ankihub_db
from ..gui.operations.deck_creation import create_collaborative_deck
from ..main.deck_unsubscribtion import unsubscribe_from_deck_and_uninstall
@@ -56,6 +56,7 @@
url_deck_base,
url_decks,
)
+from .media_sync import media_sync
from .operations import AddonQueryOp
from .operations.ankihub_sync import sync_with_ankihub
from .operations.subdecks import (
@@ -646,8 +647,13 @@ def on_note_type_selected(
if not confirm:
return
+ ah_did = self._selected_ah_did()
note_type = aqt.mw.col.models.by_name(note_type_selector.name)
- add_note_type(self._selected_ah_did(), note_type)
+ new_note_type = add_note_type(ah_did, note_type)
+
+ media_names = get_media_names_from_notetype(new_note_type["id"])
+ if media_names:
+ media_sync.start_media_upload(media_names, ah_did)
tooltip("Changes updated", parent=self)
self._update_add_note_type_btn_state()
@@ -741,7 +747,13 @@ def on_note_type_selected(note_type_selector: SearchableSelectionDialog, MODEL_N
if not confirm:
return
- update_note_type_templates_and_styles(self._selected_ah_did(), note_type)
+ ah_did = self._selected_ah_did()
+ update_note_type_templates_and_styles(ah_did, note_type)
+
+ media_names = get_media_names_from_notetype(note_type["id"])
+ if media_names:
+ media_sync.start_media_upload(media_names, ah_did)
+
tooltip("Changes updated", parent=self)
self._update_templates_btn_state()
diff --git a/tests/addon/test_integration.py b/tests/addon/test_integration.py
index fc8845922..85aadad2a 100644
--- a/tests/addon/test_integration.py
+++ b/tests/addon/test_integration.py
@@ -4612,6 +4612,131 @@ def test_with_deck_not_installed(
assert hasattr(dialog, "deck_not_installed_label")
+ @pytest.mark.parametrize("has_media", [True, False])
+ def test_publish_note_type_media_upload(
+ self,
+ has_media: bool,
+ anki_session_with_addon_data: AnkiSession,
+ install_ah_deck: InstallAHDeck,
+ qtbot: QtBot,
+ mocker: MockerFixture,
+ requests_mock: Mocker,
+ mock_study_deck_dialog_with_cb: MockStudyDeckDialogWithCB,
+ ):
+ """Test media upload when publishing a new note type."""
+ with anki_session_with_addon_data.profile_loaded():
+ self._mock_dependencies(mocker)
+
+ # Setup deck as owner
+ ah_did = install_ah_deck()
+ config.deck_config(ah_did).user_relation = UserDeckRelation.OWNER
+
+ # Create note type with optional media
+ note_type = copy.deepcopy(aqt.mw.col.models.by_name("Basic"))
+ note_type["name"] = "Test Note Type"
+ note_type["id"] = 0
+ note_type["tmpls"][0]["qfmt"] = '
{{Front}}' if has_media else "{{Front}}"
+
+ new_mid = NotetypeId(aqt.mw.col.models.add_dict(note_type).id)
+ note_type = aqt.mw.col.models.get(new_mid)
+
+ # Setup mocks
+ anki_did = config.deck_config(ah_did).anki_id
+ mocker.patch.object(
+ AnkiHubClient,
+ "get_deck_subscriptions",
+ return_value=[DeckFactory.create(ah_did=ah_did, anki_did=anki_did)],
+ )
+ mock_study_deck_dialog_with_cb(
+ "ankihub.gui.decks_dialog.SearchableSelectionDialog",
+ deck_name=note_type["name"],
+ )
+ mocker.patch("ankihub.gui.decks_dialog.ask_user", return_value=True)
+
+ expected_data = note_type.copy()
+ expected_data["name"] = f"{note_type['name']} (Testdeck / user1)"
+ requests_mock.post(
+ f"{config.api_url}/decks/{ah_did}/create-note-type/",
+ json=_to_ankihub_note_type(expected_data),
+ )
+ mock_media_upload = mocker.patch("ankihub.gui.decks_dialog.media_sync.start_media_upload")
+
+ # Trigger publish
+ dialog = DeckManagementDialog()
+ dialog.display_subscribe_window()
+ dialog.decks_list.setCurrentRow(0)
+ qtbot.wait(200)
+ dialog.add_note_type_btn.click()
+ qtbot.wait(200)
+
+ # Verify media upload
+ if has_media:
+ mock_media_upload.assert_called_once_with({"test.jpg"}, ah_did)
+ else:
+ mock_media_upload.assert_not_called()
+
+ @pytest.mark.parametrize("has_media", [True, False])
+ def test_update_templates_media_upload(
+ self,
+ has_media: bool,
+ anki_session_with_addon_data: AnkiSession,
+ install_ah_deck: InstallAHDeck,
+ import_ah_note_type: ImportAHNoteType,
+ ankihub_basic_note_type: NotetypeDict,
+ qtbot: QtBot,
+ mocker: MockerFixture,
+ requests_mock: Mocker,
+ mock_study_deck_dialog_with_cb: MockStudyDeckDialogWithCB,
+ ):
+ """Test media upload when publishing template/style updates."""
+ with anki_session_with_addon_data.profile_loaded():
+ self._mock_dependencies(mocker)
+
+ # Setup deck as owner with existing note type
+ ah_did = install_ah_deck()
+ config.deck_config(ah_did).user_relation = UserDeckRelation.OWNER
+ anki_did = config.deck_config(ah_did).anki_id
+
+ # Import note type to AnkiHub DB
+ import_ah_note_type(ah_did=ah_did, note_type=ankihub_basic_note_type)
+ note_type = ankihub_basic_note_type
+
+ # Modify note type with optional media
+ note_type["tmpls"][0]["qfmt"] = '
{{Front}}' if has_media else "{{Front}}"
+ aqt.mw.col.models.update_dict(note_type)
+
+ # Setup mocks
+ mocker.patch.object(
+ AnkiHubClient,
+ "get_deck_subscriptions",
+ return_value=[DeckFactory.create(ah_did=ah_did, anki_did=anki_did)],
+ )
+ mock_study_deck_dialog_with_cb(
+ "ankihub.gui.decks_dialog.SearchableSelectionDialog",
+ deck_name=note_type["name"],
+ )
+ mocker.patch("ankihub.gui.decks_dialog.ask_user", return_value=True)
+
+ requests_mock.patch(
+ f"{config.api_url}/decks/{ah_did}/note-types/{note_type['id']}/",
+ json=_to_ankihub_note_type(note_type),
+ )
+ mock_media_upload = mocker.patch("ankihub.gui.decks_dialog.media_sync.start_media_upload")
+
+ # Trigger update
+ dialog = DeckManagementDialog()
+ dialog.display_subscribe_window()
+ dialog.decks_list.setCurrentRow(0)
+ qtbot.wait(200)
+ dialog.update_templates_btn.click()
+ qtbot.wait(200)
+
+ # Verify media upload
+ if has_media:
+ mock_media_upload.assert_called_once_with({"updated.jpg"}, ah_did)
+ else:
+ mock_media_upload.assert_not_called()
+
def _mock_dependencies(self, mocker: MockerFixture) -> None:
# Mock the config to return that the user is logged in
mocker.patch.object(config, "is_logged_in", return_value=True)