Repository: inveniosoftware/invenio-communities (language: python)

Issue:
OAI-PMH sets for public communities



Discussion / hints:

Fix patch:
diff --git a/invenio_communities/communities/services/components.py b/invenio_communities/communities/services/components.py
index f396c6d1e..e9a367f42 100644
--- a/invenio_communities/communities/services/components.py
+++ b/invenio_communities/communities/services/components.py
@@ -3,6 +3,7 @@
 # This file is part of Invenio.
 # Copyright (C) 2016-2022 CERN.
 # Copyright (C) 2022 Northwestern University.
+# Copyright (C) 2022 Graz University of Technology.
 #
 # Invenio is free software; you can redistribute it and/or modify it
 # under the terms of the MIT License; see LICENSE file for more details.
@@ -12,6 +13,8 @@
 from flask_babelex import lazy_gettext as _
 
 from invenio_access.permissions import system_identity, system_process
+from invenio_db import db
+from invenio_oaiserver.models import OAISet
 from invenio_pidstore.errors import PIDAlreadyExists
 from invenio_records_resources.services.records.components import \
     ServiceComponent
@@ -182,3 +185,81 @@ def featured_create(self, identity, data=None, record=None, **kwargs):
                 _("The community is not public"),
                 field_name="community_id"
             )
+
+
+class OAISetComponent(ServiceComponent):
+    """Service component for OAI set integration."""
+
+    _oai_sets_prefix = "community"
+
+    def _retrieve_set(self, id):
+        return OAISet.query.filter(OAISet.spec == self._create_set_spec(id)).first()
+
+    def _create_set_spec(self, community_id):
+        return "{prefix}-{id}".format(prefix=self._oai_sets_prefix, id=community_id)
+
+    def _create_set_description(self, community_title):
+        # NOTE: Does not require translation since this description can also be changed by an admin
+        return "Records belonging to the community '{title}'".format(title=community_title)
+
+    def _create_set_from_community(self, record,):
+        community_id = record.pid.pid_value
+        community_title = record.metadata["title"]
+
+        community_set = OAISet()
+        community_set.name = community_title
+        community_set.description = self._create_set_description(community_title)
+        community_set.spec = self._create_set_spec(community_id)
+        community_set.search_pattern = "parent.communities.ids:{id}".format(id=community_id)
+
+        return community_set
+
+    def create(self, identity, data=None, record=None, **kwargs):
+        """Create a set for the community."""
+        if record.access.visibility != "public":
+            return
+
+        community_set = self._create_set_from_community(record)
+        # NOTE: will be indexed via a listener in oaiserver module
+        db.session.add(community_set)
+
+
+    def delete(self, identity, data=None, record=None, **kwargs):
+        """Remove the set for the community."""
+        community_set = self._retrieve_set(kwargs.get("id") or record.pid.pid_value)
+        if not community_set:
+            return
+
+        # NOTE: will be removed from index via listener in oaiserver module
+        db.session.delete(community_set)
+
+
+    def update(self, identity, data=None, record=None, **kwargs):
+        """Update set accordingly."""
+        if record.access.visibility != "public":
+            # delete set if the community is not public anymore
+            self.delete(identity, data, record, **kwargs)
+            return
+
+        community_set = self._retrieve_set(record.pid.pid_value)
+        if not community_set:
+            # create new set because community is now public
+            self.create(identity, data, record, **kwargs)
+            return
+
+        # just update set with new name and description
+        community_set.name = record.metadata["title"]
+        community_set.description = self._create_set_description(community_set.name)
+        db.session.add(community_set)
+
+
+    def rename(self, identity, record=None, data=None, **kwargs):
+        """Delete old set and create new set on ID change"""
+        if record.access.visibility != "public":
+            return
+
+        # Since we want to delete the set created with the old set, we can not
+        # use the already updated pid_value
+        self.delete(identity, data=data, record=record, **kwargs, id=record.get("id"))
+        community_set = self._create_set_from_community(record)
+        db.session.add(community_set)
diff --git a/invenio_communities/communities/services/config.py b/invenio_communities/communities/services/config.py
index 16e156fb4..4c40b522a 100644
--- a/invenio_communities/communities/services/config.py
+++ b/invenio_communities/communities/services/config.py
@@ -26,8 +26,8 @@
 
 from ...permissions import CommunityPermissionPolicy
 from ..schema import CommunityFeaturedSchema, CommunitySchema
-from .components import CommunityAccessComponent, FeaturedCommunityComponent, OwnershipComponent, \
-    PIDComponent
+from .components import CommunityAccessComponent, FeaturedCommunityComponent, OAISetComponent, \
+    OwnershipComponent, PIDComponent
 
 from flask_babelex import lazy_gettext as _
 
@@ -97,6 +97,7 @@ class CommunityServiceConfig(RecordServiceConfig):
         CommunityAccessComponent,
         OwnershipComponent,
         FeaturedCommunityComponent,
+        OAISetComponent,
     ]
 
 
diff --git a/setup.cfg b/setup.cfg
index 1c607d09d..4970c5620 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -26,10 +26,11 @@ packages = find:
 python_requires = >=3.6
 zip_safe = False
 install_requires =
+    invenio-oaiserver>=1.4.1,<1.5.0
     invenio-requests>=0.3.8,<0.4.0
+    invenio-search-ui>=2.1.0,<2.2.0
     invenio-users-resources>=0.1.1,<0.2.0
     invenio-vocabularies>=0.11.2,<0.12.0
