invenio-app-ils 4.1.0__py2.py3-none-any.whl → 4.3.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 (36) hide show
  1. invenio_app_ils/__init__.py +1 -1
  2. invenio_app_ils/circulation/api.py +123 -26
  3. invenio_app_ils/circulation/config.py +14 -2
  4. invenio_app_ils/circulation/loaders/__init__.py +2 -0
  5. invenio_app_ils/circulation/loaders/schemas/json/loan_request.py +2 -1
  6. invenio_app_ils/circulation/loaders/schemas/json/loan_self_checkout.py +19 -0
  7. invenio_app_ils/circulation/notifications/messages.py +1 -0
  8. invenio_app_ils/circulation/serializers/__init__.py +1 -0
  9. invenio_app_ils/circulation/serializers/response.py +1 -0
  10. invenio_app_ils/circulation/templates/invenio_app_ils_circulation/notifications/self_checkout.html +19 -0
  11. invenio_app_ils/circulation/views.py +100 -12
  12. invenio_app_ils/documents/jsonresolvers/document_stock.py +1 -1
  13. invenio_app_ils/documents/loaders/jsonschemas/document.py +1 -1
  14. invenio_app_ils/eitems/loaders/jsonschemas/eitems.py +1 -1
  15. invenio_app_ils/errors.py +103 -4
  16. invenio_app_ils/items/api.py +13 -1
  17. invenio_app_ils/items/loaders/jsonschemas/items.py +1 -1
  18. invenio_app_ils/items/search.py +13 -0
  19. invenio_app_ils/items/serializers/item.py +4 -4
  20. invenio_app_ils/patrons/anonymization.py +39 -9
  21. invenio_app_ils/permissions.py +49 -36
  22. invenio_app_ils/records/views.py +1 -1
  23. invenio_app_ils/records_relations/api.py +1 -0
  24. invenio_app_ils/relations/api.py +6 -1
  25. invenio_app_ils/series/loaders/jsonschemas/series.py +1 -1
  26. {invenio_app_ils-4.1.0.dist-info → invenio_app_ils-4.3.0.dist-info}/METADATA +16 -5
  27. {invenio_app_ils-4.1.0.dist-info → invenio_app_ils-4.3.0.dist-info}/RECORD +35 -34
  28. {invenio_app_ils-4.1.0.dist-info → invenio_app_ils-4.3.0.dist-info}/WHEEL +1 -1
  29. {invenio_app_ils-4.1.0.dist-info → invenio_app_ils-4.3.0.dist-info}/entry_points.txt +0 -1
  30. tests/api/circulation/test_loan_checkout.py +171 -66
  31. tests/api/ils/items/test_items_crud.py +46 -4
  32. tests/data/items.json +49 -27
  33. tests/api/ils/items/test_items_update.py +0 -40
  34. {invenio_app_ils-4.1.0.dist-info → invenio_app_ils-4.3.0.dist-info}/AUTHORS.rst +0 -0
  35. {invenio_app_ils-4.1.0.dist-info → invenio_app_ils-4.3.0.dist-info}/LICENSE +0 -0
  36. {invenio_app_ils-4.1.0.dist-info → invenio_app_ils-4.3.0.dist-info}/top_level.txt +0 -0
invenio_app_ils/errors.py CHANGED
@@ -98,11 +98,17 @@ class RecordHasReferencesError(IlsException):
98
98
  self.record_id = record_id
99
99
 
100
100
 
101
+ class ItemCannotCirculateError(IlsException):
102
+ """The item cannot circulate."""
103
+
104
+ description = "This item cannot circulate."
105
+
106
+
101
107
  class ItemHasActiveLoanError(IlsException):
102
108
  """The item which we are trying to update has an active loan."""
103
109
 
104
110
  description = (
105
- "Could not update item because it has an active loan with " "pid: {loan_pid}."
111
+ "Could not update item because it has an active loan with pid: {loan_pid}."
106
112
  )
107
113
 
108
114
  def __init__(self, loan_pid, **kwargs):
