From fdfe19d768cbe6611a5b61dc5efb7a24233a8573 Mon Sep 17 00:00:00 2001
From: Maximilian Moser <maximilian.moser@tuwien.ac.at>
Date: Fri, 22 Jan 2021 16:02:17 +0100
Subject: [PATCH] Adjust module for December release and change too many things
 at once

* apply Black formatting
* use plural for sub-commands, for consistency
* fix convert_to_recid() to always fetch the first PID of type 'recid'
* add command to reindex all (or some) records
* update method for instantiating RecordServices: allow the configuration
  of a factory function that creates a RecordService as desired
  (default: fetches services from current_rdm_records)
---
 invenio_utilities_tuw/cli/cli.py              |  8 +--
 .../cli/{draft.py => drafts.py}               | 39 +++++-----
 invenio_utilities_tuw/cli/files.py            | 24 ++++---
 .../cli/{record.py => records.py}             | 71 ++++++++++++++-----
 invenio_utilities_tuw/cli/utils.py            | 27 ++++---
 invenio_utilities_tuw/config.py               | 20 ++++--
 invenio_utilities_tuw/utils.py                | 51 +++++++++++++
 7 files changed, 173 insertions(+), 67 deletions(-)
 rename invenio_utilities_tuw/cli/{draft.py => drafts.py} (92%)
 rename invenio_utilities_tuw/cli/{record.py => records.py} (66%)
 create mode 100644 invenio_utilities_tuw/utils.py

diff --git a/invenio_utilities_tuw/cli/cli.py b/invenio_utilities_tuw/cli/cli.py
index 283fd6f..cfe3a1c 100644
--- a/invenio_utilities_tuw/cli/cli.py
+++ b/invenio_utilities_tuw/cli/cli.py
@@ -2,9 +2,9 @@
 
 import click
 
-from .draft import draft
+from .drafts import drafts
 from .files import files
-from .record import record
+from .records import records
 from .users import users
 
 
@@ -14,7 +14,7 @@ def utilities():
     pass
 
 
-utilities.add_command(draft)
+utilities.add_command(drafts)
 utilities.add_command(files)
-utilities.add_command(record)
+utilities.add_command(records)
 utilities.add_command(users)
diff --git a/invenio_utilities_tuw/cli/draft.py b/invenio_utilities_tuw/cli/drafts.py
similarity index 92%
rename from invenio_utilities_tuw/cli/draft.py
rename to invenio_utilities_tuw/cli/drafts.py
index d0e625b..738ea61 100644
--- a/invenio_utilities_tuw/cli/draft.py
+++ b/invenio_utilities_tuw/cli/drafts.py
@@ -9,13 +9,8 @@ import click
 from flask.cli import with_appcontext
 from invenio_files_rest.models import ObjectVersion
 from invenio_rdm_records.records.models import DraftMetadata
-from invenio_rdm_records.services.services import (
-    BibliographicDraftFilesService as DraftFileService,
-)
-from invenio_rdm_records.services.services import (
-    BibliographicRecordService as RecordService,
-)
 
