invenio-app-ils 6.1.0__py2.py3-none-any.whl → 7.0.0__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- invenio_app_ils/__init__.py +2 -2
- invenio_app_ils/circulation/errors.py +12 -0
- invenio_app_ils/circulation/indexer.py +70 -0
- invenio_app_ils/circulation/stats/api.py +101 -1
- invenio_app_ils/circulation/stats/schemas.py +110 -0
- invenio_app_ils/circulation/stats/serializers/__init__.py +13 -0
- invenio_app_ils/circulation/stats/serializers/response.py +37 -0
- invenio_app_ils/circulation/stats/serializers/schema.py +33 -0
- invenio_app_ils/circulation/stats/views.py +74 -3
- invenio_app_ils/config.py +55 -1
- invenio_app_ils/ext.py +7 -2
- invenio_app_ils/literature/covers_builder.py +13 -1
- invenio_app_ils/permissions.py +1 -0
- invenio_app_ils/stats/event_builders.py +44 -0
- invenio_app_ils/stats/processors.py +56 -0
- invenio_app_ils/stats/templates/aggregations/loan_transitions/__init__.py +8 -0
- invenio_app_ils/stats/templates/aggregations/loan_transitions/os-v2/__init__.py +8 -0
- invenio_app_ils/stats/templates/aggregations/loan_transitions/os-v2/aggr-loan-transitions-v1.json +32 -0
- invenio_app_ils/stats/templates/events/loan_transitions/__init__.py +8 -0
- invenio_app_ils/stats/templates/events/loan_transitions/os-v2/__init__.py +8 -0
- invenio_app_ils/stats/templates/events/loan_transitions/os-v2/loan-transitions-v1.json +40 -0
- {invenio_app_ils-6.1.0.dist-info → invenio_app_ils-7.0.0.dist-info}/METADATA +20 -4
- {invenio_app_ils-6.1.0.dist-info → invenio_app_ils-7.0.0.dist-info}/RECORD +28 -17
- {invenio_app_ils-6.1.0.dist-info → invenio_app_ils-7.0.0.dist-info}/WHEEL +0 -0
- {invenio_app_ils-6.1.0.dist-info → invenio_app_ils-7.0.0.dist-info}/entry_points.txt +0 -0
- {invenio_app_ils-6.1.0.dist-info → invenio_app_ils-7.0.0.dist-info}/licenses/AUTHORS.rst +0 -0
- {invenio_app_ils-6.1.0.dist-info → invenio_app_ils-7.0.0.dist-info}/licenses/LICENSE +0 -0
- {invenio_app_ils-6.1.0.dist-info → invenio_app_ils-7.0.0.dist-info}/top_level.txt +0 -0
invenio_app_ils/__init__.py
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
2
|
#
|
|
3
|
-
# Copyright (C) 2018-
|
|
3
|
+
# Copyright (C) 2018-2026 CERN.
|
|
4
4
|
#
|
|
5
5
|
# invenio-app-ils is free software; you can redistribute it and/or modify it
|
|
6
6
|
# under the terms of the MIT License; see LICENSE file for more details.
|
|
7
7
|
|
|
8
8
|
"""invenio-app-ils."""
|
|
9
9
|
|
|
10
|
-
__version__ = "
|
|
10
|
+
__version__ = "7.0.0"
|
|
11
11
|
|
|
12
12
|
__all__ = ("__version__",)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Copyright (C) 2025 CERN.
|
|
4
|
+
#
|
|
5
|
+
# Invenio-App-Ils is free software; you can redistribute it and/or modify
|
|
6
|
+
# it under the terms of the MIT License; see LICENSE file for more details.
|
|
7
|
+
|
|
8
|
+
"""Circulation exceptions."""
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class LoanTransitionEventsIndexMissingError(Exception):
|
|
12
|
+
"""Error raised when the loan transition events index is missing."""
|
|
@@ -15,7 +15,9 @@ from invenio_circulation.pidstore.pids import CIRCULATION_LOAN_PID_TYPE
|
|
|
15
15
|
from invenio_circulation.proxies import current_circulation
|
|
16
16
|
from invenio_indexer.api import RecordIndexer
|
|
17
17
|
from invenio_pidstore.errors import PIDDeletedError
|
|
18
|
+
from invenio_search import current_search_client
|
|
18
19
|
|
|
20
|
+
from invenio_app_ils.circulation.errors import LoanTransitionEventsIndexMissingError
|
|
19
21
|
from invenio_app_ils.circulation.utils import resolve_item_from_loan
|
|
20
22
|
from invenio_app_ils.documents.api import DOCUMENT_PID_TYPE
|
|
21
23
|
from invenio_app_ils.indexer import ReferencedRecordsIndexer
|
|
@@ -97,3 +99,71 @@ def index_extra_fields_for_loan(loan_dict):
|
|
|
97
99
|
|
|
98
100
|
can_circulate_items_count = document["circulation"]["can_circulate_items_count"]
|
|
99
101
|
loan_dict["can_circulate_items_count"] = can_circulate_items_count
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def index_stats_fields_for_loan(loan_dict):
|
|
105
|
+
"""Indexer hook to modify the loan record dict before indexing"""
|
|
106
|
+
|
|
107
|
+
creation_date = datetime.fromisoformat(loan_dict["_created"]).date()
|
|
108
|
+
start_date = (
|
|
109
|
+
datetime.fromisoformat(loan_dict["start_date"]).date()
|
|
110
|
+
if loan_dict.get("start_date")
|
|
111
|
+
else None
|
|
112
|
+
)
|
|
113
|
+
end_date = (
|
|
114
|
+
datetime.fromisoformat(loan_dict["end_date"]).date()
|
|
115
|
+
if loan_dict.get("end_date")
|
|
116
|
+
else None
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
# Collect extra information relevant for stats
|
|
120
|
+
stats = {}
|
|
121
|
+
|
|
122
|
+
# Time ranges in days
|
|
123
|
+
if start_date and end_date:
|
|
124
|
+
loan_duration = (end_date - start_date).days
|
|
125
|
+
stats["loan_duration"] = loan_duration
|
|
126
|
+
|
|
127
|
+
if creation_date and start_date:
|
|
128
|
+
waiting_time = (start_date - creation_date).days
|
|
129
|
+
stats["waiting_time"] = waiting_time if waiting_time >= 0 else None
|
|
130
|
+
|
|
131
|
+
# Document availability during loan request
|
|
132
|
+
stat_events_index_name = "events-stats-loan-transitions"
|
|
133
|
+
if not current_search_client.indices.exists(index=stat_events_index_name):
|
|
134
|
+
raise LoanTransitionEventsIndexMissingError()
|
|
135
|
+
|
|
136
|
+
loan_pid = loan_dict["pid"]
|
|
137
|
+
search_body = {
|
|
138
|
+
"query": {
|
|
139
|
+
"bool": {
|
|
140
|
+
"must": [
|
|
141
|
+
{"term": {"trigger": "request"}},
|
|
142
|
+
{"term": {"pid_value": loan_pid}},
|
|
143
|
+
],
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
search_result = current_search_client.search(
|
|
149
|
+
index=stat_events_index_name, body=search_body
|
|
150
|
+
)
|
|
151
|
+
hits = search_result["hits"]["hits"]
|
|
152
|
+
if len(hits) == 1:
|
|
153
|
+
request_transition_event = hits[0]["_source"]
|
|
154
|
+
available_items_during_request_count = request_transition_event[
|
|
155
|
+
"extra_data"
|
|
156
|
+
]["available_items_during_request_count"]
|
|
157
|
+
stats["available_items_during_request"] = (
|
|
158
|
+
available_items_during_request_count > 0
|
|
159
|
+
)
|
|
160
|
+
elif len(hits) > 1:
|
|
161
|
+
raise ValueError(
|
|
162
|
+
f"Multiple request transition events for loan {loan_pid}."
|
|
163
|
+
"Expected zero or one."
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
if not "extra_data" in loan_dict:
|
|
168
|
+
loan_dict["extra_data"] = {}
|
|
169
|
+
loan_dict["extra_data"]["stats"] = stats
|
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
2
|
#
|
|
3
|
-
# Copyright (C) 2019 CERN.
|
|
3
|
+
# Copyright (C) 2019-2025 CERN.
|
|
4
4
|
#
|
|
5
5
|
# invenio-app-ils is free software; you can redistribute it and/or modify it
|
|
6
6
|
# under the terms of the MIT License; see LICENSE file for more details.
|
|
7
7
|
|
|
8
8
|
"""APIs for ILS circulation statistics."""
|
|
9
9
|
|
|
10
|
+
from invenio_search.engine import dsl
|
|
11
|
+
|
|
10
12
|
from invenio_app_ils.circulation.search import get_most_loaned_documents
|
|
13
|
+
from invenio_app_ils.circulation.stats.schemas import (
|
|
14
|
+
_OS_NATIVE_AGGREGATE_FUNCTION_TYPES,
|
|
15
|
+
)
|
|
11
16
|
from invenio_app_ils.proxies import current_app_ils
|
|
12
17
|
|
|
13
18
|
|
|
@@ -49,3 +54,98 @@ def fetch_most_loaned_documents(from_date, to_date, bucket_size):
|
|
|
49
54
|
)
|
|
50
55
|
|
|
51
56
|
return res
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _generate_metric_agg_field_name(metric):
|
|
60
|
+
"""Return the aggregation name used for a metric.
|
|
61
|
+
|
|
62
|
+
:param metric: Must include 'field' and 'aggregation' keys.
|
|
63
|
+
:returns: The aggregation field name in the form '<aggregation>_<field>'.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
return f"{metric['aggregation']}__{metric['field']}"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def get_loan_statistics(date_fields, search, requested_group_by, requested_metrics):
|
|
70
|
+
"""Aggregate loan statistics for requested metrics.
|
|
71
|
+
|
|
72
|
+
:param date_fields: List of date fields for the record type.
|
|
73
|
+
Date fields require different handling when using them to group by.
|
|
74
|
+
:param search: The base search object to apply aggregations on
|
|
75
|
+
:param requested_group_by: List of group dictionaries with 'field' and optional 'interval' keys.
|
|
76
|
+
Example: [{"field": "start_date", "interval": "monthly"}, {"field": "state"}]
|
|
77
|
+
:param requested_metrics: List of metric dictionaries with 'field' and 'aggregation' keys.
|
|
78
|
+
Example: [{"field": "loan_duration", "aggregation": "avg"}]
|
|
79
|
+
:returns: OpenSearch aggregation results with multi-terms histogram and optional metrics
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
# Build composite aggregation
|
|
83
|
+
sources = []
|
|
84
|
+
for grouping in requested_group_by:
|
|
85
|
+
grouping_field = grouping["field"]
|
|
86
|
+
|
|
87
|
+
if grouping_field in date_fields:
|
|
88
|
+
sources.append(
|
|
89
|
+
{
|
|
90
|
+
grouping_field: {
|
|
91
|
+
"date_histogram": {
|
|
92
|
+
"field": grouping_field,
|
|
93
|
+
"calendar_interval": grouping["interval"],
|
|
94
|
+
"format": "yyyy-MM-dd",
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
)
|
|
99
|
+
else:
|
|
100
|
+
sources.append({grouping_field: {"terms": {"field": grouping_field}}})
|
|
101
|
+
|
|
102
|
+
composite_agg = dsl.A("composite", sources=sources, size=1000)
|
|
103
|
+
|
|
104
|
+
for metric in requested_metrics:
|
|
105
|
+
agg_name = _generate_metric_agg_field_name(metric)
|
|
106
|
+
|
|
107
|
+
grouping_field = metric["field"]
|
|
108
|
+
agg_type = metric["aggregation"]
|
|
109
|
+
field_config = {"field": grouping_field}
|
|
110
|
+
if agg_type in _OS_NATIVE_AGGREGATE_FUNCTION_TYPES:
|
|
111
|
+
composite_agg = composite_agg.metric(
|
|
112
|
+
agg_name, dsl.A(agg_type, **field_config)
|
|
113
|
+
)
|
|
114
|
+
elif agg_type == "median":
|
|
115
|
+
composite_agg = composite_agg.metric(
|
|
116
|
+
agg_name, dsl.A("percentiles", percents=[50], **field_config)
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
search.aggs.bucket("loan_aggregations", composite_agg)
|
|
120
|
+
|
|
121
|
+
# Only retrieve aggregation results
|
|
122
|
+
search = search[:0]
|
|
123
|
+
result = search.execute()
|
|
124
|
+
|
|
125
|
+
# Parse aggregation results
|
|
126
|
+
buckets = []
|
|
127
|
+
if hasattr(result.aggregations, "loan_aggregations"):
|
|
128
|
+
for bucket in result.aggregations.loan_aggregations.buckets:
|
|
129
|
+
metrics_data = {}
|
|
130
|
+
for metric in requested_metrics:
|
|
131
|
+
agg_name = _generate_metric_agg_field_name(metric)
|
|
132
|
+
|
|
133
|
+
if hasattr(bucket, agg_name):
|
|
134
|
+
agg_result = getattr(bucket, agg_name)
|
|
135
|
+
agg_type = metric["aggregation"]
|
|
136
|
+
|
|
137
|
+
if agg_type in _OS_NATIVE_AGGREGATE_FUNCTION_TYPES:
|
|
138
|
+
metrics_data[agg_name] = agg_result.value
|
|
139
|
+
elif agg_type == "median":
|
|
140
|
+
median_value = agg_result.values.get("50.0")
|
|
141
|
+
metrics_data[agg_name] = median_value
|
|
142
|
+
|
|
143
|
+
bucket_data = {
|
|
144
|
+
"key": bucket.key.to_dict(),
|
|
145
|
+
"doc_count": bucket.doc_count,
|
|
146
|
+
"metrics": metrics_data,
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
buckets.append(bucket_data)
|
|
150
|
+
|
|
151
|
+
return buckets
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Copyright (C) 2025 CERN.
|
|
4
|
+
#
|
|
5
|
+
# invenio-app-ils is free software; you can redistribute it and/or modify it
|
|
6
|
+
# under the terms of the MIT License; see LICENSE file for more details.
|
|
7
|
+
|
|
8
|
+
"""Marshmallow schemas for loan statistics validation."""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import re
|
|
12
|
+
|
|
13
|
+
from marshmallow import (
|
|
14
|
+
Schema,
|
|
15
|
+
ValidationError,
|
|
16
|
+
fields,
|
|
17
|
+
pre_load,
|
|
18
|
+
validate,
|
|
19
|
+
validates_schema,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
from invenio_app_ils.errors import InvalidParameterError
|
|
23
|
+
|
|
24
|
+
_OS_VALID_FIELD_NAME_PATTERN = re.compile(r"^[A-Za-z0-9_.]+$")
|
|
25
|
+
_OS_NATIVE_AGGREGATE_FUNCTION_TYPES = {"avg", "sum", "min", "max"}
|
|
26
|
+
_VALID_AGGREGATE_FUNCTION_TYPES = _OS_NATIVE_AGGREGATE_FUNCTION_TYPES.union({"median"})
|
|
27
|
+
_VALID_DATE_INTERVALS = {"1d", "1w", "1M", "1q", "1y"}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def validate_field_name(field_name):
|
|
31
|
+
"""Validate a field name for search to prevent injection attacks.
|
|
32
|
+
|
|
33
|
+
:param field_name: The field name to validate
|
|
34
|
+
:raises InvalidParameterError: If field name is invalid or potentially malicious
|
|
35
|
+
"""
|
|
36
|
+
if not _OS_VALID_FIELD_NAME_PATTERN.match(field_name):
|
|
37
|
+
raise InvalidParameterError(
|
|
38
|
+
description=(
|
|
39
|
+
f"Invalid field name '{field_name}'. "
|
|
40
|
+
"Field names may contain only alphanumeric characters, underscores, "
|
|
41
|
+
"and dots."
|
|
42
|
+
)
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class SecureFieldNameField(fields.String):
|
|
47
|
+
"""Marshmallow field that validates field names to prevent injection attacks."""
|
|
48
|
+
|
|
49
|
+
def _deserialize(self, value, attr, data, **kwargs):
|
|
50
|
+
"""Deserialize and validate field name."""
|
|
51
|
+
|
|
52
|
+
field_name = super()._deserialize(value, attr, data, **kwargs)
|
|
53
|
+
validate_field_name(field_name)
|
|
54
|
+
return field_name
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class GroupByItemSchema(Schema):
|
|
58
|
+
field = SecureFieldNameField(required=True)
|
|
59
|
+
interval = fields.String(validate=validate.OneOf(_VALID_DATE_INTERVALS))
|
|
60
|
+
|
|
61
|
+
@validates_schema
|
|
62
|
+
def validate_date_fields(self, data, **kwargs):
|
|
63
|
+
"""Validate that date fields have an interval and non-date fields do not."""
|
|
64
|
+
|
|
65
|
+
date_fields = self.context["date_fields"]
|
|
66
|
+
field = data.get("field")
|
|
67
|
+
interval = data.get("interval")
|
|
68
|
+
if field in date_fields and not interval:
|
|
69
|
+
raise ValidationError(
|
|
70
|
+
{"interval": ["Interval is required for date fields."]}
|
|
71
|
+
)
|
|
72
|
+
if field not in date_fields and interval is not None:
|
|
73
|
+
raise ValidationError(
|
|
74
|
+
{"interval": ["Interval must not be provided for non-date fields."]}
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class MetricItemSchema(Schema):
|
|
79
|
+
"""Schema for validating a single metric item."""
|
|
80
|
+
|
|
81
|
+
field = SecureFieldNameField(required=True)
|
|
82
|
+
aggregation = fields.String(
|
|
83
|
+
required=True, validate=validate.OneOf(_VALID_AGGREGATE_FUNCTION_TYPES)
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class HistogramParamsSchema(Schema):
|
|
88
|
+
"""Schema for validating the query string parameters for the histogram endpoint"""
|
|
89
|
+
|
|
90
|
+
metrics = fields.List(fields.Nested(MetricItemSchema), required=False)
|
|
91
|
+
group_by = fields.List(
|
|
92
|
+
fields.Nested(GroupByItemSchema), required=True, validate=validate.Length(min=1)
|
|
93
|
+
)
|
|
94
|
+
q = fields.String()
|
|
95
|
+
|
|
96
|
+
def __init__(self, date_fields, *args, **kwargs):
|
|
97
|
+
super().__init__(*args, **kwargs)
|
|
98
|
+
self.context = {"date_fields": set(date_fields)}
|
|
99
|
+
|
|
100
|
+
@pre_load
|
|
101
|
+
def parse_query_string(self, data, **kwargs):
|
|
102
|
+
"""Parse the metrics and group_by parameters from JSON strings."""
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
for key in ("metrics", "group_by"):
|
|
106
|
+
# default value as the field "metrics" is not required
|
|
107
|
+
data[key] = json.loads(data.get(key, "[]"))
|
|
108
|
+
except Exception as e:
|
|
109
|
+
raise ValidationError from e
|
|
110
|
+
return data
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# This file is part of Invenio.
|
|
4
|
+
# Copyright (C) 2025-2025 CERN.
|
|
5
|
+
#
|
|
6
|
+
# Invenio is free software; you can redistribute it and/or modify it
|
|
7
|
+
# under the terms of the MIT License; see LICENSE file for more details.
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
from invenio_app_ils.circulation.stats.serializers.response import loan_stats_responsify
|
|
11
|
+
from invenio_app_ils.circulation.stats.serializers.schema import HistogramStatsV1
|
|
12
|
+
|
|
13
|
+
loan_stats_response = loan_stats_responsify(HistogramStatsV1, "application/json")
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# This file is part of Invenio.
|
|
4
|
+
# Copyright (C) 2025-2025 CERN.
|
|
5
|
+
#
|
|
6
|
+
# Invenio is free software; you can redistribute it and/or modify it
|
|
7
|
+
# under the terms of the MIT License; see LICENSE file for more details.
|
|
8
|
+
|
|
9
|
+
"""Invenio App ILS loan stats response serializers."""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
|
|
13
|
+
from flask import current_app
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def loan_stats_responsify(schema_class, mimetype):
|
|
17
|
+
"""Loan stats response serializer.
|
|
18
|
+
|
|
19
|
+
:param schema_class: Schema instance.
|
|
20
|
+
:param mimetype: MIME type of response.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def view(data, code=200, headers=None):
|
|
24
|
+
"""Generate the response object."""
|
|
25
|
+
# return jsonify(data), code
|
|
26
|
+
response_data = schema_class().dump(data)
|
|
27
|
+
|
|
28
|
+
response = current_app.response_class(
|
|
29
|
+
json.dumps(response_data), mimetype=mimetype
|
|
30
|
+
)
|
|
31
|
+
response.status_code = code
|
|
32
|
+
|
|
33
|
+
if headers is not None:
|
|
34
|
+
response.headers.extend(headers)
|
|
35
|
+
return response
|
|
36
|
+
|
|
37
|
+
return view
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# This file is part of Invenio.
|
|
4
|
+
# Copyright (C) 2025-2025 CERN.
|
|
5
|
+
#
|
|
6
|
+
# Invenio is free software; you can redistribute it and/or modify it
|
|
7
|
+
# under the terms of the MIT License; see LICENSE file for more details.
|
|
8
|
+
|
|
9
|
+
"""Invenio App ILS loan stats serializers schema."""
|
|
10
|
+
|
|
11
|
+
from marshmallow import Schema, fields
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class BucketSchema(Schema):
|
|
15
|
+
"""Schema for a single histogram bucket."""
|
|
16
|
+
|
|
17
|
+
doc_count = fields.Int(required=True)
|
|
18
|
+
key = fields.Dict(keys=fields.String(), values=fields.String())
|
|
19
|
+
|
|
20
|
+
metrics = fields.Dict(
|
|
21
|
+
keys=fields.String(),
|
|
22
|
+
values=fields.Float(),
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class HistogramStatsV1(Schema):
|
|
27
|
+
"""Schema for a stats histogram response."""
|
|
28
|
+
|
|
29
|
+
buckets = fields.List(
|
|
30
|
+
fields.Nested(BucketSchema),
|
|
31
|
+
required=True,
|
|
32
|
+
description="Statistics buckets.",
|
|
33
|
+
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
2
|
#
|
|
3
|
-
# Copyright (C) 2019 CERN.
|
|
3
|
+
# Copyright (C) 2019-2025 CERN.
|
|
4
4
|
#
|
|
5
5
|
# invenio-app-ils is free software; you can redistribute it and/or modify it
|
|
6
6
|
# under the terms of the MIT License; see LICENSE file for more details.
|
|
@@ -10,11 +10,21 @@
|
|
|
10
10
|
from datetime import datetime
|
|
11
11
|
|
|
12
12
|
from flask import Blueprint, current_app, request
|
|
13
|
+
from invenio_circulation.pidstore.pids import CIRCULATION_LOAN_PID_TYPE
|
|
14
|
+
from invenio_circulation.proxies import current_circulation
|
|
13
15
|
from invenio_pidstore import current_pidstore
|
|
16
|
+
from invenio_records_rest.query import default_search_factory
|
|
14
17
|
from invenio_records_rest.utils import obj_or_import_string
|
|
15
18
|
from invenio_rest import ContentNegotiatedMethodView
|
|
16
|
-
|
|
17
|
-
|
|
19
|
+
from marshmallow.exceptions import ValidationError
|
|
20
|
+
|
|
21
|
+
from invenio_app_ils.circulation.stats.api import (
|
|
22
|
+
fetch_most_loaned_documents,
|
|
23
|
+
get_loan_statistics,
|
|
24
|
+
)
|
|
25
|
+
from invenio_app_ils.circulation.stats.schemas import HistogramParamsSchema
|
|
26
|
+
from invenio_app_ils.circulation.stats.serializers import loan_stats_response
|
|
27
|
+
from invenio_app_ils.circulation.views import IlsCirculationResource
|
|
18
28
|
from invenio_app_ils.config import RECORDS_REST_MAX_RESULT_WINDOW
|
|
19
29
|
from invenio_app_ils.documents.api import DOCUMENT_PID_FETCHER, DOCUMENT_PID_TYPE
|
|
20
30
|
from invenio_app_ils.errors import InvalidParameterError
|
|
@@ -46,11 +56,33 @@ def create_most_loaned_documents_view(blueprint, app):
|
|
|
46
56
|
)
|
|
47
57
|
|
|
48
58
|
|
|
59
|
+
def create_loan_histogram_view(blueprint, app):
|
|
60
|
+
"""Add url rule for loan histogram view."""
|
|
61
|
+
|
|
62
|
+
endpoints = app.config.get("RECORDS_REST_ENDPOINTS")
|
|
63
|
+
document_endpoint = endpoints.get(CIRCULATION_LOAN_PID_TYPE)
|
|
64
|
+
default_media_type = document_endpoint.get("default_media_type")
|
|
65
|
+
loan_stats_serializers = {"application/json": loan_stats_response}
|
|
66
|
+
|
|
67
|
+
loan_stats_view_func = LoanHistogramResource.as_view(
|
|
68
|
+
LoanHistogramResource.view_name,
|
|
69
|
+
serializers=loan_stats_serializers,
|
|
70
|
+
default_media_type=default_media_type,
|
|
71
|
+
ctx={},
|
|
72
|
+
)
|
|
73
|
+
blueprint.add_url_rule(
|
|
74
|
+
"/circulation/loans/stats",
|
|
75
|
+
view_func=loan_stats_view_func,
|
|
76
|
+
methods=["GET"],
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
49
80
|
def create_circulation_stats_blueprint(app):
|
|
50
81
|
"""Add statistics views to the blueprint."""
|
|
51
82
|
blueprint = Blueprint("invenio_app_ils_circulation_stats", __name__, url_prefix="")
|
|
52
83
|
|
|
53
84
|
create_most_loaned_documents_view(blueprint, app)
|
|
85
|
+
create_loan_histogram_view(blueprint, app)
|
|
54
86
|
|
|
55
87
|
return blueprint
|
|
56
88
|
|
|
@@ -131,3 +163,42 @@ class MostLoanedDocumentsResource(ContentNegotiatedMethodView):
|
|
|
131
163
|
pid_fetcher=current_pidstore.fetchers[DOCUMENT_PID_FETCHER],
|
|
132
164
|
search_result=most_loaned_documents,
|
|
133
165
|
)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class LoanHistogramResource(IlsCirculationResource):
|
|
169
|
+
"""Loan stats resource."""
|
|
170
|
+
|
|
171
|
+
view_name = "loan_histogram"
|
|
172
|
+
|
|
173
|
+
@need_permissions("stats-loans")
|
|
174
|
+
def get(self, **kwargs):
|
|
175
|
+
"""Get loan statistics."""
|
|
176
|
+
|
|
177
|
+
loan_cls = current_circulation.loan_record_cls
|
|
178
|
+
loan_date_fields = (
|
|
179
|
+
loan_cls.DATE_FIELDS + loan_cls.DATETIME_FIELDS + ["_created"]
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
schema = HistogramParamsSchema(loan_date_fields)
|
|
183
|
+
try:
|
|
184
|
+
parsed_args = schema.load(request.args.to_dict())
|
|
185
|
+
except ValidationError as e:
|
|
186
|
+
raise InvalidParameterError(description=e.messages) from e
|
|
187
|
+
|
|
188
|
+
# Construct search to allow for filtering with the q parameter
|
|
189
|
+
search_cls = current_circulation.loan_search_cls
|
|
190
|
+
search = search_cls()
|
|
191
|
+
search, _ = default_search_factory(self, search)
|
|
192
|
+
|
|
193
|
+
aggregation_buckets = get_loan_statistics(
|
|
194
|
+
loan_date_fields,
|
|
195
|
+
search,
|
|
196
|
+
parsed_args["group_by"],
|
|
197
|
+
parsed_args["metrics"],
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
response = {
|
|
201
|
+
"buckets": aggregation_buckets,
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return self.make_response(response, 200)
|
invenio_app_ils/config.py
CHANGED
|
@@ -43,6 +43,7 @@ from invenio_app_ils.locations.indexer import LocationIndexer
|
|
|
43
43
|
from invenio_app_ils.patrons.indexer import PatronIndexer
|
|
44
44
|
from invenio_app_ils.series.indexer import SeriesIndexer
|
|
45
45
|
from invenio_app_ils.stats.event_builders import ils_record_changed_event_builder
|
|
46
|
+
from invenio_app_ils.stats.processors import LoansEventsIndexer
|
|
46
47
|
from invenio_app_ils.vocabularies.indexer import VocabularyIndexer
|
|
47
48
|
|
|
48
49
|
from .document_requests.api import (
|
|
@@ -244,13 +245,21 @@ CELERY_BEAT_SCHEDULE = {
|
|
|
244
245
|
"ils-record-changes-updates",
|
|
245
246
|
"ils-record-changes-insertions",
|
|
246
247
|
"ils-record-changes-deletions",
|
|
248
|
+
"loan-transitions",
|
|
247
249
|
)
|
|
248
250
|
],
|
|
249
251
|
},
|
|
250
252
|
"stats-aggregate-events": {
|
|
251
253
|
"task": "invenio_stats.tasks.aggregate_events",
|
|
252
254
|
"schedule": timedelta(hours=3),
|
|
253
|
-
"args": [
|
|
255
|
+
"args": [
|
|
256
|
+
(
|
|
257
|
+
"record-view-agg",
|
|
258
|
+
"file-download-agg",
|
|
259
|
+
"ils-record-changes-agg",
|
|
260
|
+
"loan-transitions-agg",
|
|
261
|
+
)
|
|
262
|
+
],
|
|
254
263
|
},
|
|
255
264
|
"clean_locations_past_closures_exceptions": {
|
|
256
265
|
"task": (
|
|
@@ -983,6 +992,25 @@ STATS_EVENTS = {
|
|
|
983
992
|
"suffix": "%Y",
|
|
984
993
|
},
|
|
985
994
|
},
|
|
995
|
+
# The following events are used to count loan state transitions.
|
|
996
|
+
# Additionally, some transitions, e.g. "request", are used to store extra data,
|
|
997
|
+
# like the number of available items when a loan is requested.
|
|
998
|
+
# The loan indexer then later queries those events and adds the information to the loan.
|
|
999
|
+
"loan-transitions": {
|
|
1000
|
+
"signal": "invenio_circulation.signals.loan_state_changed",
|
|
1001
|
+
"templates": "invenio_app_ils.stats.templates.events.loan_transitions",
|
|
1002
|
+
"event_builders": [
|
|
1003
|
+
"invenio_app_ils.stats.event_builders.loan_transition_event_builder",
|
|
1004
|
+
],
|
|
1005
|
+
"cls": LoansEventsIndexer,
|
|
1006
|
+
"params": {
|
|
1007
|
+
"preprocessors": [
|
|
1008
|
+
"invenio_app_ils.stats.processors.add_loan_transition_unique_id",
|
|
1009
|
+
],
|
|
1010
|
+
"double_click_window": 0,
|
|
1011
|
+
"suffix": "%Y",
|
|
1012
|
+
},
|
|
1013
|
+
},
|
|
986
1014
|
}
|
|
987
1015
|
|
|
988
1016
|
STATS_AGGREGATIONS = {
|
|
@@ -1042,6 +1070,19 @@ STATS_AGGREGATIONS = {
|
|
|
1042
1070
|
query_modifiers=[],
|
|
1043
1071
|
),
|
|
1044
1072
|
),
|
|
1073
|
+
"loan-transitions-agg": dict(
|
|
1074
|
+
templates="invenio_app_ils.stats.templates.aggregations.loan_transitions",
|
|
1075
|
+
cls=StatAggregator,
|
|
1076
|
+
params=dict(
|
|
1077
|
+
event="loan-transitions",
|
|
1078
|
+
field="trigger",
|
|
1079
|
+
interval="day",
|
|
1080
|
+
index_interval="year",
|
|
1081
|
+
copy_fields=dict(),
|
|
1082
|
+
metric_fields=dict(),
|
|
1083
|
+
query_modifiers=[],
|
|
1084
|
+
),
|
|
1085
|
+
),
|
|
1045
1086
|
}
|
|
1046
1087
|
|
|
1047
1088
|
STATS_QUERIES = {
|
|
@@ -1116,6 +1157,18 @@ STATS_QUERIES = {
|
|
|
1116
1157
|
aggregated_fields=["user_id"],
|
|
1117
1158
|
),
|
|
1118
1159
|
),
|
|
1160
|
+
"loan-transitions": dict(
|
|
1161
|
+
cls=DateHistogramQuery,
|
|
1162
|
+
permission_factory=backoffice_read_permission,
|
|
1163
|
+
params=dict(
|
|
1164
|
+
index="stats-loan-transitions",
|
|
1165
|
+
copy_fields=dict(),
|
|
1166
|
+
required_filters=dict(trigger="trigger"),
|
|
1167
|
+
metric_fields=dict(
|
|
1168
|
+
count=("sum", "count", {}),
|
|
1169
|
+
),
|
|
1170
|
+
),
|
|
1171
|
+
),
|
|
1119
1172
|
}
|
|
1120
1173
|
|
|
1121
1174
|
# List of available vocabularies
|
|
@@ -1189,6 +1242,7 @@ DB_VERSIONING_USER_MODEL = None
|
|
|
1189
1242
|
|
|
1190
1243
|
# Feature Toggles
|
|
1191
1244
|
ILS_SELF_CHECKOUT_ENABLED = False
|
|
1245
|
+
ILS_EXTEND_INDICES_WITH_STATS_ENABLED = False
|
|
1192
1246
|
|
|
1193
1247
|
# Use default frontpage
|
|
1194
1248
|
THEME_FRONTPAGE = False
|
invenio_app_ils/ext.py
CHANGED
|
@@ -20,10 +20,13 @@ from invenio_app_ils.records.metadata_extensions import (
|
|
|
20
20
|
)
|
|
21
21
|
|
|
22
22
|
from .circulation import config as circulation_config
|
|
23
|
-
from .circulation.indexer import
|
|
23
|
+
from .circulation.indexer import (
|
|
24
|
+
index_extra_fields_for_loan,
|
|
25
|
+
index_stats_fields_for_loan,
|
|
26
|
+
)
|
|
24
27
|
from .circulation.receivers import register_circulation_signals
|
|
25
28
|
from .document_requests.api import DOCUMENT_REQUEST_PID_TYPE
|
|
26
|
-
from .documents.api import DOCUMENT_PID_TYPE
|
|
29
|
+
from .documents.api import DOCUMENT_PID_TYPE
|
|
27
30
|
from .eitems.api import EITEM_PID_TYPE
|
|
28
31
|
from .files.receivers import register_files_signals
|
|
29
32
|
from .internal_locations.api import INTERNAL_LOCATION_PID_TYPE
|
|
@@ -327,3 +330,5 @@ def before_loan_index_hook(sender, json=None, record=None, index=None, **kwargs)
|
|
|
327
330
|
:param kwargs: Any other parameters.
|
|
328
331
|
"""
|
|
329
332
|
index_extra_fields_for_loan(json)
|
|
333
|
+
if current_app.config["ILS_EXTEND_INDICES_WITH_STATS_ENABLED"]:
|
|
334
|
+
index_stats_fields_for_loan(json)
|
|
@@ -13,7 +13,19 @@ from flask import url_for
|
|
|
13
13
|
def build_ils_demo_cover_urls(metadata):
|
|
14
14
|
"""Build working ulrs for demo data."""
|
|
15
15
|
cover_metadata = metadata.get("cover_metadata", {})
|
|
16
|
-
|
|
16
|
+
|
|
17
|
+
isbn = (
|
|
18
|
+
cover_metadata.get("ISBN", "")
|
|
19
|
+
or cover_metadata.get("isbn", "")
|
|
20
|
+
or metadata.get("isbn", "")
|
|
21
|
+
or ""
|
|
22
|
+
)
|
|
23
|
+
identifiers = metadata.get("identifiers", [])
|
|
24
|
+
if not isbn and identifiers:
|
|
25
|
+
for identifier in identifiers:
|
|
26
|
+
if identifier.get("scheme") == "ISBN":
|
|
27
|
+
isbn = identifier.get("value", "")
|
|
28
|
+
break
|
|
17
29
|
if isbn:
|
|
18
30
|
return build_openlibrary_urls(isbn)
|
|
19
31
|
return build_placeholder_urls()
|
invenio_app_ils/permissions.py
CHANGED
|
@@ -14,6 +14,7 @@ from flask import g
|
|
|
14
14
|
from flask_login import current_user
|
|
15
15
|
|
|
16
16
|
from invenio_app_ils.permissions import backoffice_permission
|
|
17
|
+
from invenio_app_ils.proxies import current_app_ils
|
|
17
18
|
from invenio_app_ils.records.api import IlsRecord
|
|
18
19
|
|
|
19
20
|
|
|
@@ -71,3 +72,46 @@ def add_record_pid_to_event(event, sender_app, record=None, **kwargs):
|
|
|
71
72
|
event.update({"pid_value": record.get("pid")})
|
|
72
73
|
|
|
73
74
|
return event
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def loan_transition_event_builder(
|
|
78
|
+
event,
|
|
79
|
+
sender_app,
|
|
80
|
+
transition=None,
|
|
81
|
+
initial_loan=None,
|
|
82
|
+
loan=None,
|
|
83
|
+
trigger=None,
|
|
84
|
+
**kwargs
|
|
85
|
+
):
|
|
86
|
+
"""Build an event for a loan state transition."""
|
|
87
|
+
event.update(
|
|
88
|
+
{
|
|
89
|
+
"timestamp": datetime.datetime.now(datetime.timezone.utc)
|
|
90
|
+
.replace(tzinfo=None)
|
|
91
|
+
.isoformat(),
|
|
92
|
+
"trigger": trigger,
|
|
93
|
+
"pid_value": loan["pid"],
|
|
94
|
+
}
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
if trigger == "request":
|
|
98
|
+
# Store how many items were available during request.
|
|
99
|
+
# This information is used by the loan indexer and added to the loan.
|
|
100
|
+
document_pid = loan["document_pid"]
|
|
101
|
+
document_class = current_app_ils.document_record_cls
|
|
102
|
+
document = document_class.get_record_by_pid(document_pid)
|
|
103
|
+
document_dict = document.replace_refs()
|
|
104
|
+
|
|
105
|
+
available_items_during_request_count = document_dict["circulation"][
|
|
106
|
+
"available_items_for_loan_count"
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
event.update(
|
|
110
|
+
{
|
|
111
|
+
"extra_data": {
|
|
112
|
+
"available_items_during_request_count": available_items_during_request_count
|
|
113
|
+
},
|
|
114
|
+
}
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
return event
|
|
@@ -8,6 +8,12 @@
|
|
|
8
8
|
|
|
9
9
|
"""ILS stats preprocessors."""
|
|
10
10
|
|
|
11
|
+
from invenio_circulation.proxies import current_circulation
|
|
12
|
+
from invenio_search.engine import search
|
|
13
|
+
from invenio_stats.processors import EventsIndexer
|
|
14
|
+
|
|
15
|
+
from invenio_app_ils.indexer import wait_es_refresh
|
|
16
|
+
|
|
11
17
|
|
|
12
18
|
def add_record_change_ids(doc):
|
|
13
19
|
"""Add unique_id and aggregation_id to the doc."""
|
|
@@ -28,3 +34,53 @@ def add_record_change_ids(doc):
|
|
|
28
34
|
doc["unique_id"] += f"__{doc['user_id']}"
|
|
29
35
|
|
|
30
36
|
return doc
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def add_loan_transition_unique_id(doc):
|
|
40
|
+
"""Add unique_id to the doc for a loan transition event."""
|
|
41
|
+
|
|
42
|
+
doc["unique_id"] = f"{doc['pid_value']}__{doc['trigger']}"
|
|
43
|
+
|
|
44
|
+
return doc
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class LoansEventsIndexer(EventsIndexer):
|
|
48
|
+
"""Events indexer for events related to loans.
|
|
49
|
+
|
|
50
|
+
Triggers a reindex on affected loans
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
def run(self):
|
|
54
|
+
"""Process events queue and reindex affected loans.
|
|
55
|
+
|
|
56
|
+
First index invenio-stats events that are related to loans.
|
|
57
|
+
Afterwards trigger a reindex of the loans for which an event occurred.
|
|
58
|
+
The loan indexer can then consume the updated invenio-stats events index.
|
|
59
|
+
This reindex is triggered so the loan index has up-to-date information.
|
|
60
|
+
|
|
61
|
+
Example:
|
|
62
|
+
When a loan is requested, an event is placed in the queue that stores the
|
|
63
|
+
loan PID and how many items were available at the time of the request.
|
|
64
|
+
When the event is indexed with this class, it is moved from the queue into
|
|
65
|
+
the events index. Afterwards, the loan is reindexed and, during this
|
|
66
|
+
process, the loan indexer gets the state of the document from the events index.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
# Collect all loan events that occurred from the queue and index them
|
|
70
|
+
actions = [action for action in self.actionsiter()]
|
|
71
|
+
res = search.helpers.bulk(self.client, actions, stats_only=True, chunk_size=50)
|
|
72
|
+
|
|
73
|
+
# Refresh changed event indices so new entries are immediately available
|
|
74
|
+
indices = {action["_index"] for action in actions}
|
|
75
|
+
for index in indices:
|
|
76
|
+
wait_es_refresh(index)
|
|
77
|
+
|
|
78
|
+
# Reindex loans that had events to ensure their index contains the most recent information
|
|
79
|
+
loan_pids = {action["_source"]["pid_value"] for action in actions}
|
|
80
|
+
loan_indexer = current_circulation.loan_indexer()
|
|
81
|
+
loan_cls = current_circulation.loan_record_cls
|
|
82
|
+
for loan_pid in loan_pids:
|
|
83
|
+
loan = loan_cls.get_record_by_pid(loan_pid)
|
|
84
|
+
loan_indexer.index(loan)
|
|
85
|
+
|
|
86
|
+
return res
|
invenio_app_ils/stats/templates/aggregations/loan_transitions/os-v2/aggr-loan-transitions-v1.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"index_patterns": [
|
|
3
|
+
"__SEARCH_INDEX_PREFIX__stats-loan-transitions-*"
|
|
4
|
+
],
|
|
5
|
+
"settings": {
|
|
6
|
+
"index": {
|
|
7
|
+
"refresh_interval": "5s"
|
|
8
|
+
}
|
|
9
|
+
},
|
|
10
|
+
"mappings": {
|
|
11
|
+
"date_detection": false,
|
|
12
|
+
"dynamic": false,
|
|
13
|
+
"numeric_detection": false,
|
|
14
|
+
"properties": {
|
|
15
|
+
"timestamp": {
|
|
16
|
+
"type": "date"
|
|
17
|
+
},
|
|
18
|
+
"updated_timestamp": {
|
|
19
|
+
"type": "date"
|
|
20
|
+
},
|
|
21
|
+
"trigger": {
|
|
22
|
+
"type": "keyword"
|
|
23
|
+
},
|
|
24
|
+
"count": {
|
|
25
|
+
"type": "integer"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"aliases": {
|
|
30
|
+
"__SEARCH_INDEX_PREFIX__stats-loan-transitions": {}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"index_patterns": [
|
|
3
|
+
"__SEARCH_INDEX_PREFIX__events-stats-loan-transitions-*"
|
|
4
|
+
],
|
|
5
|
+
"settings": {
|
|
6
|
+
"index": {
|
|
7
|
+
"refresh_interval": "5s"
|
|
8
|
+
}
|
|
9
|
+
},
|
|
10
|
+
"mappings": {
|
|
11
|
+
"date_detection": false,
|
|
12
|
+
"dynamic": false,
|
|
13
|
+
"numeric_detection": false,
|
|
14
|
+
"properties": {
|
|
15
|
+
"timestamp": {
|
|
16
|
+
"type": "date"
|
|
17
|
+
},
|
|
18
|
+
"updated_timestamp": {
|
|
19
|
+
"type": "date"
|
|
20
|
+
},
|
|
21
|
+
"unique_id": {
|
|
22
|
+
"type": "keyword"
|
|
23
|
+
},
|
|
24
|
+
"trigger": {
|
|
25
|
+
"type": "keyword"
|
|
26
|
+
},
|
|
27
|
+
"pid_value": {
|
|
28
|
+
"type": "keyword"
|
|
29
|
+
},
|
|
30
|
+
"extra_data": {
|
|
31
|
+
"type": "object",
|
|
32
|
+
"dynamic": true,
|
|
33
|
+
"enabled": true
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"aliases": {
|
|
38
|
+
"__SEARCH_INDEX_PREFIX__events-stats-loan-transitions": {}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: invenio-app-ils
|
|
3
|
-
Version:
|
|
3
|
+
Version: 7.0.0
|
|
4
4
|
Summary: Invenio Integrated Library System.
|
|
5
5
|
Home-page: https://github.com/inveniosoftware/invenio-app-ils
|
|
6
6
|
Author: CERN
|
|
@@ -28,7 +28,7 @@ Requires-Dist: invenio-theme<5.0.0,>=4.3.0
|
|
|
28
28
|
Requires-Dist: invenio-access<5.0.0,>=4.1.0
|
|
29
29
|
Requires-Dist: invenio-accounts<7.0.0,>=6.1.1
|
|
30
30
|
Requires-Dist: invenio-oauth2server<4.0.0,>=3.2.0
|
|
31
|
-
Requires-Dist: invenio-oauthclient<
|
|
31
|
+
Requires-Dist: invenio-oauthclient<7.0.0,>=6.0.0
|
|
32
32
|
Requires-Dist: invenio-userprofiles<5.0.0,>=4.0.0
|
|
33
33
|
Requires-Dist: invenio-indexer<4.0.0,>=3.1.0
|
|
34
34
|
Requires-Dist: invenio-jsonschemas<3.0.0,>=2.1.0
|
|
@@ -39,7 +39,7 @@ Requires-Dist: invenio-records<4.0.0,>=3.0.2
|
|
|
39
39
|
Requires-Dist: invenio-files-rest<4.0.0,>=3.2.0
|
|
40
40
|
Requires-Dist: invenio-banners<6.0.0,>=5.0.0
|
|
41
41
|
Requires-Dist: invenio-pages<8.0.0,>=7.1.0
|
|
42
|
-
Requires-Dist: invenio-circulation<
|
|
42
|
+
Requires-Dist: invenio-circulation<5.0.0,>=4.0.0
|
|
43
43
|
Requires-Dist: invenio-opendefinition<3.0.0,>=2.0.0a2
|
|
44
44
|
Requires-Dist: invenio-pidrelations<2.0.0,>=1.0.0
|
|
45
45
|
Requires-Dist: invenio-stats<6.0.0,>=5.1.1
|
|
@@ -95,7 +95,7 @@ https://invenioils.docs.cern.ch
|
|
|
95
95
|
|
|
96
96
|
|
|
97
97
|
..
|
|
98
|
-
Copyright (C) 2018-
|
|
98
|
+
Copyright (C) 2018-2026 CERN.
|
|
99
99
|
|
|
100
100
|
invenio-app-ils is free software; you can redistribute it and/or modify it
|
|
101
101
|
under the terms of the MIT License; see LICENSE file for more details.
|
|
@@ -103,6 +103,22 @@ https://invenioils.docs.cern.ch
|
|
|
103
103
|
Changes
|
|
104
104
|
=======
|
|
105
105
|
|
|
106
|
+
Version 7.0.0 (released 2026-01-06)
|
|
107
|
+
|
|
108
|
+
- stats: generalize stat tracking loan extensions to track all transitions
|
|
109
|
+
- stats: raise error when loan is indexed while loan transition events index does not exits
|
|
110
|
+
- stats: add feature flag for extending record indices with stats
|
|
111
|
+
- stats: warn when loan gets indexed while loan-transitions index does not exist
|
|
112
|
+
- breaking change: global: update invenio-circulation
|
|
113
|
+
- breaking change: stats: add loan stats endpoint and extend loans index
|
|
114
|
+
- breaking change: stats: add stat to track loan extensions
|
|
115
|
+
|
|
116
|
+
Version 6.1.1 (released 2025-12-10)
|
|
117
|
+
|
|
118
|
+
- tests: move covers builder test into correct folder
|
|
119
|
+
- setup: bump oauthclient major version
|
|
120
|
+
- global: bump OpenSearch to v3.2.0
|
|
121
|
+
|
|
106
122
|
Version 6.1.0 (released 2025-10-05)
|
|
107
123
|
|
|
108
124
|
- fix: make terms query that gets eitems by creator use correct keyword
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
invenio_app_ils/__init__.py,sha256=
|
|
1
|
+
invenio_app_ils/__init__.py,sha256=PahYW3-X7OmOEAd50oUaLeWnJYyXxrMqEF0kLbIiTUQ,285
|
|
2
2
|
invenio_app_ils/cli.py,sha256=GGXMuXUlO9i8S9fpBXwj5wfPcK8aYe0leSDB3V--7Bs,58609
|
|
3
|
-
invenio_app_ils/config.py,sha256=
|
|
3
|
+
invenio_app_ils/config.py,sha256=R70-ZkUXwWWKcQkt2KiIgZtP2cVt0zMPsIfULEhxZ94,47229
|
|
4
4
|
invenio_app_ils/errors.py,sha256=HB_iWj-aYxzTzzO6hWb66mUPZdqpWYHgmr2H2t3j3wU,13293
|
|
5
|
-
invenio_app_ils/ext.py,sha256=
|
|
5
|
+
invenio_app_ils/ext.py,sha256=k6I7KcLDILvsgkkVRBFIT2e7bArNAaPHkZVheBlLigI,11664
|
|
6
6
|
invenio_app_ils/facets.py,sha256=x-ID7vL34zqbxJi7VC3EJSee13l_Jk0CfPZN3RHZrM8,4207
|
|
7
7
|
invenio_app_ils/fetchers.py,sha256=GY5A6BXaqMB9HKvJuTcio3JYoF15t6eqMo3yzQKTqac,520
|
|
8
8
|
invenio_app_ils/indexer.py,sha256=ngXRx2liufDzgsSIoCeusk6q0Y1uskQ3QiVLeAi3KRg,2212
|
|
9
9
|
invenio_app_ils/minters.py,sha256=8JW-45fL9Oy_13eZjlk8kL_12jJGZK59qbKlCeseQgA,537
|
|
10
|
-
invenio_app_ils/permissions.py,sha256=
|
|
10
|
+
invenio_app_ils/permissions.py,sha256=NLt55h_Vil3L_vnAzSVdCdkUyjUUSEic2gql0SnZbWs,7763
|
|
11
11
|
invenio_app_ils/proxies.py,sha256=IGBwXQSOxpXHjD8PWFG1nqUm70xGAgzWT8Y0AKdCGiI,453
|
|
12
12
|
invenio_app_ils/search_permissions.py,sha256=cJWDgShgzXwxAtCtb7qXCIUfNSrQy_oe0zMuT0TzXgA,5073
|
|
13
13
|
invenio_app_ils/signals.py,sha256=KaN8yQUVq-2O2IKQQvPLtMjqp1S3AU1LYPlRyw9U8Pg,395
|
|
@@ -40,7 +40,8 @@ invenio_app_ils/assets/semantic-ui/templates/.gitkeep,sha256=47DEQpj8HBSa-_TImW-
|
|
|
40
40
|
invenio_app_ils/circulation/__init__.py,sha256=Gd0KAsGdhPdz0ACEQ9k8xSeOIxZr3xRK_FiE8U3RQWs,248
|
|
41
41
|
invenio_app_ils/circulation/api.py,sha256=jKty3IqTOK0XJdeXLlDqnZH2uhdVzd4xWuVphQe4YKw,14643
|
|
42
42
|
invenio_app_ils/circulation/config.py,sha256=pcB5JEjxXrieRRO-8RBUbxgf5dkhRGXXI2BvWfQUUCs,11433
|
|
43
|
-
invenio_app_ils/circulation/
|
|
43
|
+
invenio_app_ils/circulation/errors.py,sha256=R0VwfKbYyq02PPKxpKg5LCW86x5l6s9n8MV1m5c85gk,367
|
|
44
|
+
invenio_app_ils/circulation/indexer.py,sha256=AZKMZF_GcWCgXwQFyWeF8Inczn7Ba_xPsjCvAHaLzX8,6196
|
|
44
45
|
invenio_app_ils/circulation/receivers.py,sha256=Ux6KTNbII3DHBvCUS0gxqbi6tNbm76_kbcaHtK0BsB4,2488
|
|
45
46
|
invenio_app_ils/circulation/search.py,sha256=l9DAr9uoaF_JbfiiXVpAFKW3NLv9bgs7D-uDwtU-fv0,6105
|
|
46
47
|
invenio_app_ils/circulation/tasks.py,sha256=w4lpbQo78aDg_vgdzQoRVe1y1NjCKrz1xNlLl5RDgs0,1515
|
|
@@ -67,8 +68,12 @@ invenio_app_ils/circulation/serializers/custom_fields.py,sha256=EQnWMCLNgModn4Br
|
|
|
67
68
|
invenio_app_ils/circulation/serializers/json.py,sha256=x625dleVLyfZU1bAWuTfk1UvEGUlWuAYUAvec4qAFHo,1553
|
|
68
69
|
invenio_app_ils/circulation/serializers/response.py,sha256=xPmwnO-bHoaaL6KgCz6ayo2FBGBlJKmb4cH9JeOb9ls,1289
|
|
69
70
|
invenio_app_ils/circulation/stats/__init__.py,sha256=X_oDxvlDZRHxfjM1J-sBisDmn_45F0MptRbvyTTI3WA,247
|
|
70
|
-
invenio_app_ils/circulation/stats/api.py,sha256=
|
|
71
|
-
invenio_app_ils/circulation/stats/
|
|
71
|
+
invenio_app_ils/circulation/stats/api.py,sha256=ggvQRUh9MdN2NtV_JI0WXKrVxXuqnPmkG_kisB2fmUE,5565
|
|
72
|
+
invenio_app_ils/circulation/stats/schemas.py,sha256=CRYLDsEyAm7NiS2IwuG0A--cnX2zxVTeEut_sKMuZPA,3684
|
|
73
|
+
invenio_app_ils/circulation/stats/views.py,sha256=o9w3MxgHaLPwT6FCaeQdtRV2L8KRPDGwNge63cs8ysQ,7356
|
|
74
|
+
invenio_app_ils/circulation/stats/serializers/__init__.py,sha256=gUOm8aLiRFTfdZH3DRPLAkygzh0WcWX6ZPod1lalEv0,490
|
|
75
|
+
invenio_app_ils/circulation/stats/serializers/response.py,sha256=Zj3BPq13Ot0RaVwdzlncqTVyLzOpuXNB8plSeopg9SQ,967
|
|
76
|
+
invenio_app_ils/circulation/stats/serializers/schema.py,sha256=FS9_C3QycWuuTmxXtGIpjXsWVSDVr01pEW-VcyQiqew,832
|
|
72
77
|
invenio_app_ils/circulation/templates/invenio_app_ils_circulation/notifications/bulk_extend.html,sha256=rSYqCysrRnlKJTE1VjnigmPCCj5vHT0jsKzqgajHNRM,1616
|
|
73
78
|
invenio_app_ils/circulation/templates/invenio_app_ils_circulation/notifications/cancel.html,sha256=Rm51UHpxuQo5syVf2_KBEdpHvxVEdyIvFMo0e8vsZxw,725
|
|
74
79
|
invenio_app_ils/circulation/templates/invenio_app_ils_circulation/notifications/checkin.html,sha256=Bwud7JhRsBBb9gArBOtbw4CGJWIRHJVw4KMxOuyxlM0,545
|
|
@@ -261,7 +266,7 @@ invenio_app_ils/items/serializers/__init__.py,sha256=OxfwU0rGarUnBvznMmiGaJx6uMw
|
|
|
261
266
|
invenio_app_ils/items/serializers/item.py,sha256=8qHJp4SFxJj_qM-Pf-Om98KYc5ytweNLJiD94ksGslQ,2967
|
|
262
267
|
invenio_app_ils/literature/__init__.py,sha256=GELxzVtOq8G2O7Uvs_mLsj0wPnS5l-6ns9mLFDGMEmQ,231
|
|
263
268
|
invenio_app_ils/literature/api.py,sha256=gbLMK36fN0KXUmygRLadeejPnJ_3CSU_8WFrgxwQ_j4,862
|
|
264
|
-
invenio_app_ils/literature/covers_builder.py,sha256=
|
|
269
|
+
invenio_app_ils/literature/covers_builder.py,sha256=eBDfWVFnMswDJ6efk1PdMLI3ut7MSziVDnznb4-CW-E,1640
|
|
265
270
|
invenio_app_ils/literature/search.py,sha256=Ny49TUkTsJB34REm-ZgahOcIeJDvH-zZk6mXKHBF47Q,1860
|
|
266
271
|
invenio_app_ils/literature/serializers/__init__.py,sha256=CnJdYvVa433JbQb-dZciGDGAdtg0-bEhjtW8fK8rZy4,931
|
|
267
272
|
invenio_app_ils/literature/serializers/csv.py,sha256=dkV5p5H5tCxLcj9bKQDbGeqwfuytfWGyPLy4YDFhZig,1130
|
|
@@ -377,8 +382,8 @@ invenio_app_ils/series/schemas/series/series-v2.0.0.json,sha256=QrNqmAeuqZysIHVB
|
|
|
377
382
|
invenio_app_ils/static/images/placeholder.png,sha256=dSqkEWkAsDHcV8GBVS52gsMhXQxLKSWjg74etFdToTE,4585
|
|
378
383
|
invenio_app_ils/static_pages/search_guide.html,sha256=8wkk5o6sJCsbPwccYRwhxO5TMiVsLdAXOEduWQStyMk,22084
|
|
379
384
|
invenio_app_ils/stats/__init__.py,sha256=tYEj7b8b25K-qS6FTv7w2bc_iLjaiTFn8INNv4-JVmM,254
|
|
380
|
-
invenio_app_ils/stats/event_builders.py,sha256=
|
|
381
|
-
invenio_app_ils/stats/processors.py,sha256=
|
|
385
|
+
invenio_app_ils/stats/event_builders.py,sha256=i9TFKr6hBzpoZTUrGYhpKQiYdEb4-WetZooEQsPcEjs,3341
|
|
386
|
+
invenio_app_ils/stats/processors.py,sha256=E8c1X94SwgK01_iMLKBfKd_7Ov_zdWQ_HexFMe8posA,3438
|
|
382
387
|
invenio_app_ils/stats/aggregations/__init__.py,sha256=Ix-wNZuG_-H6P-bJ2FS43AlDQwnXegMvdy6pb-FHejY,284
|
|
383
388
|
invenio_app_ils/stats/aggregations/aggr_file_download/__init__.py,sha256=NR2hvgsR555O6Szy4eGC_XpNT4VZo888lMp6faeXffA,294
|
|
384
389
|
invenio_app_ils/stats/aggregations/aggr_file_download/os-v1/__init__.py,sha256=NR2hvgsR555O6Szy4eGC_XpNT4VZo888lMp6faeXffA,294
|
|
@@ -398,10 +403,16 @@ invenio_app_ils/stats/templates/aggregations/__init__.py,sha256=u5NGrHHOqWBgFs2-
|
|
|
398
403
|
invenio_app_ils/stats/templates/aggregations/ils_record_changes/__init__.py,sha256=Fae3O56C56coEeyOZqcW4Pr9p3anlR6C1MZidpF4ots,254
|
|
399
404
|
invenio_app_ils/stats/templates/aggregations/ils_record_changes/os-v2/__init__.py,sha256=Fae3O56C56coEeyOZqcW4Pr9p3anlR6C1MZidpF4ots,254
|
|
400
405
|
invenio_app_ils/stats/templates/aggregations/ils_record_changes/os-v2/aggr-ils-record-changes-v1.json,sha256=YDDLh51xr-fEm3PMDka1DYyiZjvHQ8L76Qc8NQQl1AM,755
|
|
406
|
+
invenio_app_ils/stats/templates/aggregations/loan_transitions/__init__.py,sha256=S3fGruV4UCxkudxkNgRjC3v-wA_3fbcZaSxkmdBtWSU,252
|
|
407
|
+
invenio_app_ils/stats/templates/aggregations/loan_transitions/os-v2/__init__.py,sha256=S3fGruV4UCxkudxkNgRjC3v-wA_3fbcZaSxkmdBtWSU,252
|
|
408
|
+
invenio_app_ils/stats/templates/aggregations/loan_transitions/os-v2/aggr-loan-transitions-v1.json,sha256=4oCysVcOEZT5llEIen2U5XD1zVH6IXOTOhsGM0LafiI,582
|
|
401
409
|
invenio_app_ils/stats/templates/events/__init__.py,sha256=vn0uewDZ-svfcjtL2Tmjh2qWdI8sLWyAgOmuEc30hwE,236
|
|
402
410
|
invenio_app_ils/stats/templates/events/ils_record_changes/__init__.py,sha256=PoP04xZy8M3jLQjKdHg8CH_1uZCPeaE6vbgL5ciBmCU,249
|
|
403
411
|
invenio_app_ils/stats/templates/events/ils_record_changes/os-v2/__init__.py,sha256=PoP04xZy8M3jLQjKdHg8CH_1uZCPeaE6vbgL5ciBmCU,249
|
|
404
412
|
invenio_app_ils/stats/templates/events/ils_record_changes/os-v2/ils-record-changes-v1.json,sha256=AyraC4_iFST-y1W8_zufUqDiLNj1Jl_BIxZRnhkPHqU,773
|
|
413
|
+
invenio_app_ils/stats/templates/events/loan_transitions/__init__.py,sha256=oQs-4ekFtKhGAOSxcF2Jj5cuHE4GxLwAvrf_ImTzCtA,247
|
|
414
|
+
invenio_app_ils/stats/templates/events/loan_transitions/os-v2/__init__.py,sha256=oQs-4ekFtKhGAOSxcF2Jj5cuHE4GxLwAvrf_ImTzCtA,247
|
|
415
|
+
invenio_app_ils/stats/templates/events/loan_transitions/os-v2/loan-transitions-v1.json,sha256=JgOqSQNGjNQpHwWPBSisMzdo2uI1dwHwsuFHY2Rycw4,762
|
|
405
416
|
invenio_app_ils/templates/logged_out.html,sha256=T-zEwtIHQENkDwO9d5QmB7oNmCofq8DuClpg-ojPfoU,356
|
|
406
417
|
invenio_app_ils/templates/invenio_app_ils/mail/footer.html,sha256=tLIgLCHNfWZibDG-vf9C-APhxn5AGJhJzBRqpwMwh04,413
|
|
407
418
|
invenio_app_ils/templates/invenio_app_ils/notifications/footer.html,sha256=tLIgLCHNfWZibDG-vf9C-APhxn5AGJhJzBRqpwMwh04,413
|
|
@@ -455,10 +466,10 @@ invenio_app_ils/vocabularies/sources/__init__.py,sha256=EMLoLQGiq9_qoZ4lKqO_J0u2
|
|
|
455
466
|
invenio_app_ils/vocabularies/sources/base.py,sha256=kmNg85cjrxqc8VErFP2SUtlyo1PnmzOBQgfoSpfTPh4,1418
|
|
456
467
|
invenio_app_ils/vocabularies/sources/json.py,sha256=KGLTJ4AX8zEDdkpi3v92oHGs-h2dUzHbNPSg99-j8o0,1021
|
|
457
468
|
invenio_app_ils/vocabularies/sources/opendefinition.py,sha256=9zbRXuTr0i5lVLt596oUhsLQPiJVf9jiM9-C7T6zbXA,2151
|
|
458
|
-
invenio_app_ils-
|
|
459
|
-
invenio_app_ils-
|
|
460
|
-
invenio_app_ils-
|
|
461
|
-
invenio_app_ils-
|
|
462
|
-
invenio_app_ils-
|
|
463
|
-
invenio_app_ils-
|
|
464
|
-
invenio_app_ils-
|
|
469
|
+
invenio_app_ils-7.0.0.dist-info/licenses/AUTHORS.rst,sha256=BaXCGzdHCmiMOl4qtVlh1qrfy2ROMVOQp6ylzy1m0ww,212
|
|
470
|
+
invenio_app_ils-7.0.0.dist-info/licenses/LICENSE,sha256=9OdaPOAO1ZOJcRQ8BrGj7QAdaJc8SRSUgBtdom49MrI,1062
|
|
471
|
+
invenio_app_ils-7.0.0.dist-info/METADATA,sha256=cCS49rJ4Df_fLLpEjbw38xy2K_t9Hr3DxiTRIebBNBE,18800
|
|
472
|
+
invenio_app_ils-7.0.0.dist-info/WHEEL,sha256=JNWh1Fm1UdwIQV075glCn4MVuCRs0sotJIq-J6rbxCU,109
|
|
473
|
+
invenio_app_ils-7.0.0.dist-info/entry_points.txt,sha256=L36OnFZlrAnCaQpXnWoLmYYJ1U6Y1K_TDUZB0nnWeO0,7748
|
|
474
|
+
invenio_app_ils-7.0.0.dist-info/top_level.txt,sha256=p-lnzfSHaDER0BHbQvDV6dvUW7_Q0prMDFaqPhBSK50,16
|
|
475
|
+
invenio_app_ils-7.0.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|