@@ -126,6 +132,7 @@ class PatronNotFoundError(IlsException):
126
132
  class PatronHasLoanOnItemError(IlsException):
127
133
  """A patron already has an active loan or a loan request on an item."""
128
134
 
135
+ code = 400
129
136
  description = "Patron '{0}' has already an active loan on item '{1}:{2}'"
130
137
 
131
138
  def __init__(self, patron_pid, item_pid, **kwargs):
@@ -135,6 +142,8 @@ class PatronHasLoanOnItemError(IlsException):
135
142
  :param prop: Missing property from loan request.
136
143
  """
137
144
  super().__init__(**kwargs)
145
+ self.patron_pid = patron_pid
146
+ self.item_pid = item_pid
138
147
  self.description = self.description.format(
139
148
  patron_pid, item_pid["type"], item_pid["value"]
140
149
  )
@@ -143,6 +152,7 @@ class PatronHasLoanOnItemError(IlsException):
143
152
  class PatronHasRequestOnDocumentError(IlsException):
144
153
  """A patron already has a loan request on a document."""
145
154
 
155
+ code = 400
146
156
  description = (
147
157
  "Patron '{patron_pid}' has already a loan "
148
158
  "request on document '{document_pid}'"
@@ -163,6 +173,7 @@ class PatronHasRequestOnDocumentError(IlsException):
163
173
  class PatronHasLoanOnDocumentError(IlsException):
164
174
  """A patron already has an active loan on a document."""
165
175
 
176
+ code = 400
166
177
  description = (
167
178
  "Patron '{patron_pid}' has already an active loan "
168
179
  "on document '{document_pid}'"
@@ -194,9 +205,54 @@ class LoanCheckoutByPatronForbidden(IlsException):
194
205
  )
195
206
 
196
207
 
208
+ class LoanSelfCheckoutItemUnavailable(IlsException):
209
+ """A patron cannot self-checkout an item."""
210
+
211
+ code = 400
212
+ description = "This literature is not available for self-checkout. Please contact the library for more information."
213
+
214
+ def get_body(self, environ=None, scope=None):
215
+ """Get the request body."""
216
+ body = dict(
217
+ status=self.code,
218
+ message=self.get_description(environ),
219
+ )
220
+
221
+ if self.supportCode:
222
+ body["supportCode"] = self.supportCode
223
+
224
+ return json.dumps(body)
225
+
226
+
227
+ class LoanSelfCheckoutItemInvalidStatus(LoanSelfCheckoutItemUnavailable):
228
+ """A patron cannot self-checkout an item that cannot circulate."""
229
+
230
+ supportCode = "SELF-CHECKOUT-001"
231
+
232
+
233
+ class LoanSelfCheckoutDocumentOverbooked(LoanSelfCheckoutItemUnavailable):
234
+ """A patron cannot self-checkout an item for an overbooked document."""
235
+
236
+ supportCode = "SELF-CHECKOUT-002"
237
+
238
+
239
+ class LoanSelfCheckoutItemActiveLoan(LoanSelfCheckoutItemUnavailable):
240
+ """A patron cannot self-checkout an item that cannot circulate."""
241
+
242
+ supportCode = "SELF-CHECKOUT-003"
243
+
244
+
245
+ class LoanSelfCheckoutItemNotFound(LoanSelfCheckoutItemUnavailable):
246
+ """A patron cannot self-checkout an item because item with provided barcode doesn't exist."""
247
+
248
+ supportCode = "SELF-CHECKOUT-004"
249
+ description = "Literature not found. Please try again with another barcode or contact the library."
250
+
251
+
197
252
  class NotImplementedConfigurationError(IlsException):
198
253
  """Exception raised when function is not implemented."""
199
254
 
