|
1 | 1 | from __future__ import annotations |
2 | 2 |
|
| 3 | +import datetime |
3 | 4 | import logging |
4 | 5 | from collections.abc import Callable |
5 | 6 | from typing import TYPE_CHECKING, Any |
6 | 7 |
|
7 | 8 | from flask_babel import lazy_gettext as _ |
8 | 9 | from pydantic import PositiveInt |
9 | 10 | from sqlalchemy.orm import Session |
| 11 | +from sqlalchemy.sql.expression import and_, or_ |
10 | 12 | from webpub_manifest_parser.odl import ODLFeedParserFactory |
11 | 13 | from webpub_manifest_parser.opds2.registry import OPDS2LinkRelationsRegistry |
12 | 14 |
|
|
18 | 20 | ConfigurationFormItemType, |
19 | 21 | FormField, |
20 | 22 | ) |
21 | | -from core.metadata_layer import FormatData |
22 | | -from core.model import Edition, RightsStatus |
| 23 | +from core.metadata_layer import FormatData, TimestampData |
| 24 | +from core.model import Edition, LicensePool, Loan, RightsStatus |
23 | 25 | from core.model.configuration import ExternalIntegration |
| 26 | +from core.monitor import CollectionMonitor |
24 | 27 | from core.opds2_import import ( |
25 | 28 | OPDS2Importer, |
26 | 29 | OPDS2ImporterSettings, |
27 | 30 | OPDS2ImportMonitor, |
28 | 31 | RWPMManifestParser, |
29 | 32 | ) |
30 | 33 | from core.util import first_or_default |
31 | | -from core.util.datetime_helpers import to_utc |
| 34 | +from core.util.datetime_helpers import to_utc, utc_now |
32 | 35 |
|
33 | 36 | if TYPE_CHECKING: |
34 | 37 | from webpub_manifest_parser.core.ast import Metadata |
@@ -316,3 +319,62 @@ def __init__( |
316 | 319 | super().__init__( |
317 | 320 | _db, collection, import_class, force_reimport=True, **import_class_kwargs |
318 | 321 | ) |
| 322 | + |
| 323 | + |
| 324 | +class ODL2LoanReaper(CollectionMonitor): |
| 325 | + """Check for loans that have expired and delete them, and update |
| 326 | + the holds queues for their pools.""" |
| 327 | + |
| 328 | + SERVICE_NAME = "ODL2 Loan Reaper" |
| 329 | + PROTOCOL = ODL2API.label() |
| 330 | + |
| 331 | + def __init__( |
| 332 | + self, |
| 333 | + _db: Session, |
| 334 | + collection: Collection, |
| 335 | + api: ODL2API | None = None, |
| 336 | + **kwargs: Any, |
| 337 | + ): |
| 338 | + super().__init__(_db, collection, **kwargs) |
| 339 | + self.api = api or ODL2API(_db, collection) |
| 340 | + |
| 341 | + def run_once(self, progress: TimestampData) -> TimestampData: |
| 342 | + # Find loans that have expired. |
| 343 | + self.log.info("Loan Reaper Job started") |
| 344 | + now = utc_now() |
| 345 | + expired_loans = ( |
| 346 | + self._db.query(Loan) |
| 347 | + .join(Loan.license_pool) |
| 348 | + .filter( |
| 349 | + and_( |
| 350 | + LicensePool.open_access == False, |
| 351 | + or_( |
| 352 | + Loan.end |
| 353 | + < now |
| 354 | + - datetime.timedelta( |
| 355 | + days=1 |
| 356 | + ), # Loans that ended before yesterday |
| 357 | + Loan.start < now - datetime.timedelta(days=90), |
| 358 | + Loan.end == None, |
| 359 | + ), # Loans that started more than 90 days ago and have no end date |
| 360 | + ) |
| 361 | + ) |
| 362 | + ) |
| 363 | + |
| 364 | + changed_pools = set() |
| 365 | + total_deleted_loans = 0 |
| 366 | + for loan in expired_loans: |
| 367 | + changed_pools.add(loan.license_pool) |
| 368 | + loan.license.checkin() |
| 369 | + self._db.delete(loan) |
| 370 | + total_deleted_loans += 1 |
| 371 | + |
| 372 | + for pool in changed_pools: |
| 373 | + self.api.update_licensepool_and_hold_queue(pool) |
| 374 | + |
| 375 | + message = "Loans deleted: %d. License pools updated: %d" % ( |
| 376 | + total_deleted_loans, |
| 377 | + len(changed_pools), |
| 378 | + ) |
| 379 | + progress = TimestampData(achievements=message) |
| 380 | + return progress |
0 commit comments