-
Notifications
You must be signed in to change notification settings - Fork 26
library: detect changes on library open hours #3367
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
# -*- coding: utf-8 -*- | ||
# | ||
# RERO ILS | ||
# Copyright (C) 2019-2023 RERO | ||
# Copyright (C) 2019-2023 UCLouvain | ||
# | ||
# This program is free software: you can redistribute it and/or modify | ||
# it under the terms of the GNU Affero General Public License as published by | ||
# the Free Software Foundation, version 3 of the License. | ||
# | ||
# This program is distributed in the hope that it will be useful, | ||
# but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
# GNU Affero General Public License for more details. | ||
# | ||
# You should have received a copy of the GNU Affero General Public License | ||
# along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
|
||
"""Invenio extensions for `Library` resources.""" | ||
from celery import current_app as celery_app | ||
from deepdiff import DeepDiff | ||
from invenio_cache import current_cache | ||
from invenio_records.extensions import RecordExtension | ||
|
||
from .tasks import calendar_changes_update_loans | ||
|
||
|
||
class LibraryCalendarChangesExtension(RecordExtension): | ||
"""Handle any changes on library calendar. | ||
|
||
When the library calendar changes, it could impact related active loans : | ||
If the loan ends now at a closed day/exception date, this loan end_date | ||
must be adapted to next_open_date. | ||
As it could potentially concern many loans, this operation can't be | ||
synchronously operated. So a detached Celery task will be called to execute | ||
changes. | ||
|
||
NOTE: If user operates multiple changes on library calendar in a small | ||
period of time, we need to ensure than only last detached task will | ||
be performed. So before running a new task, we MUST abort previously | ||
running task for the same library. | ||
""" | ||
|
||
def __init__(self, tracked_fields): | ||
"""Initialization method. | ||
|
||
:param tracked_fields: (list<String>) list of tracked fields. | ||
""" | ||
if not isinstance(tracked_fields, list) or not tracked_fields: | ||
raise TypeError("'tracked_fields' is required") | ||
self._changes_detected = False | ||
self._tracked_fields = tracked_fields | ||
super().__init__() | ||
|
||
def pre_commit(self, record): | ||
"""Called before a record is committed. | ||
|
||
:param record: the new record data. | ||
""" | ||
db_record = record.db_record() | ||
for field_name in self._tracked_fields: | ||
original_field = db_record.get(field_name) | ||
new_field = record.get(field_name) | ||
if DeepDiff(original_field, new_field, ignore_order=True): | ||
self._changes_detected = True | ||
break | ||
|
||
def post_commit(self, record): | ||
"""Called after a record is committed. | ||
|
||
:param record: the `Library` updated record | ||
""" | ||
if self._changes_detected: | ||
self._changes_detected = False # Reset changes detection | ||
task = calendar_changes_update_loans.s(record).apply_async() | ||
self._cache_current_task(record, task) | ||
|
||
@staticmethod | ||
def _cache_current_task(record, task): | ||
"""Store the task_id into the application cache. | ||
|
||
:param record: the touched library record. | ||
:param task: the task related to the library. | ||
""" | ||
content = current_cache.get('library-calendar-changes') or {} | ||
# If a previous task is still present into this cache entry, revoke it. | ||
# DEV NOTE : the task MUST clean (remove) this cache entry when task is | ||
# finished. | ||
if task_id := content.pop(record.pid, None): | ||
celery_app.control.revoke(task_id, terminate=True) | ||
content[record.pid] = task.id | ||
current_cache.set('library-calendar-changes', content) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
# -*- coding: utf-8 -*- | ||
# | ||
# RERO ILS | ||
# Copyright (C) 2019-2023 RERO | ||
# Copyright (C) 2019-2023 UCLouvain | ||
# | ||
# This program is free software: you can redistribute it and/or modify | ||
# it under the terms of the GNU Affero General Public License as published by | ||
# the Free Software Foundation, version 3 of the License. | ||
# | ||
# This program is distributed in the hope that it will be useful, | ||
# but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
# GNU Affero General Public License for more details. | ||
# | ||
# You should have received a copy of the GNU Affero General Public License | ||
# along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
|
||
"""Tasks on `Library` resource.""" | ||
import contextlib | ||
|
||
from celery import shared_task | ||
from invenio_cache import current_cache | ||
|
||
from .exceptions import LibraryNeverOpen | ||
|
||
|
||
@shared_task(name='library-calendar-changes-update-loans') | ||
def calendar_changes_update_loans(record_data): | ||
"""Task to update related loans if library calendar changes. | ||
|
||
If a library calendar changes, we will ensure that pending loans end_dates | ||
are still coherent with this calendar. If the end_date is now detected as | ||
a closed library date, the end_date of the loan will be updated to the | ||
next opening day of the library. | ||
|
||
..notes.. | ||
we only check about 'closed date' changes into the library calendar. In | ||
case of new opening day/date is set, pending loan end_date shouldn't be | ||
"down dated" (it will cause more problems with possible already sent | ||
notification). | ||
Example : | ||
* Patron receive a notification at Monday telling that | ||
the return date is Wednesday (because Tuesday is closed). | ||
* Next we edit the calendar to set all tuesday as opening day. In this | ||
use case the loan end_date will set to "Tuesday" (or maybe is already | ||
overdue if the loan is for multiple weeks). | ||
* Patron checkouts the book on Wednesday (as mentioned in received | ||
notification) but a fee will be possibly created. | ||
|
||
:param record_data: Data representing the library to check. | ||
""" | ||
|
||
def _at_finish(): | ||
"""Inner method called when the task finished.""" | ||
# DEV NOTES :: Clean the cache to remove task_id when finished | ||
# We build a behavior ot avoid multiple concurrent similar tasks at | ||
# same time. The parent process running the task should place a stamp | ||
# when the task is registered. So, to specify another similar task | ||
# must be started without collision, we need to clean this stamp. | ||
# DEV NOTES :: Why not using 'decorator' | ||
# A better way should be to use `@clean_cache` decorator ; this | ||
# decorator should take the key to clean as argument. But we didn't | ||
# know the key because it's created from `record_data`. This is why | ||
# it's easier to create a small specific function. | ||
cache_content = current_cache.get('library-calendar-changes') or {} | ||
cache_content.pop(library.pid, {}) | ||
current_cache.set('library-calendar-changes', cache_content) | ||
|
||
from rero_ils.modules.loans.api import LoansIndexer, \ | ||
get_on_loan_loans_for_library | ||
from .api import Library | ||
|
||
library = Library(record_data) | ||
zannkukai marked this conversation as resolved.
Show resolved
Hide resolved
|
||
active_loan_counter = 0 | ||
changed_loan_uuids = [] | ||
for loan in get_on_loan_loans_for_library(library.pid): | ||
rerowep marked this conversation as resolved.
Show resolved
Hide resolved
|
||
active_loan_counter += 1 | ||
if not library.is_open(loan.end_date): | ||
with contextlib.suppress(LibraryNeverOpen): | ||
loan['end_date'] = library \ | ||
.next_open(loan.end_date) \ | ||
.astimezone(library.get_timezone()) \ | ||
.replace(hour=23, minute=59, second=0, microsecond=0)\ | ||
.isoformat() | ||
changed_loan_uuids.append(loan.id) | ||
loan.update(loan, dbcommit=True, reindex=False) | ||
indexer = LoansIndexer() | ||
indexer.bulk_index(changed_loan_uuids) | ||
indexer.process_bulk_queue() | ||
zannkukai marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
_at_finish() | ||
return active_loan_counter, len(changed_loan_uuids) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.