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.
Files changed (28) hide show
  1. invenio_app_ils/__init__.py +2 -2
  2. invenio_app_ils/circulation/errors.py +12 -0
  3. invenio_app_ils/circulation/indexer.py +70 -0
  4. invenio_app_ils/circulation/stats/api.py +101 -1
  5. invenio_app_ils/circulation/stats/schemas.py +110 -0
  6. invenio_app_ils/circulation/stats/serializers/__init__.py +13 -0
  7. invenio_app_ils/circulation/stats/serializers/response.py +37 -0
  8. invenio_app_ils/circulation/stats/serializers/schema.py +33 -0
  9. invenio_app_ils/circulation/stats/views.py +74 -3
  10. invenio_app_ils/config.py +55 -1
  11. invenio_app_ils/ext.py +7 -2
  12. invenio_app_ils/literature/covers_builder.py +13 -1
  13. invenio_app_ils/permissions.py +1 -0
  14. invenio_app_ils/stats/event_builders.py +44 -0
  15. invenio_app_ils/stats/processors.py +56 -0
  16. invenio_app_ils/stats/templates/aggregations/loan_transitions/__init__.py +8 -0
  17. invenio_app_ils/stats/templates/aggregations/loan_transitions/os-v2/__init__.py +8 -0
  18. invenio_app_ils/stats/templates/aggregations/loan_transitions/os-v2/aggr-loan-transitions-v1.json +32 -0
  19. invenio_app_ils/stats/templates/events/loan_transitions/__init__.py +8 -0
  20. invenio_app_ils/stats/templates/events/loan_transitions/os-v2/__init__.py +8 -0
  21. invenio_app_ils/stats/templates/events/loan_transitions/os-v2/loan-transitions-v1.json +40 -0
  22. {invenio_app_ils-6.1.0.dist-info → invenio_app_ils-7.0.0.dist-info}/METADATA +20 -4
  23. {invenio_app_ils-6.1.0.dist-info → invenio_app_ils-7.0.0.dist-info}/RECORD +28 -17
  24. {invenio_app_ils-6.1.0.dist-info → invenio_app_ils-7.0.0.dist-info}/WHEEL +0 -0
  25. {invenio_app_ils-6.1.0.dist-info → invenio_app_ils-7.0.0.dist-info}/entry_points.txt +0 -0
  26. {invenio_app_ils-6.1.0.dist-info → invenio_app_ils-7.0.0.dist-info}/licenses/AUTHORS.rst +0 -0
  27. {invenio_app_ils-6.1.0.dist-info → invenio_app_ils-7.0.0.dist-info}/licenses/LICENSE +0 -0
  28. {invenio_app_ils-6.1.0.dist-info → invenio_app_ils-7.0.0.dist-info}/top_level.txt +0 -0
@@ -1,12 +1,12 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  #
3
- # Copyright (C) 2018-2024 CERN.
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__ = "6.1.0"
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
- from invenio_app_ils.circulation.stats.api import fetch_most_loaned_documents
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": [("record-view-agg", "file-download-agg", "ils-record-changes-agg")],
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 index_extra_fields_for_loan
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, Document
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
- isbn = cover_metadata.get("ISBN", "")
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()
@@ -205,6 +205,7 @@ _is_backoffice_permission = [
205
205
  ]
206
206
  _is_backoffice_read_permission = [
207
207
  "stats-most-loaned",
208
+ "stats-loans",
208
209
  "get-notifications-sent-to-patron",
209
210
  ]
210
211
  _is_patron_owner_permission = [
@@ -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
@@ -0,0 +1,8 @@
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
+ """Loan transitions aggregation templates."""
@@ -0,0 +1,8 @@
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
+ """Loan transitions aggregation templates."""
@@ -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,8 @@
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
+ """Loan transitions events templates."""
@@ -0,0 +1,8 @@
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
+ """Loan transitions events templates."""
@@ -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: 6.1.0
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<6.0.0,>=5.1.0
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<4.0.0,>=3.0.0a1
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-2024 CERN.
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=3tt_Rb6riwShcmz5O5Mf00XKZ4aQ2Lj7M0FETDnpJSE,285
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=aMdID-W4Jm8zI5K4Nvwvk86SG5OCuRZFS7wTWnXrAKM,45294
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=SULPfx1FxBLaOaKcyNKAAIyvZAuvTH3AcH_a8aXTSYo,11522
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=lYdUQosqdSfPaGKLUZuqrwD0KWOP8_915qJiK4qXYho,7744
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/indexer.py,sha256=T24J5QqcB38LRJgirkAXL2tmFOucYToDRegxgFW96ag,3887
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=YLdW_TobeoJWrCX1OHgpaW46JBb-FpyiFkeabZLiAgI,1807
71
- invenio_app_ils/circulation/stats/views.py,sha256=_I9qgSnvmKs5sjJ4zpH1pLoGYCLA8C3mVMfFLTlzVEM,4933
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=_MzoKZik_7UZRKl8szuhcLSD-o8reRSyV8VSbcIbh8g,1285
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=p628CvL9x1tothBah-giK5nxuA8MBVKHseTFRSTFUiI,2085
381
- invenio_app_ils/stats/processors.py,sha256=vQnAt6ijJqUC8buHtCg-hj1rhQamTLD5f9jTN1oxRBk,1184
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-6.1.0.dist-info/licenses/AUTHORS.rst,sha256=BaXCGzdHCmiMOl4qtVlh1qrfy2ROMVOQp6ylzy1m0ww,212
459
- invenio_app_ils-6.1.0.dist-info/licenses/LICENSE,sha256=9OdaPOAO1ZOJcRQ8BrGj7QAdaJc8SRSUgBtdom49MrI,1062
460
- invenio_app_ils-6.1.0.dist-info/METADATA,sha256=OOedket0em7jsbxIAWgpwBpJgMTDbvCG3RYujoPd2Zs,18095
461
- invenio_app_ils-6.1.0.dist-info/WHEEL,sha256=JNWh1Fm1UdwIQV075glCn4MVuCRs0sotJIq-J6rbxCU,109
462
- invenio_app_ils-6.1.0.dist-info/entry_points.txt,sha256=L36OnFZlrAnCaQpXnWoLmYYJ1U6Y1K_TDUZB0nnWeO0,7748
463
- invenio_app_ils-6.1.0.dist-info/top_level.txt,sha256=p-lnzfSHaDER0BHbQvDV6dvUW7_Q0prMDFaqPhBSK50,16
464
- invenio_app_ils-6.1.0.dist-info/RECORD,,
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,,