diff --git a/addon_imps/citations/mendeley.py b/addon_imps/citations/mendeley.py index d7b786ee..0a429859 100644 --- a/addon_imps/citations/mendeley.py +++ b/addon_imps/citations/mendeley.py @@ -31,16 +31,43 @@ async def list_root_collections(self) -> ItemSampleResult: ] ) + async def get_item_info(self, item_id: str): + if item_id == "ROOT": + return ItemResult( + item_id="ROOT", + item_type=ItemType.COLLECTION, + item_name="ROOT", + ) + item_type, parsed_id = item_id.split(":", maxsplit=1) + if item_type == "collection": + return await self._fetch_collection(parsed_id) + elif item_type == "document": + return await self._fetch_item_details(parsed_id) + + async def _fetch_collection(self, item_id: str): + async with self.network.GET(f"folders/{item_id}") as response: + collection = await response.json_content() + return ItemResult( + item_id=f'{ItemType.COLLECTION}:{collection["id"]}', + item_name=collection["name"], + item_type=ItemType.COLLECTION, + ) + async def list_collection_items( self, collection_id: str, filter_items: ItemType | None = None, ) -> ItemSampleResult: + parsed_id = ( + collection_id + if collection_id == "ROOT" + else collection_id.split(":", maxsplit=1)[1] + ) tasks = [] if filter_items != ItemType.COLLECTION: - tasks.append(self._fetch_collection_documents(collection_id)) + tasks.append(self._fetch_collection_documents(parsed_id)) if filter_items != ItemType.DOCUMENT: - tasks.append(self._fetch_subcollections(collection_id)) + tasks.append(self._fetch_subcollections(parsed_id)) items = await join_list(tasks) return ItemSampleResult(items=items, total_count=len(items)) @@ -81,7 +108,7 @@ async def _fetch_item_details( item_name = item_details.get("title", f"Untitled Document {item_id}") csl_data = _citation_for_mendeley_document(item_id, item_details) return ItemResult( - item_id=item_id, + item_id=f"{ItemType.DOCUMENT}:{item_id}", item_name=item_name, item_type=ItemType.DOCUMENT, item_path=item_details.get("path", []), @@ -93,7 +120,7 @@ def _parse_collection_response( ) -> ItemSampleResult: items = [ ItemResult( - item_id=collection["id"], + item_id=f'{ItemType.COLLECTION}:{collection["id"]}', item_name=collection["name"], item_type=ItemType.COLLECTION, ) diff --git a/addon_imps/citations/zotero_org.py b/addon_imps/citations/zotero_org.py index f8650aee..1e173a56 100644 --- a/addon_imps/citations/zotero_org.py +++ b/addon_imps/citations/zotero_org.py @@ -7,6 +7,9 @@ ) +ROOT_ITEM_ID = "ROOT" + + class ZoteroOrgCitationImp(CitationAddonImp): async def get_external_account_id(self, auth_result_extras: dict[str, str]) -> str: user_id = auth_result_extras.get("userID") @@ -57,7 +60,7 @@ async def list_root_collections(self) -> ItemSampleResult: collections = await response.json_content() items = [ ItemResult( - item_id=f'{collection["id"]}:', + item_id=f'{ItemType.COLLECTION}:{collection["id"]}:{ROOT_ITEM_ID}', item_name=collection["data"].get("name", "Unnamed Library"), item_type=ItemType.COLLECTION, ) @@ -65,19 +68,26 @@ async def list_root_collections(self) -> ItemSampleResult: ] items.append( ItemResult( - item_id="personal:", + item_id=f"{ItemType.COLLECTION}:personal:{ROOT_ITEM_ID}", item_name="My Library", item_type=ItemType.COLLECTION, ) ) return ItemSampleResult(items=items, total_count=len(items)) + async def get_item_info(self, item_id: str) -> ItemResult: + item_type, library, id_ = item_id.split(":") + if item_type == ItemType.COLLECTION: + return await self._fetch_collection(library, id_) + elif item_type == ItemType.DOCUMENT: + return await self._fetch_document(library, id_) + async def list_collection_items( self, collection_id: str, filter_items: ItemType | None = None, ) -> ItemSampleResult: - library, collection = collection_id.split(":") + _, library, collection = collection_id.split(":") tasks = [] if filter_items != ItemType.COLLECTION: tasks.append(self.fetch_collection_documents(library, collection)) @@ -87,17 +97,20 @@ async def list_collection_items( return ItemSampleResult(items=all_items, total_count=len(all_items)) async def fetch_subcollections(self, library, collection): - prefix = self.resolve_collection_prefix(library, collection) - async with self.network.GET(f"{prefix}/collections/top") as response: + prefix = f"{self.resolve_collection_prefix(library, collection)}/collections" + if collection == "ROOT": + prefix = f"{prefix}/top" + async with self.network.GET(prefix) as response: items_json = await response.json_content() - return [ - ItemResult( - item_id=f'{library}:{item["key"]}', - item_name=item["data"].get("name", "Unnamed title"), - item_type=ItemType.COLLECTION, - ) - for item in items_json - ] + return [self._parse_collection(item, library) for item in items_json] + + @staticmethod + def _parse_collection(item: dict, library: str) -> ItemResult: + return ItemResult( + item_id=f'{ItemType.COLLECTION}:{library}:{item["key"]}', + item_name=item["data"].get("name", "Unnamed title"), + item_type=ItemType.COLLECTION, + ) async def fetch_collection_documents(self, library, collection): prefix = self.resolve_collection_prefix(library, collection) @@ -105,17 +118,18 @@ async def fetch_collection_documents(self, library, collection): f"{prefix}/items/top", query={"format": "csljson"} ) as response: items_json = await response.json_content() - return [ - ItemResult( - item_id=f'{library}:{item["id"]}', - item_name=item.get("title", "Unnamed title"), - item_type=ItemType.DOCUMENT, - csl=item, - ) - for item in items_json["items"] - ] + return [self._parse_document(item, library) for item in items_json["items"]] + + @staticmethod + def _parse_document(item: dict, library: str) -> ItemResult: + return ItemResult( + item_id=f'{ItemType.DOCUMENT}:{library}:{item["id"]}', + item_name=item.get("title", "Unnamed title"), + item_type=ItemType.DOCUMENT, + csl=item, + ) - def resolve_collection_prefix(self, library: str, collection): + def resolve_collection_prefix(self, library: str, collection="ROOT"): if library == "personal": prefix = f"users/{self.config.external_account_id}" else: @@ -123,3 +137,17 @@ def resolve_collection_prefix(self, library: str, collection): if collection != "ROOT": prefix = f"{prefix}/collections/{collection}" return prefix + + async def _fetch_collection(self, library: str, collection_id: str) -> ItemResult: + prefix = self.resolve_collection_prefix(library, collection_id) + async with self.network.GET(prefix, query={"format": "csljson"}) as response: + raw_collection = await response.json_content() + return self._parse_collection(raw_collection, library) + + async def _fetch_document(self, library: str, document_id: str) -> ItemResult: + prefix = self.resolve_collection_prefix(library) + async with self.network.GET( + f"{prefix}/items/{document_id}", query={"format": "csljson"} + ) as response: + raw_collection = await response.json_content() + return self._parse_document(raw_collection, library) diff --git a/addon_imps/tests/citations/test_mendeley.py b/addon_imps/tests/citations/test_mendeley.py index c667b7d5..664c593d 100644 --- a/addon_imps/tests/citations/test_mendeley.py +++ b/addon_imps/tests/citations/test_mendeley.py @@ -110,7 +110,7 @@ async def test_fetch_collection_documents(self): expected_items = [ ItemResult( - item_id="doc1", + item_id="document:doc1", item_name="Doc Title 1", item_type=ItemType.DOCUMENT, item_path=[], @@ -122,7 +122,7 @@ async def test_fetch_collection_documents(self): }, ), ItemResult( - item_id="doc2", + item_id="document:doc2", item_name="Doc Title 2", item_type=ItemType.DOCUMENT, item_path=[], @@ -150,10 +150,14 @@ async def test_fetch_subcollections(self): expected_result = ItemSampleResult( items=[ ItemResult( - item_id="1", item_name="Collection 1", item_type=ItemType.COLLECTION + item_id="collection:1", + item_name="Collection 1", + item_type=ItemType.COLLECTION, ), ItemResult( - item_id="2", item_name="Collection 2", item_type=ItemType.COLLECTION + item_id="collection:2", + item_name="Collection 2", + item_type=ItemType.COLLECTION, ), ], total_count=2, @@ -205,7 +209,7 @@ async def test_list_collection_items(self): ) in cases: with self.subTest(item_filter): result = await self.mendeley_imp.list_collection_items( - "collection_id", filter_items=item_filter + "collection:collection_id", filter_items=item_filter ) for call in calls_to_be_made: call.assert_awaited_once_with("collection_id") diff --git a/addon_imps/tests/citations/test_zotero.py b/addon_imps/tests/citations/test_zotero.py index 63ed8d0e..34e81cdd 100644 --- a/addon_imps/tests/citations/test_zotero.py +++ b/addon_imps/tests/citations/test_zotero.py @@ -55,17 +55,17 @@ async def test_list_root_collections(self): expected_items = [ ItemResult( - item_id="collection-1:", + item_id="collection:collection-1:ROOT", item_name="Collection 1", item_type=ItemType.COLLECTION, ), ItemResult( - item_id="collection-2:", + item_id="collection:collection-2:ROOT", item_name="Collection 2", item_type=ItemType.COLLECTION, ), ItemResult( - item_id="personal:", + item_id="collection:personal:ROOT", item_name="My Library", item_type=ItemType.COLLECTION, ), @@ -108,7 +108,9 @@ async def test_list_collection_items(self): self.zotero_imp.fetch_subcollections = create_autospec( self.zotero_imp.fetch_subcollections, return_value=collections ) - result = await self.zotero_imp.list_collection_items("personal:collection-123") + result = await self.zotero_imp.list_collection_items( + "collection:personal:collection-123" + ) expected_items = docs + collections self.zotero_imp.fetch_collection_documents.assert_awaited_once_with( @@ -156,7 +158,7 @@ async def test_list_collection_items_with_filter(self): for item_type, call, not_call in cases: with self.subTest(item_type): result = await self.zotero_imp.list_collection_items( - "personal:collection-123", filter_items=item_type + "collection:personal:collection-123", filter_items=item_type ) call.assert_awaited_once_with("personal", "collection-123") not_call.assert_not_called() @@ -180,13 +182,13 @@ async def test_fetch_collection_documents(self): ) expected_result = [ ItemResult( - item_id="personal:item-1", + item_id="document:personal:item-1", item_name="Item Title 1", item_type=ItemType.DOCUMENT, csl={"id": "item-1", "title": "Item Title 1"}, ), ItemResult( - item_id="personal:item-2", + item_id="document:personal:item-2", item_name="Item Title 2", item_type=ItemType.DOCUMENT, csl={"id": "item-2", "title": "Item Title 2"}, @@ -214,17 +216,28 @@ async def test_fetch_subcollections(self): ) expected_result = [ ItemResult( - item_id="personal:collection-1", + item_id="collection:personal:collection-1", item_name="Collection 1", item_type=ItemType.COLLECTION, ), ItemResult( - item_id="personal:collection-2", + item_id="collection:personal:collection-2", item_name="Collection 2", item_type=ItemType.COLLECTION, ), ] - result = await self.zotero_imp.fetch_subcollections("personal", "collection") + with self.subTest("ROOT collection"): + result = await self.zotero_imp.fetch_subcollections("personal", "ROOT") - self.assertEqual(result, expected_result) - self.zotero_imp.network.GET.assert_called_once_with("la/collections/top") + self.assertEqual(result, expected_result) + self.zotero_imp.network.GET.assert_called_once_with("la/collections/top") + + self.zotero_imp.network.GET.reset_mock() + + with self.subTest("ordinary collection"): + result = await self.zotero_imp.fetch_subcollections( + "personal", "some-collection" + ) + + self.assertEqual(result, expected_result) + self.zotero_imp.network.GET.assert_called_once_with("la/collections") diff --git a/addon_service/management/commands/migrate_authorized_account.py b/addon_service/management/commands/migrate_authorized_account.py index 37fcbff8..d81cc14a 100644 --- a/addon_service/management/commands/migrate_authorized_account.py +++ b/addon_service/management/commands/migrate_authorized_account.py @@ -114,9 +114,9 @@ def get_root_folder_for_provider(node_settings, service_name): case "bitbucket": return f"repository:{node_settings.user}/{node_settings.repo}" case "zotero": - return f"{node_settings.library_id}/{node_settings.list_id}" + return f"collection:{node_settings.library_id}:{node_settings.list_id}" case "mendeley": - return node_settings.list_id + return f"collection:{node_settings.list_id}" case "boa": return None diff --git a/addon_toolkit/interfaces/citation.py b/addon_toolkit/interfaces/citation.py index 7295a316..2fdb8aec 100644 --- a/addon_toolkit/interfaces/citation.py +++ b/addon_toolkit/interfaces/citation.py @@ -77,6 +77,10 @@ def list_collection_items( ) -> ItemSampleResult: """Lists directories (or collections) and sources (books) inside root""" + @immediate_operation(capability=AddonCapabilities.ACCESS) + def get_item_info(self, item_id: str) -> ItemResult: + """Lists directories (or collections) and sources (books) inside root""" + @dataclasses.dataclass(frozen=True) class CitationAddonImp(AddonImp):