-    invenio-search-ui>=2.1.0,<2.2.0
 
 [options.extras_require]
 tests =


Test patch:
diff --git a/tests/communities/test_components.py b/tests/communities/test_components.py
new file mode 100644
index 000000000..3a91a18f9
--- /dev/null
+++ b/tests/communities/test_components.py
@@ -0,0 +1,119 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2022 Graz University of Technology.
+#
+# Invenio-Communities 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 components."""
+
+from copy import deepcopy
+from datetime import datetime
+import pytest
+from invenio_access.permissions import system_identity
+
+
+from invenio_communities.communities.services.components import OAISetComponent
+from invenio_oaiserver.models import OAISet
+
+
+def _retrieve_oaiset(service, community):
+    comp = OAISetComponent(service)
+    return OAISet.query.filter(OAISet.spec == comp._create_set_spec(community.get("id"))).first()
+
+
+@pytest.fixture()
+def comm(community_service, minimal_community, location):
+    """Create minimal public community."""
+    c = deepcopy(minimal_community)
+    c["id"] = "public-{id}".format(id=str(datetime.utcnow().timestamp()).replace(".","-"))
+    return community_service.create(data=c, identity=system_identity)
+
+@pytest.fixture()
+def comm_restricted(community_service, minimal_community, location):
+    """Create minimal restricted community."""
+    c = deepcopy(minimal_community)
+    c["access"]["visibility"] = "restricted"
+    c["id"] = "restricted-{id}".format(id=str(datetime.utcnow().timestamp()).replace(".","-"))
+    return community_service.create(data=c, identity=system_identity)
+
+
+def test_oai_set_create_community(community_service, comm, comm_restricted):
+    """Set should only be created for public community."""
+    oaiset = _retrieve_oaiset(service=community_service, community=comm.data)
+    assert oaiset != None
+    assert comm.data.get("id") in oaiset.search_pattern
+    assert comm.data.get("id") in oaiset.spec
+    assert comm.data.get("metadata", {}).get("title") in oaiset.description
+    assert comm.data.get("metadata", {}).get("title") in oaiset.name
+
+    oaiset = _retrieve_oaiset(service=community_service, community=comm_restricted.data)
+    assert oaiset == None
+
+
+def test_oai_set_delete_community(community_service, comm, comm_restricted):
+    """Set should be deleted for public community."""
+    oaiset = _retrieve_oaiset(service=community_service, community=comm.data)
+    assert oaiset != None
+
+    community_service.delete(identity=system_identity, id_=comm.data.get("id"))
+    oaiset = _retrieve_oaiset(service=community_service, community=comm.data)
+    assert oaiset == None
+
+    # nothing should happen for restricted communities
+    community_service.delete(identity=system_identity, id_=comm_restricted.data.get("id"))
+
+
+def test_oai_set_renamed(community_service, comm, comm_restricted):
+    """Set should be deleted and new one created if public community is renamed."""
+    new_data = deepcopy(comm.data)
+    new_data["id"] = "new-id-who-dis"
+    r = community_service.rename(identity=system_identity, id_=comm.data.get("id"), data=new_data)
+
+    # set with old id should not be there anymore
+    old_oaiset = _retrieve_oaiset(service=community_service, community=comm.data)
+    assert old_oaiset == None
+
+    oaiset = _retrieve_oaiset(service=community_service, community=r.data)
+    assert oaiset != None
+    assert r.data.get("id") in oaiset.search_pattern
+    assert r.data.get("id") in oaiset.spec
+    assert r.data.get("metadata", {}).get("title") in oaiset.description
+    assert r.data.get("metadata", {}).get("title") in oaiset.name
+
+    # nothing should happen for restricted communities
+    new_data = deepcopy(comm_restricted.data)
+    new_data["id"] = "new-id-restricted-who-dis"
+    r = community_service.rename(identity=system_identity, id_=comm_restricted.data.get("id"), data=new_data)
+    old_oaiset = _retrieve_oaiset(service=community_service, community=r.data)
+    assert old_oaiset == None
+
+
+def test_oai_set_update(community_service, comm, comm_restricted):
+    """Set should be updated accordingly."""
+    new_data = deepcopy(comm.data)
+    new_data["access"] = comm_restricted.data.get("access")
+    community_service.update(identity=system_identity, id_=comm.data.get("id"), data=new_data)
+    # community not public anymore -> no set should be found
+    oaiset = _retrieve_oaiset(service=community_service, community=comm.data)
+    assert oaiset == None
+
+    community_service.update(identity=system_identity, id_=comm.data.get("id"), data=comm.data)
+    # community public again -> set should be found
+    oaiset = _retrieve_oaiset(service=community_service, community=comm.data)
+    assert oaiset != None
+    assert comm.data.get("id") in oaiset.search_pattern
+    assert comm.data.get("id") in oaiset.spec
+    assert comm.data.get("metadata", {}).get("title") in oaiset.description
+    assert comm.data.get("metadata", {}).get("title") in oaiset.name
+
+    new_data = deepcopy(comm.data)
+    new_data["metadata"]["title"] = "new title"
+    community_service.update(identity=system_identity, id_=comm.data.get("id"), data=new_data)
+    # community updated title -> set should update name/description
+    oaiset = _retrieve_oaiset(service=community_service, community=comm.data)
+    assert oaiset != None
+    assert comm.data.get("id") in oaiset.search_pattern
+    assert comm.data.get("id") in oaiset.spec
+    assert new_data.get("metadata", {}).get("title") in oaiset.description
+    assert new_data.get("metadata", {}).get("title") in oaiset.name