255
+ code = 500
200
256
  description = (
201
257
  "Function is not implemented. Implement this function in your module "
202
258
  "and pass it to the config variable"
@@ -211,15 +267,20 @@ class NotImplementedConfigurationError(IlsException):
211
267
  class MissingRequiredParameterError(IlsException):
212
268
  """Exception raised when required parameter is missing."""
213
269
 
270
+ code = 400
271
+
214
272
 
215
273
  class InvalidParameterError(IlsException):
216
274
  """Exception raised when an invalid parameter is has been given."""
217
275
 
276
+ code = 400
277
+
218
278
 
219
279
  class DocumentNotFoundError(IlsException):
220
280
  """Raised when a document could not be found."""
221
281
 
222
- description = "Document PID '{}' was not found"
282
+ code = 404
283
+ description = "Document PID '{}' was not found."
223
284
 
224
285
  def __init__(self, document_pid, **kwargs):
225
286
  """Initialize exception."""
@@ -227,10 +288,38 @@ class DocumentNotFoundError(IlsException):
227
288
  self.description = self.description.format(document_pid)
228
289
 
229
290
 
291
+ class ItemNotFoundError(IlsException):
292
+ """Raised when an item could not be found."""
293
+
294
+ code = 404
295
+ description = "Item not found."
296
+
297
+ def __init__(self, pid=None, barcode=None, **kwargs):
298
+ """Initialize exception."""
299
+ super().__init__(**kwargs)
300
+ if pid:
301
+ self.description += " PID: {}".format(pid)
302
+ if barcode:
303
+ self.description += " Barcode: {}".format(barcode)
304
+
305
+
306
+ class MultipleItemsBarcodeFoundError(IlsException):
307
+ """Raised when multiple items with the same barcode has been found."""
308
+
309
+ code = 500
310
+ description = "Multiple items with barcode {} found."
311
+
312
+ def __init__(self, barcode, **kwargs):
313
+ """Initialize exception."""
314
+ super().__init__(**kwargs)
315
+ self.description = self.description.format(barcode)
316
+
317
+
230
318
  class LocationNotFoundError(IlsException):
231
319
  """Raised when a location could not be found."""
232
320
 
233
- description = "Location PID '{}' was not found"
321
+ code = 404
322
+ description = "Location PID '{}' was not found."
234
323
 
235
324
  def __init__(self, location_pid, **kwargs):
236
325
  """Initialize exception."""
@@ -241,7 +330,8 @@ class LocationNotFoundError(IlsException):
241
330
  class InternalLocationNotFoundError(IlsException):
242
331
  """Raised when an internal location could not be found."""
243
332
 
244
- description = "Internal Location PID '{}' was not found"
333
+ code = 404
334
+ description = "Internal Location PID '{}' was not found."
245
335
 
246
336
  def __init__(self, internal_location_pid, **kwargs):
247
337
  """Initialize exception."""
@@ -252,6 +342,7 @@ class InternalLocationNotFoundError(IlsException):
252
342
  class UnknownItemPidTypeError(IlsException):
253
343
  """Raised when the given item PID type is unknown."""
254
344
 
345
+ code = 400
255
346
  description = "Unknown Item PID type '{}'"
256
347
 
257
348
  def __init__(self, pid_type, **kwargs):
@@ -293,6 +384,14 @@ class DocumentRequestError(IlsException):
293
384
  super().__init__(description=description)
294
385
 
295
386
 
387
+ class DocumentOverbookedError(IlsException):
388
+ """Raised when a document is overbooked."""
389
+
390
+ def __init__(self, description):
391
+ """Initialize exception."""
392
+ super().__init__(description=description)
393
+
394
+
296
395
  class VocabularyError(IlsException):
297
396
  """Generic vocabulary exception."""
298
397
 
@@ -160,15 +160,27 @@ class Item(IlsRecord):
160
160
  )
161
161
  }
162
162
 
163
+ @classmethod
164
+ def enforce_constraints(cls, data, **kwargs):
165
+ """Enforce constraints.
166
+
167
+ :param data (dict): dict that can be mutated to enforce constraints.
168
+ """
169
+ # barcode is a required field and it should be always uppercase
170
+ data["barcode"] = data["barcode"].upper()
171
+
163
172
  @classmethod
164
173
  def create(cls, data, id_=None, **kwargs):
165
174
  """Create Item record."""
