def test_apply_removes_old_formats_based_on_replacement_policy(self): edition, pool = self._edition(with_license_pool=True) # Start with one delivery mechanism for this pool. for lpdm in pool.delivery_mechanisms: self._db.delete(lpdm) old_lpdm = pool.set_delivery_mechanism( Representation.PDF_MEDIA_TYPE, DeliveryMechanism.ADOBE_DRM, RightsStatus.IN_COPYRIGHT, None, ) # And it has been loaned. patron = self._patron() loan, ignore = pool.loan_to(patron, fulfillment=old_lpdm) assert old_lpdm == loan.fulfillment # We have new circulation data that has a different format. format = FormatData( content_type=Representation.EPUB_MEDIA_TYPE, drm_scheme=DeliveryMechanism.ADOBE_DRM, ) circulation_data = CirculationData( formats=[format], data_source=edition.data_source, primary_identifier=edition.primary_identifier, ) # If we apply the new CirculationData with formats false in the policy, # we'll add the new format, but keep the old one as well. replacement_policy = ReplacementPolicy(formats=False) circulation_data.apply(self._db, pool.collection, replacement_policy) assert 2 == len(pool.delivery_mechanisms) assert set( [Representation.PDF_MEDIA_TYPE, Representation.EPUB_MEDIA_TYPE]) == set([ lpdm.delivery_mechanism.content_type for lpdm in pool.delivery_mechanisms ]) assert old_lpdm == loan.fulfillment # But if we make formats true in the policy, we'll delete the old format # and remove it from its loan. replacement_policy = ReplacementPolicy(formats=True) circulation_data.apply(self._db, pool.collection, replacement_policy) assert 1 == len(pool.delivery_mechanisms) assert (Representation.EPUB_MEDIA_TYPE == pool.delivery_mechanisms[0].delivery_mechanism.content_type) assert None == loan.fulfillment
def test_apply_creates_work_and_presentation_edition_if_needed(self): edition = self._edition() # This pool doesn't have a presentation edition or a work yet. pool = self._licensepool(edition) # We have new circulation data for this pool. circulation_data = CirculationData( formats=[], data_source=edition.data_source, primary_identifier=edition.primary_identifier, ) # If we apply the new CirculationData the work gets both a # presentation and a work. replacement_policy = ReplacementPolicy() circulation_data.apply(self._db, pool.collection, replacement_policy) assert edition == pool.presentation_edition assert pool.work != None # If we have another new pool for the same book in another # collection, it will share the work. collection = self._collection() pool2 = self._licensepool(edition, collection=collection) circulation_data.apply(self._db, pool2.collection, replacement_policy) assert edition == pool2.presentation_edition assert pool.work == pool2.work
def process_book(self, bibliographic, availability): license_pool, new_license_pool = availability.license_pool( self._db, self.collection) now = datetime.datetime.utcnow() edition, new_edition = bibliographic.edition(self._db) license_pool.edition = edition policy = ReplacementPolicy( identifiers=False, subjects=True, contributions=True, formats=True, ) availability.apply( self._db, license_pool.collection, replace=policy, ) if new_edition: bibliographic.apply(edition, self.collection, replace=policy) if new_license_pool or new_edition: # At this point we have done work equivalent to that done by # the EnkiBibliographicCoverageProvider. Register that the # work has been done so we don't have to do it again. identifier = edition.primary_identifier self.bibliographic_coverage_provider.handle_success(identifier) self.bibliographic_coverage_provider.add_coverage_record_for( identifier) for library in self.collection.libraries: self.analytics.collect_event( library, license_pool, CirculationEvent.DISTRIBUTOR_TITLE_ADD, now) return edition, license_pool
def test_explicit_formatdata(self): # Creating an edition with an open-access download will # automatically create a delivery mechanism. edition, pool = self._edition(with_open_access_download=True) # Let's also add a DRM format. drm_format = FormatData( content_type=Representation.PDF_MEDIA_TYPE, drm_scheme=DeliveryMechanism.ADOBE_DRM, ) circulation_data = CirculationData( formats=[drm_format], data_source=edition.data_source, primary_identifier=edition.primary_identifier, ) circulation_data.apply(self._db, pool.collection) [epub, pdf] = sorted(pool.delivery_mechanisms, key=lambda x: x.delivery_mechanism.content_type) assert epub.resource == pool.best_open_access_resource assert Representation.PDF_MEDIA_TYPE == pdf.delivery_mechanism.content_type assert DeliveryMechanism.ADOBE_DRM == pdf.delivery_mechanism.drm_scheme # If we tell Metadata to replace the list of formats, we only # have the one format we manually created. replace = ReplacementPolicy(formats=True, ) circulation_data.apply(self._db, pool.collection, replace=replace) [pdf] = pool.delivery_mechanisms assert Representation.PDF_MEDIA_TYPE == pdf.delivery_mechanism.content_type
def test_rights_status_commercial_link_with_rights(self): identifier = IdentifierData( Identifier.OVERDRIVE_ID, "abcd", ) link = LinkData( rel=Hyperlink.DRM_ENCRYPTED_DOWNLOAD, media_type=Representation.EPUB_MEDIA_TYPE, href=self._url, rights_uri=RightsStatus.IN_COPYRIGHT, ) format = FormatData( content_type=link.media_type, drm_scheme=DeliveryMechanism.ADOBE_DRM, link=link, rights_uri=RightsStatus.IN_COPYRIGHT, ) circulation_data = CirculationData( data_source=DataSource.OVERDRIVE, primary_identifier=identifier, links=[link], formats=[format], ) replace = ReplacementPolicy(formats=True, ) pool, ignore = circulation_data.license_pool(self._db, self._default_collection) circulation_data.apply(self._db, pool.collection, replace) assert False == pool.open_access assert 1 == len(pool.delivery_mechanisms) assert (RightsStatus.IN_COPYRIGHT == pool.delivery_mechanisms[0].rights_status.uri)
def test_rights_status_open_access_link_with_rights(self): identifier = IdentifierData( Identifier.OVERDRIVE_ID, "abcd", ) link = LinkData( rel=Hyperlink.OPEN_ACCESS_DOWNLOAD, media_type=Representation.EPUB_MEDIA_TYPE, href=self._url, rights_uri=RightsStatus.CC_BY_ND, ) circulation_data = CirculationData( data_source=DataSource.OVERDRIVE, primary_identifier=identifier, links=[link], ) replace = ReplacementPolicy(formats=True, ) pool, ignore = circulation_data.license_pool(self._db, self._default_collection) circulation_data.apply(self._db, pool.collection, replace) assert True == pool.open_access assert 1 == len(pool.delivery_mechanisms) assert RightsStatus.CC_BY_ND == pool.delivery_mechanisms[ 0].rights_status.uri
def test_rights_status_default_rights_passed_in(self): identifier = IdentifierData( Identifier.GUTENBERG_ID, "abcd", ) link = LinkData( rel=Hyperlink.DRM_ENCRYPTED_DOWNLOAD, media_type=Representation.EPUB_MEDIA_TYPE, href=self._url, ) circulation_data = CirculationData( data_source=DataSource.OA_CONTENT_SERVER, primary_identifier=identifier, default_rights_uri=RightsStatus.CC_BY, links=[link], ) replace = ReplacementPolicy(formats=True, ) pool, ignore = circulation_data.license_pool(self._db, self._default_collection) circulation_data.apply(self._db, pool.collection, replace) assert True == pool.open_access assert 1 == len(pool.delivery_mechanisms) # The rights status is the one that was passed in to CirculationData. assert RightsStatus.CC_BY == pool.delivery_mechanisms[ 0].rights_status.uri
def test_rights_status_default_rights_from_data_source(self): identifier = IdentifierData( Identifier.GUTENBERG_ID, "abcd", ) link = LinkData( rel=Hyperlink.DRM_ENCRYPTED_DOWNLOAD, media_type=Representation.EPUB_MEDIA_TYPE, href=self._url, ) circulation_data = CirculationData( data_source=DataSource.OA_CONTENT_SERVER, primary_identifier=identifier, links=[link], ) replace = ReplacementPolicy(formats=True, ) # This pool starts off as not being open-access. pool, ignore = circulation_data.license_pool(self._db, self._default_collection) assert False == pool.open_access circulation_data.apply(self._db, pool.collection, replace) # The pool became open-access because it was given a # link that came from the OS content server. assert True == pool.open_access assert 1 == len(pool.delivery_mechanisms) # The rights status is the default for the OA content server. assert (RightsStatus.GENERIC_OPEN_ACCESS == pool.delivery_mechanisms[0].rights_status.uri)
def __init__(self, collection, api_class=OdiloAPI, **kwargs): """Constructor. :param collection: Provide bibliographic coverage to all Odilo books in the given Collection. :param api_class: Instantiate this class with the given Collection, rather than instantiating OdiloAPI. """ super(OdiloBibliographicCoverageProvider, self).__init__(collection, **kwargs) if isinstance(api_class, OdiloAPI): # Use a previously instantiated OdiloAPI instance # rather than creating a new one. self.api = api_class else: # A web application should not use this option because it # will put a non-scoped session in the mix. _db = Session.object_session(collection) self.api = api_class(_db, collection) self.replacement_policy = ReplacementPolicy( identifiers=True, subjects=True, contributions=True, links=True, formats=True, rights=True, link_content=True, # even_if_not_apparently_updated=False, analytics=Analytics(self._db))
def process_book(self, bibliographic): """Make the local database reflect the state of the remote Enki collection for the given book. :param bibliographic: A Metadata object with attached CirculationData :return: A 2-tuple (LicensePool, Edition). If possible, a presentation-ready Work will be created for the LicensePool. """ availability = bibliographic.circulation edition, new_edition = bibliographic.edition(self._db) now = utc_now() policy = ReplacementPolicy( identifiers=False, subjects=True, contributions=True, formats=True, ) bibliographic.apply(edition, self.collection, replace=policy) license_pool, ignore = availability.license_pool(self._db, self.collection) if new_edition: for library in self.collection.libraries: self.analytics.collect_event( library, license_pool, CirculationEvent.DISTRIBUTOR_TITLE_ADD, now ) return edition, license_pool
def process_book(self, bibliographic, availability): analytics = Analytics(self._db) license_pool, new_license_pool = availability.license_pool( self._db, self.collection, analytics) edition, new_edition = bibliographic.edition(self._db) license_pool.edition = edition policy = ReplacementPolicy( identifiers=False, subjects=True, contributions=True, formats=True, analytics=analytics, ) availability.apply(self._db, self.collection, replace=policy) if new_edition: bibliographic.apply(edition, self.collection, replace=policy) if new_license_pool or new_edition: # At this point we have done work equivalent to that done by # the Axis360BibliographicCoverageProvider. Register that the # work has been done so we don't have to do it again. identifier = edition.primary_identifier self.bibliographic_coverage_provider.handle_success(identifier) self.bibliographic_coverage_provider.add_coverage_record_for( identifier) return edition, license_pool
def test_implicit_format_for_open_access_link(self): # A format is a delivery mechanism. We handle delivery on open access # pools from our mirrored content in S3. # Tests that when a link is open access, a pool can be delivered. edition, pool = self._edition(with_license_pool=True) # This is the delivery mechanism created by default when you # create a book with _edition(). [epub] = pool.delivery_mechanisms assert Representation.EPUB_MEDIA_TYPE == epub.delivery_mechanism.content_type assert DeliveryMechanism.ADOBE_DRM == epub.delivery_mechanism.drm_scheme link = LinkData( rel=Hyperlink.OPEN_ACCESS_DOWNLOAD, media_type=Representation.PDF_MEDIA_TYPE, href=self._url, ) circulation_data = CirculationData( data_source=DataSource.GUTENBERG, primary_identifier=edition.primary_identifier, links=[link], ) replace = ReplacementPolicy(formats=True, ) circulation_data.apply(self._db, pool.collection, replace) # We destroyed the default delivery format and added a new, # open access delivery format. [pdf] = pool.delivery_mechanisms assert Representation.PDF_MEDIA_TYPE == pdf.delivery_mechanism.content_type assert DeliveryMechanism.NO_DRM == pdf.delivery_mechanism.drm_scheme circulation_data = CirculationData( data_source=DataSource.GUTENBERG, primary_identifier=edition.primary_identifier, links=[], ) replace = ReplacementPolicy( formats=True, links=True, ) circulation_data.apply(self._db, pool.collection, replace) # Now we have no formats at all. assert 0 == len(pool.delivery_mechanisms)
def default_circulation_replacement_policy(self): return ReplacementPolicy( identifiers=False, subjects=True, contributions=True, formats=True, analytics=Analytics(self._db), )
def test_mirror_open_access_link_mirror_failure(self): mirrors = dict(books_mirror=MockS3Uploader(fail=True), covers_mirror=None) h = DummyHTTPClient() edition, pool = self._edition(with_license_pool=True) data_source = DataSource.lookup(self._db, DataSource.GUTENBERG) policy = ReplacementPolicy(mirrors=mirrors, http_get=h.do_get) circulation_data = CirculationData( data_source=edition.data_source, primary_identifier=edition.primary_identifier, ) link = LinkData( rel=Hyperlink.OPEN_ACCESS_DOWNLOAD, media_type=Representation.EPUB_MEDIA_TYPE, href=self._url, ) link_obj, ignore = edition.primary_identifier.add_link( rel=link.rel, href=link.href, data_source=data_source, media_type=link.media_type, content=link.content, ) h.queue_response(200, media_type=Representation.EPUB_MEDIA_TYPE) circulation_data.mirror_link(pool, data_source, link, link_obj, policy) representation = link_obj.resource.representation # The representation was fetched successfully. assert None == representation.fetch_exception assert representation.fetched_at != None # But mirroing failed. assert representation.mirror_exception != None assert None == representation.mirrored_at assert link.media_type == representation.media_type assert link.href == representation.url # The mirror url was never set. assert None == representation.mirror_url # Book content is still there since it wasn't mirrored. assert representation.content != None # The license pool is suppressed when mirroring fails. assert True == pool.suppressed assert representation.mirror_exception in pool.license_exception
def update_licensepool_for_identifier(self, isbn, availability): """Update availability information for a single book. If the book has never been seen before, a new LicensePool will be created for the book. The book's LicensePool will be updated with current approximate circulation information (we can tell if it's available, but not how many copies). Bibliographic coverage will be ensured for the OneClick Identifier. Work will be created for the LicensePool and set as presentation-ready. :param isbn the identifier OneClick uses :param availability boolean denoting if book can be lent to patrons """ # find a license pool to match the isbn, and see if it'll need a metadata update later license_pool, is_new_pool = LicensePool.for_foreign_id( self._db, DataSource.ONECLICK, Identifier.ONECLICK_ID, isbn, collection=self.collection ) if is_new_pool: # This is the first time we've seen this book. Make sure its # identifier has bibliographic coverage. self.bibliographic_coverage_provider.ensure_coverage( license_pool.identifier ) # now tell the licensepool if it's lendable policy = ReplacementPolicy( identifiers=False, subjects=True, contributions=True, formats=True, analytics=Analytics(self._db), ) # licenses_available can be 0 or 999, depending on whether the book is # lendable or not. licenses_available = 999 if not availability: licenses_available = 0 circulation_data = CirculationData(data_source=DataSource.ONECLICK, primary_identifier=license_pool.identifier, licenses_available=licenses_available) license_pool, circulation_changed = circulation_data.apply( self._db, self.collection, replace=policy, ) return license_pool, is_new_pool, circulation_changed
def update_formats(self, licensepool): """Update the format information for a single book. """ info = self.metadata_lookup(licensepool.identifier) metadata = OverdriveRepresentationExtractor.book_info_to_metadata( info, include_bibliographic=False, include_formats=True) circulation_data = metadata.circulation replace = ReplacementPolicy(formats=True, ) circulation_data.apply(licensepool, replace)
def __init__(self, collection, *args, **kwargs): _db = Session.object_session(collection) replacement_policy = kwargs.pop('replacement_policy', None) if not replacement_policy: mirror = MirrorUploader.sitewide(_db) replacement_policy = ReplacementPolicy(mirror=mirror, links=True) # Only process identifiers that have been registered for coverage. kwargs['registered_only'] = kwargs.get('registered_only', True) super(IntegrationClientCoverImageCoverageProvider, self).__init__(collection, *args, replacement_policy=replacement_policy, **kwargs)
def test_format_change_may_change_open_access_status(self): # In this test, whenever we call CirculationData.apply(), we # want to destroy the old list of formats and recreate it. replace_formats = ReplacementPolicy(formats=True) # Here's a seemingly ordinary non-open-access LicensePool. edition, pool = self._edition(with_license_pool=True) assert False == pool.open_access # One day, we learn that it has an open-access delivery mechanism. link = LinkData( rel=Hyperlink.OPEN_ACCESS_DOWNLOAD, media_type=Representation.EPUB_MEDIA_TYPE, href=self._url, rights_uri=RightsStatus.CC_BY_ND, ) circulation_data = CirculationData( data_source=pool.data_source, primary_identifier=pool.identifier, links=[link], ) # Applying this information turns the pool into an open-access pool. circulation_data.apply(self._db, pool.collection, replace=replace_formats) assert True == pool.open_access # Then we find out it was a mistake -- the book is in copyright. format = FormatData( Representation.EPUB_MEDIA_TYPE, DeliveryMechanism.NO_DRM, rights_uri=RightsStatus.IN_COPYRIGHT, ) circulation_data = CirculationData( data_source=pool.data_source, primary_identifier=pool.identifier, formats=[format], ) circulation_data.apply(self._db, pool.collection, replace=replace_formats) # The original LPDM has been removed and only the new one remains. assert False == pool.open_access assert 1 == len(pool.delivery_mechanisms)
def update_formats(self, licensepool): """Update the format information for a single book. """ info = self.metadata_lookup(licensepool.identifier) metadata = OverdriveRepresentationExtractor.book_info_to_metadata( info, include_bibliographic=False, include_formats=True) circulation_data = metadata.circulation # The identifier in the CirculationData needs to match the # identifier associated with the LicensePool -- otherwise # a new LicensePool will be created. circulation_data._primary_identifier.identifier = licensepool.identifier.identifier replace = ReplacementPolicy(formats=True) _db = Session.object_session(licensepool) circulation_data.apply(_db, licensepool.collection, replace)
def test_mirror_open_access_link_fetch_failure(self): mirrors = dict(books_mirror=MockS3Uploader()) h = DummyHTTPClient() edition, pool = self._edition(with_license_pool=True) data_source = DataSource.lookup(self._db, DataSource.GUTENBERG) policy = ReplacementPolicy(mirrors=mirrors, http_get=h.do_get) circulation_data = CirculationData( data_source=edition.data_source, primary_identifier=edition.primary_identifier, ) link = LinkData( rel=Hyperlink.OPEN_ACCESS_DOWNLOAD, media_type=Representation.EPUB_MEDIA_TYPE, href=self._url, ) link_obj, ignore = edition.primary_identifier.add_link( rel=link.rel, href=link.href, data_source=data_source, media_type=link.media_type, content=link.content, ) h.queue_response(403) circulation_data.mirror_link(pool, data_source, link, link_obj, policy) representation = link_obj.resource.representation # Fetch failed, so we should have a fetch exception but no mirror url. assert representation.fetch_exception != None assert None == representation.mirror_exception assert None == representation.mirror_url assert link.href == representation.url assert representation.fetched_at != None assert None == representation.mirrored_at # The license pool is suppressed when fetch fails. assert True == pool.suppressed assert representation.fetch_exception in pool.license_exception
def update_consolidated_copy(self, _db, copy_info, analytics=None): """Process information about the current status of a consolidated copy from the consolidated copies feed. """ identifier = copy_info.get("identifier") licenses = copy_info.get("licenses") available = copy_info.get("available") identifier_data = IdentifierData(Identifier.URI, identifier) circulation_data = CirculationData( data_source=self.data_source_name, primary_identifier=identifier_data, licenses_owned=licenses, licenses_available=available, ) replacement_policy = ReplacementPolicy(analytics=analytics) pool, ignore = circulation_data.apply(_db, self.collection(_db), replacement_policy) # Update licenses reserved if there are holds. if len(pool.holds) > 0 and pool.licenses_available > 0: self.update_hold_queue(pool)
def test_annotate_metadata(self): """Verify that annotate_metadata calls load_circulation_data and load_cover_link appropriately. """ # First, test an unsuccessful annotation. class MockNoCirculationData(DirectoryImportScript): """Do nothing when load_circulation_data is called. Explode if load_cover_link is called. """ def load_circulation_data(self, *args): self.load_circulation_data_args = args return None def load_cover_link(self, *args): raise Exception("Explode!") gutenberg = DataSource.lookup(self._db, DataSource.GUTENBERG) identifier = IdentifierData(Identifier.GUTENBERG_ID, "11111") identifier_obj, ignore = identifier.load(self._db) metadata = Metadata( title=self._str, data_source=gutenberg, primary_identifier=identifier ) mirror = object() policy = ReplacementPolicy(mirror=mirror) cover_directory = object() ebook_directory = object() rights_uri = object() script = MockNoCirculationData(self._db) args = (metadata, policy, cover_directory, ebook_directory, rights_uri) script.annotate_metadata(*args) # load_circulation_data was called. eq_( (identifier_obj, gutenberg, ebook_directory, mirror, metadata.title, rights_uri), script.load_circulation_data_args ) # But because load_circulation_data returned None, # metadata.circulation_data was not modified and # load_cover_link was not called (which would have raised an # exception). eq_(None, metadata.circulation) # Test a successful annotation with no cover image. class MockNoCoverLink(DirectoryImportScript): """Return an object when load_circulation_data is called. Do nothing when load_cover_link is called. """ def load_circulation_data(self, *args): return "Some circulation data" def load_cover_link(self, *args): self.load_cover_link_args = args return None script = MockNoCoverLink(self._db) script.annotate_metadata(*args) # The Metadata object was annotated with the return value of # load_circulation_data. eq_("Some circulation data", metadata.circulation) # load_cover_link was called. eq_( (identifier_obj, gutenberg, cover_directory, mirror), script.load_cover_link_args ) # But since it provided no cover link, metadata.links was empty. eq_([], metadata.links) # Finally, test a completely successful annotation. class MockWithCoverLink(DirectoryImportScript): """Mock success for both load_circulation_data and load_cover_link. """ def load_circulation_data(self, *args): return "Some circulation data" def load_cover_link(self, *args): return "A cover link" metadata.circulation = None script = MockWithCoverLink(self._db) script.annotate_metadata(*args) eq_("Some circulation data", metadata.circulation) eq_(['A cover link'], metadata.links)
def change_book_cover(self, identifier_type, identifier, mirrors=None): """Save a new book cover based on the submitted form.""" self.require_librarian(flask.request.library) data_source = DataSource.lookup(self._db, DataSource.LIBRARY_STAFF) work = self.load_work(flask.request.library, identifier_type, identifier) if isinstance(work, ProblemDetail): return work rights_uri = flask.request.form.get("rights_status") rights_explanation = flask.request.form.get("rights_explanation") if not rights_uri: return INVALID_IMAGE.detailed( _("You must specify the image's license.")) collection = self._get_collection_from_pools(identifier_type, identifier) if isinstance(collection, ProblemDetail): return collection # Look for an appropriate mirror to store this cover image. Since the # mirror should be used for covers, we don't need a mirror for books. mirrors = mirrors or dict(covers_mirror=MirrorUploader.for_collection( collection, ExternalIntegrationLink.COVERS), books_mirror=None) if not mirrors.get(ExternalIntegrationLink.COVERS): return INVALID_CONFIGURATION_OPTION.detailed( _("Could not find a storage integration for uploading the cover." )) image = self.generate_cover_image(work, identifier_type, identifier) if isinstance(image, ProblemDetail): return image original, derivation_settings, cover_href, cover_rights_explanation = self._original_cover_info( image, work, data_source, rights_uri, rights_explanation) buffer = StringIO() image.save(buffer, format="PNG") content = buffer.getvalue() if not cover_href: cover_href = Hyperlink.generic_uri( data_source, work.presentation_edition.primary_identifier, Hyperlink.IMAGE, content=content) cover_data = LinkData( Hyperlink.IMAGE, href=cover_href, media_type=Representation.PNG_MEDIA_TYPE, content=content, rights_uri=rights_uri, rights_explanation=cover_rights_explanation, original=original, transformation_settings=derivation_settings, ) presentation_policy = PresentationCalculationPolicy( choose_edition=False, set_edition_metadata=False, classify=False, choose_summary=False, calculate_quality=False, choose_cover=True, regenerate_opds_entries=True, regenerate_marc_record=True, update_search_index=False, ) replacement_policy = ReplacementPolicy( links=True, # link_content is false because we already have the content. # We don't want the metadata layer to try to fetch it again. link_content=False, mirrors=mirrors, presentation_calculation_policy=presentation_policy, ) metadata = Metadata(data_source, links=[cover_data]) metadata.apply(work.presentation_edition, collection, replace=replacement_policy) # metadata.apply only updates the edition, so we also need # to update the work. work.calculate_presentation(policy=presentation_policy) return Response(_("Success"), 200)
def add_with_metadata(self, collection_details): """Adds identifiers with their metadata to a Collection's catalog""" client = authenticated_client_from_request(self._db) if isinstance(client, ProblemDetail): return client collection = collection_from_details(self._db, client, collection_details) data_source = DataSource.lookup(self._db, collection.name, autocreate=True) messages = [] feed = feedparser.parse(request.data) entries = feed.get("entries", []) entries_by_urn = {entry.get('id'): entry for entry in entries} identifiers_by_urn, invalid_urns = Identifier.parse_urns( self._db, entries_by_urn.keys()) messages = list() for urn in invalid_urns: messages.append( OPDSMessage(urn, INVALID_URN.status_code, INVALID_URN.detail)) for urn, identifier in identifiers_by_urn.items(): entry = entries_by_urn[urn] status = HTTP_OK description = "Already in catalog" if identifier not in collection.catalog: collection.catalog_identifier(identifier) status = HTTP_CREATED description = "Successfully added" message = OPDSMessage(urn, status, description) # Get a cover if it exists. image_types = set([Hyperlink.IMAGE, Hyperlink.THUMBNAIL_IMAGE]) images = [ l for l in entry.get("links", []) if l.get("rel") in image_types ] links = [ LinkData(image.get("rel"), image.get("href")) for image in images ] # Create an edition to hold the title and author. LicensePool.calculate_work # refuses to create a Work when there's no title, and if we have a title, author # and language we can attempt to look up the edition in OCLC. title = entry.get("title") or "Unknown Title" author = ContributorData(sort_name=(entry.get("author") or Edition.UNKNOWN_AUTHOR), roles=[Contributor.PRIMARY_AUTHOR_ROLE]) language = entry.get("dcterms_language") presentation = PresentationCalculationPolicy( choose_edition=False, set_edition_metadata=False, classify=False, choose_summary=False, calculate_quality=False, choose_cover=False, regenerate_opds_entries=False, ) replace = ReplacementPolicy( presentation_calculation_policy=presentation) metadata = Metadata( data_source, primary_identifier=IdentifierData(identifier.type, identifier.identifier), title=title, language=language, contributors=[author], links=links, ) edition, ignore = metadata.edition(self._db) metadata.apply(edition, collection, replace=replace) messages.append(message) title = "%s Catalog Item Additions for %s" % (collection.protocol, client.url) url = self.collection_feed_url("add_with_metadata", collection) addition_feed = AcquisitionFeed(self._db, title, url, [], VerboseAnnotator, precomposed_entries=messages) return feed_response(addition_feed)
def update_licensepool_for_identifier(self, isbn, availability, medium): """Update availability information for a single book. If the book has never been seen before, a new LicensePool will be created for the book. The book's LicensePool will be updated with current approximate circulation information (we can tell if it's available, but not how many copies). Bibliographic coverage will be ensured for the OneClick Identifier. Work will be created for the LicensePool and set as presentation-ready. :param isbn the identifier OneClick uses :param availability boolean denoting if book can be lent to patrons :param medium: The name OneClick uses for the book's medium. """ # find a license pool to match the isbn, and see if it'll need a metadata update later license_pool, is_new_pool = LicensePool.for_foreign_id( self._db, DataSource.RB_DIGITAL, Identifier.RB_DIGITAL_ID, isbn, collection=self.collection ) if is_new_pool: # This is the first time we've seen this book. Make sure its # identifier has bibliographic coverage. self.bibliographic_coverage_provider.ensure_coverage( license_pool.identifier ) # now tell the licensepool if it's lendable policy = ReplacementPolicy( identifiers=False, subjects=True, contributions=True, formats=True, analytics=Analytics(self._db), ) # We don't know exactly how many licenses are available, but # we know that it's either zero (book is not lendable) or greater # than zero (book is lendable) licenses_available = 1 if not availability: licenses_available = 0 # Because the book showed up in availability, we know we own # at least one license to it. licenses_owned = 1 # If possible, create a FormatData object representing # how the book is available. formats = [] # Note that these strings are different from the similar strings # found in "fileFormat" when looking at a patron's loans. # "ebook" (a medium) versus "EPUB" (a format). Unfortunately we # don't get the file format when checking the book's # availability before a patron has checked it out. delivery_type = None drm_scheme = None medium = medium.lower() if medium == 'ebook': delivery_type = Representation.EPUB_MEDIA_TYPE # OneClick doesn't tell us the DRM scheme at this # point, but some of their EPUBs do have Adobe DRM. # Also, their DRM usage may change in the future. drm_scheme = DeliveryMechanism.ADOBE_DRM elif medium == 'eaudio': # A questionable assumption. delivery_type = Representation.MP3_MEDIA_TYPE if delivery_type: formats.append(FormatData(delivery_type, drm_scheme)) circulation_data = CirculationData( data_source=DataSource.RB_DIGITAL, primary_identifier=license_pool.identifier, licenses_owned=licenses_owned, licenses_available=licenses_available, formats=formats, ) license_pool, circulation_changed = circulation_data.apply( self._db, self.collection, replace=policy, ) return license_pool, is_new_pool, circulation_changed
def test_rights_status_open_access_link_no_rights_uses_data_source_default( self): identifier = IdentifierData( Identifier.GUTENBERG_ID, "abcd", ) # Here's a CirculationData that will create an open-access # LicensePoolDeliveryMechanism. link = LinkData( rel=Hyperlink.OPEN_ACCESS_DOWNLOAD, media_type=Representation.EPUB_MEDIA_TYPE, href=self._url, ) circulation_data = CirculationData( data_source=DataSource.GUTENBERG, primary_identifier=identifier, links=[link], ) replace_formats = ReplacementPolicy(formats=True, ) pool, ignore = circulation_data.license_pool(self._db, self._default_collection) pool.open_access = False # Applying this CirculationData to a LicensePool makes it # open-access. circulation_data.apply(self._db, pool.collection, replace_formats) assert True == pool.open_access assert 1 == len(pool.delivery_mechanisms) # The delivery mechanism's rights status is the default for # the data source. assert (RightsStatus.PUBLIC_DOMAIN_USA == pool.delivery_mechanisms[0].rights_status.uri) # Even if a commercial source like Overdrive should offer a # link with rel="open access", unless we know it's an # open-access link we will give it a RightsStatus of # IN_COPYRIGHT. identifier = IdentifierData( Identifier.OVERDRIVE_ID, "abcd", ) link = LinkData( rel=Hyperlink.OPEN_ACCESS_DOWNLOAD, media_type=Representation.EPUB_MEDIA_TYPE, href=self._url, ) circulation_data = CirculationData( data_source=DataSource.OVERDRIVE, primary_identifier=identifier, links=[link], ) pool, ignore = circulation_data.license_pool(self._db, self._default_collection) pool.open_access = False circulation_data.apply(self._db, pool.collection, replace_formats) assert (RightsStatus.IN_COPYRIGHT == pool.delivery_mechanisms[0].rights_status.uri) assert False == pool.open_access
def test_open_access_content_mirrored(self): # Make sure that open access material links are translated to our S3 buckets, and that # commercial material links are left as is. # Note: Mirroring tests passing does not guarantee that all code now # correctly calls on CirculationData, as well as Metadata. This is a risk. mirrors = dict(books_mirror=MockS3Uploader(), covers_mirror=None) mirror_type = ExternalIntegrationLink.OPEN_ACCESS_BOOKS # Here's a book. edition, pool = self._edition(with_license_pool=True) # Here's a link to the content of the book, which will be mirrored. link_mirrored = LinkData( rel=Hyperlink.OPEN_ACCESS_DOWNLOAD, href="http://example.com/", media_type=Representation.EPUB_MEDIA_TYPE, content="i am a tiny book", ) # This link will not be mirrored. link_unmirrored = LinkData( rel=Hyperlink.DRM_ENCRYPTED_DOWNLOAD, href="http://example.com/2", media_type=Representation.EPUB_MEDIA_TYPE, content="i am a pricy book", ) # Apply the metadata. policy = ReplacementPolicy(mirrors=mirrors) metadata = Metadata( data_source=edition.data_source, links=[link_mirrored, link_unmirrored], ) metadata.apply(edition, pool.collection, replace=policy) # make sure the refactor is done right, and metadata does not upload assert 0 == len(mirrors[mirror_type].uploaded) circulation_data = CirculationData( data_source=edition.data_source, primary_identifier=edition.primary_identifier, links=[link_mirrored, link_unmirrored], ) circulation_data.apply(self._db, pool.collection, replace=policy) # make sure the refactor is done right, and circulation does upload assert 1 == len(mirrors[mirror_type].uploaded) # Only the open-access link has been 'mirrored'. [book] = mirrors[mirror_type].uploaded # It's remained an open-access link. assert [Hyperlink.OPEN_ACCESS_DOWNLOAD ] == [x.rel for x in book.resource.links] # It's been 'mirrored' to the appropriate S3 bucket. assert book.mirror_url.startswith( "https://test-content-bucket.s3.amazonaws.com/") expect = "/%s/%s.epub" % (edition.primary_identifier.identifier, edition.title) assert book.mirror_url.endswith(expect) # make sure the mirrored link is safely on edition sorted_edition_links = sorted(pool.identifier.links, key=lambda x: x.rel) unmirrored_representation, mirrored_representation = [ edlink.resource.representation for edlink in sorted_edition_links ] assert mirrored_representation.mirror_url.startswith( "https://test-content-bucket.s3.amazonaws.com/") # make sure the unmirrored link is safely on edition assert "http://example.com/2" == unmirrored_representation.url # make sure the unmirrored link has not been translated to an S3 URL assert None == unmirrored_representation.mirror_url