Exemple #1
0
 def single_fulfillment_feed(cls,
                             circulation,
                             loan,
                             fulfillment,
                             test_mode=False):
     db = Session.object_session(loan)
     work = loan.license_pool.work or loan.license_pool.presentation_edition.work
     annotator = cls(circulation,
                     None,
                     loan.patron.library,
                     active_loans_by_work={},
                     active_holds_by_work={},
                     active_fulfillments_by_work={work: fulfillment},
                     test_mode=test_mode)
     identifier = loan.license_pool.identifier
     url = annotator.url_for(
         'loan_or_hold_detail',
         identifier_type=identifier.type,
         identifier=identifier.identifier,
         library_short_name=loan.patron.library.short_name,
         _external=True)
     if not work:
         return AcquisitionFeed(db, "Active loan for unknown work", url, [],
                                annotator)
     return AcquisitionFeed.single_entry(db, work, annotator)
Exemple #2
0
    def active_loans_for(cls, circulation, patron, test_mode=False):
        db = Session.object_session(patron)
        active_loans_by_work = {}
        for loan in patron.loans:
            work = loan.work
            if work:
                active_loans_by_work[work] = loan
        active_holds_by_work = {}
        for hold in patron.holds:
            work = hold.work
            if work:
                active_holds_by_work[work] = hold

        annotator = cls(circulation,
                        None,
                        patron,
                        active_loans_by_work,
                        active_holds_by_work,
                        test_mode=test_mode)
        url = annotator.url_for('active_loans', _external=True)
        works = patron.works_on_loan_or_on_hold()

        feed_obj = AcquisitionFeed(db, "Active loans and holds", url, works,
                                   annotator)
        annotator.annotate_feed(feed_obj, None)
        return feed_obj
Exemple #3
0
    def test_acquisition_feed_includes_license_information(self):
        work = self._work(with_open_access_download=True)
        pool = work.license_pools[0]

        # These numbers are impossible, but it doesn't matter for
        # purposes of this test.
        pool.open_access = False
        pool.licenses_owned = 100
        pool.licenses_available = 50
        pool.patrons_in_hold_queue = 25
        self._db.commit()

        works = self._db.query(Work)
        feed = AcquisitionFeed(
            self._db, "test", "url", works,
            CirculationManagerAnnotator(None, Fantasy, test_mode=True))
        u = unicode(feed)
        holds_re = re.compile('<opds:holds\W+total="25"\W*/>', re.S)
        assert holds_re.search(u) is not None

        copies_re = re.compile('<opds:copies[^>]+available="50"', re.S)
        assert copies_re.search(u) is not None

        copies_re = re.compile('<opds:copies[^>]+total="100"', re.S)
        assert copies_re.search(u) is not None
Exemple #4
0
    def metadata_needed_for(self, collection_details):
        """Returns identifiers in the collection that could benefit from
        distributor metadata on the circulation manager.
        """
        client = authenticated_client_from_request(self._db)
        if isinstance(client, ProblemDetail):
            return client

        collection = collection_from_details(self._db, client,
                                             collection_details)

        resolver = IdentifierResolutionCoverageProvider
        unresolved_identifiers = collection.unresolved_catalog(
            self._db, resolver.DATA_SOURCE_NAME, resolver.OPERATION)

        # Omit identifiers that currently have metadata pending for
        # the IntegrationClientCoverImageCoverageProvider.
        data_source = DataSource.lookup(self._db,
                                        collection.name,
                                        autocreate=True)
        is_awaiting_metadata = self._db.query(
            CoverageRecord.id, CoverageRecord.identifier_id).filter(
                CoverageRecord.data_source_id == data_source.id,
                CoverageRecord.status == CoverageRecord.REGISTERED,
                CoverageRecord.operation ==
                IntegrationClientCoverImageCoverageProvider.OPERATION,
            ).subquery()

        unresolved_identifiers = unresolved_identifiers.outerjoin(
            is_awaiting_metadata,
            Identifier.id == is_awaiting_metadata.c.identifier_id).filter(
                is_awaiting_metadata.c.id == None)

        # Add a message for each unresolved identifier
        pagination = load_pagination_from_request(default_size=25)
        feed_identifiers = pagination.apply(unresolved_identifiers).all()
        messages = list()
        for identifier in feed_identifiers:
            messages.append(
                OPDSMessage(identifier.urn, HTTP_ACCEPTED, "Metadata needed."))

        title = "%s Metadata Requests for %s" % (collection.protocol,
                                                 client.url)
        metadata_request_url = self.collection_feed_url(
            'metadata_needed_for', collection)

        request_feed = AcquisitionFeed(self._db,
                                       title,
                                       metadata_request_url, [],
                                       VerboseAnnotator,
                                       precomposed_entries=messages)

        self.add_pagination_links_to_feed(pagination, unresolved_identifiers,
                                          request_feed, 'metadata_needed_for',
                                          collection)

        return feed_response(request_feed)