166
175
  cls.build_resolver_fields(data)
167
- return super().create(data, id_=id_, **kwargs)
176
+ cls.enforce_constraints(data, **kwargs)
177
+ created = super().create(data, id_=id_, **kwargs)
178
+ return created
168
179
 
169
180
  def update(self, *args, **kwargs):
170
181
  """Update Item record."""
171
182
  super().update(*args, **kwargs)
183
+ self.enforce_constraints(self)
172
184
  self.build_resolver_fields(self)
173
185
 
174
186
  def delete(self, **kwargs):
@@ -15,8 +15,8 @@ from invenio_app_ils.records.loaders.schemas.changed_by import (
15
15
  ChangedBySchema,
16
16
  set_changed_by,
17
17
  )
18
- from invenio_app_ils.records.loaders.schemas.price import PriceSchema
19
18
  from invenio_app_ils.records.loaders.schemas.identifiers import IdentifierSchema
19
+ from invenio_app_ils.records.loaders.schemas.price import PriceSchema
20
20
 
21
21
 
22
22
  class ISBNSchema(Schema):
@@ -41,6 +41,19 @@ class ItemSearch(RecordsSearch):
41
41
 
42
42
  return search
43
43
 
44
+ def search_by_barcode(self, barcode, filter_states=None, exclude_states=None):
45
+ """Retrieve items matching the given barcode."""
46
+ search = self
47
+
48
+ search = search.filter("term", barcode=barcode)
49
+
50
+ if filter_states:
51
+ search = search.filter("terms", status=filter_states)
52
+ elif exclude_states:
53
+ search = search.exclude("terms", status=exclude_states)
54
+
55
+ return search
56
+
44
57
  def search_by_internal_location_pid(
45
58
  self,
46
59
  internal_location_pid=None,
@@ -49,7 +49,7 @@ class ItemCSVSerializer(CSVSerializer):
49
49
  pid, record, links_factory=links_factory, **kwargs
50
50
  )
51
51
  filter_circulation(item)
52
- field_cover_metadata(item.get('metadata', {}).get("document", {}))
52
+ field_cover_metadata(item.get("metadata", {}).get("document", {}))
53
53
  return item
54
54
 
55
55
  def transform_search_hit(self, pid, record_hit, links_factory=None, **kwargs):
@@ -58,7 +58,7 @@ class ItemCSVSerializer(CSVSerializer):
58
58
  pid, record_hit, links_factory=links_factory, **kwargs
59
59
  )
60
60
  filter_circulation(hit)
61
- field_cover_metadata(hit.get('metadata', {}).get("document", {}))
61
+ field_cover_metadata(hit.get("metadata", {}).get("document", {}))
62
62
  return hit
63
63
 
64
64
 
@@ -71,7 +71,7 @@ class ItemJSONSerializer(JSONSerializer):
71
71
  pid, record, links_factory=links_factory, **kwargs
72
72
  )
73
73
  filter_circulation(item)
74
- field_cover_metadata(item.get('metadata', {}).get("document", {}))
74
+ field_cover_metadata(item.get("metadata", {}).get("document", {}))
75
75
  return item
76
76
 
77
77
  def transform_search_hit(self, pid, record_hit, links_factory=None, **kwargs):
@@ -80,5 +80,5 @@ class ItemJSONSerializer(JSONSerializer):
80
80
  pid, record_hit, links_factory=links_factory, **kwargs
81
81
  )
82
82
  filter_circulation(hit)
83
- field_cover_metadata(hit.get('metadata', {}).get("document", {}))
83
+ field_cover_metadata(hit.get("metadata", {}).get("document", {}))
84
84
  return hit
@@ -12,18 +12,22 @@ from copy import deepcopy
12
12
 
13
13
  from flask import current_app
14
14
  from invenio_accounts.models import LoginInformation, SessionActivity, User, userrole
15
+ from invenio_circulation.pidstore.pids import CIRCULATION_LOAN_PID_TYPE
15
16
  from invenio_circulation.proxies import current_circulation
