diff --git a/README.rst b/README.rst index 7d03bc9d..5046f458 100644 --- a/README.rst +++ b/README.rst @@ -1,5 +1,5 @@ .. - Copyright (C) 2021 Graz University of Technology. + Copyright (C) 2021-2024 Graz University of Technology. Invenio-Records-Marc21 is free software; you can redistribute it and/or modify it under the terms of the MIT License; see LICENSE file for more details. @@ -35,20 +35,51 @@ Install Choose a version of elasticsearch and a DB, then run: .. code-block:: console - + pipenv run pip install -e .[all] pipenv run pip install invenio-search[elasticsearch7] pipenv run pip install invenio-db[postgresql,versioning] -Service -========= +Setting Up Statistics +===================== + +To enable and configure the statistics feature using MARC21 records in Invenio, you need to update your `invenio.cfg` file with specific configurations that integrate MARC21 statistics with Invenio's standard statistics modules. + +### Configuration Steps + +1. **Import Required Configurations:** + + Before updating the configuration values, ensure that you import the necessary settings from both the `invenio_records_marc21` module and the `invenio_app_rdm` module. Add the following lines to your `invenio.cfg`: + +.. code-block:: console + + from invenio_records_marc21.config import MARC21_STATS_CELERY_TASKS, MARC21_STATS_EVENTS, MARC21_STATS_AGGREGATIONS, MARC21_STATS_QUERIES + from invenio_app_rdm.config import CELERY_BEAT_SCHEDULE, STATS_EVENTS, STATS_AGGREGATIONS, STATS_QUERIES + +Update Celery Beat Schedule: + +Integrate MARC21-specific scheduled tasks with Invenio's scheduler: + +.. code-block:: console + + CELERY_BEAT_SCHEDULE.update(MARC21_STATS_CELERY_TASKS) + + +Update Events, Aggregations, and Queries: + +Merge MARC21 statistics configurations with the global statistics settings: + +.. code-block:: console + + STATS_EVENTS.update(MARC21_STATS_EVENTS) + STATS_AGGREGATIONS.update(MARC21_STATS_AGGREGATIONS) + STATS_QUERIES.update(MARC21_STATS_QUERIES) -** Create Marc21 Record** Tests ========= .. code-block:: console - pipenv run ./run-tests.sh \ No newline at end of file + pipenv run ./run-tests.sh diff --git a/invenio_records_marc21/config.py b/invenio_records_marc21/config.py index 7df860aa..1c5d8a95 100644 --- a/invenio_records_marc21/config.py +++ b/invenio_records_marc21/config.py @@ -2,7 +2,7 @@ # # This file is part of Invenio. # -# Copyright (C) 2021-2023 Graz University of Technology. +# Copyright (C) 2021-2024 Graz University of Technology. # # Invenio-Records-Marc21 is free software; you can redistribute it and/or # modify it under the terms of the MIT License; see LICENSE file for more @@ -13,15 +13,20 @@ from __future__ import absolute_import, print_function import idutils -from celery.schedules import crontab +from celery.schedules import crontab, timedelta from flask_principal import RoleNeed from invenio_i18n import lazy_gettext as _ from invenio_rdm_records.services import facets as rdm_facets from invenio_rdm_records.services.pids import providers +from invenio_stats.aggregations import StatAggregator +from invenio_stats.contrib.event_builders import build_file_unique_id +from invenio_stats.processors import EventsIndexer, anonymize_user, flag_robots +from invenio_stats.queries import TermsQuery from .resources.serializers.datacite import Marc21DataCite43JSONSerializer from .services import facets from .services.pids import Marc21DataCitePIDProvider +from .utils import build_record_unique_id MARC21_FACETS = { "access_status": { @@ -42,6 +47,12 @@ "field": "files.types", }, }, + "mostviewed": dict( + title=_("Most viewed"), fields=["-stats.all_versions.unique_views"] + ), + "mostdownloaded": dict( + title=_("Most downloaded"), fields=["-stats.all_versions.unique_downloads"] + ), } MARC21_SORT_OPTIONS = { @@ -69,6 +80,12 @@ title=_("Least recently updated"), fields=["updated"], ), + "mostviewed": dict( + title=_("Most viewed"), fields=["-stats.all_versions.unique_views"] + ), + "mostdownloaded": dict( + title=_("Most downloaded"), fields=["-stats.all_versions.unique_downloads"] + ), } MARC21_SEARCH_DRAFTS = { @@ -89,6 +106,8 @@ "newest", "oldest", "version", + "mostviewed", + "mostdownloaded", ], } """Record search configuration.""" @@ -262,3 +281,201 @@ def make_doi(prefix, record): MARC21_RECORD_CURATOR_NEEDS = [RoleNeed("Marc21Curator")] """This Role is to modify records only, no creation, no deletion possible.""" + + +# Statistics configuration + +MARC21_STATS_CELERY_TASKS = { + # indexing of statistics events & aggregations + "marc21-stats-process-events": { + "task": "invenio_stats.tasks.process_events", + "args": [("marc21-record-view", "marc21-file-download")], + "schedule": crontab( + minute="20,50", + ), # Every hour at minute 20 and 50 + }, + "marc21-stats-aggregate-events": { + "task": "invenio_stats.tasks.aggregate_events", + "args": [ + ( + "marc21-record-view-agg", + "marc21-file-download-agg", + ) + ], + "schedule": crontab(minute="5"), # Every hour at minute 5 + }, + "marc21-reindex-stats": { + "task": "invenio_records_marc21.services.tasks.marc21_reindex_stats", + "args": [ + ( + "stats-marc21-record-view", + "stats-marc21-file-download", + ) + ], + "schedule": crontab(minute="10"), + }, +} + +# Invenio-Stats +# ============= +# See https://invenio-stats.readthedocs.io/en/latest/configuration.html + +MARC21_STATS_EVENTS = { + "marc21-file-download": { + "templates": "invenio_records_marc21.records.statistics.templates.events.marc21_file_download", + "event_builders": [ + "invenio_rdm_records.resources.stats.file_download_event_builder", + "invenio_rdm_records.resources.stats.check_if_via_api", + ], + "cls": EventsIndexer, + "params": { + "preprocessors": [flag_robots, anonymize_user, build_file_unique_id] + }, + }, + "marc21-record-view": { + "templates": "invenio_records_marc21.records.statistics.templates.events.marc21_record_view", + "event_builders": [ + "invenio_rdm_records.resources.stats.record_view_event_builder", + "invenio_rdm_records.resources.stats.check_if_via_api", + "invenio_rdm_records.resources.stats.drop_if_via_api", + ], + "cls": EventsIndexer, + "params": { + "preprocessors": [flag_robots, anonymize_user, build_record_unique_id], + }, + }, +} + +MARC21_STATS_AGGREGATIONS = { + "marc21-file-download-agg": { + "templates": "invenio_records_marc21.records.statistics.templates.aggregations.aggr_marc21_file_download", + "cls": StatAggregator, + "params": { + "event": "marc21-file-download", + "field": "unique_id", + "interval": "day", + "index_interval": "month", + "copy_fields": { + "file_id": "file_id", + "file_key": "file_key", + "bucket_id": "bucket_id", + "recid": "recid", + "parent_recid": "parent_recid", + }, + "metric_fields": { + "unique_count": ( + "cardinality", + "unique_session_id", + {"precision_threshold": 1000}, + ), + "volume": ("sum", "size", {}), + }, + }, + }, + "marc21-record-view-agg": { + "templates": "invenio_records_marc21.records.statistics.templates.aggregations.aggr_marc21_record_view", + "cls": StatAggregator, + "params": { + "event": "marc21-record-view", + "field": "unique_id", + "interval": "day", + "index_interval": "month", + "copy_fields": { + "recid": "recid", + "parent_recid": "parent_recid", + "via_api": "via_api", + }, + "metric_fields": { + "unique_count": ( + "cardinality", + "unique_session_id", + {"precision_threshold": 1000}, + ), + }, + "query_modifiers": [lambda query, **_: query.filter("term", via_api=False)], + }, + }, +} + +MARC21_STATS_QUERIES = { + "marc21-record-view": { + "cls": TermsQuery, + "permission_factory": None, + "params": { + "index": "stats-marc21-record-view", + "doc_type": "marc21-record-view-day-aggregation", + "copy_fields": { + "recid": "recid", + "parent_recid": "parent_recid", + }, + "query_modifiers": [], + "required_filters": { + "recid": "recid", + }, + "metric_fields": { + "views": ("sum", "count", {}), + "unique_views": ("sum", "unique_count", {}), + }, + }, + }, + "marc21-record-view-all-versions": { + "cls": TermsQuery, + "permission_factory": None, + "params": { + "index": "stats-marc21-record-view", + "doc_type": "marc21-record-view-day-aggregation", + "copy_fields": { + "parent_recid": "parent_recid", + }, + "query_modifiers": [], + "required_filters": { + "parent_recid": "parent_recid", + }, + "metric_fields": { + "views": ("sum", "count", {}), + "unique_views": ("sum", "unique_count", {}), + }, + }, + }, + "marc21-record-download": { + "cls": TermsQuery, + "permission_factory": None, + "params": { + "index": "stats-marc21-file-download", + "doc_type": "marc21-file-download-day-aggregation", + "copy_fields": { + "recid": "recid", + "parent_recid": "parent_recid", + }, + "query_modifiers": [], + "required_filters": { + "recid": "recid", + }, + "metric_fields": { + "downloads": ("sum", "count", {}), + "unique_downloads": ("sum", "unique_count", {}), + "data_volume": ("sum", "volume", {}), + }, + }, + }, + "marc21-record-download-all-versions": { + "cls": TermsQuery, + "permission_factory": None, + "params": { + "index": "stats-marc21-file-download", + "doc_type": "marc21-file-download-day-aggregation", + "copy_fields": { + "parent_recid": "parent_recid", + }, + "query_modifiers": [], + "required_filters": { + "parent_recid": "parent_recid", + }, + "metric_fields": { + "downloads": ("sum", "count", {}), + "unique_downloads": ("sum", "unique_count", {}), + "data_volume": ("sum", "volume", {}), + }, + }, + }, +} diff --git a/invenio_records_marc21/records/api.py b/invenio_records_marc21/records/api.py index 3de856dc..2bb559a0 100644 --- a/invenio_records_marc21/records/api.py +++ b/invenio_records_marc21/records/api.py @@ -2,7 +2,7 @@ # # This file is part of Invenio. # -# Copyright (C) 2021 Graz University of Technology. +# Copyright (C) 2021-2024 Graz University of Technology. # # Invenio-Records-Marc21 is free software; you can redistribute it and/or # modify it under the terms of the MIT License; see LICENSE file for more @@ -16,12 +16,14 @@ from invenio_drafts_resources.records.api import ParentRecord as BaseParentRecord from invenio_drafts_resources.records.systemfields import ParentField from invenio_pidstore.models import PIDStatus +from invenio_rdm_records.records.dumpers import StatisticsDumperExt from invenio_rdm_records.records.systemfields import ( HasDraftCheckField, ParentRecordAccessField, RecordAccessField, RecordDeletionStatusField, ) +from invenio_records.dumpers import SearchDumper from invenio_records.systemfields import ConstantField, DictField, ModelField from invenio_records_resources.records.api import FileRecord as BaseFileRecord from invenio_records_resources.records.systemfields import ( @@ -32,7 +34,9 @@ ) from . import models +from .dumpers import Marc21StatisticsDumperExt from .systemfields import ( + Marc21RecordStatisticsField, Marc21Status, MarcDraftProvider, MarcPIDFieldContext, @@ -69,7 +73,17 @@ class DraftFile(BaseFileRecord): record_cls = None # defined below -class Marc21Draft(Draft): +class CommonFieldsMixin: + """Common fields for Marc21 records.""" + + dumper = SearchDumper( + extensions=[ + Marc21StatisticsDumperExt("stats"), + ] + ) + + +class Marc21Draft(Draft, CommonFieldsMixin): """Marc21 draft API.""" model_cls = models.DraftMetadata @@ -118,7 +132,7 @@ class RecordFile(BaseFileRecord): record_cls = None # defined below -class Marc21Record(Record): +class Marc21Record(Record, CommonFieldsMixin): """Define API for Marc21 create and manipulate.""" model_cls = models.RecordMetadata @@ -162,5 +176,7 @@ class Marc21Record(Record): deletion_status = RecordDeletionStatusField() + stats = Marc21RecordStatisticsField() + RecordFile.record_cls = Marc21Record diff --git a/invenio_records_marc21/records/dumpers/__init__.py b/invenio_records_marc21/records/dumpers/__init__.py new file mode 100644 index 00000000..1f2bdcdd --- /dev/null +++ b/invenio_records_marc21/records/dumpers/__init__.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Invenio. +# +# Copyright (C) 2024 Graz University of Technology. +# +# invenio-records-marc21 is free software; you can redistribute it and/or modify +# it under the terms of the MIT License; see LICENSE file for more details. + +"""Statistics integration for RDM records.""" + +from .statistics import Marc21StatisticsDumperExt + +__all__ = Marc21StatisticsDumperExt diff --git a/invenio_records_marc21/records/dumpers/statistics.py b/invenio_records_marc21/records/dumpers/statistics.py new file mode 100644 index 00000000..39076392 --- /dev/null +++ b/invenio_records_marc21/records/dumpers/statistics.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +# +# # Copyright (C) 2024 Graz University of Technology. +# +# invenio-records-marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. + +"""Search dumpers for access-control information.""" + +from flask import current_app +from invenio_rdm_records.records.dumpers import StatisticsDumperExt +from invenio_records.dictutils import dict_lookup + +from ..statistics import Marc21Statistics + + +class Marc21StatisticsDumperExt(StatisticsDumperExt): + """Search dumper extension for record statistics. + + On dump, it fetches the record's download & view statistics via Invenio-Stats + queries and dumps them into a field so that they are indexed in the search engine. + On load, it keeps the dumped values in the data dictionary, in order to enable + the record schema to dump them if present. + """ + + def dump(self, record, data): + """Dump the download & view statistics to the data dictionary.""" + if record.is_draft: + return + + recid = record.pid.pid_value + parent_recid = record.parent.pid.pid_value + + try: + parent_data = dict_lookup(data, self.keys, parent=True) + parent_data[self.key] = Marc21Statistics.get_record_stats( + recid=recid, parent_recid=parent_recid + ) + except KeyError as e: + current_app.logger.warning(e) diff --git a/invenio_records_marc21/records/mappings/os-v2/marc21records/marc21/marc21-v2.0.0.json b/invenio_records_marc21/records/mappings/os-v2/marc21records/marc21/marc21-v2.0.0.json index 070f87e6..d8330ce8 100644 --- a/invenio_records_marc21/records/mappings/os-v2/marc21records/marc21/marc21-v2.0.0.json +++ b/invenio_records_marc21/records/mappings/os-v2/marc21records/marc21/marc21-v2.0.0.json @@ -731,6 +731,48 @@ "type": "keyword" } } + }, + "stats": { + "properties": { + "this_version": { + "properties": { + "views": { + "type": "integer" + }, + "unique_views": { + "type": "integer" + }, + "downloads": { + "type": "integer" + }, + "unique_downloads": { + "type": "integer" + }, + "data_volume": { + "type": "double" + } + } + }, + "all_versions": { + "properties": { + "views": { + "type": "integer" + }, + "unique_views": { + "type": "integer" + }, + "downloads": { + "type": "integer" + }, + "unique_downloads": { + "type": "integer" + }, + "data_volume": { + "type": "double" + } + } + } + } } } } diff --git a/invenio_records_marc21/records/statistics/__init__.py b/invenio_records_marc21/records/statistics/__init__.py new file mode 100644 index 00000000..b3fde027 --- /dev/null +++ b/invenio_records_marc21/records/statistics/__init__.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Invenio. +# +# # Copyright (C) 2024 Graz University of Technology. +# +# invenio-records-marc21 is free software; you can redistribute it and/or modify +# it under the terms of the MIT License; see LICENSE file for more details. + +"""Statistics integration for marc21 records.""" + +from .api import Marc21Statistics + +__all__ = ("Marc21Statistics",) diff --git a/invenio_records_marc21/records/statistics/api.py b/invenio_records_marc21/records/statistics/api.py new file mode 100644 index 00000000..974cfa65 --- /dev/null +++ b/invenio_records_marc21/records/statistics/api.py @@ -0,0 +1,87 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Invenio. +# +# Copyright (C) 2019 CERN. +# Copyright (C) 2022 TU Wien. +# Copyright (C) 2024 Graz University of Technology. +# +# invenio-records-marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. + +"""Permission factories for invenio-records-marc21. + +In contrast to the very liberal defaults provided by Invenio-Stats, these permission +factories deny access unless otherwise specified. +""" + +from flask import current_app +from invenio_stats.proxies import current_stats + + +class Marc21Statistics: + """Statistics API class.""" + + prefix = "marc21-record" + + @classmethod + def _get_query(cls, query_name): + """Build the statistics query from configuration.""" + query_config = current_stats.queries[query_name] + return query_config.cls(name=query_config.name, **query_config.params) + + @classmethod + def get_record_stats(cls, recid, parent_recid): + """Fetch the statistics for the given record.""" + try: + views = cls._get_query(f"{cls.prefix}-view").run(recid=recid) + views_all = cls._get_query(f"{cls.prefix}-view-all-versions").run( + parent_recid=parent_recid + ) + except Exception as e: + # e.g. opensearchpy.exceptions.NotFoundError + # when the aggregation search index hasn't been created yet + current_app.logger.warning(e) + + fallback_result = { + "views": 0, + "unique_views": 0, + } + views = views_all = downloads = downloads_all = fallback_result + + try: + downloads = cls._get_query(f"{cls.prefix}-download").run(recid=recid) + downloads_all = cls._get_query(f"{cls.prefix}-download-all-versions").run( + parent_recid=parent_recid + ) + except Exception as e: + # same as above, but for failure in the download statistics + # because they are a separate index that can fail independently + current_app.logger.warning(e) + + fallback_result = { + "downloads": 0, + "unique_downloads": 0, + "data_volume": 0, + } + downloads = downloads_all = fallback_result + + stats = { + "this_version": { + "views": views["views"], + "unique_views": views["unique_views"], + "downloads": downloads["downloads"], + "unique_downloads": downloads["unique_downloads"], + "data_volume": downloads["data_volume"], + }, + "all_versions": { + "views": views_all["views"], + "unique_views": views_all["unique_views"], + "downloads": downloads_all["downloads"], + "unique_downloads": downloads_all["unique_downloads"], + "data_volume": downloads_all["data_volume"], + }, + } + + return stats diff --git a/invenio_records_marc21/records/statistics/templates/__init__.py b/invenio_records_marc21/records/statistics/templates/__init__.py new file mode 100644 index 00000000..71a3d6d9 --- /dev/null +++ b/invenio_records_marc21/records/statistics/templates/__init__.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Invenio. +# Copyright (C) 2018 CERN. +# Copyright (C) 2024 Graz University of Technology. +# +# invenio-records-marc21 is free software; you can redistribute it and/or modify +# it under the terms of the MIT License; see LICENSE file for more details. + +"""Statistics search index templates.""" diff --git a/invenio_records_marc21/records/statistics/templates/aggregations/__init__.py b/invenio_records_marc21/records/statistics/templates/aggregations/__init__.py new file mode 100644 index 00000000..e284c0cb --- /dev/null +++ b/invenio_records_marc21/records/statistics/templates/aggregations/__init__.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Invenio. +# Copyright (C) 2018 CERN. +# Copyright (C) 2023 TU Wien. +# +# invenio-records-marc21 is free software; you can redistribute it and/or modify +# it under the terms of the MIT License; see LICENSE file for more details. + +"""Aggregations search index templates.""" diff --git a/invenio_records_marc21/records/statistics/templates/aggregations/aggr_marc21_file_download/__init__.py b/invenio_records_marc21/records/statistics/templates/aggregations/aggr_marc21_file_download/__init__.py new file mode 100644 index 00000000..23a64c96 --- /dev/null +++ b/invenio_records_marc21/records/statistics/templates/aggregations/aggr_marc21_file_download/__init__.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Invenio. +# Copyright (C) 2018 CERN. +# Copyright (C) 2023 TU Wien. +# Copyright (C) 2024 Graz University of Technology. +# +# invenio-records-marc21 is free software; you can redistribute it and/or modify +# it under the terms of the MIT License; see LICENSE file for more details. + +"""File download aggregations search index templates.""" diff --git a/invenio_records_marc21/records/statistics/templates/aggregations/aggr_marc21_file_download/os-v2/__init__.py b/invenio_records_marc21/records/statistics/templates/aggregations/aggr_marc21_file_download/os-v2/__init__.py new file mode 100644 index 00000000..9ab24e84 --- /dev/null +++ b/invenio_records_marc21/records/statistics/templates/aggregations/aggr_marc21_file_download/os-v2/__init__.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Invenio. +# Copyright (C) 2019 CERN. +# Copyright (C) 2023 TU Wien. +# Copyright (C) 2024 Graz University of Technology. +# +# invenio-records-marc21 is free software; you can redistribute it and/or modify +# it under the terms of the MIT License; see LICENSE file for more details. + +"""File download aggregations OpenSearch index templates.""" diff --git a/invenio_records_marc21/records/statistics/templates/aggregations/aggr_marc21_file_download/os-v2/aggr-marc21-file-download-v1.0.0.json b/invenio_records_marc21/records/statistics/templates/aggregations/aggr_marc21_file_download/os-v2/aggr-marc21-file-download-v1.0.0.json new file mode 100644 index 00000000..e0e40ad6 --- /dev/null +++ b/invenio_records_marc21/records/statistics/templates/aggregations/aggr_marc21_file_download/os-v2/aggr-marc21-file-download-v1.0.0.json @@ -0,0 +1,72 @@ +{ + "index_patterns": ["__SEARCH_INDEX_PREFIX__stats-marc21-file-download-*"], + "settings": { + "index": { + "refresh_interval": "5s" + } + }, + "mappings": { + "dynamic_templates": [ + { + "date_fields": { + "match_mapping_type": "date", + "mapping": { + "type": "date", + "format": "date_optional_time" + } + } + } + ], + "date_detection": false, + "dynamic": "strict", + "numeric_detection": false, + "properties": { + "timestamp": { + "type": "date", + "format": "date_optional_time" + }, + "count": { + "type": "integer" + }, + "unique_count": { + "type": "integer" + }, + "file_id": { + "type": "keyword" + }, + "file_key": { + "type": "keyword" + }, + "bucket_id": { + "type": "keyword" + }, + "volume": { + "type": "double" + }, + "unique_id": { + "type": "keyword" + }, + "record_id": { + "type": "keyword" + }, + "recid": { + "type": "keyword" + }, + "parent_id": { + "type": "keyword" + }, + "parent_recid": { + "type": "keyword" + }, + "via_api": { + "type": "boolean" + }, + "updated_timestamp": { + "type": "date" + } + } + }, + "aliases": { + "__SEARCH_INDEX_PREFIX__stats-marc21-file-download": {} + } +} diff --git a/invenio_records_marc21/records/statistics/templates/aggregations/aggr_marc21_record_view/__init__.py b/invenio_records_marc21/records/statistics/templates/aggregations/aggr_marc21_record_view/__init__.py new file mode 100644 index 00000000..b34a3ed0 --- /dev/null +++ b/invenio_records_marc21/records/statistics/templates/aggregations/aggr_marc21_record_view/__init__.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Invenio. +# Copyright (C) 2018 CERN. +# Copyright (C) 2023 TU Wien. +# Copyright (C) 2024 Graz University of Technology. +# +# invenio-records-marc21 is free software; you can redistribute it and/or modify +# it under the terms of the MIT License; see LICENSE file for more details. + +"""Record views aggregations search index templates.""" diff --git a/invenio_records_marc21/records/statistics/templates/aggregations/aggr_marc21_record_view/os-v2/__init__.py b/invenio_records_marc21/records/statistics/templates/aggregations/aggr_marc21_record_view/os-v2/__init__.py new file mode 100644 index 00000000..4532c39d --- /dev/null +++ b/invenio_records_marc21/records/statistics/templates/aggregations/aggr_marc21_record_view/os-v2/__init__.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Invenio. +# Copyright (C) 2019 CERN. +# Copyright (C) 2023 TU Wien. +# Copyright (C) 2024 Graz University of Technology. +# +# invenio-records-marc21 is free software; you can redistribute it and/or modify +# it under the terms of the MIT License; see LICENSE file for more details. + +"""Record view aggregations OpenSearch index templates.""" diff --git a/invenio_records_marc21/records/statistics/templates/aggregations/aggr_marc21_record_view/os-v2/aggr-marc21-record-view-v1.0.0.json b/invenio_records_marc21/records/statistics/templates/aggregations/aggr_marc21_record_view/os-v2/aggr-marc21-record-view-v1.0.0.json new file mode 100644 index 00000000..6f72042f --- /dev/null +++ b/invenio_records_marc21/records/statistics/templates/aggregations/aggr_marc21_record_view/os-v2/aggr-marc21-record-view-v1.0.0.json @@ -0,0 +1,49 @@ +{ + "index_patterns": ["__SEARCH_INDEX_PREFIX__stats-marc21-record-view-*"], + "settings": { + "index": { + "refresh_interval": "5s" + } + }, + "mappings": { + "date_detection": false, + "dynamic": "strict", + "numeric_detection": false, + "properties": { + "timestamp": { + "type": "date", + "format": "date_optional_time" + }, + "count": { + "type": "integer" + }, + "unique_count": { + "type": "integer" + }, + "record_id": { + "type": "keyword" + }, + "recid": { + "type": "keyword" + }, + "parent_id": { + "type": "keyword" + }, + "parent_recid": { + "type": "keyword" + }, + "unique_id": { + "type": "keyword" + }, + "via_api": { + "type": "boolean" + }, + "updated_timestamp": { + "type": "date" + } + } + }, + "aliases": { + "__SEARCH_INDEX_PREFIX__stats-marc21-record-view": {} + } +} diff --git a/invenio_records_marc21/records/statistics/templates/events/__init__.py b/invenio_records_marc21/records/statistics/templates/events/__init__.py new file mode 100644 index 00000000..c1f7bdd2 --- /dev/null +++ b/invenio_records_marc21/records/statistics/templates/events/__init__.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Invenio. +# Copyright (C) 2018 CERN. +# Copyright (C) 2023 TU Wien. +# Copyright (C) 2024 Graz University of Technology. +# +# invenio-records-marc21 is free software; you can redistribute it and/or modify +# it under the terms of the MIT License; see LICENSE file for more details. + +"""Statistics events search index templates.""" diff --git a/invenio_records_marc21/records/statistics/templates/events/marc21_file_download/__init__.py b/invenio_records_marc21/records/statistics/templates/events/marc21_file_download/__init__.py new file mode 100644 index 00000000..137f4043 --- /dev/null +++ b/invenio_records_marc21/records/statistics/templates/events/marc21_file_download/__init__.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Invenio. +# Copyright (C) 2019 CERN. +# Copyright (C) 2023 TU Wien. +# Copyright (C) 2024 Graz University of Technology. +# +# invenio-records-marc21 is free software; you can redistribute it and/or modify +# it under the terms of the MIT License; see LICENSE file for more details. + +"""File download event OpenSearch index templates.""" diff --git a/invenio_records_marc21/records/statistics/templates/events/marc21_file_download/os-v2/__init__.py b/invenio_records_marc21/records/statistics/templates/events/marc21_file_download/os-v2/__init__.py new file mode 100644 index 00000000..137f4043 --- /dev/null +++ b/invenio_records_marc21/records/statistics/templates/events/marc21_file_download/os-v2/__init__.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Invenio. +# Copyright (C) 2019 CERN. +# Copyright (C) 2023 TU Wien. +# Copyright (C) 2024 Graz University of Technology. +# +# invenio-records-marc21 is free software; you can redistribute it and/or modify +# it under the terms of the MIT License; see LICENSE file for more details. + +"""File download event OpenSearch index templates.""" diff --git a/invenio_records_marc21/records/statistics/templates/events/marc21_file_download/os-v2/marc21-file-download-v1.0.0.json b/invenio_records_marc21/records/statistics/templates/events/marc21_file_download/os-v2/marc21-file-download-v1.0.0.json new file mode 100644 index 00000000..ae4a9513 --- /dev/null +++ b/invenio_records_marc21/records/statistics/templates/events/marc21_file_download/os-v2/marc21-file-download-v1.0.0.json @@ -0,0 +1,96 @@ +{ + "index_patterns": ["__SEARCH_INDEX_PREFIX__events-stats-marc21-file-download-*"], + "settings": { + "index": { + "refresh_interval": "5s" + } + }, + "mappings": { + "dynamic_templates": [ + { + "date_fields": { + "match_mapping_type": "date", + "mapping": { + "type": "date", + "format": "strict_date_hour_minute_second" + } + } + } + ], + "date_detection": false, + "dynamic": "strict", + "numeric_detection": false, + "properties": { + "timestamp": { + "type": "date", + "format": "strict_date_hour_minute_second" + }, + "bucket_id": { + "type": "keyword" + }, + "file_id": { + "type": "keyword" + }, + "file_key": { + "type": "keyword" + }, + "unique_id": { + "type": "keyword" + }, + "country": { + "type": "keyword" + }, + "visitor_id": { + "type": "keyword" + }, + "is_machine": { + "type": "boolean" + }, + "is_robot": { + "type": "boolean" + }, + "unique_session_id": { + "type": "keyword" + }, + "size": { + "type": "double" + }, + "referrer": { + "type": "keyword" + }, + "ip_address": { + "type": "keyword" + }, + "user_agent": { + "type": "keyword" + }, + "user_id": { + "type": "keyword" + }, + "session_id": { + "type": "keyword" + }, + "record_id": { + "type": "keyword" + }, + "recid": { + "type": "keyword" + }, + "parent_id": { + "type": "keyword" + }, + "parent_recid": { + "type": "keyword" + }, + "via_api": { + "type": "boolean" + }, + "updated_timestamp": { + "type": "date" + } + } + }, + "aliases": { + "__SEARCH_INDEX_PREFIX__events-stats-marc21-file-download": {} + } +} diff --git a/invenio_records_marc21/records/statistics/templates/events/marc21_record_view/__init__.py b/invenio_records_marc21/records/statistics/templates/events/marc21_record_view/__init__.py new file mode 100644 index 00000000..8e768f23 --- /dev/null +++ b/invenio_records_marc21/records/statistics/templates/events/marc21_record_view/__init__.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Invenio. +# Copyright (C) 2018 CERN. +# Copyright (C) 2023 TU Wien. +# Copyright (C) 2024 Graz University of Technology. +# +# invenio-records-marc21 is free software; you can redistribute it and/or modify +# it under the terms of the MIT License; see LICENSE file for more details. + +"""Record views search index templates.""" diff --git a/invenio_records_marc21/records/statistics/templates/events/marc21_record_view/os-v2/__init__.py b/invenio_records_marc21/records/statistics/templates/events/marc21_record_view/os-v2/__init__.py new file mode 100644 index 00000000..13ad5be1 --- /dev/null +++ b/invenio_records_marc21/records/statistics/templates/events/marc21_record_view/os-v2/__init__.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Invenio. +# Copyright (C) 2019 CERN. +# Copyright (C) 2023 TU Wien. +# Copyright (C) 2024 Graz University of Technology. +# +# invenio-records-marc21 is free software; you can redistribute it and/or modify +# it under the terms of the MIT License; see LICENSE file for more details. + +"""Record view event OpenSearch index templates.""" diff --git a/invenio_records_marc21/records/statistics/templates/events/marc21_record_view/os-v2/marc21-record-view-v1.0.0.json b/invenio_records_marc21/records/statistics/templates/events/marc21_record_view/os-v2/marc21-record-view-v1.0.0.json new file mode 100644 index 00000000..c2284bdd --- /dev/null +++ b/invenio_records_marc21/records/statistics/templates/events/marc21_record_view/os-v2/marc21-record-view-v1.0.0.json @@ -0,0 +1,75 @@ +{ + "index_patterns": [ + "__SEARCH_INDEX_PREFIX__events-stats-marc21-record-view-*" + ], + "settings": { + "index": { + "refresh_interval": "5s" + } + }, + "mappings": { + "date_detection": false, + "dynamic": "strict", + "numeric_detection": false, + "properties": { + "timestamp": { + "type": "date", + "format": "strict_date_hour_minute_second" + }, + "labels": { + "type": "keyword" + }, + "country": { + "type": "keyword" + }, + "visitor_id": { + "type": "keyword" + }, + "is_robot": { + "type": "boolean" + }, + "unique_id": { + "type": "keyword" + }, + "unique_session_id": { + "type": "keyword" + }, + "referrer": { + "type": "keyword" + }, + "ip_address": { + "type": "keyword" + }, + "user_agent": { + "type": "keyword" + }, + "user_id": { + "type": "keyword" + }, + "session_id": { + "type": "keyword" + }, + "record_id": { + "type": "keyword" + }, + "recid": { + "type": "keyword" + }, + "parent_id": { + "type": "keyword" + }, + "parent_recid": { + "type": "keyword" + }, + "via_api": { + "type": "boolean" + }, + "updated_timestamp": { + "type": "date" + } + } + }, + "aliases": { + "__SEARCH_INDEX_PREFIX__events-stats-marc21-record-view": {} + } +} diff --git a/invenio_records_marc21/records/systemfields/__init__.py b/invenio_records_marc21/records/systemfields/__init__.py index f31d67a7..e9d34fa9 100644 --- a/invenio_records_marc21/records/systemfields/__init__.py +++ b/invenio_records_marc21/records/systemfields/__init__.py @@ -14,6 +14,7 @@ from .context import MarcPIDFieldContext from .providers import MarcDraftProvider, MarcRecordProvider from .resolver import MarcResolver +from .statistics import Marc21RecordStatisticsField from .status import Marc21Status __all__ = ( @@ -21,5 +22,6 @@ "MarcDraftProvider", "MarcRecordProvider", "MarcResolver", + "Marc21RecordStatisticsField", "Marc21Status", ) diff --git a/invenio_records_marc21/records/systemfields/statistics.py b/invenio_records_marc21/records/systemfields/statistics.py new file mode 100644 index 00000000..ee870cb4 --- /dev/null +++ b/invenio_records_marc21/records/systemfields/statistics.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2023 TU Wien. +# Copyright (C) 2024 Graz University of Technology. +# +# invenio-records-marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. + +"""Cached transient field for record statistics.""" + +from invenio_rdm_records.records.systemfields import RecordStatisticsField +from invenio_search.proxies import current_search_client +from invenio_search.utils import build_alias_name + +from ..statistics import Marc21Statistics + + +class Marc21RecordStatisticsField(RecordStatisticsField): + """Field for lazy fetching and caching (but not storing) of tugraz record statistics.""" + + api = Marc21Statistics + """""" + + def _get_record_stats(self, record): + """Get the record's statistics from either record or aggregation index.""" + stats = None + recid, parent_recid = record["id"], record.parent["id"] + + try: + # for more consistency between search results and each record's details, + # we try to get the statistics from the record's search index first + # note: this field is dumped into the record's data before indexing + # by the search dumper extension "StatisticsDumperExt" + res = current_search_client.get( + index=build_alias_name(record.index._name), + id=record.id, + params={"_source_includes": "stats"}, + ) + stats = res["_source"]["stats"] + except Exception: + stats = None + + # as a fallback, use the more up-to-date aggregations indices + return stats or self.api.get_record_stats( + recid=recid, parent_recid=parent_recid + ) diff --git a/invenio_records_marc21/resources/serializers/schema.py b/invenio_records_marc21/resources/serializers/schema.py index 5b6c2adf..977139bc 100644 --- a/invenio_records_marc21/resources/serializers/schema.py +++ b/invenio_records_marc21/resources/serializers/schema.py @@ -29,4 +29,5 @@ class Meta: "links", "files", "versions", + "stats", ) diff --git a/invenio_records_marc21/services/config.py b/invenio_records_marc21/services/config.py index 40dd7865..d4f0f80b 100644 --- a/invenio_records_marc21/services/config.py +++ b/invenio_records_marc21/services/config.py @@ -106,7 +106,7 @@ class Marc21RecordServiceConfig(RecordServiceConfig, ConfiguratorMixin): search_option_cls=Marc21SearchOptions, ) search_drafts = FromConfigSearchOptions( - "MARC21_SEARCH", + "MARC21_SEARCH_DRAFTS", "MARC21_SORT_OPTIONS", "MARC21_FACETS", search_option_cls=Marc21SearchDraftsOptions, diff --git a/invenio_records_marc21/services/schemas/__init__.py b/invenio_records_marc21/services/schemas/__init__.py index 6852cb32..7014e0fa 100644 --- a/invenio_records_marc21/services/schemas/__init__.py +++ b/invenio_records_marc21/services/schemas/__init__.py @@ -2,7 +2,7 @@ # # This file is part of Invenio. # -# Copyright (C) 2021-2023 Graz University of Technology. +# Copyright (C) 2021-2024 Graz University of Technology. # # Invenio-Records-Marc21 is free software; you can redistribute it and/or # modify it under the terms of the MIT License; see LICENSE file for more @@ -26,6 +26,7 @@ from .metadata import MetadataSchema from .pids import PIDSchema +from .statistics import Marc21StatisticSchema def validate_scheme(scheme): @@ -72,6 +73,8 @@ class Marc21RecordSchema(BaseRecordSchema, FieldPermissionsMixin): is_published = Boolean(dump_only=True) status = Str(dump_only=True) + stats = NestedAttribute(Marc21StatisticSchema, dump_only=True) + # Add version to record schema # versions = NestedAttribute(VersionsSchema, dump_only=True) diff --git a/invenio_records_marc21/services/schemas/statistics.py b/invenio_records_marc21/services/schemas/statistics.py new file mode 100644 index 00000000..ffa3015e --- /dev/null +++ b/invenio_records_marc21/services/schemas/statistics.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2024 Graz University of Technology. +# +# invenio-records-marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. + +"""Tugraz statistic record schemas.""" + + +from marshmallow import Schema, fields + + +class PartialStatisticSchema(Schema): + """Schema for a part of the record statistics. + + This fits both the statistics for "this version" as well as + "all versions", because they have the same shape. + """ + + views = fields.Int() + unique_views = fields.Int() + downloads = fields.Int() + unique_downloads = fields.Int() + data_volume = fields.Float() + + +class Marc21StatisticSchema(Schema): + """Schema for the entire record statistics.""" + + this_version = fields.Nested(PartialStatisticSchema) + all_versions = fields.Nested(PartialStatisticSchema) diff --git a/invenio_records_marc21/services/tasks.py b/invenio_records_marc21/services/tasks.py index b59ac460..9083f123 100644 --- a/invenio_records_marc21/services/tasks.py +++ b/invenio_records_marc21/services/tasks.py @@ -2,7 +2,7 @@ # # This file is part of Invenio. # -# Copyright (C) 2021 Graz University of Technology. +# Copyright (C) 2021-2024 Graz University of Technology. # # Invenio-Records-Marc21 is free software; you can redistribute it and/or # modify it under the terms of the MIT License; see LICENSE file for more @@ -10,10 +10,17 @@ """Marc21 Celery tasks.""" +from datetime import datetime, timedelta +from functools import partial + import arrow from celery import shared_task from flask import current_app from invenio_access.permissions import system_identity +from invenio_search.engine import dsl +from invenio_search.proxies import current_search_client +from invenio_search.utils import prefix_index +from invenio_stats.bookmark import BookmarkAPI from ..proxies import current_records_marc21 from .errors import EmbargoNotLiftedError @@ -39,3 +46,39 @@ def update_expired_embargos(): " lifted" ) continue + + +@shared_task(ignore_result=True) +def marc21_reindex_stats(stats_indices): + """Reindex the documents where the stats have changed.""" + bm = BookmarkAPI(current_search_client, "marc21_stats_reindex", "day") + last_run = bm.get_bookmark() + if not last_run: + # If this is the first time that we run, let's do it for the documents of the last week + last_run = (datetime.utcnow() - timedelta(days=7)).isoformat() + reindex_start_time = datetime.utcnow().isoformat() + indices = ",".join(map(lambda x: prefix_index(x) + "*", stats_indices)) + + all_parents = set() + query = dsl.Search( + using=current_search_client, + index=indices, + ).filter({"range": {"updated_timestamp": {"gte": last_run}}}) + + for result in query.scan(): + parent_id = result.parent_recid + all_parents.add(parent_id) + + if all_parents: + all_parents_list = list(all_parents) + step = 10000 + end = len(list(all_parents)) + for i in range(0, end, step): + records_q = dsl.Q("terms", parent__id=all_parents_list[i : i + step]) + current_records_marc21.record_service.reindex( + params={"allversions": True}, + identity=system_identity, + search_query=records_q, + ) + bm.set_bookmark(reindex_start_time) + return "%d documents reindexed" % len(all_parents) diff --git a/invenio_records_marc21/templates/invenio_records_marc21/landing_page/helpers/side_bar.html b/invenio_records_marc21/templates/invenio_records_marc21/landing_page/helpers/side_bar.html index f3e891a1..737352c8 100644 --- a/invenio_records_marc21/templates/invenio_records_marc21/landing_page/helpers/side_bar.html +++ b/invenio_records_marc21/templates/invenio_records_marc21/landing_page/helpers/side_bar.html @@ -1,11 +1,15 @@ {# -*- coding: utf-8 -*- - Copyright (C) 2021-2023 Graz University of Technology. + Copyright (C) 2021-2024 Graz University of Technology. Invenio-Records-Marc21 is free software; you can redistribute it and/or modify it under the terms of the MIT License; see LICENSE file for more details. #} diff --git a/invenio_records_marc21/templates/invenio_records_marc21/landing_page/helpers/statistics.html b/invenio_records_marc21/templates/invenio_records_marc21/landing_page/helpers/statistics.html new file mode 100644 index 00000000..7195b9c4 --- /dev/null +++ b/invenio_records_marc21/templates/invenio_records_marc21/landing_page/helpers/statistics.html @@ -0,0 +1,32 @@ +{# -*- coding: utf-8 -*- + Copyright (C) 2024 Graz University of Technology. + + invenio-records-marc21 is free software; you can redistribute it and/or modify + it under the terms of the MIT License; see LICENSE file for more details. +#} +
+
+ {% set all_versions = record.stats.all_versions %} {% set this_version = + record.stats.this_version %} + +
+
+ {{ all_versions.unique_views|compact_number(max_value=1_000_000) }} +
+
+ + {{ _("Views") }} +
+
+ +
+
+ {{ all_versions.unique_downloads|compact_number(max_value=1_000_000) }} +
+
+ + {{ _("Downloads") }} +
+
+
+
diff --git a/invenio_records_marc21/templates/invenio_records_marc21/landing_page/record.html b/invenio_records_marc21/templates/invenio_records_marc21/landing_page/record.html index 02154e20..785c49d4 100644 --- a/invenio_records_marc21/templates/invenio_records_marc21/landing_page/record.html +++ b/invenio_records_marc21/templates/invenio_records_marc21/landing_page/record.html @@ -1,7 +1,7 @@ {# This file is part of Invenio. -Copyright (C) 2021 Graz University of Technology. +Copyright (C) 2021-2024 Graz University of Technology. Invenio-Records-Marc21 is free software; you can redistribute it and/or modify it under the terms of the MIT License; see LICENSE file for more @@ -15,6 +15,13 @@ {%- set metadata = record.ui.metadata %} +{% block head_title %} + + {% for title in metadata.get('titles') %} + {{title}} + {% endfor %} + +{% endblock head_title %} {%- block page_body %}
diff --git a/invenio_records_marc21/ui/records/records.py b/invenio_records_marc21/ui/records/records.py index acfe3628..1c0f5292 100644 --- a/invenio_records_marc21/ui/records/records.py +++ b/invenio_records_marc21/ui/records/records.py @@ -16,6 +16,7 @@ from invenio_base.utils import obj_or_import_string from invenio_previewer.extensions import default from invenio_previewer.proxies import current_previewer +from invenio_stats.proxies import current_stats from ...resources.serializers.ui import Marc21UIJSONSerializer from .decorators import ( @@ -72,6 +73,11 @@ def open(self): def record_detail(record=None, files=None, pid_value=None, is_preview=False): """Record detail page (aka landing page).""" files_dict = None if files is None else files.to_dict() + + # emit a record view stats event + emitter = current_stats.get_event_emitter("marc21-record-view") + if record is not None and emitter is not None: + emitter(current_app, record=record._record, via_api=False) return render_template( "invenio_records_marc21/landing_page/record.html", record=Marc21UIJSONSerializer().dump_obj(record.to_dict()), @@ -154,4 +160,11 @@ def record_file_preview( def record_file_download(file_item=None, pid_value=None, is_preview=False, **kwargs): """Download a file from a record.""" download = bool(request.args.get("download")) + + # emit a file download stats event + emitter = current_stats.get_event_emitter("marc21-file-download") + if file_item is not None and emitter is not None: + obj = file_item._file.object_version + emitter(current_app, record=file_item._record, obj=obj, via_api=False) + return file_item.send_file(as_attachment=download) diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/search/components/Marc21RecordResultsListItem.js b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/search/components/Marc21RecordResultsListItem.js index 4c849840..c128a409 100644 --- a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/search/components/Marc21RecordResultsListItem.js +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/search/components/Marc21RecordResultsListItem.js @@ -1,6 +1,6 @@ // This file is part of Invenio. // -// Copyright (C) 2021-2023 Graz University of Technology. +// Copyright (C) 2021-2024 Graz University of Technology. // // Invenio-Records-Marc21 is free software; you can redistribute it and/or // modify it under the terms of the MIT License; see LICENSE file for more @@ -11,6 +11,7 @@ import { truncate, get } from "lodash"; import { Button, Item, Label } from "semantic-ui-react"; import { EditButton } from "@js/invenio_records_marc21/components/EditButton"; import { i18next } from "@translations/invenio_records_marc21/i18next"; +import { Marc21Stats } from "./Marc21Stats"; export const Marc21RecordResultsListItem = ({ dashboard, result, index }) => { const version = get(result, "revision_id", null); @@ -29,6 +30,9 @@ export const Marc21RecordResultsListItem = ({ dashboard, result, index }) => { const creators = get(result, "ui.metadata.authors", []); const titles = get(result, "ui.metadata.titles", ["No titles"]); + const uniqueViews = get(result, "stats.all_versions.unique_views", 0); + const uniqueDownloads = get(result, "stats.all_versions.unique_downloads", 0); + const copyright = get(result, "ui.metadata.copyright", []); let published_at = null; if (copyright.length > 0) { @@ -100,13 +104,20 @@ export const Marc21RecordResultsListItem = ({ dashboard, result, index }) => { {subject.miscellaneous_information} ))} - {createdDate && ( -
+ +
+ {createdDate && ( {i18next.t("Uploaded on")} {createdDate} -
- )} + )} + + + +
diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/search/components/Marc21Stats.js b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/search/components/Marc21Stats.js new file mode 100644 index 00000000..52378f4d --- /dev/null +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/search/components/Marc21Stats.js @@ -0,0 +1,53 @@ +// This file is part of Invenio. +// +// Copyright (C) 2024 Graz University of Technology. +// +// Invenio-Records-Marc21 is free software; you can redistribute it and/or +// modify it under the terms of the MIT License; see LICENSE file for more +// details. + +import PropTypes from "prop-types"; +import React from "react"; +import { Icon, Label, Popup } from "semantic-ui-react"; +import { i18next } from "@translations/invenio_records_marc21/i18next"; + +export const Marc21Stats = ({ uniqueViews, uniqueDownloads }) => { + return ( + <> + {uniqueViews != null && ( + + + {uniqueViews} + + } + /> + )} + {uniqueDownloads != null && ( + + + {uniqueDownloads} + + } + /> + )} + + ); +}; + +Marc21Stats.propTypes = { + uniqueViews: PropTypes.number, + uniqueDownloads: PropTypes.number, +}; + +Marc21Stats.defaultProps = { + uniqueViews: null, + uniqueDownloads: null, +}; diff --git a/invenio_records_marc21/utils.py b/invenio_records_marc21/utils.py index 4646dfaa..ac44245b 100644 --- a/invenio_records_marc21/utils.py +++ b/invenio_records_marc21/utils.py @@ -2,7 +2,7 @@ # # This file is part of Invenio. # -# Copyright (C) 2023 Graz University of Technology. +# Copyright (C) 2023-2024 Graz University of Technology. # # Invenio-Records-Marc21 is free software; you can redistribute it and/or # modify it under the terms of the MIT License; see LICENSE file for more @@ -205,3 +205,9 @@ def create_fake_data(): } return data_to_use + + +def build_record_unique_id(doc): + """Build record unique identifier.""" + doc["unique_id"] = "{0}_{1}".format(doc["recid"], doc["parent_recid"]) + return doc diff --git a/setup.cfg b/setup.cfg index 63c0bea1..44ea5d81 100644 --- a/setup.cfg +++ b/setup.cfg @@ -34,6 +34,7 @@ zip_safe = False install_requires = arrow>=1.0.0 invenio-rdm-records>=4.0.0 + invenio-stats>=1.0.0 fastjsonschema>=2.16.0 lxml>=4.6.2