Exemple #5
0
 def test_acquisition_feed_includes_problem_reporting_link(self):
     w1 = self._work(with_open_access_download=True)
     self._db.commit()
     feed = AcquisitionFeed(
         self._db, "test", "url", [w1],
         CirculationManagerAnnotator(None, Fantasy, test_mode=True))
     feed = feedparser.parse(unicode(feed))
     [entry] = feed['entries']
     [issues_link] = [x for x in entry['links'] if x['rel'] == 'issues']
     assert '/report' in issues_link['href']
Exemple #6
0
 def get_parsed_feed(self, works, lane=None):
     if not lane:
         lane = self._lane(display_name="Main Lane")
     feed = AcquisitionFeed(
         self._db, "test", "url", works,
         CirculationManagerAnnotator(None,
                                     lane,
                                     self._default_library,
                                     test_mode=True))
     return feedparser.parse(unicode(feed))
Exemple #7
0
 def confirm_related_books_link():
     """Tests the presence of a /related_books link in a feed."""
     feed = AcquisitionFeed(
         self._db, "test", "url", [work],
         CirculationManagerAnnotator(None, Fantasy, test_mode=True))
     feed = feedparser.parse(unicode(feed))
     [entry] = feed['entries']
     [recommendations_link
      ] = [x for x in entry['links'] if x['rel'] == 'related']
     eq_(OPDSFeed.ACQUISITION_FEED_TYPE, recommendations_link['type'])
     assert '/related_books' in recommendations_link['href']
Exemple #8
0
    def remove_items(self, collection_details):
        """Removes identifiers from 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)

        urns = request.args.getlist('urn')
        messages = []
        identifiers_by_urn, failures = Identifier.parse_urns(self._db, urns)

        for urn in failures:
            message = OPDSMessage(urn, INVALID_URN.status_code,
                                  INVALID_URN.detail)
            messages.append(message)

        # Find the IDs of the subset of provided identifiers that are
        # in the catalog, so we know which ones to delete and give a
        # 200 message. Also get a SQLAlchemy clause that selects only
        # those IDs.
        matching_ids, identifier_match_clause = self._in_catalog_subset(
            collection, identifiers_by_urn)

        # Use that clause to delete all of the relevant catalog
        # entries.
        delete_stmt = collections_identifiers.delete().where(
            identifier_match_clause)
        self._db.execute(delete_stmt)

        # IDs that matched get a 200 message; all others get a 404
        # message.
        for urn, identifier in identifiers_by_urn.items():
            if identifier.id in matching_ids:
                status = HTTP_OK
                description = "Successfully removed"
            else:
                status = HTTP_NOT_FOUND
                description = "Not in catalog"
            message = OPDSMessage(urn, status, description)
            messages.append(message)

        title = "%s Catalog Item Removal for %s" % (collection.protocol,
                                                    client.url)
        url = self.collection_feed_url("remove", collection, urn=urns)
        removal_feed = AcquisitionFeed(self._db,
                                       title,
                                       url, [],
                                       VerboseAnnotator,
                                       precomposed_entries=messages)

        return feed_response(removal_feed)
Exemple #9
0
    def add_items(self, collection_details):
        """Adds identifiers 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)
        urns = request.args.getlist('urn')
        messages = []
        identifiers_by_urn, failures = Identifier.parse_urns(self._db, urns)

        for urn in failures:
            message = OPDSMessage(urn, INVALID_URN.status_code,
                                  INVALID_URN.detail)
            messages.append(message)

        # Find the subset of incoming identifiers that are already
        # in the catalog.
        already_in_catalog, ignore = self._in_catalog_subset(
            collection, identifiers_by_urn)

        # Everything else needs to be added to the catalog.
        needs_to_be_added = [
            x for x in identifiers_by_urn.values()
            if x.id not in already_in_catalog
        ]
        collection.catalog_identifiers(needs_to_be_added)

        for urn, identifier in identifiers_by_urn.items():
            if identifier.id in already_in_catalog:
                status = HTTP_OK
                description = "Already in catalog"
            else:
                status = HTTP_CREATED
                description = "Successfully added"

            messages.append(OPDSMessage(urn, status, description))

        title = "%s Catalog Item Additions for %s" % (collection.protocol,
                                                      client.url)
        url = self.collection_feed_url('add', collection, urn=urns)
        addition_feed = AcquisitionFeed(self._db,
                                        title,
                                        url, [],
                                        VerboseAnnotator,
                                        precomposed_entries=messages)

        return feed_response(addition_feed)