16
17
  from invenio_db import db
17
18
  from invenio_oauthclient.models import RemoteAccount, RemoteToken, UserIdentity
18
19
  from invenio_search.engine import search as inv_search
19
20
  from invenio_userprofiles.models import UserProfile
20
21
 
22
+ from invenio_app_ils.acquisition.api import ORDER_PID_TYPE
21
23
  from invenio_app_ils.acquisition.proxies import current_ils_acq
22
24
  from invenio_app_ils.circulation.search import (
23
25
  get_active_loans_by_patron_pid,
24
26
  get_loans_by_patron_pid,
25
27
  )
28
+ from invenio_app_ils.document_requests.api import DOCUMENT_REQUEST_PID_TYPE
26
29
  from invenio_app_ils.errors import AnonymizationActiveLoansError, PatronNotFoundError
30
+ from invenio_app_ils.ill.api import BORROWING_REQUEST_PID_TYPE
27
31
  from invenio_app_ils.ill.proxies import current_ils_ill
28
32
  from invenio_app_ils.notifications.models import NotificationsLogs
29
33
  from invenio_app_ils.patrons.api import get_patron_or_unknown_dump
@@ -106,9 +110,32 @@ def anonymize_patron_data(patron_pid, force=False):
106
110
  cls = current_app.config["ILS_PATRON_ANONYMOUS_CLASS"]
107
111
  anonymous_patron_fields = cls().dumps_loader()
108
112
 
113
+ Loan = current_circulation.loan_record_cls
114
+ BorrowingRequest = current_ils_ill.borrowing_request_record_cls
115
+ DocumentRequest = current_app_ils.document_request_record_cls
116
+ Order = current_ils_acq.order_record_cls
117
+
118
+ anonymized_records = {
119
+ CIRCULATION_LOAN_PID_TYPE: {
120
+ "indexer": current_circulation.loan_indexer(),
121
+ "records": [],
122
+ },
123
+ BORROWING_REQUEST_PID_TYPE: {
124
+ "indexer": current_ils_ill.borrowing_request_indexer_cls(),
125
+ "records": [],
126
+ },
127
+ DOCUMENT_REQUEST_PID_TYPE: {
128
+ "indexer": current_app_ils.document_request_indexer,
129
+ "records": [],
130
+ },
131
+ ORDER_PID_TYPE: {
132
+ "indexer": current_ils_acq.order_indexer,
133
+ "records": [],
134
+ },
135
+ }
136
+
109
137
  patron_loans = get_loans_by_patron_pid(patron_pid).scan()
110
138
 
111
- Loan = current_circulation.loan_record_cls
112
139
  indices = 0
113
140
  for hit in patron_loans:
114
141
  loan = Loan.get_record_by_pid(hit.pid)
@@ -131,7 +158,7 @@ def anonymize_patron_data(patron_pid, force=False):
131
158
  loan["patron_pid"] = anonymous_patron_fields["pid"]
132
159
  loan["patron"] = anonymous_patron_fields
133
160
  loan.commit()
134
- current_circulation.loan_indexer().index(loan)
161
+ anonymized_records[CIRCULATION_LOAN_PID_TYPE]["records"].append(loan)
135
162
  indices += 1
136
163
 
137
164
  BorrowingRequestsSearch = current_ils_ill.borrowing_request_search_cls
@@ -139,14 +166,12 @@ def anonymize_patron_data(patron_pid, force=False):
139
166
  BorrowingRequestsSearch().search_by_patron_pid(patron_pid).scan()
140
167
  )
141
168
 
142
- BorrowingRequest = current_ils_ill.borrowing_request_record_cls
143
- indexer = current_ils_ill.borrowing_request_indexer_cls()
144
169
  for hit in patron_borrowing_requests:
145
170
  borrowing_request = BorrowingRequest.get_record_by_pid(hit.pid)
146
171
  borrowing_request["patron"] = anonymous_patron_fields
147
172
  borrowing_request["patron_pid"] = anonymous_patron_fields["pid"]
148
173
  borrowing_request.commit()
