From 9e569476d94d2e6cb11908d1860f38b1f2de63ed Mon Sep 17 00:00:00 2001 From: alejandromumo Date: Mon, 21 Oct 2024 16:05:20 +0200 Subject: [PATCH 1/2] collections: added task to compute num of records * added task to compute number of records for all the collections * added "collection_id" parameter to record search * added service methods to read collections (many, all) * added tests * collections: refactor 'resolve' to 'read' * collections: rename 'search_records' method * collections: update read method signature --- invenio_rdm_records/collections/api.py | 30 +++++-- invenio_rdm_records/collections/models.py | 20 ++++- invenio_rdm_records/collections/results.py | 22 +++-- invenio_rdm_records/collections/schema.py | 5 +- invenio_rdm_records/collections/service.py | 93 +++++++++++++++++++-- invenio_rdm_records/collections/tasks.py | 31 +++++++ setup.cfg | 1 + tests/collections/test_collections_api.py | 8 +- tests/collections/test_collections_tasks.py | 44 ++++++++++ tests/services/test_collections_service.py | 79 +++++++++++++++++ 10 files changed, 304 insertions(+), 29 deletions(-) create mode 100644 invenio_rdm_records/collections/tasks.py create mode 100644 tests/collections/test_collections_tasks.py diff --git a/invenio_rdm_records/collections/api.py b/invenio_rdm_records/collections/api.py index fc2bc3753..10a686935 100644 --- a/invenio_rdm_records/collections/api.py +++ b/invenio_rdm_records/collections/api.py @@ -69,10 +69,10 @@ def create(cls, slug, title, query, ctree=None, parent=None, order=None, depth=2 ) @classmethod - def resolve(cls, *, id_=None, slug=None, ctree_id=None, depth=2): - """Resolve a collection by ID or slug. + def read(cls, *, id_=None, slug=None, ctree_id=None, depth=2): + """Read a collection by ID or slug. - To resolve by slug, the collection tree ID must be provided. + To read by slug, the collection tree ID must be provided. """ res = None if id_: @@ -89,10 +89,21 @@ def resolve(cls, *, id_=None, slug=None, ctree_id=None, depth=2): return res @classmethod - def resolve_many(cls, ids_=None, depth=2): - """Resolve many collections by ID.""" - _ids = ids_ or [] - return [cls(c, depth) for c in cls.model_cls.read_many(_ids)] + def read_many(cls, ids_, depth=2): + """Read many collections by ID.""" + return [cls(c, depth) for c in cls.model_cls.read_many(ids_)] + + @classmethod + def read_all(cls, depth=2): + """Read all collections.""" + return [cls(c, depth) for c in cls.model_cls.read_all()] + + def update(self, **kwargs): + """Update the collection.""" + if "search_query" in kwargs: + Collection.validate_query(kwargs["search_query"]) + self.model.update(**kwargs) + return self def add(self, slug, title, query, order=None, depth=2): """Add a subcollection to the collection.""" @@ -128,7 +139,10 @@ def query(self): @cached_property def ancestors(self): """Get the collection ancestors.""" - return Collection.resolve_many(self.split_path_to_ids()) + ids_ = self.split_path_to_ids() + if not ids_: + return [] + return Collection.read_many(ids_) @cached_property def subcollections(self): diff --git a/invenio_rdm_records/collections/models.py b/invenio_rdm_records/collections/models.py index 740242b1b..5d9c0820a 100644 --- a/invenio_rdm_records/collections/models.py +++ b/invenio_rdm_records/collections/models.py @@ -156,9 +156,25 @@ def get_by_slug(cls, slug, tree_id): return cls.query.filter(cls.slug == slug, cls.tree_id == tree_id).one_or_none() @classmethod - def read_many(cls, ids): + def read_many(cls, ids_): """Get many collections by ID.""" - return cls.query.filter(cls.id.in_(ids)).order_by(cls.path, cls.order) + return cls.query.filter(cls.id.in_(ids_)).order_by(cls.path, cls.order) + + @classmethod + def read_all(cls): + """Get all collections. + + The collections are ordered by ``path`` and ``order``, which means: + + - By path: the collections are ordered in a breadth-first manner (first come the root collection, then the next level, and so on) + - By order: between the same level collections, they are ordered by the specified order field. + """ + return cls.query.order_by(cls.path, cls.order) + + def update(self, **kwargs): + """Update a collection.""" + for key, value in kwargs.items(): + setattr(self, key, value) @classmethod def get_children(cls, model): diff --git a/invenio_rdm_records/collections/results.py b/invenio_rdm_records/collections/results.py index d0acb50af..21906002c 100644 --- a/invenio_rdm_records/collections/results.py +++ b/invenio_rdm_records/collections/results.py @@ -66,7 +66,9 @@ def to_dict(self): res = { "root": self._collection.id, self._collection.id: { - **self._schema.dump(self._collection), + **self._schema.dump( + self._collection, context={"identity": self._identity} + ), "children": list(), "links": self._links_tpl.expand(self._identity, self._collection), }, @@ -76,7 +78,7 @@ def to_dict(self): if _c.id not in res: # Add the subcollection to the dictionary res[_c.id] = { - **self._schema.dump(_c), + **self._schema.dump(_c, context={"identity": self._identity}), "children": list(), "links": self._links_tpl.expand(self._identity, _c), } @@ -121,22 +123,30 @@ def query(self): class CollectionList(ServiceListResult): """Collection list item.""" - def __init__(self, collections): + def __init__(self, identity, collections, schema, links_tpl, links_item_tpl): """Instantiate a Collection list item.""" + self._identity = identity self._collections = collections + self._schema = schema + self._links_tpl = links_tpl + self._links_item_tpl = links_item_tpl def to_dict(self): """Serialize the collection list to a dictionary.""" res = [] for collection in self._collections: - _r = collection.to_dict() - _r["links"] = CollectionItem(collection).links + _r = CollectionItem( + self._identity, collection, self._schema, self._links_item_tpl + ).to_dict() res.append(_r) return res def __iter__(self): """Iterate over the collections.""" - return iter(self._collections) + return ( + CollectionItem(self._identity, x, self._schema, self._links_item_tpl) + for x in self._collections + ) class CollectionTreeItem: diff --git a/invenio_rdm_records/collections/schema.py b/invenio_rdm_records/collections/schema.py index f1241c778..f2368368b 100644 --- a/invenio_rdm_records/collections/schema.py +++ b/invenio_rdm_records/collections/schema.py @@ -14,7 +14,8 @@ class CollectionSchema(Schema): slug = fields.Str() title = fields.Str() - depth = fields.Int() + depth = fields.Int(dump_only=True) order = fields.Int() - id = fields.Int() + id = fields.Int(dump_only=True) num_records = fields.Int() + search_query = fields.Str(load_only=True) diff --git a/invenio_rdm_records/collections/service.py b/invenio_rdm_records/collections/service.py index a526200b7..15484aca6 100644 --- a/invenio_rdm_records/collections/service.py +++ b/invenio_rdm_records/collections/service.py @@ -9,13 +9,19 @@ import os from flask import current_app, url_for +from invenio_records_resources.services import ServiceSchemaWrapper from invenio_records_resources.services.base import Service from invenio_records_resources.services.uow import ModelCommitOp, unit_of_work +from invenio_rdm_records.proxies import ( + current_community_records_service, + current_rdm_records_service, +) + from .api import Collection, CollectionTree from .errors import LogoNotFoundError from .links import CollectionLinkstemplate -from .results import CollectionItem, CollectionTreeList +from .results import CollectionItem, CollectionList, CollectionTreeList class CollectionsService(Service): @@ -30,7 +36,7 @@ def __init__(self, config): @property def collection_schema(self): """Get the collection schema.""" - return self.config.schema() + return ServiceSchemaWrapper(self, schema=self.config.schema) @property def links_item_tpl(self): @@ -46,7 +52,11 @@ def links_item_tpl(self): def create( self, identity, community_id, tree_slug, slug, title, query, uow=None, **kwargs ): - """Create a new collection.""" + """Create a new collection. + + The created collection will be added to the collection tree as a root collection (no parent). + If a parent is needed, use the ``add`` method. + """ self.require_permission(identity, "update", community_id=community_id) ctree = CollectionTree.resolve(slug=tree_slug, community_id=community_id) collection = self.collection_cls.create( @@ -59,9 +69,8 @@ def create( def read( self, - /, - *, identity=None, + *, id_=None, slug=None, community_id=None, @@ -74,10 +83,10 @@ def read( To resolve by slug, the collection tree ID and community ID must be provided. """ if id_: - collection = self.collection_cls.resolve(id_=id_, depth=depth) + collection = self.collection_cls.read(id_=id_, depth=depth) elif slug and tree_slug and community_id: ctree = CollectionTree.resolve(slug=tree_slug, community_id=community_id) - collection = self.collection_cls.resolve( + collection = self.collection_cls.read( slug=slug, ctree_id=ctree.id, depth=depth ) else: @@ -121,6 +130,31 @@ def add(self, identity, collection, slug, title, query, uow=None, **kwargs): identity, new_collection, self.collection_schema, self.links_item_tpl ) + @unit_of_work() + def update(self, identity, collection_or_id, data=None, uow=None): + """Update a collection.""" + if isinstance(collection_or_id, int): + collection = self.collection_cls.read(id_=collection_or_id) + else: + collection = collection_or_id + self.require_permission( + identity, "update", community_id=collection.community.id + ) + + data = data or {} + + valid_data, errors = self.collection_schema.load( + data, context={"identity": identity}, raise_errors=True + ) + + res = collection.update(**valid_data) + + uow.register(ModelCommitOp(res.model)) + + return CollectionItem( + identity, collection, self.collection_schema, self.links_item_tpl + ) + def read_logo(self, identity, slug): """Read a collection logo. @@ -131,3 +165,48 @@ def read_logo(self, identity, slug): if _exists: return url_for("static", filename=logo_path) raise LogoNotFoundError() + + def read_many(self, identity, ids_, depth=2): + """Get many collections.""" + self.require_permission(identity, "read") + + if ids_ is None: + raise ValueError("IDs must be provided.") + + if ids_ == []: + raise ValueError("Use read_all to get all collections.") + + res = self.collection_cls.read_many(ids_, depth=depth) + return CollectionList( + identity, res, self.collection_schema, None, self.links_item_tpl + ) + + def read_all(self, identity, depth=2): + """Get all collections.""" + self.require_permission(identity, "read") + res = self.collection_cls.read_all(depth=depth) + return CollectionList( + identity, res, self.collection_schema, None, self.links_item_tpl + ) + + def search_collection_records(self, identity, collection_or_id, params=None): + """Search records in a collection.""" + params = params or {} + + if isinstance(collection_or_id, int): + collection = self.collection_cls.read(id_=collection_or_id) + else: + collection = collection_or_id + + params.update({"collection_id": collection.id}) + if collection.community: + res = current_community_records_service.search( + identity, + community_id=collection.community.id, + params=params, + ) + else: + raise NotImplementedError( + "Search for collections without community not supported." + ) + return res diff --git a/invenio_rdm_records/collections/tasks.py b/invenio_rdm_records/collections/tasks.py new file mode 100644 index 000000000..951c9d2d2 --- /dev/null +++ b/invenio_rdm_records/collections/tasks.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2024 CERN. +# +# Invenio-RDM-Records is free software; you can redistribute it and/or modify +# it under the terms of the MIT License; see LICENSE file for more details. +"""Collections celery tasks.""" + +from celery import shared_task +from flask import current_app +from invenio_access.permissions import system_identity + +from invenio_rdm_records.proxies import current_rdm_records + + +@shared_task(ignore_result=True) +def update_collections_size(): + """Calculate and update the size of all the collections.""" + collections_service = current_rdm_records.collections_service + res = collections_service.read_all(system_identity, depth=0) + for citem in res: + try: + collection = citem._collection + res = collections_service.search_collection_records( + system_identity, collection + ) + collections_service.update( + system_identity, collection, data={"num_records": res.total} + ) + except Exception as e: + current_app.logger.exception(str(e)) diff --git a/setup.cfg b/setup.cfg index 450fe0bf4..0d73d84c0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -109,6 +109,7 @@ invenio_celery.tasks = invenio_rdm_records_access_requests = invenio_rdm_records.requests.access.tasks invenio_rdm_records_iiif = invenio_rdm_records.services.iiif.tasks invenio_rdm_records_user_moderation = invenio_rdm_records.requests.user_moderation.tasks + invenio_rdm_records_collections = invenio_rdm_records.collections.tasks invenio_db.models = invenio_rdm_records = invenio_rdm_records.records.models invenio_rdm_records_collections = invenio_rdm_records.collections.models diff --git a/tests/collections/test_collections_api.py b/tests/collections/test_collections_api.py index eff421b3b..ddb91e10b 100644 --- a/tests/collections/test_collections_api.py +++ b/tests/collections/test_collections_api.py @@ -27,7 +27,7 @@ def test_create(running_app, db, community, community_owner): ctree=tree, ) - read_c = Collection.resolve(id_=collection.id) + read_c = Collection.read(id_=collection.id) assert read_c.id == collection.id assert read_c.title == "My Collection" assert read_c.collection_tree.id == tree.id @@ -40,7 +40,7 @@ def test_create(running_app, db, community, community_owner): ctree=tree.id, ) - read_c = Collection.resolve(id_=collection.id) + read_c = Collection.read(id_=collection.id) assert read_c.id == collection.id assert collection.title == "My Collection 2" assert collection.collection_tree.id == tree.id @@ -63,11 +63,11 @@ def test_resolve(running_app, db, community): ) # Read by ID - read_by_id = Collection.resolve(id_=collection.id) + read_by_id = Collection.read(id_=collection.id) assert read_by_id.id == collection.id # Read by slug - read_by_slug = Collection.resolve(slug="my-collection", ctree_id=tree.id) + read_by_slug = Collection.read(slug="my-collection", ctree_id=tree.id) assert read_by_slug.id == read_by_id.id == collection.id diff --git a/tests/collections/test_collections_tasks.py b/tests/collections/test_collections_tasks.py new file mode 100644 index 000000000..85bd63d99 --- /dev/null +++ b/tests/collections/test_collections_tasks.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2024 CERN. +# +# Invenio-RDM-Records is free software; you can redistribute it and/or modify +# it under the terms of the MIT License; see LICENSE file for more details. +"""Test celery tasks of collections.""" + +from copy import deepcopy + +from invenio_rdm_records.collections.api import Collection, CollectionTree +from invenio_rdm_records.collections.tasks import update_collections_size + + +def test_update_collections_size(app, db, record_factory, minimal_record, community): + """Test update_collections_size task.""" + tree = CollectionTree.create( + title="Tree 1", + order=10, + community_id=community.id, + slug="tree-1", + ) + + collection = Collection.create( + title="My Collection", + query="metadata.title:foo", + slug="my-collection", + ctree=tree, + ) + update_collections_size() + + # Check that the collections have been updated + collection = Collection.read(id_=collection.id) + assert collection.num_records == 0 + + # Add a record that matches the collection + rec = deepcopy(minimal_record) + rec["metadata"]["title"] = "foo" + record = record_factory.create_record(record_dict=rec, community=community) + + update_collections_size() + + collection = Collection.read(id_=collection.id) + assert collection.num_records == 1 diff --git a/tests/services/test_collections_service.py b/tests/services/test_collections_service.py index 428617b6e..e56a80d69 100644 --- a/tests/services/test_collections_service.py +++ b/tests/services/test_collections_service.py @@ -250,3 +250,82 @@ def test_collections_results( } assert not list(dictdiffer.diff(expected, r_dict)) + + +def test_update(running_app, db, add_collections, collections_service, community_owner): + """Test updating a collection.""" + collections = add_collections() + c0 = collections[0] + + # Update by ID + collections_service.update( + community_owner.identity, + c0.id, + data={"slug": "New slug"}, + ) + + res = collections_service.read( + identity=community_owner.identity, + id_=c0.id, + ) + + assert res.to_dict()[c0.id]["slug"] == "New slug" + + # Update by object + collections_service.update( + community_owner.identity, + c0, + data={"slug": "New slug 2"}, + ) + + res = collections_service.read( + identity=community_owner.identity, + id_=c0.id, + ) + assert res.to_dict()[c0.id]["slug"] == "New slug 2" + + +def test_read_many( + running_app, db, add_collections, collections_service, community_owner +): + """Test reading multiple collections.""" + collections = add_collections() + c0 = collections[0] + c1 = collections[1] + + # Read two collections + res = collections_service.read_many( + community_owner.identity, + ids_=[c0.id, c1.id], + depth=0, + ) + + res = res.to_dict() + assert len(res) == 2 + assert res[0]["root"] == c0.id + assert res[1]["root"] == c1.id + + +def test_read_all( + running_app, db, add_collections, collections_service, community_owner +): + """Test reading all collections.""" + collections = add_collections() + c0 = collections[0] + c1 = collections[1] + + # Read all collections + res = collections_service.read_all(community_owner.identity, depth=0) + + res = res.to_dict() + assert len(res) == 2 + assert res[0]["root"] == c0.id + assert res[1]["root"] == c1.id + + +def test_read_invalid(running_app, db, collections_service, community_owner): + """Test reading a non-existing collection.""" + with pytest.raises(ValueError): + collections_service.read( + identity=community_owner.identity, + ) From 27eeac6dbdfd8e141a02faa19e63108cc8bda2b3 Mon Sep 17 00:00:00 2001 From: alejandromumo Date: Fri, 25 Oct 2024 17:10:12 +0100 Subject: [PATCH 2/2] collections: move records search into service * Added resource for collections * Moved records search from community records to collections service --- .../collections/resources/__init__.py | 7 +++ .../collections/resources/config.py | 50 +++++++++++++++++++ .../collections/resources/resource.py | 43 ++++++++++++++++ invenio_rdm_records/collections/searchapp.py | 5 -- .../collections/services/__init__.py | 7 +++ .../collections/{ => services}/config.py | 6 +-- .../collections/{ => services}/links.py | 1 + .../collections/{ => services}/results.py | 0 .../collections/{ => services}/schema.py | 0 .../collections/{ => services}/service.py | 7 ++- invenio_rdm_records/ext.py | 12 ++++- invenio_rdm_records/resources/args.py | 6 --- invenio_rdm_records/resources/config.py | 4 +- .../services/community_records/service.py | 8 --- invenio_rdm_records/views.py | 6 +++ setup.cfg | 1 + tests/services/test_collections_service.py | 5 ++ .../test_service_community_records.py | 35 ------------- 18 files changed, 135 insertions(+), 68 deletions(-) create mode 100644 invenio_rdm_records/collections/resources/__init__.py create mode 100644 invenio_rdm_records/collections/resources/config.py create mode 100644 invenio_rdm_records/collections/resources/resource.py create mode 100644 invenio_rdm_records/collections/services/__init__.py rename invenio_rdm_records/collections/{ => services}/config.py (86%) rename invenio_rdm_records/collections/{ => services}/links.py (96%) rename invenio_rdm_records/collections/{ => services}/results.py (100%) rename invenio_rdm_records/collections/{ => services}/schema.py (100%) rename invenio_rdm_records/collections/{ => services}/service.py (97%) diff --git a/invenio_rdm_records/collections/resources/__init__.py b/invenio_rdm_records/collections/resources/__init__.py new file mode 100644 index 000000000..d9b40da79 --- /dev/null +++ b/invenio_rdm_records/collections/resources/__init__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2024 CERN. +# +# Invenio-RDM is free software; you can redistribute it and/or modify +# it under the terms of the MIT License; see LICENSE file for more details. +"""Collection resource module.""" diff --git a/invenio_rdm_records/collections/resources/config.py b/invenio_rdm_records/collections/resources/config.py new file mode 100644 index 000000000..581dfea71 --- /dev/null +++ b/invenio_rdm_records/collections/resources/config.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2024 CERN. +# +# Invenio-RDM is free software; you can redistribute it and/or modify +# it under the terms of the MIT License; see LICENSE file for more details. +"""Collection resource config.""" + +from flask_resources import ( + HTTPJSONException, + JSONSerializer, + ResourceConfig, + ResponseHandler, + create_error_handler, +) +from invenio_records_resources.resources.records.args import SearchRequestArgsSchema +from invenio_records_resources.resources.records.headers import etag_headers +from marshmallow.fields import Integer + +from invenio_rdm_records.resources.serializers import UIJSONSerializer + +from ..errors import CollectionNotFound + + +class CollectionsResourceConfig(ResourceConfig): + """Configuration for the Collection resource.""" + + blueprint_name = "collections" + url_prefix = "/collections" + + routes = { + "search-records": "//records", + } + + request_view_args = {"id": Integer()} + request_search_args = SearchRequestArgsSchema + error_handlers = { + CollectionNotFound: create_error_handler( + HTTPJSONException( + code=404, + description="Collection was not found.", + ) + ), + } + response_handlers = { + "application/json": ResponseHandler(JSONSerializer(), headers=etag_headers), + "application/vnd.inveniordm.v1+json": ResponseHandler( + UIJSONSerializer(), headers=etag_headers + ), + } diff --git a/invenio_rdm_records/collections/resources/resource.py b/invenio_rdm_records/collections/resources/resource.py new file mode 100644 index 000000000..2b505e184 --- /dev/null +++ b/invenio_rdm_records/collections/resources/resource.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2024 CERN. +# +# Invenio-RDM is free software; you can redistribute it and/or modify +# it under the terms of the MIT License; see LICENSE file for more details. +"""Collection resource.""" + +from flask import g +from flask_resources import Resource, resource_requestctx, response_handler, route +from invenio_records_resources.resources.records.resource import ( + request_search_args, + request_view_args, +) + + +class CollectionsResource(Resource): + """Collection resource.""" + + def __init__(self, config, service): + """Instantiate the resource.""" + super().__init__(config) + self.service = service + + def create_url_rules(self): + """Create the URL rules for the record resource.""" + routes = self.config.routes + return [ + route("GET", routes["search-records"], self.search_records), + ] + + @request_view_args + @request_search_args + @response_handler(many=True) + def search_records(self): + """Search records in a collection.""" + id_ = resource_requestctx.view_args["id"] + records = self.service.search_collection_records( + g.identity, + id_, + params=resource_requestctx.args, + ) + return records.to_dict(), 200 diff --git a/invenio_rdm_records/collections/searchapp.py b/invenio_rdm_records/collections/searchapp.py index a48e2699a..2a05e26b1 100644 --- a/invenio_rdm_records/collections/searchapp.py +++ b/invenio_rdm_records/collections/searchapp.py @@ -23,10 +23,5 @@ def search_app_context(): sort_options=current_app.config["RDM_SORT_OPTIONS"], headers={"Accept": "application/vnd.inveniordm.v1+json"}, pagination_options=(10, 25, 50, 100), - # endpoint=/communities/eu/records - # endpoint=/api/records - # hidden_params=[ - # ["q", collection.query] - # ] ) } diff --git a/invenio_rdm_records/collections/services/__init__.py b/invenio_rdm_records/collections/services/__init__.py new file mode 100644 index 000000000..10d60ea45 --- /dev/null +++ b/invenio_rdm_records/collections/services/__init__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2024 CERN. +# +# Invenio-RDM is free software; you can redistribute it and/or modify +# it under the terms of the MIT License; see LICENSE file for more details. +"""Collection service module.""" diff --git a/invenio_rdm_records/collections/config.py b/invenio_rdm_records/collections/services/config.py similarity index 86% rename from invenio_rdm_records/collections/config.py rename to invenio_rdm_records/collections/services/config.py index 835a19a36..62d6264a4 100644 --- a/invenio_rdm_records/collections/config.py +++ b/invenio_rdm_records/collections/services/config.py @@ -28,11 +28,7 @@ class CollectionServiceConfig(ServiceConfig, ConfiguratorMixin): schema = CollectionSchema links_item = { - "search": ConditionalLink( - cond=lambda coll, ctx: coll.community, - if_=CollectionLink("/api/communities/{community}/records"), - else_="/api/records", - ), + "search": CollectionLink("/api/collections/{id}/records"), "self_html": ConditionalLink( cond=lambda coll, ctx: coll.community, if_=CollectionLink( diff --git a/invenio_rdm_records/collections/links.py b/invenio_rdm_records/collections/services/links.py similarity index 96% rename from invenio_rdm_records/collections/links.py rename to invenio_rdm_records/collections/services/links.py index ecff01f29..4ba6f0064 100644 --- a/invenio_rdm_records/collections/links.py +++ b/invenio_rdm_records/collections/services/links.py @@ -28,5 +28,6 @@ def vars(collection, vars): "community": collection.community.slug, "tree": collection.collection_tree.slug, "collection": collection.slug, + "id": collection.id, } ) diff --git a/invenio_rdm_records/collections/results.py b/invenio_rdm_records/collections/services/results.py similarity index 100% rename from invenio_rdm_records/collections/results.py rename to invenio_rdm_records/collections/services/results.py diff --git a/invenio_rdm_records/collections/schema.py b/invenio_rdm_records/collections/services/schema.py similarity index 100% rename from invenio_rdm_records/collections/schema.py rename to invenio_rdm_records/collections/services/schema.py diff --git a/invenio_rdm_records/collections/service.py b/invenio_rdm_records/collections/services/service.py similarity index 97% rename from invenio_rdm_records/collections/service.py rename to invenio_rdm_records/collections/services/service.py index 15484aca6..d7b312ba4 100644 --- a/invenio_rdm_records/collections/service.py +++ b/invenio_rdm_records/collections/services/service.py @@ -18,8 +18,8 @@ current_rdm_records_service, ) -from .api import Collection, CollectionTree -from .errors import LogoNotFoundError +from ..api import Collection, CollectionTree +from ..errors import LogoNotFoundError from .links import CollectionLinkstemplate from .results import CollectionItem, CollectionList, CollectionTreeList @@ -198,12 +198,11 @@ def search_collection_records(self, identity, collection_or_id, params=None): else: collection = collection_or_id - params.update({"collection_id": collection.id}) if collection.community: res = current_community_records_service.search( identity, community_id=collection.community.id, - params=params, + extra_filter=collection.query, ) else: raise NotImplementedError( diff --git a/invenio_rdm_records/ext.py b/invenio_rdm_records/ext.py index f96743995..be975dd37 100644 --- a/invenio_rdm_records/ext.py +++ b/invenio_rdm_records/ext.py @@ -19,8 +19,10 @@ from invenio_records_resources.resources.files import FileResource from . import config -from .collections.config import CollectionServiceConfig -from .collections.service import CollectionsService +from .collections.resources.config import CollectionsResourceConfig +from .collections.resources.resource import CollectionsResource +from .collections.services.config import CollectionServiceConfig +from .collections.services.service import CollectionsService from .oaiserver.resources.config import OAIPMHServerResourceConfig from .oaiserver.resources.resources import OAIPMHServerResource from .oaiserver.services.config import OAIPMHServerServiceConfig @@ -278,6 +280,12 @@ def init_resource(self, app): config=RDMCommunityRecordsResourceConfig.build(app), ) + # Collections + self.collections_resource = CollectionsResource( + service=self.collections_service, + config=CollectionsResourceConfig, + ) + # OAI-PMH self.oaipmh_server_resource = OAIPMHServerResource( service=self.oaipmh_server_service, diff --git a/invenio_rdm_records/resources/args.py b/invenio_rdm_records/resources/args.py index 1d9e73fbf..90531a126 100644 --- a/invenio_rdm_records/resources/args.py +++ b/invenio_rdm_records/resources/args.py @@ -19,9 +19,3 @@ class RDMSearchRequestArgsSchema(SearchRequestArgsSchema): locale = fields.Str() status = fields.Str() include_deleted = fields.Bool() - - -class CommunityRecordsSearchRequestArgsSchema(SearchRequestArgsSchema): - """Extend schema with collection_id field.""" - - collection_id = fields.Integer() diff --git a/invenio_rdm_records/resources/config.py b/invenio_rdm_records/resources/config.py index 522372ee5..192ab61ad 100644 --- a/invenio_rdm_records/resources/config.py +++ b/invenio_rdm_records/resources/config.py @@ -46,7 +46,7 @@ ReviewStateError, ValidationErrorWithMessageAsList, ) -from .args import CommunityRecordsSearchRequestArgsSchema, RDMSearchRequestArgsSchema +from .args import RDMSearchRequestArgsSchema from .deserializers import ROCrateJSONDeserializer from .deserializers.errors import DeserializerError from .errors import HTTPJSONException, HTTPJSONValidationWithMessageAsListException @@ -558,8 +558,6 @@ class RDMCommunityRecordsResourceConfig(RecordResourceConfig, ConfiguratorMixin) default=record_serializers, ) - request_search_args = CommunityRecordsSearchRequestArgsSchema - class RDMRecordCommunitiesResourceConfig(CommunityResourceConfig, ConfiguratorMixin): """Record communities resource config.""" diff --git a/invenio_rdm_records/services/community_records/service.py b/invenio_rdm_records/services/community_records/service.py index 04be70ab3..7209f676b 100644 --- a/invenio_rdm_records/services/community_records/service.py +++ b/invenio_rdm_records/services/community_records/service.py @@ -67,14 +67,6 @@ def search( if extra_filter is not None: community_filter = community_filter & extra_filter - # Search in a specific collection - if "collection_id" in params: - collections_service = current_rdm_records.collections_service - collection = collections_service.read( - identity=identity, id_=params["collection_id"] - ) - community_filter &= collection.query - search = self._search( "search", identity, diff --git a/invenio_rdm_records/views.py b/invenio_rdm_records/views.py index 800d7a121..a9e4ab7f9 100644 --- a/invenio_rdm_records/views.py +++ b/invenio_rdm_records/views.py @@ -102,3 +102,9 @@ def create_iiif_bp(app): """Create IIIF blueprint.""" ext = app.extensions["invenio-rdm-records"] return ext.iiif_resource.as_blueprint() + + +def create_collections_bp(app): + """Create collections blueprint.""" + ext = app.extensions["invenio-rdm-records"] + return ext.collections_resource.as_blueprint() diff --git a/setup.cfg b/setup.cfg index 0d73d84c0..2e1f9052c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -97,6 +97,7 @@ invenio_base.api_blueprints = invenio_rdm_record_communities = invenio_rdm_records.views:create_record_communities_bp invenio_rdm_record_requests = invenio_rdm_records.views:create_record_requests_bp invenio_iiif = invenio_rdm_records.views:create_iiif_bp + invenio_rdm_records_collections = invenio_rdm_records.views:create_collections_bp invenio_base.api_finalize_app = invenio_rdm_records = invenio_rdm_records.ext:api_finalize_app invenio_base.blueprints = diff --git a/tests/services/test_collections_service.py b/tests/services/test_collections_service.py index e56a80d69..8915b02b7 100644 --- a/tests/services/test_collections_service.py +++ b/tests/services/test_collections_service.py @@ -170,6 +170,7 @@ def test_collections_results( "links": { "search": "/api/communities/blr/records", "self_html": "/communities/blr/collections/tree-1/collection-1", + "search": f"/api/collections/{c0.id}/records", }, "num_records": 0, "order": c0.order, @@ -183,6 +184,7 @@ def test_collections_results( "links": { "search": "/api/communities/blr/records", "self_html": "/communities/blr/collections/tree-1/collection-2", + "search": f"/api/collections/{c1.id}/records", }, "num_records": 0, "order": c1.order, @@ -215,6 +217,7 @@ def test_collections_results( "links": { "search": "/api/communities/blr/records", "self_html": "/communities/blr/collections/tree-1/collection-1", + "search": f"/api/collections/{c0.id}/records", }, "num_records": 0, "order": c0.order, @@ -228,6 +231,7 @@ def test_collections_results( "links": { "search": "/api/communities/blr/records", "self_html": "/communities/blr/collections/tree-1/collection-2", + "search": f"/api/collections/{c1.id}/records", }, "num_records": 0, "order": c1.order, @@ -241,6 +245,7 @@ def test_collections_results( "links": { "search": "/api/communities/blr/records", "self_html": "/communities/blr/collections/tree-1/collection-3", + "search": f"/api/collections/{c3.id}/records", }, "num_records": 0, "order": c3.order, diff --git a/tests/services/test_service_community_records.py b/tests/services/test_service_community_records.py index 9c48c40ae..67885f4bb 100644 --- a/tests/services/test_service_community_records.py +++ b/tests/services/test_service_community_records.py @@ -159,38 +159,3 @@ def test_search_community_records( community_id=str(community.id), ) assert results.to_dict()["hits"]["total"] == 3 - - -def test_search_community_records_in_collections( - community, record_community, service, uploader, minimal_record -): - """Test search for records in a community collection.""" - rec1 = deepcopy(minimal_record) - rec1["metadata"]["title"] = "Another record" - record_community.create_record(record_dict=rec1) - record_community.create_record(record_dict=minimal_record) - ctree = CollectionTree.create( - title="Tree 1", - order=10, - community_id=community.id, - slug="tree-1", - ) - collection = Collection.create( - title="My Collection", - query='metadata.title:"Another record"', - slug="my-collection", - ctree=ctree, - ) - all_results = service.search( - uploader.identity, - community_id=str(community.id), - ) - assert all_results.total == 2 - - results = service.search( - uploader.identity, - community_id=str(community.id), - params={"collection_id": str(collection.id)}, - ) - assert results.total == 1 - assert results.to_dict()["hits"]["hits"][0]["metadata"]["title"] == "Another record"