Exemple #10
0
 def test_acquisition_feed_includes_annotations_link(self):
     w1 = self._work(with_open_access_download=True)
     self._db.commit()
     feed = AcquisitionFeed(
         self._db, "test", "url", [w1],
         CirculationManagerAnnotator(None, Fantasy, test_mode=True))
     feed = feedparser.parse(unicode(feed))
     [entry] = feed['entries']
     [annotations_link] = [
         x for x in entry['links']
         if x['rel'] == 'http://www.w3.org/ns/oa#annotationservice'
     ]
     assert '/annotations' in annotations_link['href']
     identifier = w1.license_pools[0].identifier
     assert identifier.identifier in annotations_link['href']
Exemple #11
0
    def add_items(self, collection_details):
        """Adds identifiers to a Collection's catalog"""
        client = authenticated_client_from_request(self._db)
        if isinstance(client, ProblemDetail):
            return client

        collection, ignore = Collection.from_metadata_identifier(
            self._db, collection_details)

        urns = request.args.getlist('urn')
        messages = []
        for urn in urns:
            message = None
            identifier = None
            try:
                identifier, ignore = Identifier.parse_urn(self._db, urn)
            except Exception as e:
                identifier = None

            if not identifier:
                message = OPDSMessage(urn, INVALID_URN.status_code,
                                      INVALID_URN.detail)
            else:
                status = HTTP_OK
                description = "Already in catalog"

                if identifier not in collection.catalog:
                    collection.catalog_identifier(self._db, identifier)
                    status = HTTP_CREATED
                    description = "Successfully added"

                message = OPDSMessage(urn, status, description)

            messages.append(message)

        title = "%s Catalog Item Additions for %s" % (collection.protocol,
                                                      client.url)
        url = cdn_url_for("add",
                          collection_metadata_identifier=collection.name,
                          urn=urns)
        addition_feed = AcquisitionFeed(self._db,
                                        title,
                                        url, [],
                                        VerboseAnnotator,
                                        precomposed_entries=messages)

        return feed_response(addition_feed)
Exemple #12
0
    def process_batch(self, batch):
        """Create an OPDS feed from a batch and upload it to the metadata client."""
        works = []
        results = []
        for identifier in batch:
            work = self.work(identifier)
            if not isinstance(work, CoverageFailure):
                works.append(work)
                results.append(identifier)
            else:
                results.append(work)
        feed = AcquisitionFeed(self._db, "Metadata Upload Feed", "", works,
                               None)
        self.upload_client.add_with_metadata(feed)

        # We grant coverage for all identifiers if the upload doesn't raise an exception.
        return results
Exemple #13
0
    def remove_items(self, collection_details):
        """Removes identifiers from a Collection's catalog"""
        client = authenticated_client_from_request(self._db)
        if isinstance(client, ProblemDetail):
            return client

        collection, ignore = Collection.from_metadata_identifier(
            self._db, collection_details)

        urns = request.args.getlist('urn')
        messages = []
        for urn in urns:
            message = None
            identifier = None
            try:
                identifier, ignore = Identifier.parse_urn(self._db, urn)
            except Exception as e:
                identifier = None

            if not identifier:
                message = OPDSMessage(urn, INVALID_URN.status_code,
                                      INVALID_URN.detail)
            else:
                if identifier in collection.catalog:
                    collection.catalog.remove(identifier)
                    message = OPDSMessage(urn, HTTP_OK, "Successfully removed")
                else:
                    message = OPDSMessage(urn, HTTP_NOT_FOUND,
                                          "Not in catalog")

            messages.append(message)

        title = "%s Catalog Item Removal for %s" % (collection.protocol,
                                                    client.url)
        url = cdn_url_for("remove",
                          collection_metadata_identifier=collection.name,
                          urn=urns)
        removal_feed = AcquisitionFeed(self._db,
                                       title,
                                       url, [],
                                       VerboseAnnotator,
                                       precomposed_entries=messages)

        return feed_response(removal_feed)