149
- indexer.index(borrowing_request)
174
+ anonymized_records[BORROWING_REQUEST_PID_TYPE]["records"].append(borrowing_request)
150
175
  indices += 1
151
176
 
152
177
  DocumentRequestSearch = current_app_ils.document_request_search_cls
@@ -154,7 +179,6 @@ def anonymize_patron_data(patron_pid, force=False):
154
179
  DocumentRequestSearch().search_by_patron_pid(patron_pid).scan()
155
180
  )
156
181
 
157
- DocumentRequest = current_app_ils.document_request_record_cls
158
182
  for hit in patron_document_requests:
159
183
  document_request = DocumentRequest.get_record_by_pid(hit.pid)
160
184
  if document_request["state"] == "PENDING":
@@ -163,19 +187,18 @@ def anonymize_patron_data(patron_pid, force=False):
163
187
  document_request["patron"] = anonymous_patron_fields
164
188
  document_request["patron_pid"] = anonymous_patron_fields["pid"]
165
189
  document_request.commit()
166
- current_app_ils.document_request_indexer.index(document_request)
190
+ anonymized_records[ORDER_PID_TYPE]["records"].append(document_request)
167
191
  indices += 1
168
192
 
169
193
  patron_acquisitions = OrderSearch().search_by_patron_pid(patron_pid).scan()
170
194
 
171
- Order = current_ils_acq.order_record_cls
172
195
  for hit in patron_acquisitions:
173
196
  acquisition = Order.get_record_by_pid(hit.pid)
174
197
  for line in acquisition["order_lines"]:
175
198
  if line.get("patron_pid") == patron_pid:
176
199
  line["patron_pid"] = anonymous_patron_fields["pid"]
177
200
  acquisition.commit()
178
- current_ils_acq.order_indexer.index(acquisition)
201
+ anonymized_records[Order._pid_type]["records"].append(acquisition)
179
202
  indices += 1
180
203
 
181
204
  # delete rows from db
@@ -185,6 +208,13 @@ def anonymize_patron_data(patron_pid, force=False):
185
208
  notifications = anonymize_patron_in_notification_logs(patron_pid)
186
209
 
187
210
  db.session.commit()
211
+
212
+ # index all after committing to DB, to ensure that no errors occurred.
213
+ for value in anonymized_records.values():
214
+ indexer, records = value["indexer"], value["records"]
215
+ for record in records:
216
+ indexer.index(record)
217
+
188
218
  if patron:
189
219
  try:
190
220
  patron_indexer = current_app_ils.patron_indexer
@@ -132,27 +132,32 @@ def patron_owner_permission(record):
132
132
 
133
133
 
134
134
  def loan_checkout_permission(*args, **kwargs):
135
- """Return permission to allow admins and librarians to checkout and patrons to self-checkout if enabled."""
135
+ """Loan checkout permissions checks.
136
+
137
+ Allow admins and librarians to checkout, patrons to self-checkout when enabled.
138
+ """
136
139
  if not has_request_context():
137
- # If from CLI, don't allow self-checkout
140
+ # CLI or Celery task
138
141
  return backoffice_permission()
142
+
139
143
  if current_user.is_anonymous:
140
144
  abort(401)
141
145
 
142
146
  is_admin_or_librarian = backoffice_permission().allows(g.identity)
143
147
  if is_admin_or_librarian:
144
148
  return backoffice_permission()
149
+
150
+ # ensure that only the loan's patron can do operations on this loan
145
151
  if len(args):
146
152
  loan = args[0]
147
153
  else:
148
- loan = kwargs.get("record", {})
149
- is_patron_current_user = current_user.id == int(loan.get("patron_pid"))
150
- if (
151
- current_app.config.get("ILS_SELF_CHECKOUT_ENABLED", False)
152
- and is_patron_current_user
153
- ):
154
+ loan = kwargs["record"]
155
+ is_patron_current_user = current_user.id == int(loan["patron_pid"])
156
+
157
+ if current_app.config["ILS_SELF_CHECKOUT_ENABLED"] and is_patron_current_user:
154
158
  return authenticated_user_permission()
