Source code for api.monitor

import logging
import os
import sys

from sqlalchemy import (
    and_,
    or_,
)

from core.monitor import (
    EditionSweepMonitor,
    ReaperMonitor,
)
from core.model import (
    Annotation,
    Collection,
    DataSource,
    Edition,
    ExternalIntegration,
    Hold,
    Identifier,
    LicensePool,
    Loan,
)
from core.util.datetime_helpers import utc_now
from .odl import (
    ODLAPI,
    SharedODLAPI,
)


[docs]class LoanlikeReaperMonitor(ReaperMonitor): SOURCE_OF_TRUTH_PROTOCOLS = [ ODLAPI.NAME, SharedODLAPI.NAME, ExternalIntegration.OPDS_FOR_DISTRIBUTORS, ] @property def where_clause(self): """We never want to automatically reap loans or holds for situations where the circulation manager is the source of truth. If we delete something we shouldn't have, we won't be able to get the 'real' information back. This means loans of open-access content and loans from collections based on a protocol found in SOURCE_OF_TRUTH_PROTOCOLS. Subclasses will append extra clauses to this filter. """ source_of_truth = or_( LicensePool.open_access==True, ExternalIntegration.protocol.in_( self.SOURCE_OF_TRUTH_PROTOCOLS ) ) source_of_truth_subquery = self._db.query(self.MODEL_CLASS.id).join( self.MODEL_CLASS.license_pool).join( LicensePool.collection).join( ExternalIntegration, Collection.external_integration_id==ExternalIntegration.id ).filter( source_of_truth ) return ~self.MODEL_CLASS.id.in_(source_of_truth_subquery)
[docs]class LoanReaper(LoanlikeReaperMonitor): """Remove expired and abandoned loans from the database.""" MODEL_CLASS = Loan MAX_AGE = 90 @property def where_clause(self): """Find loans that have either expired, or that were created a long time ago and have no definite end date. """ start_field = self.MODEL_CLASS.start end_field = self.MODEL_CLASS.end superclause = super(LoanReaper, self).where_clause now = utc_now() expired = end_field < now very_old_with_no_clear_end_date = and_( start_field < self.cutoff, end_field == None ) return and_(superclause, or_(expired, very_old_with_no_clear_end_date))
ReaperMonitor.REGISTRY.append(LoanReaper)
[docs]class HoldReaper(LoanlikeReaperMonitor): """Remove seemingly abandoned holds from the database.""" MODEL_CLASS = Hold MAX_AGE = 365 @property def where_clause(self): """Find holds that were created a long time ago and either have no end date or have an end date in the past. The 'end date' for a hold is just an estimate, but if the estimate is in the future it's better to keep the hold around. """ start_field = self.MODEL_CLASS.start end_field = self.MODEL_CLASS.end superclause = super(HoldReaper, self).where_clause end_date_in_past = end_field < utc_now() probably_abandoned = and_( start_field < self.cutoff, or_(end_field == None, end_date_in_past) ) return and_(superclause, probably_abandoned)
ReaperMonitor.REGISTRY.append(HoldReaper)
[docs]class IdlingAnnotationReaper(ReaperMonitor): """Remove idling annotations for inactive loans.""" MODEL_CLASS = Annotation TIMESTAMP_FIELD = 'timestamp' MAX_AGE = 60 @property def where_clause(self): """The annotation must have motivation=IDLING, must be at least 60 days old (meaning there has been no attempt to read the book for 60 days), and must not be associated with one of the patron's active loans or holds. """ superclause = super(IdlingAnnotationReaper, self).where_clause restrictions = [] for t in Loan, Hold: active_subquery = self._db.query( Annotation.id ).join( t, t.patron_id==Annotation.patron_id ).join( LicensePool, and_(LicensePool.id==t.license_pool_id, LicensePool.identifier_id==Annotation.identifier_id) ) restrictions.append( ~Annotation.id.in_(active_subquery) ) return and_( superclause, Annotation.motivation==Annotation.IDLING, *restrictions )
ReaperMonitor.REGISTRY.append(IdlingAnnotationReaper)