+from ..utils import get_draft_file_service, get_record_service
 from .utils import (
     convert_to_recid,
     create_record_from_metadata,
@@ -51,18 +46,18 @@ option_pid_value = click.option(
 
 
 @click.group()
-def draft():
+def drafts():
     """Utility commands for creation and publication of drafts."""
     pass
 
 
-@draft.command("list")
+@drafts.command("list")
 @option_as_user
 @with_appcontext
 def list_drafts(user):
     """List all drafts accessible to the given user."""
     identity = get_identity_for_user(user)
-    service = RecordService()
+    service = get_record_service()
     recids = [
         dm.json["id"]
         for dm in DraftMetadata.query.all()
@@ -79,7 +74,7 @@ def list_drafts(user):
             pass
 
 
-@draft.command("create")
+@drafts.command("create")
 @click.argument("metadata_path", type=click.Path(exists=True))
 @option_as_user
 @click.option(
@@ -127,7 +122,7 @@ def create_draft(metadata_path, publish, user):
                 msg = "ignored in '{}': {}".format(deposit_files_path, ignored)
                 click.secho(msg, fg="red", err=True)
 
-        service = DraftFileService()
+        service = get_draft_file_service()
         service.init_files(
             id_=recid, identity=identity, data=[{"key": fn} for fn in file_names]
         )
@@ -144,13 +139,13 @@ def create_draft(metadata_path, publish, user):
         raise Exception("neither a file nor a directory: %s" % metadata_path)
 
     if publish:
-        service = RecordService()
+        service = get_record_service()
         service.publish(id_=recid, identity=identity)
 
     click.secho(recid, fg="green")
 
 
-@draft.command("update")
+@drafts.command("update")
 @click.argument("metadata_file", type=click.File("r"))
 @option_pid_value
 @option_pid_type
@@ -166,7 +161,7 @@ def update_draft(metadata_file, pid, pid_type, user, patch):
     """Update the specified draft's metadata."""
     pid = convert_to_recid(pid, pid_type)
     identity = get_identity_for_user(user)
-    service = RecordService()
+    service = get_record_service()
     metadata = json.load(metadata_file)
 
     if patch:
@@ -177,7 +172,7 @@ def update_draft(metadata_file, pid, pid_type, user, patch):
     click.secho(pid, fg="green")
 
 
-@draft.command("publish")
+@drafts.command("publish")
 @option_pid_value
 @option_pid_type
 @option_as_user
@@ -186,12 +181,12 @@ def publish_draft(pid, pid_type, user):
     """Publish the specified draft."""
     pid = convert_to_recid(pid, pid_type)
     identity = get_identity_for_user(user)
-    service = RecordService()
+    service = get_record_service()
     service.publish(id_=pid, identity=identity)
     click.secho(pid, fg="green")
 
 
-@draft.command("delete")
+@drafts.command("delete")
 @option_pid_value
 @option_pid_type
 @option_as_user
@@ -200,12 +195,12 @@ def delete_draft(pid, pid_type, user):
     """Delete the specified draft."""
     pid = convert_to_recid(pid, pid_type)
     identity = get_identity_for_user(user)
-    service = RecordService()
+    service = get_record_service()
     service.delete_draft(id_=pid, identity=identity)
     click.secho(pid, fg="red")
 
 
-@draft.group()
+@drafts.group()
 def files():
     """Manage files deposited with the draft."""
     pass
@@ -221,7 +216,7 @@ def add_files(filepaths, pid, pid_type, user):
     """Add the specified files to the draft."""
     recid = convert_to_recid(pid, pid_type)
     identity = get_identity_for_user(user)
-    service = DraftFileService()
+    service = get_draft_file_service()
 
     paths = []
     for file_path in filepaths:
@@ -271,7 +266,7 @@ def remove_files(filekeys, pid, pid_type, user):
     """Remove the deposited files."""
     recid = convert_to_recid(pid, pid_type)
     identity = get_identity_for_user(user)
-    service = DraftFileService()
+    service = get_draft_file_service()
 
     for file_key in filekeys:
         service.delete_file(id_=recid, file_key=file_key, identity=identity)
@@ -288,7 +283,7 @@ def list_files(pid, pid_type, user):
     """Show a list of files deposited with the draft."""
     recid = convert_to_recid(pid, pid_type)
     identity = get_identity_for_user(user)
-    service = DraftFileService()
+    service = get_draft_file_service()
     file_results = service.list_files(id_=recid, identity=identity)
     for f in file_results.entries:
         ov = ObjectVersion.get(f["bucket_id"], f["key"], f["version_id"])
diff --git a/invenio_utilities_tuw/cli/files.py b/invenio_utilities_tuw/cli/files.py
index 9db2438..242f301 100644
--- a/invenio_utilities_tuw/cli/files.py
+++ b/invenio_utilities_tuw/cli/files.py
@@ -5,15 +5,12 @@ from collections import defaultdict
 import click
 from flask.cli import with_appcontext
 from invenio_db import db
-from invenio_files_rest.models import Bucket, ObjectVersion, FileInstance
-from invenio_rdm_records.records.models import RecordMetadata, DraftMetadata
-from invenio_rdm_records.services.services import (
-    BibliographicRecordService as RecordService,
-)
+from invenio_files_rest.models import Bucket, FileInstance, ObjectVersion
+from invenio_rdm_records.records.models import DraftMetadata, RecordMetadata
 
+from ..utils import get_record_service
 from .utils import convert_to_recid, get_identity_for_user
 
-
 option_as_user = click.option(
     "--as-user",
     "-u",
@@ -64,7 +61,7 @@ def list_deleted_files(user, pid, pid_type):
     (via its PID).
     """
     recid = convert_to_recid(pid, pid_type) if pid else None
-    service = RecordService()
+    service = get_record_service()
     identity = get_identity_for_user(user)
 
     # if a PID was specified, limit the cleaning to this record's bucket
@@ -106,7 +103,7 @@ def hard_delete_files(user, pid, pid_type):
     (via its PID).
     """
     recid = convert_to_recid(pid, pid_type) if pid else None
-    service = RecordService()
+    service = get_record_service()
     identity = get_identity_for_user(user)
 
     # if a PID was specified, limit the cleaning to this record's bucket
@@ -152,7 +149,13 @@ def list_orphan_files():
     """List files that aren't referenced in any records (anymore)."""
     # TODO iterate over all records & drafts, get their buckets
     #      and check which buckets from the db aren't listed
-    bucket_ids = set((r.bucket.id for r in (RecordMetadata.query.all() + DraftMetadata.query.all()) if r.bucket is not None))
+    bucket_ids = set(
+        (
+            r.bucket.id
+            for r in (RecordMetadata.query.all() + DraftMetadata.query.all())
+            if r.bucket is not None
+        )
+    )
     print(len(bucket_ids))
     buckets = Bucket.query.filter(~Bucket.id.in_(bucket_ids)).all()
     print(len(buckets))
@@ -171,9 +174,8 @@ def list_orphan_files():
 @with_appcontext
 def clean_files(user):
     """Remove files that do not have associated ObjectVersions (anymore)."""
-    service = RecordService()
+    service = get_record_service()
     identity = get_identity_for_user(user)
-    service = RecordService()
     service.require_permission(identity, "delete")
 
     for fi in (f for f in FileInstance.query.all() if not f.objects):
diff --git a/invenio_utilities_tuw/cli/record.py b/invenio_utilities_tuw/cli/records.py
similarity index 66%
rename from invenio_utilities_tuw/cli/record.py
rename to invenio_utilities_tuw/cli/records.py
index f4aebdd..840e54e 100644
--- a/invenio_utilities_tuw/cli/record.py
+++ b/invenio_utilities_tuw/cli/records.py
@@ -5,17 +5,12 @@ import json
 import click
 from flask.cli import with_appcontext
 from invenio_files_rest.models import ObjectVersion
-from invenio_rdm_records.records.models import RecordMetadata
-from invenio_rdm_records.services.services import (
-    BibliographicRecordService as RecordService,
-)
-from invenio_rdm_records.services.services import (
-    BibliographicRecordFilesService as RecordFileService,
-)
 
+from ..utils import get_record_file_service, get_record_service
 from .utils import (
     convert_to_recid,
     get_identity_for_user,
+    get_object_uuid,
     patch_metadata,
 )
 
@@ -42,26 +37,37 @@ option_pid_value = click.option(
     "pid",
     metavar="PID_VALUE",
     required=True,
-    help="persistent identifier of the record draft to operate on",
+    help="persistent identifier of the record to operate on",
+)
+option_pid_values = click.option(
+    "--pid",
+    "-p",
+    "pids",
+    metavar="PID_VALUE",
+    required=False,
+    multiple=True,
+    help="persistent identifier of the record to operate on (can be specified multiple times)",
 )
 
 
 @click.group()
-def record():
+def records():
     """Utility commands for creation and publication of drafts."""
     pass
 
 
-@record.command("list")
+@records.command("list")
 @option_as_user
 @with_appcontext
 def list_records(user):
     """List all records accessible to the given user."""
     identity = get_identity_for_user(user)
-    service = RecordService()
+    service = get_record_service()
+    rec_model_cls = service.record_cls.model_cls
+
     recids = [
         rec.json["id"]
-        for rec in RecordMetadata.query.all()
+        for rec in rec_model_cls.query
         if rec is not None and rec.json is not None
     ]
 
@@ -75,7 +81,7 @@ def list_records(user):
             raise
 
 
-@record.command("update")
+@records.command("update")
 @click.argument("metadata_file", type=click.File("r"))
 @option_pid_value
 @option_pid_type
@@ -91,7 +97,7 @@ def update_record(metadata_file, pid, pid_type, user, patch):
     """Update the specified draft's metadata."""
     pid = convert_to_recid(pid, pid_type)
     identity = get_identity_for_user(user)
-    service = RecordService()
+    service = get_record_service()
     metadata = json.load(metadata_file)
 
     if patch:
@@ -102,7 +108,7 @@ def update_record(metadata_file, pid, pid_type, user, patch):
     click.secho(pid, fg="green")
 
 
-@record.command("delete")
+@records.command("delete")
 @click.confirmation_option(prompt="are you sure you want to delete this record?")
 @option_pid_value
 @option_pid_type
@@ -112,13 +118,13 @@ def delete_record(pid, pid_type, user):
     """Delete the specified record."""
     identity = get_identity_for_user(user)
     recid = convert_to_recid(pid, pid_type)
-    service = RecordService()
+    service = get_record_service()
     service.delete(id_=recid, identity=identity)
 
     click.secho(recid, fg="red")
 
 
-@record.command("files")
+@records.command("files")
 @option_pid_value
 @option_pid_type
 @option_as_user
@@ -127,9 +133,38 @@ def list_files(pid, pid_type, user):
     """Show a list of files deposited with the record."""
     recid = convert_to_recid(pid, pid_type)
     identity = get_identity_for_user(user)
-    service = RecordFileService()
+    service = get_record_file_service()
     file_results = service.list_files(id_=recid, identity=identity)
     for f in file_results.entries:
         ov = ObjectVersion.get(f["bucket_id"], f["key"], f["version_id"])
         fi = ov.file
         click.secho("{}\t{}\t{}".format(ov.key, fi.uri, fi.checksum), fg="green")
+
+
+@records.command("reindex")
+@option_pid_values
+@option_pid_type
+@option_as_user
+@with_appcontext
+def reindex_records(pids, pid_type, user):
+    """Reindex all available (or just the specified) records."""
+    service = get_record_service()
+
+    # basically, this is just a check whether the user exists,
+    # since there's no permission for re-indexing
+    get_identity_for_user(user)
+
+    if pids:
+        records = [
+            service.record_cls.get_record(get_object_uuid(pid, pid_type))
+            for pid in pids
+        ]
+    else:
+        records = [
+            service.record_cls.get_record(meta.id)
+            for meta in service.record_cls.model_cls.query
+            if meta is not None and meta.json is not None
+        ]
+
+    for record in records:
+        service.indexer.index(record)
diff --git a/invenio_utilities_tuw/cli/utils.py b/invenio_utilities_tuw/cli/utils.py
index 2798346..095e5ad 100644
--- a/invenio_utilities_tuw/cli/utils.py
+++ b/invenio_utilities_tuw/cli/utils.py
@@ -7,9 +7,8 @@ from invenio_access import any_user
 from invenio_access.utils import get_identity
 from invenio_accounts import current_accounts
 from invenio_pidstore.models import PersistentIdentifier
-from invenio_rdm_records.services.services import (
-    BibliographicRecordService as RecordService,
-)
+
+from ..utils import get_record_service
 
 
 def create_record_from_metadata(metadata_file_path, identity):
@@ -21,7 +20,7 @@ def create_record_from_metadata(metadata_file_path, identity):
     if metadata is None:
         raise Exception("not a valid json file: %s" % metadata_file_path)
 
-    service = RecordService()
+    service = get_record_service()
     draft = service.create(identity=identity, data=metadata)
     return draft
 
@@ -60,13 +59,25 @@ def get_identity_for_user(user):
     return identity
 
 
+def get_object_uuid(pid_value, pid_type):
+    """Fetch the UUID of the referenced object."""
+    uuid = (
+        PersistentIdentifier.query.filter_by(pid_value=pid_value, pid_type=pid_type)
+        .first()
+        .object_uuid
+    )
+
+    return uuid
+
+
 def convert_to_recid(pid_value, pid_type):
     """Fetch the recid of the referenced object."""
     if pid_type != "recid":
-        pid_value = (
-            PersistentIdentifier.query.filter_by(pid_value=pid_value, pid_type=pid_type)
-            .first()
-            .pid_value
+        object_uuid = get_object_uuid(pid_value=pid_value, pid_type=pid_type)
+        query = PersistentIdentifier.query.filter_by(
+            object_uuid=object_uuid,
+            pid_type="recid",
         )
+        pid_value = query.first().pid_value
 
     return pid_value
diff --git a/invenio_utilities_tuw/config.py b/invenio_utilities_tuw/config.py
index 4cb4ab8..8f80f09 100644
--- a/invenio_utilities_tuw/config.py
+++ b/invenio_utilities_tuw/config.py
@@ -8,11 +8,23 @@
 
 """Some utilities for InvenioRDM."""
 
-# TODO: This is an example file. Remove it if your package does not use any
-# extra configuration variables.
+from invenio_rdm_records.proxies import current_rdm_records
 
-UTILITIES_TUW_DEFAULT_VALUE = "foobar"
-"""Default value for the application."""
 
 UTILITIES_TUW_BASE_TEMPLATE = "invenio_utilities_tuw/base.html"
 """Default base template for the demo page."""
+
+UTILITIES_TUW_RECORD_SERVICE_FACTORY = (
+    lambda: current_rdm_records.records_service
+)
+"""Factory function for creating a RecordService."""
+
+UTILITIES_TUW_RECORD_FILES_SERVICE_FACTORY = (
+    lambda: current_rdm_records.record_files_service
+)
+"""Factory function for creating a RecordFileService."""
+
+UTILITIES_TUW_DRAFT_FILES_SERVICE_FACTORY = (
+    lambda: current_rdm_records.draft_files_service
+)
+"""Factory function for creating a DraftFileService."""
diff --git a/invenio_utilities_tuw/utils.py b/invenio_utilities_tuw/utils.py
new file mode 100644
index 0000000..dcf5389
--- /dev/null
+++ b/invenio_utilities_tuw/utils.py
@@ -0,0 +1,51 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2020-2021 TU Wien.
+#
+# Invenio-Utilities-TUW is free software; you can redistribute it and/or
+# modify it under the terms of the MIT License; see LICENSE file for more
+# details.
+
+"""Utility functions for Invenio-Utilities-TUW."""
+
+
+from flask import current_app
+from invenio_rdm_records.proxies import current_rdm_records
+from werkzeug.utils import import_string
+
+
+def get_or_import(value, default=None):
+    """Try an import if value is an endpoint string, or return value itself."""
+    if isinstance(value, str):
+        return import_string(value)
+    elif value:
+        return value
+
+    return default
+
+
+def get_record_service():
+    """Get the configured RecordService."""
+    factory = current_app.config.get(
+        "UTILITIES_TUW_RECORD_SERVICE_FACTORY",
+        lambda: current_rdm_records.records_service,
+    )
+    return factory()
+
+
+def get_record_file_service():
+    """Get the configured RecordFileService."""
+    factory = current_app.config.get(
+        "UTILITIES_TUW_RECORD_FILES_SERVICE_FACTORY",
+        lambda: current_rdm_records.record_files_service,
+    )
+    return factory()
+
+
+def get_draft_file_service():
+    """Get the configured DraftFilesService."""
+    factory = current_app.config.get(
+        "UTILITIES_TUW_DRAFT_FILES_SERVICE_FACTORY",
+        lambda: current_rdm_records.draft_files_service,
+    )
+    return factory()
-- 
GitLab