155
- raise LoanCheckoutByPatronForbidden(int(loan.get("patron_pid")), current_user.id)
159
+
160
+ raise LoanCheckoutByPatronForbidden(int(loan["patron_pid"]), current_user.id)
156
161
 
157
162
 
158
163
  class PatronOwnerPermission(Permission):
@@ -163,36 +168,44 @@ class PatronOwnerPermission(Permission):
163
168
  super().__init__(UserNeed(int(record["patron_pid"])), backoffice_access_action)
164
169
 
165
170
 
171
+ _is_authenticated_user = [
172
+ "circulation-loan-request",
173
+ "patron-loans",
174
+ "bulk-loan-extension",
175
+ ]
176
+ _is_backoffice_permission = [
177
+ "circulation-loan-force-checkout",
178
+ "circulation-overdue-loan-notification",
179
+ "circulation-loan-update-dates",
180
+ "relations-create",
181
+ "relations-delete",
182
+ "stats-most-loaned",
183
+ "document-request-actions",
184
+ "bucket-create",
185
+ "ill-brwreq-patron-loan-create",
186
+ "ill-brwreq-patron-loan-extension-accept",
187
+ "ill-brwreq-patron-loan-extension-decline",
188
+ "send-notification-to-patron",
189
+ ]
190
+ _is_patron_owner_permission = [
191
+ "document-request-decline",
192
+ "ill-brwreq-patron-loan-extension-request",
193
+ ]
194
+
195
+
166
196
  def views_permissions_factory(action):
167
197
  """Return ILS views permissions factory."""
168
- is_authenticated_user = [
169
- "circulation-loan-request",
170
- "patron-loans",
171
- "bulk-loan-extension",
172
- ]
173
- is_backoffice_permission = [
174
- "circulation-loan-checkout",
175
- "circulation-loan-force-checkout",
176
- "circulation-overdue-loan-notification",
177
- "circulation-loan-update-dates",
178
- "relations-create",
179
- "relations-delete",
180
- "stats-most-loaned",
181
- "document-request-actions",
182
- "bucket-create",
183
- "ill-brwreq-patron-loan-create",
184
- "ill-brwreq-patron-loan-extension-accept",
185
- "ill-brwreq-patron-loan-extension-decline",
186
- "send-notification-to-patron",
187
- ]
188
- is_patron_owner_permission = [
189
- "document-request-decline",
190
- "ill-brwreq-patron-loan-extension-request",
191
- ]
192
- if action in is_authenticated_user:
198
+ if action in _is_authenticated_user:
193
199
  return authenticated_user_permission()
194
- elif action in is_backoffice_permission:
200
+ elif action in _is_backoffice_permission:
195
201
  return backoffice_permission()
196
- elif action in is_patron_owner_permission:
202
+ elif action in _is_patron_owner_permission:
197
203
  return PatronOwnerPermission
204
+ elif action == "circulation-loan-checkout":
205
+ return backoffice_permission()
206
+ elif (
207
+ action == "circulation-loan-self-checkout"
208
+ and current_app.config["ILS_SELF_CHECKOUT_ENABLED"]
209
+ ):
210
+ return authenticated_user_permission()
198
211
  return deny_all()
@@ -20,7 +20,7 @@ from invenio_app_ils.errors import StatsError
20
20
  from invenio_app_ils.permissions import backoffice_permission
21
21
  from invenio_app_ils.records.permissions import RecordPermission
22
22
  from invenio_app_ils.series.api import SERIES_PID_TYPE
23
- from invenio_app_ils.signals import record_viewed, file_downloaded
23
+ from invenio_app_ils.signals import file_downloaded, record_viewed
24
24
 
25
25
 
26
26
  def create_document_stats_blueprint(app):
@@ -425,6 +425,7 @@ class IlsRecordWithRelations(IlsRecord):
425
425
  def relations(self):
426
426
  """Get record relations."""
427
427
  from .retriever import get_relations
428
+
428
429
  return get_relations(self)
