invenio-app-ils 4.4.0__py2.py3-none-any.whl → 4.5.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 +1 -1
- invenio_app_ils/circulation/api.py +11 -6
- invenio_app_ils/closures/api.py +70 -2
- invenio_app_ils/closures/serializers/__init__.py +16 -0
- invenio_app_ils/closures/serializers/response.py +35 -0
- invenio_app_ils/closures/serializers/schema.py +28 -0
- invenio_app_ils/closures/views.py +66 -0
- {invenio_app_ils-4.4.0.dist-info → invenio_app_ils-4.5.0.dist-info}/METADATA +8 -2
- {invenio_app_ils-4.4.0.dist-info → invenio_app_ils-4.5.0.dist-info}/RECORD +17 -13
- {invenio_app_ils-4.4.0.dist-info → invenio_app_ils-4.5.0.dist-info}/WHEEL +1 -1
- {invenio_app_ils-4.4.0.dist-info → invenio_app_ils-4.5.0.dist-info}/entry_points.txt +1 -0
- tests/api/circulation/test_loan_checkout.py +29 -3
- tests/api/ils/test_closures.py +46 -8
- tests/data/items.json +11 -0
- {invenio_app_ils-4.4.0.dist-info → invenio_app_ils-4.5.0.dist-info/licenses}/AUTHORS.rst +0 -0
- {invenio_app_ils-4.4.0.dist-info → invenio_app_ils-4.5.0.dist-info/licenses}/LICENSE +0 -0
- {invenio_app_ils-4.4.0.dist-info → invenio_app_ils-4.5.0.dist-info}/top_level.txt +0 -0
invenio_app_ils/__init__.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
2
|
#
|
|
3
|
-
# Copyright (C) 2018-
|
|
3
|
+
# Copyright (C) 2018-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.
|
|
@@ -78,9 +78,9 @@ def _validate_delivery(delivery):
|
|
|
78
78
|
)
|
|
79
79
|
|
|
80
80
|
|
|
81
|
-
def _set_item_to_can_circulate(
|
|
81
|
+
def _set_item_to_can_circulate(item_pid_value):
|
|
82
82
|
"""Change the item status to CAN_CIRCULATE."""
|
|
83
|
-
item = Item.get_record_by_pid(
|
|
83
|
+
item = Item.get_record_by_pid(item_pid_value)
|
|
84
84
|
if item["status"] != "CAN_CIRCULATE":
|
|
85
85
|
item["status"] = "CAN_CIRCULATE"
|
|
86
86
|
item.commit()
|
|
@@ -245,7 +245,7 @@ def checkout_loan(
|
|
|
245
245
|
_validate_delivery(optional_delivery)
|
|
246
246
|
|
|
247
247
|
if force:
|
|
248
|
-
_set_item_to_can_circulate(item_pid)
|
|
248
|
+
_set_item_to_can_circulate(item_pid["value"])
|
|
249
249
|
|
|
250
250
|
return _checkout_loan(
|
|
251
251
|
item_pid,
|
|
@@ -256,14 +256,19 @@ def checkout_loan(
|
|
|
256
256
|
)
|
|
257
257
|
|
|
258
258
|
|
|
259
|
-
def _ensure_item_loanable_via_self_checkout(
|
|
259
|
+
def _ensure_item_loanable_via_self_checkout(item_pid_value):
|
|
260
260
|
"""Self-checkout: return loanable item or raise when not loanable.
|
|
261
261
|
|
|
262
262
|
Implements the self-checkout rules to loan an item.
|
|
263
263
|
"""
|
|
264
|
-
item = current_app_ils.item_record_cls.get_record_by_pid(
|
|
264
|
+
item = current_app_ils.item_record_cls.get_record_by_pid(item_pid_value)
|
|
265
265
|
item_dict = item.replace_refs()
|
|
266
266
|
|
|
267
|
+
if item_dict["status"] == "MISSING":
|
|
268
|
+
_set_item_to_can_circulate(item_pid_value)
|
|
269
|
+
item = current_app_ils.item_record_cls.get_record_by_pid(item_pid_value)
|
|
270
|
+
item_dict = item.replace_refs()
|
|
271
|
+
|
|
267
272
|
if item_dict["status"] != "CAN_CIRCULATE":
|
|
268
273
|
raise ItemCannotCirculateError()
|
|
269
274
|
|
invenio_app_ils/closures/api.py
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
2
|
#
|
|
3
|
-
# Copyright (C) 2020 CERN.
|
|
3
|
+
# Copyright (C) 2020-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
|
"""ILS location closures API."""
|
|
9
9
|
|
|
10
|
-
from datetime import timedelta
|
|
10
|
+
from datetime import date, timedelta
|
|
11
|
+
from calendar import day_name
|
|
11
12
|
|
|
12
13
|
import arrow
|
|
13
14
|
|
|
@@ -64,3 +65,70 @@ def find_next_open_date(location_pid, date):
|
|
|
64
65
|
"location %s is open after the given date %s."
|
|
65
66
|
"Please check opening/closures dates." % (location_pid, date.isoformat())
|
|
66
67
|
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def get_closure_periods(location, start, end):
|
|
71
|
+
"""
|
|
72
|
+
Return date ranges within a specified period when the location is closed.
|
|
73
|
+
"""
|
|
74
|
+
if (
|
|
75
|
+
not location
|
|
76
|
+
or not isinstance(start, date)
|
|
77
|
+
or not isinstance(end, date)
|
|
78
|
+
or start > end
|
|
79
|
+
):
|
|
80
|
+
raise ValueError("Invalid input parameters")
|
|
81
|
+
|
|
82
|
+
one_day_delta = timedelta(days=1)
|
|
83
|
+
|
|
84
|
+
weekday_name_to_index = {name.lower(): i for i, name in enumerate(day_name)}
|
|
85
|
+
closed_weekdays = set()
|
|
86
|
+
for weekday_rule in location.get("opening_weekdays", []):
|
|
87
|
+
if weekday_rule.get("is_open") is False:
|
|
88
|
+
weekday_str = weekday_rule.get("weekday", "").lower()
|
|
89
|
+
closed_weekdays.add(weekday_name_to_index[weekday_str])
|
|
90
|
+
|
|
91
|
+
exception_overrides = {}
|
|
92
|
+
for exception in location.get("opening_exceptions", []):
|
|
93
|
+
is_closed_override = not exception.get("is_open")
|
|
94
|
+
|
|
95
|
+
ex_start = date.fromisoformat(exception["start_date"])
|
|
96
|
+
ex_end = date.fromisoformat(exception["end_date"])
|
|
97
|
+
|
|
98
|
+
current_ex_date = ex_start
|
|
99
|
+
while current_ex_date <= ex_end:
|
|
100
|
+
exception_overrides[current_ex_date] = is_closed_override
|
|
101
|
+
current_ex_date += one_day_delta
|
|
102
|
+
|
|
103
|
+
closure_periods = []
|
|
104
|
+
closure_streak_start = None
|
|
105
|
+
current_date = start
|
|
106
|
+
|
|
107
|
+
while current_date <= end:
|
|
108
|
+
is_date_closed = False
|
|
109
|
+
|
|
110
|
+
override_status = exception_overrides.get(current_date)
|
|
111
|
+
|
|
112
|
+
if override_status is not None:
|
|
113
|
+
is_date_closed = override_status
|
|
114
|
+
else:
|
|
115
|
+
if current_date.weekday() in closed_weekdays:
|
|
116
|
+
is_date_closed = True
|
|
117
|
+
|
|
118
|
+
if is_date_closed:
|
|
119
|
+
if closure_streak_start is None:
|
|
120
|
+
closure_streak_start = current_date
|
|
121
|
+
else:
|
|
122
|
+
if closure_streak_start is not None:
|
|
123
|
+
closure_periods.append(
|
|
124
|
+
{"start": closure_streak_start, "end": current_date - one_day_delta}
|
|
125
|
+
)
|
|
126
|
+
closure_streak_start = None
|
|
127
|
+
|
|
128
|
+
current_date += one_day_delta
|
|
129
|
+
|
|
130
|
+
# Handle trailing closed period
|
|
131
|
+
if closure_streak_start is not None:
|
|
132
|
+
closure_periods.append({"start": closure_streak_start, "end": end})
|
|
133
|
+
|
|
134
|
+
return closure_periods
|
|
@@ -0,0 +1,16 @@
|
|
|
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.closures.serializers.response import closure_periods_responsify
|
|
11
|
+
from invenio_app_ils.closures.serializers.schema import ClosurePeriodsV1
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
closure_periods_response = closure_periods_responsify(
|
|
15
|
+
ClosurePeriodsV1, "application/json"
|
|
16
|
+
)
|
|
@@ -0,0 +1,35 @@
|
|
|
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 Closures response serializers."""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
from flask import current_app
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def closure_periods_responsify(schema_class, mimetype):
|
|
16
|
+
"""Closure periods response serializer.
|
|
17
|
+
|
|
18
|
+
:param schema_class: Schema instance.
|
|
19
|
+
:param mimetype: MIME type of response.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def view(data, code=200, headers=None):
|
|
23
|
+
"""Generate the response object."""
|
|
24
|
+
response_data = schema_class().dump(data)
|
|
25
|
+
|
|
26
|
+
response = current_app.response_class(
|
|
27
|
+
json.dumps(response_data), mimetype=mimetype
|
|
28
|
+
)
|
|
29
|
+
response.status_code = code
|
|
30
|
+
|
|
31
|
+
if headers is not None:
|
|
32
|
+
response.headers.extend(headers)
|
|
33
|
+
return response
|
|
34
|
+
|
|
35
|
+
return view
|
|
@@ -0,0 +1,28 @@
|
|
|
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 Closures response schemas."""
|
|
10
|
+
|
|
11
|
+
from marshmallow import Schema, fields
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class DateRangeSchema(Schema):
|
|
15
|
+
"""Schema for a single closure period."""
|
|
16
|
+
|
|
17
|
+
start = fields.Date(required=True, description="Start date of the closure period.")
|
|
18
|
+
end = fields.Date(required=True, description="End date of the closure period.")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ClosurePeriodsV1(Schema):
|
|
22
|
+
"""Schema for a list of closure periods."""
|
|
23
|
+
|
|
24
|
+
closure_periods = fields.List(
|
|
25
|
+
fields.Nested(DateRangeSchema),
|
|
26
|
+
required=True,
|
|
27
|
+
description="List of closure date periods.",
|
|
28
|
+
)
|
|
@@ -0,0 +1,66 @@
|
|
|
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 Closure views."""
|
|
10
|
+
|
|
11
|
+
from flask import Blueprint, abort
|
|
12
|
+
from flask_login import current_user
|
|
13
|
+
from datetime import date
|
|
14
|
+
from invenio_records_rest.views import pass_record
|
|
15
|
+
from invenio_rest import ContentNegotiatedMethodView
|
|
16
|
+
from invenio_app_ils.locations.api import LOCATION_PID_TYPE
|
|
17
|
+
from invenio_app_ils.closures.serializers import closure_periods_response
|
|
18
|
+
from invenio_app_ils.closures.api import get_closure_periods
|
|
19
|
+
from invenio_app_ils.permissions import backoffice_permission
|
|
20
|
+
from invenio_app_ils.records.permissions import RecordPermission
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def create_closures_blueprint(app):
|
|
24
|
+
"""Create location closed periods blueprint."""
|
|
25
|
+
blueprint = Blueprint("invenio_app_ils_closures", __name__, url_prefix="")
|
|
26
|
+
endpoints = app.config["RECORDS_REST_ENDPOINTS"]
|
|
27
|
+
|
|
28
|
+
options = endpoints[LOCATION_PID_TYPE]
|
|
29
|
+
default_media_type = options.get("default_media_type", "")
|
|
30
|
+
closure_periods_serializers = {"application/json": closure_periods_response}
|
|
31
|
+
|
|
32
|
+
closure_periods_view = LocationClosurePeriodsResource.as_view(
|
|
33
|
+
LocationClosurePeriodsResource.view_name,
|
|
34
|
+
serializers=closure_periods_serializers,
|
|
35
|
+
default_media_type=default_media_type,
|
|
36
|
+
)
|
|
37
|
+
blueprint.add_url_rule(
|
|
38
|
+
"{0}/closure_periods/<int:year>".format(options["item_route"]),
|
|
39
|
+
view_func=closure_periods_view,
|
|
40
|
+
methods=["GET"],
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
return blueprint
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class LocationClosurePeriodsResource(ContentNegotiatedMethodView):
|
|
47
|
+
"""Returns the closure periods of a Location"""
|
|
48
|
+
|
|
49
|
+
view_name = "location_closure_periods"
|
|
50
|
+
|
|
51
|
+
@pass_record
|
|
52
|
+
def get(self, pid, record, year, **kwargs):
|
|
53
|
+
"""Get the date ranges for which a location is closure based in the specified year"""
|
|
54
|
+
|
|
55
|
+
factory = RecordPermission(record, "read")
|
|
56
|
+
if not factory.is_public() and not backoffice_permission().can():
|
|
57
|
+
if not current_user.is_authenticated:
|
|
58
|
+
abort(401)
|
|
59
|
+
abort(403)
|
|
60
|
+
|
|
61
|
+
start_date = date(year, 1, 1)
|
|
62
|
+
end_date = date(year, 12, 31)
|
|
63
|
+
|
|
64
|
+
closure_periods = get_closure_periods(record, start_date, end_date)
|
|
65
|
+
|
|
66
|
+
return self.make_response({"closure_periods": closure_periods}, 200)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: invenio-app-ils
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.5.0
|
|
4
4
|
Summary: Invenio Integrated Library System.
|
|
5
5
|
Home-page: https://github.com/inveniosoftware/invenio-app-ils
|
|
6
6
|
Author: CERN
|
|
@@ -66,6 +66,7 @@ Requires-Dist: lorem>=0.1.1; extra == "lorem"
|
|
|
66
66
|
Provides-Extra: opensearch2
|
|
67
67
|
Requires-Dist: invenio-search[opensearch2]<3.0.0,>=2.0.0; extra == "opensearch2"
|
|
68
68
|
Provides-Extra: docs
|
|
69
|
+
Dynamic: license-file
|
|
69
70
|
|
|
70
71
|
..
|
|
71
72
|
Copyright (C) 2018-2020 CERN.
|
|
@@ -101,6 +102,11 @@ https://invenioils.docs.cern.ch
|
|
|
101
102
|
Changes
|
|
102
103
|
=======
|
|
103
104
|
|
|
105
|
+
Version 4.5.0 (released 2025-06-04)
|
|
106
|
+
|
|
107
|
+
- closures: added endpoint for closure periods in a year
|
|
108
|
+
- circulation: self-checkout of MISSING items marks them as CAN_CIRCULATE
|
|
109
|
+
- tests: Fix comment on running tests by bumping Postgres & adding param
|
|
104
110
|
|
|
105
111
|
Version 4.4.0 (released 2025-02-21)
|
|
106
112
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
invenio_app_ils/__init__.py,sha256=
|
|
1
|
+
invenio_app_ils/__init__.py,sha256=5FxyNegCXBoMVkuZG-tPYBUFB9w78RX2ImxZl6S7QRY,285
|
|
2
2
|
invenio_app_ils/cli.py,sha256=wD-SSrkbEhsndI7N9E39p-T1MXj8LR8pvsWFH-JHHVA,57478
|
|
3
3
|
invenio_app_ils/config.py,sha256=b-3nNceV4s93eS-AS3WEj9eMopROa2hEZ7BI5jDnlAg,40857
|
|
4
4
|
invenio_app_ils/errors.py,sha256=HB_iWj-aYxzTzzO6hWb66mUPZdqpWYHgmr2H2t3j3wU,13293
|
|
@@ -35,7 +35,7 @@ invenio_app_ils/acquisition/mappings/v7/acq_orders/order-v1.0.0.json,sha256=KMaS
|
|
|
35
35
|
invenio_app_ils/acquisition/schemas/__init__.py,sha256=HulLvvDz0W5owDGxVLIaastuZ4o6T5-MYGw0RPuY90g,233
|
|
36
36
|
invenio_app_ils/acquisition/schemas/acq_orders/order-v1.0.0.json,sha256=_Kd3oX_7gJJQSqSc2TxQulXletYu0wEeTdQKkXr7kLs,5688
|
|
37
37
|
invenio_app_ils/circulation/__init__.py,sha256=Gd0KAsGdhPdz0ACEQ9k8xSeOIxZr3xRK_FiE8U3RQWs,248
|
|
38
|
-
invenio_app_ils/circulation/api.py,sha256=
|
|
38
|
+
invenio_app_ils/circulation/api.py,sha256=ac-WrxQYZainV3QkOLyVzF4MVwfdcpRVXg-gd9Vi0tc,14495
|
|
39
39
|
invenio_app_ils/circulation/config.py,sha256=aVl7kLA2fobQuQy2XR9J_fQMV-ZaVxE2niMSTirDQM0,11387
|
|
40
40
|
invenio_app_ils/circulation/indexer.py,sha256=T24J5QqcB38LRJgirkAXL2tmFOucYToDRegxgFW96ag,3887
|
|
41
41
|
invenio_app_ils/circulation/receivers.py,sha256=Ux6KTNbII3DHBvCUS0gxqbi6tNbm76_kbcaHtK0BsB4,2488
|
|
@@ -79,8 +79,12 @@ invenio_app_ils/circulation/templates/invenio_app_ils_circulation/notifications/
|
|
|
79
79
|
invenio_app_ils/circulation/transitions/__init__.py,sha256=XaEYQVWOWhZ_0JPxTg0Lx53BmyWJXOfM3_nm2KipYhY,237
|
|
80
80
|
invenio_app_ils/circulation/transitions/transitions.py,sha256=9W5h4RHm72wMQuY3hKgFHLKwnxN68gkN98RwBlCjsro,1361
|
|
81
81
|
invenio_app_ils/closures/__init__.py,sha256=zJ2fI8Y34Vm-1uL9qLNeOQoqGKlhgfmiH8YcvvgnQP4,242
|
|
82
|
-
invenio_app_ils/closures/api.py,sha256=
|
|
82
|
+
invenio_app_ils/closures/api.py,sha256=5i9C1_gDR0-HLeCtU4-aGcvosc9cSR3YVK6gpXJObdo,4441
|
|
83
83
|
invenio_app_ils/closures/tasks.py,sha256=Uy8W1OPyuByLW4OFdSHVtH9sXmT2tP_uB57FxkV_Vag,4343
|
|
84
|
+
invenio_app_ils/closures/views.py,sha256=cWgCV58D7Pnsg56WLEcWBFQ97jEUgnNb-yvLvWRuvYk,2403
|
|
85
|
+
invenio_app_ils/closures/serializers/__init__.py,sha256=PT1NBZEFk3X7u-7JUQ0U6Pbv12wpD7gPCVIpsZAPdp4,494
|
|
86
|
+
invenio_app_ils/closures/serializers/response.py,sha256=kMSpqllro-UgUOquIRUWywqm77GNdJeeaaaYzpKVMnU,937
|
|
87
|
+
invenio_app_ils/closures/serializers/schema.py,sha256=yFQYCPgd-m3tZAk019FR8TDV0BXovYcKW39o7MLGeAc,816
|
|
84
88
|
invenio_app_ils/demo_data/documents.json,sha256=n0urrP68azieRMjgwGvKWoJSNXyTwpVxqzS4RInN3Ig,6342
|
|
85
89
|
invenio_app_ils/demo_data/items.json,sha256=J0IGbnIsFefgvGbgMWVCcvDIW5RJa48WoDPNBImUWKQ,1799
|
|
86
90
|
invenio_app_ils/document_requests/__init__.py,sha256=yPDfvJcGPy0dJ0f_hsJ5OQ5EcQ-WJODFPLjuWmSXSdU,237
|
|
@@ -438,6 +442,8 @@ invenio_app_ils/vocabularies/sources/__init__.py,sha256=EMLoLQGiq9_qoZ4lKqO_J0u2
|
|
|
438
442
|
invenio_app_ils/vocabularies/sources/base.py,sha256=kmNg85cjrxqc8VErFP2SUtlyo1PnmzOBQgfoSpfTPh4,1418
|
|
439
443
|
invenio_app_ils/vocabularies/sources/json.py,sha256=KGLTJ4AX8zEDdkpi3v92oHGs-h2dUzHbNPSg99-j8o0,1021
|
|
440
444
|
invenio_app_ils/vocabularies/sources/opendefinition.py,sha256=9zbRXuTr0i5lVLt596oUhsLQPiJVf9jiM9-C7T6zbXA,2151
|
|
445
|
+
invenio_app_ils-4.5.0.dist-info/licenses/AUTHORS.rst,sha256=BaXCGzdHCmiMOl4qtVlh1qrfy2ROMVOQp6ylzy1m0ww,212
|
|
446
|
+
invenio_app_ils-4.5.0.dist-info/licenses/LICENSE,sha256=9OdaPOAO1ZOJcRQ8BrGj7QAdaJc8SRSUgBtdom49MrI,1062
|
|
441
447
|
tests/__init__.py,sha256=eweSnIIeVVFHlmAFG9zkUQ8EXJMYfx8S6DIbsMVzinM,236
|
|
442
448
|
tests/conftest.py,sha256=vL0D8_Vj34eA1aHD5bzLITQ2_oNugnPoUzv1lIVAUbI,7154
|
|
443
449
|
tests/helpers.py,sha256=_x3SB-P6YusPHJ-jnKSf5GtuevHcNaNQBVqKh9eZnJ0,3227
|
|
@@ -452,7 +458,7 @@ tests/api/acquisition/test_acq_providers_permissions.py,sha256=aVshFVUuKwUfjRZgD
|
|
|
452
458
|
tests/api/circulation/__init__.py,sha256=AWnR9VZzm4IiO97eKJNn1LLtQJ1sRgjgrxChJ1Wxsuw,235
|
|
453
459
|
tests/api/circulation/test_expired_loans.py,sha256=nVGu1H7G8KlPGatx_YG2NCEYpeniZnGkwL4gsg_uyp8,2798
|
|
454
460
|
tests/api/circulation/test_loan_bulk_extend.py,sha256=jDYYyPXn5p6yn55pUVp4ivVSh9GcCvU5C-P8OlubMto,5606
|
|
455
|
-
tests/api/circulation/test_loan_checkout.py,sha256=
|
|
461
|
+
tests/api/circulation/test_loan_checkout.py,sha256=OFJI57lJZPzheJGuIiCukTzzkZePO2lrlphGlXKJT38,17512
|
|
456
462
|
tests/api/circulation/test_loan_default_durations.py,sha256=SWQmAJwe5GSoji1Hibx6DjGorJQZ2tYfiHPkuPZKTJo,1681
|
|
457
463
|
tests/api/circulation/test_loan_document_resolver.py,sha256=ZNu-otdQ8398ksTV2XdgpK3WcbeR8-X-qaRnlPM2_6w,556
|
|
458
464
|
tests/api/circulation/test_loan_extend.py,sha256=vkpyss5XkzeHVuGUXU8eOlF9J4esSjrVr5oNx-nXqEU,4158
|
|
@@ -476,7 +482,7 @@ tests/api/ill/test_ill_providers_permissions.py,sha256=q07t8RQEQ_68H4n01r3lWoNyu
|
|
|
476
482
|
tests/api/ils/__init__.py,sha256=9wpf3d3iO-AJ7rNFlnBciyU8QHD7RUp6ShxjZ8s4Gks,227
|
|
477
483
|
tests/api/ils/test_anonymization.py,sha256=dnWioSC-598zH9-jeH3IxyWOJLVw3M6L7SnICJM3mMU,6650
|
|
478
484
|
tests/api/ils/test_apis.py,sha256=2fqYcl6X8V5C8YpNqoVAwpHQH3SrerGZNcgwbanHmH4,2680
|
|
479
|
-
tests/api/ils/test_closures.py,sha256=
|
|
485
|
+
tests/api/ils/test_closures.py,sha256=o5WtrC9R88XAJYbvq76-3mhfL_SZhZ7yk7HObsX4XCc,11250
|
|
480
486
|
tests/api/ils/test_errors.py,sha256=8_cVOU5K2jneUrYRGz4-6IyZUNIPoh92hNqA1er-ync,4313
|
|
481
487
|
tests/api/ils/test_facets.py,sha256=ffig7FH6mnjaRoL1yELL2X06Q9yp3elmpkvtK6oLdo0,2766
|
|
482
488
|
tests/api/ils/test_internal_locations.py,sha256=MdSEjloixYIUWdIvc-1vcLbPXClWrzVunpqCqJ81LLY,3517
|
|
@@ -519,7 +525,7 @@ tests/data/eitems.json,sha256=2qwmHZkTf718Jizmk7YAO1v_8gs4yOm4dytzmvD-asQ,2122
|
|
|
519
525
|
tests/data/ill_borrowing_requests.json,sha256=XG3xQnBizpeXrgUsNHphqLA7wQKEYEdLbkYwTZdqOIM,1603
|
|
520
526
|
tests/data/ill_providers.json,sha256=M1NPVcjyP3_3r4Ynrltf5ZJ8QReBVHzV2fYSVUmj5Nc,169
|
|
521
527
|
tests/data/internal_locations.json,sha256=CMXYQ98d9Hhu7YnaQoxrUGAoyIe43kTikE3CIhXPq9E,396
|
|
522
|
-
tests/data/items.json,sha256=
|
|
528
|
+
tests/data/items.json,sha256=JMz2ojY79t0_x_55Mjt4BEL2LP1KOj82jXx6o-vCvm4,8849
|
|
523
529
|
tests/data/loans.json,sha256=xVTnEc9IuJdu0Fspd0HDNHM8ZO26lir3EEkwaAvTGIM,3668
|
|
524
530
|
tests/data/loans_most_loaned.json,sha256=ut1bOCFHCShPv_HotAjVV59gfwNfjpxEahyPCOZmQgY,3039
|
|
525
531
|
tests/data/locations.json,sha256=2-xvSY4x8MJsDzlK-aleXaJw6mPMaQe7rJqwX1YqxfM,1637
|
|
@@ -529,10 +535,8 @@ tests/templates/notifications/title_body.html,sha256=W-pa6eSm7glK9JEcTVo4G7yC3Q1
|
|
|
529
535
|
tests/templates/notifications/title_body_html.html,sha256=wPDP-DpQ0c2AX3oIfLvJLRp7J_rMoesIc9nLx3apkDc,146
|
|
530
536
|
tests/templates/notifications/title_body_html_ctx.html,sha256=qqfTZ9zKVXFqyXIcqt5BLHEJptRlUDPBPtLxPfTmNzQ,193
|
|
531
537
|
tests/templates/notifications/title_only.html,sha256=aoZ0veSleRYdKPXWgN6fdXLaz3r-KAPzdfHSuHPR2Uo,45
|
|
532
|
-
invenio_app_ils-4.
|
|
533
|
-
invenio_app_ils-4.
|
|
534
|
-
invenio_app_ils-4.
|
|
535
|
-
invenio_app_ils-4.
|
|
536
|
-
invenio_app_ils-4.
|
|
537
|
-
invenio_app_ils-4.4.0.dist-info/top_level.txt,sha256=MQTU2NrM0if5YAyIsKmQ0k35skJ3IUyQT7eCD2IWiNQ,22
|
|
538
|
-
invenio_app_ils-4.4.0.dist-info/RECORD,,
|
|
538
|
+
invenio_app_ils-4.5.0.dist-info/METADATA,sha256=GiTij7n__fnC2H4vz4pUnuwvUh3lIiEblB88lR8DZv4,16771
|
|
539
|
+
invenio_app_ils-4.5.0.dist-info/WHEEL,sha256=JNWh1Fm1UdwIQV075glCn4MVuCRs0sotJIq-J6rbxCU,109
|
|
540
|
+
invenio_app_ils-4.5.0.dist-info/entry_points.txt,sha256=iBVLFVqDZl-jiWvOfKldxDq9xsrbmI8GZhWhqsj8Ba0,7678
|
|
541
|
+
invenio_app_ils-4.5.0.dist-info/top_level.txt,sha256=MQTU2NrM0if5YAyIsKmQ0k35skJ3IUyQT7eCD2IWiNQ,22
|
|
542
|
+
invenio_app_ils-4.5.0.dist-info/RECORD,,
|
|
@@ -24,6 +24,7 @@ ils_rest = invenio_app_ils.ext:InvenioAppIlsREST
|
|
|
24
24
|
[invenio_base.api_blueprints]
|
|
25
25
|
ils_circulation = invenio_app_ils.circulation.views:create_circulation_blueprint
|
|
26
26
|
ils_circulation_stats = invenio_app_ils.circulation.stats.views:create_circulation_stats_blueprint
|
|
27
|
+
ils_closures = invenio_app_ils.closures.views:create_closures_blueprint
|
|
27
28
|
ils_document_request = invenio_app_ils.document_requests.views:create_document_request_action_blueprint
|
|
28
29
|
ils_document_stats = invenio_app_ils.records.views:create_document_stats_blueprint
|
|
29
30
|
ils_files = invenio_app_ils.files.views:create_files_blueprint
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
2
|
#
|
|
3
|
-
# Copyright (C) 2018-
|
|
3
|
+
# Copyright (C) 2018-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.
|
|
@@ -249,15 +249,29 @@ def test_self_checkout_search(app, client, json_headers, users, testdata):
|
|
|
249
249
|
assert res.status_code == 400
|
|
250
250
|
|
|
251
251
|
# test that an error is returned when the item cannot circulate
|
|
252
|
-
|
|
252
|
+
in_binding_item_barcode = "123456789-74"
|
|
253
253
|
url = url_for("invenio_app_ils_circulation.loan_self_checkout")
|
|
254
|
-
res = client.get(f"{url}?barcode={
|
|
254
|
+
res = client.get(f"{url}?barcode={in_binding_item_barcode}", headers=json_headers)
|
|
255
255
|
assert res.status_code == 400
|
|
256
256
|
# assert that the payload will contain the key error with a msg
|
|
257
257
|
response = res.get_json()
|
|
258
258
|
assert LoanSelfCheckoutItemInvalidStatus.description in response["message"]
|
|
259
259
|
assert LoanSelfCheckoutItemInvalidStatus.supportCode in response["supportCode"]
|
|
260
260
|
|
|
261
|
+
# test that no error is returned when the item is marked as missing
|
|
262
|
+
missing_item_barcode = "123456789-1"
|
|
263
|
+
missing_item_pid = "itemid-1"
|
|
264
|
+
url = url_for("invenio_app_ils_circulation.loan_self_checkout")
|
|
265
|
+
res = client.get(f"{url}?barcode={missing_item_barcode}", headers=json_headers)
|
|
266
|
+
assert res.status_code == 200
|
|
267
|
+
|
|
268
|
+
# assert item is no longer marked as missing
|
|
269
|
+
response = res.get_json()
|
|
270
|
+
item_pid = response["metadata"]["pid"]
|
|
271
|
+
assert item_pid == missing_item_pid
|
|
272
|
+
item = Item.get_record_by_pid(item_pid)
|
|
273
|
+
assert item["status"] == "CAN_CIRCULATE"
|
|
274
|
+
|
|
261
275
|
# create a loan on the same patron, and another one on another patron
|
|
262
276
|
user_login(client, "librarian", users)
|
|
263
277
|
url = url_for("invenio_app_ils_circulation.loan_checkout")
|
|
@@ -326,6 +340,7 @@ def test_self_checkout(app, client, json_headers, users, testdata):
|
|
|
326
340
|
current_search.flush_and_refresh(index="*")
|
|
327
341
|
|
|
328
342
|
def _self_checkout(patron, item_pid, document_pid):
|
|
343
|
+
url = url_for("invenio_app_ils_circulation.loan_self_checkout")
|
|
329
344
|
params = deepcopy(NEW_LOAN)
|
|
330
345
|
params["document_pid"] = document_pid
|
|
331
346
|
params["item_pid"] = dict(type="pitmid", value=item_pid)
|
|
@@ -394,3 +409,14 @@ def test_self_checkout(app, client, json_headers, users, testdata):
|
|
|
394
409
|
assert res.status_code == 202
|
|
395
410
|
response = res.get_json()
|
|
396
411
|
assert response["metadata"]["delivery"]["method"] == "SELF-CHECKOUT"
|
|
412
|
+
|
|
413
|
+
# test self-checkout with item marked as missing raises no error and marks the item as CAN_CIRCULATE
|
|
414
|
+
missing_item_pid = "itemid-1"
|
|
415
|
+
app.config["ILS_SELF_CHECKOUT_ENABLED"] = True
|
|
416
|
+
patron2 = user_login(client, "patron2", users)
|
|
417
|
+
res = _self_checkout(patron2, missing_item_pid, "docid-1")
|
|
418
|
+
assert res.status_code == 202
|
|
419
|
+
response = res.get_json()
|
|
420
|
+
assert response["metadata"]["delivery"]["method"] == "SELF-CHECKOUT"
|
|
421
|
+
item = Item.get_record_by_pid(missing_item_pid)
|
|
422
|
+
assert item["status"] == "CAN_CIRCULATE"
|
tests/api/ils/test_closures.py
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import json
|
|
11
11
|
|
|
12
12
|
import arrow
|
|
13
|
+
import pytest
|
|
13
14
|
from flask import url_for
|
|
14
15
|
|
|
15
16
|
from invenio_app_ils.closures.api import find_next_open_date
|
|
@@ -21,6 +22,7 @@ _LOCATION_PID = "locid-1"
|
|
|
21
22
|
_LOCATION_NAME = "Location name"
|
|
22
23
|
_ITEM_ENDPOINT = "invenio_records_rest.locid_item"
|
|
23
24
|
_LIST_ENDPOINT = "invenio_records_rest.locid_list"
|
|
25
|
+
_CLOSURE_PERIODS_ENDPOINT = "invenio_app_ils_closures.location_closure_periods"
|
|
24
26
|
_WEEKDAYS = [
|
|
25
27
|
"monday",
|
|
26
28
|
"tuesday",
|
|
@@ -261,7 +263,8 @@ def test_location_validation(client, json_headers, users, testdata):
|
|
|
261
263
|
)
|
|
262
264
|
|
|
263
265
|
|
|
264
|
-
|
|
266
|
+
@pytest.fixture
|
|
267
|
+
def location_closures_testdata(db, testdata):
|
|
265
268
|
def _update_location_closures_data(closed_weekdays, exceptions):
|
|
266
269
|
Location = current_app_ils.location_record_cls
|
|
267
270
|
record = Location.get_record_by_pid(_LOCATION_PID)
|
|
@@ -273,13 +276,6 @@ def test_find_next_open_date(app, db, testdata):
|
|
|
273
276
|
|
|
274
277
|
return record
|
|
275
278
|
|
|
276
|
-
def _test(start_date, expected_next_open_date):
|
|
277
|
-
next_open = find_next_open_date(_LOCATION_PID, _date_from_string(start_date))
|
|
278
|
-
if expected_next_open_date:
|
|
279
|
-
assert next_open == _date_from_string(expected_next_open_date)
|
|
280
|
-
else:
|
|
281
|
-
assert next_open is None
|
|
282
|
-
|
|
283
279
|
"""
|
|
284
280
|
Mon. Tue. Wed. Thu. Fri. Sat. Sun.
|
|
285
281
|
27 28 29 30 31 -01 -02
|
|
@@ -300,6 +296,15 @@ def test_find_next_open_date(app, db, testdata):
|
|
|
300
296
|
]
|
|
301
297
|
_update_location_closures_data(closed_weekdays, exceptions)
|
|
302
298
|
|
|
299
|
+
|
|
300
|
+
def test_find_next_open_date(app, location_closures_testdata):
|
|
301
|
+
def _test(start_date, expected_next_open_date):
|
|
302
|
+
next_open = find_next_open_date(_LOCATION_PID, _date_from_string(start_date))
|
|
303
|
+
if expected_next_open_date:
|
|
304
|
+
assert next_open == _date_from_string(expected_next_open_date)
|
|
305
|
+
else:
|
|
306
|
+
assert next_open is None
|
|
307
|
+
|
|
303
308
|
_test("2000-01-01", "2000-01-06")
|
|
304
309
|
_test("2000-01-04", "2000-01-06")
|
|
305
310
|
_test("2000-01-06", "2000-01-06")
|
|
@@ -313,3 +318,36 @@ def test_find_next_open_date(app, db, testdata):
|
|
|
313
318
|
_test("2000-01-30", "2000-01-30")
|
|
314
319
|
|
|
315
320
|
_test("2000-02-05", "2000-02-07")
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def test_get_closure_periods(client, location_closures_testdata, json_headers):
|
|
324
|
+
"""Test get closure periods."""
|
|
325
|
+
url = url_for(
|
|
326
|
+
_CLOSURE_PERIODS_ENDPOINT,
|
|
327
|
+
pid_value=_LOCATION_PID,
|
|
328
|
+
year=2000,
|
|
329
|
+
)
|
|
330
|
+
res = client.get(url, headers=json_headers)
|
|
331
|
+
assert res.status_code in _HTTP_OK
|
|
332
|
+
|
|
333
|
+
hits = json.loads(res.data.decode("utf-8"))["closure_periods"]
|
|
334
|
+
hits = [
|
|
335
|
+
(_date_from_string(hit["start"]), _date_from_string(hit["end"])) for hit in hits
|
|
336
|
+
]
|
|
337
|
+
|
|
338
|
+
def _is_in_interval(date, interval):
|
|
339
|
+
return interval[0] <= date <= interval[1]
|
|
340
|
+
|
|
341
|
+
def _test(date, closed):
|
|
342
|
+
date = _date_from_string(date)
|
|
343
|
+
assert (
|
|
344
|
+
any(_is_in_interval(date, hit) for hit in hits) == closed
|
|
345
|
+
), f"Date {date} is wrongfully marked as {'Open' if closed else 'Closed'}"
|
|
346
|
+
|
|
347
|
+
_test("2000-01-01", True)
|
|
348
|
+
_test("2000-01-06", False)
|
|
349
|
+
_test("2000-01-22", True)
|
|
350
|
+
_test("2000-01-23", True)
|
|
351
|
+
_test("2000-01-30", False)
|
|
352
|
+
_test("2000-02-01", False)
|
|
353
|
+
_test("2000-02-05", True)
|
tests/data/items.json
CHANGED
|
@@ -289,5 +289,16 @@
|
|
|
289
289
|
"medium": "NOT_SPECIFIED",
|
|
290
290
|
"status": "CAN_CIRCULATE",
|
|
291
291
|
"document": {}
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
"pid": "itemid-74",
|
|
295
|
+
"created_by": { "type": "script", "value": "demo" },
|
|
296
|
+
"barcode": "123456789-74",
|
|
297
|
+
"document_pid": "docid-17",
|
|
298
|
+
"internal_location_pid": "ilocid-1",
|
|
299
|
+
"circulation_restriction": "NO_RESTRICTION",
|
|
300
|
+
"medium": "NOT_SPECIFIED",
|
|
301
|
+
"status": "IN_BINDING",
|
|
302
|
+
"document": {}
|
|
292
303
|
}
|
|
293
304
|
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|