Skip to content

Commit c093534

Browse files
Fulfill works
1 parent 39c031f commit c093534

File tree

4 files changed

+63
-179
lines changed

4 files changed

+63
-179
lines changed

api/circulation.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
from core.model.patron import LoanCheckout
5151
from core.util.datetime_helpers import utc_now
5252
from core.util.log import LoggerMixin
53-
53+
from core.util.http import HTTP, BadResponseException
5454

5555
class CirculationInfo:
5656
def __init__(
@@ -1496,7 +1496,7 @@ def fulfill(
14961496
:return: A FulfillmentInfo object.
14971497
14981498
"""
1499-
fulfillment: FulfillmentInfo
1499+
fulfillment: Fulfillment
15001500
loan = get_one(
15011501
self._db,
15021502
Loan,

api/controller/loan.py

Lines changed: 21 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@
5353
from core.model import DataSource, DeliveryMechanism, Loan, Patron, Representation
5454
from core.util.http import RemoteIntegrationException
5555
from core.util.opds_writer import OPDSFeed
56-
from core.util.problem_detail import ProblemDetail
56+
from core.util.problem_detail import ProblemDetail, BaseProblemDetailException, ProblemDetail
57+
from core.exceptions import BaseError
5758

5859

5960
class LoanController(CirculationManagerController):
@@ -395,95 +396,33 @@ def fulfill(
395396

396397
try:
397398
fulfillment = self.circulation.fulfill(
398-
patron,
399+
patron, # type: ignore[arg-type]
399400
credential,
400401
requested_license_pool,
401402
mechanism,
402403
)
403-
except DeliveryMechanismConflict as e:
404-
return DELIVERY_CONFLICT.detailed(str(e))
405-
except NoActiveLoan as e:
406-
return NO_ACTIVE_LOAN.detailed(
407-
_("Can't fulfill loan because you have no active loan for this book."),
408-
status_code=e.status_code,
409-
)
410-
except FormatNotAvailable as e:
411-
return NO_ACCEPTABLE_FORMAT.with_debug(str(e), status_code=e.status_code)
412-
except CannotFulfill as e:
413-
return CANNOT_FULFILL.with_debug(str(e), status_code=e.status_code)
414-
except DeliveryMechanismError as e:
415-
return BAD_DELIVERY_MECHANISM.with_debug(str(e), status_code=e.status_code)
416-
417-
# A subclass of FulfillmentInfo may want to bypass the whole
418-
# response creation process.
419-
response = fulfillment.as_response
420-
421-
if response is not None:
422-
print("fulfill response: ", response)
423-
return response
424-
425-
headers = dict()
426-
encoding_header = dict()
427-
if (
428-
fulfillment.data_source_name == DataSource.ENKI
429-
and mechanism.delivery_mechanism.drm_scheme_media_type
430-
== DeliveryMechanism.NO_DRM
404+
except (CirculationException, RemoteInitiatedServerError) as e:
405+
return e.problem_detail
406+
407+
# TODO: This should really be turned into its own Fulfillment class,
408+
# so each integration can choose when to return a feed response like
409+
# this, and when to return a direct response.
410+
if mechanism.delivery_mechanism.is_streaming and isinstance(
411+
fulfillment, UrlFulfillment
431412
):
432-
encoding_header["Accept-Encoding"] = "deflate"
433-
434-
if mechanism.delivery_mechanism.is_streaming:
435-
# If this is a streaming delivery mechanism, create an OPDS entry
436-
# with a fulfillment link to the streaming reader url.
437-
feed = OPDSAcquisitionFeed.single_entry_loans_feed(
413+
# If this is a streaming delivery mechanism (note: E-kirjasto does not stream),
414+
# create an OPDS entry with a fulfillment link to the streaming reader url.
415+
return OPDSAcquisitionFeed.single_entry_loans_feed(
438416
self.circulation, loan, fulfillment=fulfillment
439417
)
440-
if isinstance(feed, ProblemDetail):
441-
print("problems")
442-
# This should typically never happen, since we've gone through the entire fulfill workflow
443-
# But for the sake of return-type completeness we are adding this here
444-
return feed
445-
if isinstance(feed, Response):
446-
print("response")
447-
return feed
448-
else:
449-
content = etree.tostring(feed)
450-
status_code = 200
451-
headers["Content-Type"] = OPDSFeed.ACQUISITION_FEED_TYPE
452-
elif fulfillment.content_link_redirect is True:
453-
# The fulfillment API has asked us to not be a proxy and instead redirect the client directly
454-
print(f"elif fulfillment.content_link_redirect is True:")
455-
return redirect(fulfillment.content_link)
456-
else:
457-
content = fulfillment.content
458-
if fulfillment.content_link:
459-
print("if fulfillment.content_link: ", fulfillment.content_link)
460-
# If we have a link to the content on a remote server, web clients may not
461-
# be able to access it if the remote server does not support CORS requests.
462-
463-
# If the pool is open access though, the web client can link directly to the
464-
# file to download it, so it's safe to redirect.
465-
if requested_license_pool.open_access:
466-
print("if requested_license_pool.open_access:")
467-
return redirect(fulfillment.content_link)
468-
469-
# Otherwise, we need to fetch the content and return it instead
470-
# of redirecting to it, since it may be downloaded through an
471-
# indirect acquisition link.
472-
try:
473-
status_code, headers, content = do_get(
474-
fulfillment.content_link, headers=encoding_header
475-
)
476-
headers = dict(headers)
477-
print(f"loanpy fulfill: code: {status_code}, headers: {headers}, fulfillment.content_link: {fulfillment.content_link}")
478-
except RemoteIntegrationException as e:
479-
return e.as_problem_detail_document(debug=False)
480-
else:
481-
status_code = 200
482-
if fulfillment.content_type:
483-
headers["Content-Type"] = fulfillment.content_type
484418

485-
print(f"fulfill at end response: {response}, status: {status_code} headers: {headers}")
486-
return Response(response=content, status=status_code, headers=headers)
419+
try:
420+
resp = fulfillment.response()
421+
print("response in loan: ", resp)
422+
return resp
423+
except BaseError as e:
424+
return e.problem_detail
425+
487426

488427
def can_fulfill_without_loan(self, library, patron, pool, lpdm):
489428
"""Is it acceptable to fulfill the given LicensePoolDeliveryMechanism

api/odl.py

Lines changed: 6 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -436,7 +436,7 @@ def checkin(self, patron: Patron, pin: str, licensepool: LicensePool) -> None:
436436
raise NotCheckedOut()
437437
loan_result = loan.one()
438438

439-
if loan_result.license_pool.open_access:
439+
if license_pool.open_access or licensepool.unlimited_access:
440440
# If this is an open-access book, we don't need to do anything.
441441
return
442442

@@ -512,7 +512,7 @@ def checkout(
512512
if loan.count() > 0:
513513
raise AlreadyCheckedOut()
514514

515-
if licensepool.open_access:
515+
if licensepool.open_access or licensepool.unlimited_access:
516516
loan_start = None
517517
loan_end = None
518518
external_identifier = None
@@ -756,39 +756,6 @@ def _unlimited_access_fulfill(
756756
content_type = resource.representation.media_type
757757
return RedirectFulfillment(content_link, content_type) # Tää pitää kattoo, ei ole circulation.py:ssä
758758

759-
# PITÄÄ POISTAA
760-
@staticmethod
761-
def _find_content_link_and_type(
762-
links: list[dict[str, str]],
763-
drm_scheme: str | None,
764-
) -> tuple[str | None, str | None]:
765-
"""Find a content link with the type information corresponding to the selected delivery mechanism.
766-
767-
:param links: List of dict-like objects containing information about available links in the LCP license file
768-
:param drm_scheme: Selected delivery mechanism DRM scheme
769-
770-
:return: Two-tuple containing a content link and content type
771-
"""
772-
candidates = []
773-
for link in links:
774-
# Depending on the format being served, the crucial information
775-
# may be in 'manifest' or in 'license'.
776-
if link.get("rel") not in ("manifest", "license"):
777-
continue
778-
href = link.get("href")
779-
type = link.get("type")
780-
candidates.append((href, type))
781-
782-
if len(candidates) == 0:
783-
# No candidates
784-
return None, None
785-
786-
# For DeMarque audiobook content, we need to translate the type property
787-
# to reflect what we have stored in our delivery mechanisms.
788-
if drm_scheme == DeliveryMechanism.FEEDBOOKS_AUDIOBOOK_DRM:
789-
drm_scheme = ODLImporter.FEEDBOOKS_AUDIO
790-
791-
return next(filter(lambda x: x[1] == drm_scheme, candidates), (None, None))
792759

793760
def _license_fulfill(
794761
self, loan: Loan, delivery_mechanism: LicensePoolDeliveryMechanism
@@ -841,67 +808,11 @@ def _fulfill(
841808
loan: Loan,
842809
delivery_mechanism: LicensePoolDeliveryMechanism,
843810
) -> Fulfillment:
844-
if loan.license_pool.open_access:
811+
if loan.license_pool.open_access or licensepool.unlimited_access:
845812
return self._unlimited_access_fulfill(loan, delivery_mechanism)
846813
else:
847814
return self._license_fulfill(loan, delivery_mechanism)
848815

849-
def _fulfill_old(
850-
self,
851-
loan: Loan,
852-
delivery_mechanism: LicensePoolDeliveryMechanism,
853-
) -> FulfillmentInfo:
854-
licensepool = loan.license_pool
855-
856-
if licensepool.open_access:
857-
expires = None
858-
requested_mechanism = delivery_mechanism.delivery_mechanism
859-
self.log.info(f"delivery mech: {requested_mechanism}")
860-
fulfillment = next(
861-
(
862-
lpdm
863-
for lpdm in licensepool.delivery_mechanisms
864-
if lpdm.delivery_mechanism == requested_mechanism
865-
),
866-
None,
867-
)
868-
self.log.info(f"fulfillment: {fulfillment}")
869-
if fulfillment is None:
870-
raise FormatNotAvailable()
871-
content_link = fulfillment.resource.representation.public_url
872-
content_type = fulfillment.resource.representation.media_type
873-
else:
874-
doc = self.get_license_status_document(loan)
875-
status = doc.get("status")
876-
877-
if status not in [self.READY_STATUS, self.ACTIVE_STATUS]:
878-
# This loan isn't available for some reason. It's possible
879-
# the distributor revoked it or the patron already returned it
880-
# through the DRM system, and we didn't get a notification
881-
# from the distributor yet.
882-
self.update_loan(loan, doc)
883-
raise CannotFulfill()
884-
885-
expires = doc.get("potential_rights", {}).get("end")
886-
expires = dateutil.parser.parse(expires)
887-
888-
links = doc.get("links", [])
889-
890-
content_link, content_type = self._find_content_link_and_type(
891-
links, delivery_mechanism.delivery_mechanism.drm_scheme
892-
)
893-
self.log.info(f"cont link: {content_link}, type: {content_type}")
894-
895-
return FulfillmentInfo(
896-
licensepool.collection,
897-
licensepool.data_source.name,
898-
licensepool.identifier.type,
899-
licensepool.identifier.identifier,
900-
content_link,
901-
content_type,
902-
None,
903-
expires,
904-
)
905816

906817
def _count_holds_before(self, holdinfo: HoldInfo, pool: LicensePool) -> int:
907818
# Count holds on the license pool that started before this hold and
@@ -1102,9 +1013,9 @@ def _place_hold(self, patron: Patron, licensepool: LicensePool) -> HoldInfo:
11021013
licensepool.data_source.name,
11031014
licensepool.identifier.type,
11041015
licensepool.identifier.identifier,
1105-
utc_now(),
1106-
None,
1107-
0,
1016+
start_date=utc_now(),
1017+
end_date=None,
1018+
hold_position=licensepool.patrons_in_hold_queue,
11081019
)
11091020
library = patron.library
11101021
self._update_hold_end_date(holdinfo, licensepool, library=library)

core/util/problem_detail.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import json as j
88
import logging
99

10+
from abc import ABC, abstractmethod
1011
from flask_babel import LazyString
1112
from pydantic import BaseModel
1213

@@ -154,3 +155,36 @@ def problem_detail(self) -> ProblemDetail:
154155
:return: ProblemDetail object associated with this exception
155156
"""
156157
return self._problem_detail
158+
159+
class BaseProblemDetailException(BaseError, ABC):
160+
"""Mixin for exceptions that can be converted into a ProblemDetail."""
161+
162+
@property
163+
@abstractmethod
164+
def problem_detail(self) -> ProblemDetail:
165+
"""Convert this object into a ProblemDetail."""
166+
...
167+
168+
169+
class ProblemDetailException(BaseProblemDetailException):
170+
"""Exception class allowing to raise and catch ProblemDetail objects."""
171+
172+
def __init__(self, problem_detail: ProblemDetail) -> None:
173+
"""Initialize a new instance of ProblemError class.
174+
175+
:param problem_detail: ProblemDetail object
176+
"""
177+
if not isinstance(problem_detail, ProblemDetail):
178+
raise ValueError(
179+
'Argument "problem_detail" must be an instance of ProblemDetail class'
180+
)
181+
super().__init__(problem_detail.title)
182+
self._problem_detail = problem_detail
183+
184+
@property
185+
def problem_detail(self) -> ProblemDetail:
186+
"""Return the ProblemDetail object associated with this exception.
187+
188+
:return: ProblemDetail object associated with this exception
189+
"""
190+
return self._problem_detail

0 commit comments

Comments
 (0)