429
430
 
430
431
  def clear(self):
@@ -17,7 +17,12 @@ from sqlalchemy import and_, or_
17
17
  from invenio_app_ils.errors import RecordRelationsError
18
18
 
19
19
  ILS_RELATION_TYPE = namedtuple(
20
- "IlsRelationType", RelationType._fields + ("relation_class", "sort_by",)
20
+ "IlsRelationType",
21
+ RelationType._fields
22
+ + (
23
+ "relation_class",
24
+ "sort_by",
25
+ ),
21
26
  )
22
27
 
23
28
  LANGUAGE_RELATION = ILS_RELATION_TYPE(
@@ -21,10 +21,10 @@ from invenio_app_ils.records.loaders.schemas.changed_by import (
21
21
  ChangedBySchema,
22
22
  set_changed_by,
23
23
  )
24
+ from invenio_app_ils.records.loaders.schemas.identifiers import IdentifierSchema
24
25
  from invenio_app_ils.records.loaders.schemas.preserve_cover_metadata import (
25
26
  preserve_cover_metadata,
26
27
  )
27
- from invenio_app_ils.records.loaders.schemas.identifiers import IdentifierSchema
28
28
  from invenio_app_ils.series.api import Series
29
29
 
30
30
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: invenio-app-ils
3
- Version: 4.1.0
3
+ Version: 4.3.0
4
4
  Summary: Invenio Integrated Library System.
5
5
  Home-page: https://github.com/inveniosoftware/invenio-app-ils
6
6
  Author: CERN
@@ -16,7 +16,7 @@ Requires-Dist: invenio-app<1.4.0,>=1.3.4
16
16
  Requires-Dist: invenio-db[mysql,postgresql]<2.0.0,>=1.0.14
17
17
  Requires-Dist: invenio-base<1.3.0,>=1.2.11
18
18
  Requires-Dist: invenio-cache<2.0.0,>=1.1.1
19
- Requires-Dist: invenio-celery<1.3.0,>=1.2.4
19
+ Requires-Dist: invenio-celery<2.0.0,>=1.2.4
20
20
  Requires-Dist: invenio-config<1.1.0,>=1.0.3
21
21
  Requires-Dist: invenio-i18n<3.0.0,>=2.0.0
22
22
  Requires-Dist: invenio-admin<1.5.0,>=1.4.0
@@ -101,6 +101,19 @@ https://invenioils.docs.cern.ch
101
101
  Changes
102
102
  =======
103
103
 
104
+ Version 4.3.0 (released 2024-11-19)
105
+
106
+ - self-checkout: use dedicated endpoints for the entire workflow for better
107
+ permissions check and error handling.
108
+ Add a new loan transition and delivery method for self-checkout.
109
+ - anonymization: ensure that re-indexing is happening after the commit to the db,
110
+ to avoid premature re-indexing (and therefore index conflict version)
111
+ when db rollback happens.
112
+
113
+ Version 4.2.0 (released 2024-11-04)
114
+
115
+ - self-checkout: barcode is now always uppercased to make searches case-insensitive
116
+
104
117
  Version 4.1.0 (released 2024-10-21)
105
118
 
106
119
  - search: apply the same search analyzers to the fields that needs to be searchable.
@@ -148,7 +161,7 @@ Version 2.0.0rc9 (released 2024-04-25)
148
161
  Version 2.0.0rc8 (released 2024-04-04)
149
162
 
150
163
  - records_relation: Simplify sorting
151
- - records_relations: Use sort_by paramemter from configs instead
164
+ - records_relations: Use sort_by parameter from configs instead
152
165
  - relations: Add functionality to sort json refs by relation_type
153
166
  - tests: circulation: Add new location for testing closures
154
167
  - circulation: loan_request: Fix dates comparison in get_offset_duration
@@ -563,5 +576,3 @@ Version 1.0.0a4 (released 2020-06-19)
563
576
  Version 1.0.0a0 (released 2020-06-05)
564
577
 
565
578
  - Initial public release.
566
-
567
-