Exemple #14
0
    def test_acquisition_feed_includes_open_access_or_borrow_link(self):
        w1 = self._work(with_open_access_download=True)
        w2 = self._work(with_open_access_download=True)
        w2.license_pools[0].open_access = False
        w2.license_pools[0].licenses_owned = 1
        self._db.commit()

        works = self._db.query(Work)
        feed = AcquisitionFeed(
            self._db, "test", "url", works,
            CirculationManagerAnnotator(None, Fantasy, test_mode=True))

        feed = feedparser.parse(unicode(feed))
        entries = sorted(feed['entries'], key=lambda x: int(x['title']))

        open_access_links, borrow_links = [x['links'] for x in entries]
        open_access_rels = [x['rel'] for x in open_access_links]
        assert OPDSFeed.BORROW_REL in open_access_rels

        borrow_rels = [x['rel'] for x in borrow_links]
        assert OPDSFeed.BORROW_REL in borrow_rels
Exemple #15
0
    def test_alternate_link_is_permalink(self):
        w1 = self._work(with_open_access_download=True)
        self._db.commit()

        works = self._db.query(Work)
        annotator = CirculationManagerAnnotator(None, Fantasy, test_mode=True)
        pool = annotator.active_licensepool_for(w1)

        feed = AcquisitionFeed(self._db, "test", "url", works, annotator)
        feed = feedparser.parse(unicode(feed))
        [entry] = feed['entries']
        eq_(entry['id'], pool.identifier.urn)

        [(alternate, type)] = [(x['href'], x['type']) for x in entry['links']
                               if x['rel'] == 'alternate']
        permalink = annotator.permalink_for(w1, pool, pool.identifier)
        eq_(alternate, permalink)
        eq_(OPDSFeed.ENTRY_TYPE, type)

        # Make sure we are using the 'permalink' controller -- we were using
        # 'work' and that was wrong.
        assert '/host/permalink' in permalink
Exemple #16
0
    def test_acquisition_feed_includes_related_books_link(self):

        work = self._work(with_license_pool=True,
                          with_open_access_download=True)

        def confirm_related_books_link():
            """Tests the presence of a /related_books link in a feed."""
            feed = AcquisitionFeed(
                self._db, "test", "url", [work],
                CirculationManagerAnnotator(None, Fantasy, test_mode=True))
            feed = feedparser.parse(unicode(feed))
            [entry] = feed['entries']
            [recommendations_link
             ] = [x for x in entry['links'] if x['rel'] == 'related']
            eq_(OPDSFeed.ACQUISITION_FEED_TYPE, recommendations_link['type'])
            assert '/related_books' in recommendations_link['href']

        # If there is a contributor, there's a related books link.
        with temp_config() as config:
            NoveListAPI.IS_CONFIGURED = None
            config['integrations'][Configuration.NOVELIST_INTEGRATION] = {}
            confirm_related_books_link()

        # If there is no possibility of related works,
        # there's no related books link.
        with temp_config() as config:
            # Remove contributors.
            self._db.delete(
                work.license_pools[0].presentation_edition.contributions[0])
            self._db.commit()

            # Turn off NoveList.
            NoveListAPI.IS_CONFIGURED = None
            config['integrations'][Configuration.NOVELIST_INTEGRATION] = {}
            feed = AcquisitionFeed(
                self._db, "test", "url", [work],
                CirculationManagerAnnotator(None, Fantasy, test_mode=True))
        feed = feedparser.parse(unicode(feed))
        [entry] = feed['entries']
        recommendations_links = [
            x for x in entry['links'] if x['rel'] == 'related'
        ]
        eq_([], recommendations_links)

        # If NoveList is configured (and thus recommendations are available),
        # there's is a related books link.
        with temp_config() as config:
            NoveListAPI.IS_CONFIGURED = None
            config['integrations'][Configuration.NOVELIST_INTEGRATION] = {
                Configuration.NOVELIST_PROFILE: "library",
                Configuration.NOVELIST_PASSWORD: "******"
            }
            confirm_related_books_link()

        # If the book is in a series, there's is a related books link.
        with temp_config() as config:
            NoveListAPI.IS_CONFIGURED = None
            config['integrations'][Configuration.NOVELIST_INTEGRATION] = {}
            work.license_pools[
                0].presentation_edition.series = "Serious Cereal Series"
            confirm_related_books_link()
Exemple #17
0
    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)