def test_search_url(self):
        fantasy_lane = Lane(self._db, "Fantasy", genres=[Fantasy]);
        annotator = CirculationManagerAnnotator(None, fantasy_lane, test_mode=True)

        search_url = annotator.search_url(fantasy_lane, "query", dict())
        assert "search" in search_url
        assert "query" in search_url
        assert "Fantasy" in search_url
示例#2
0
    def test_search_url(self):
        annotator = CirculationManagerAnnotator(None,
                                                self.lane,
                                                test_mode=True)

        search_url = annotator.search_url(self.lane, "query", dict())
        assert "search" in search_url
        assert "query" in search_url
        assert "Fantasy" in search_url
示例#3
0
class TestOPDS(VendorIDTest):
    def setup(self):
        super(TestOPDS, self).setup()
        parent = self._lane(display_name="Fiction",
                            languages=["eng"],
                            fiction=True)
        self.lane = self._lane(display_name="Fantasy", languages=["eng"])
        self.lane.add_genre(Fantasy.name)
        self.lane.parent = parent
        self.annotator = CirculationManagerAnnotator(None,
                                                     self.lane,
                                                     self._default_library,
                                                     test_mode=True)

        # Initialize library with Adobe Vendor ID details
        self._default_library.library_registry_short_name = "FAKE"
        self._default_library.library_registry_shared_secret = "s3cr3t5"

        # A ContributorLane to test code that handles it differently.
        self.contributor_lane = ContributorLane(self._default_library,
                                                "Someone",
                                                languages=["eng"],
                                                audiences=None)

    def test_default_lane_url(self):
        default_lane_url = self.annotator.default_lane_url()
        assert "groups" in default_lane_url
        assert str(self.lane.id) not in default_lane_url

    def test_groups_url(self):
        groups_url_no_lane = self.annotator.groups_url(None)
        assert "groups" in groups_url_no_lane
        assert str(self.lane.id) not in groups_url_no_lane

        groups_url_fantasy = self.annotator.groups_url(self.lane)
        assert "groups" in groups_url_fantasy
        assert str(self.lane.id) in groups_url_fantasy

    def test_feed_url(self):
        # A regular Lane.
        feed_url_fantasy = self.annotator.feed_url(self.lane, dict(), dict())
        assert "feed" in feed_url_fantasy
        assert str(self.lane.id) in feed_url_fantasy

        # A QueryGeneratedLane.
        self.annotator.lane = self.contributor_lane
        feed_url_contributor = self.annotator.feed_url(self.contributor_lane,
                                                       dict(), dict())
        assert self.contributor_lane.ROUTE in feed_url_contributor
        assert self.contributor_lane.contributor_name in feed_url_contributor

    def test_search_url(self):
        search_url = self.annotator.search_url(self.lane, "query", dict())
        assert "search" in search_url
        assert "query" in search_url
        assert str(self.lane.id) in search_url

    def test_facet_url(self):
        # A regular Lane.
        facets = dict(collection="main")
        facet_url = self.annotator.facet_url(facets)
        assert "collection=main" in facet_url
        assert str(self.lane.id) in facet_url

        # A QueryGeneratedLane.
        self.annotator.lane = self.contributor_lane

        facet_url_contributor = self.annotator.facet_url(facets)
        assert "collection=main" in facet_url_contributor
        assert self.contributor_lane.ROUTE in facet_url_contributor
        assert self.contributor_lane.contributor_name in facet_url_contributor

    def test_alternate_link_is_permalink(self):
        work = self._work(with_open_access_download=True)
        works = self._db.query(Work)
        annotator = CirculationManagerAnnotator(None,
                                                self.lane,
                                                self._default_library,
                                                test_mode=True)
        pool = annotator.active_licensepool_for(work)

        feed = self.get_parsed_feed([work])
        [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 = self.annotator.permalink_for(work, 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

    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))

    def assert_link_on_entry(self,
                             entry,
                             link_type=None,
                             rels=None,
                             partials_by_rel=None):
        """Asserts that a link with a certain 'rel' value exists on a
        given feed or entry, as well as its link 'type' value and parts
        of its 'href' value.
        """
        def get_link_by_rel(rel):
            try:
                [link] = [x for x in entry['links'] if x['rel'] == rel]
            except ValueError as e:
                raise AssertionError
            if link_type:
                eq_(link_type, link.type)
            return link

        if rels:
            [get_link_by_rel(rel) for rel in rels]

        partials_by_rel = partials_by_rel or dict()
        for rel, uri_partials in partials_by_rel.items():
            link = get_link_by_rel(rel)
            if not isinstance(uri_partials, list):
                uri_partials = [uri_partials]
            for part in uri_partials:
                assert part in link.href

    def test_work_entry_includes_problem_reporting_link(self):
        work = self._work(with_open_access_download=True)
        feed = self.get_parsed_feed([work])
        [entry] = feed.entries
        expected_rel_and_partial = {'issues': '/report'}
        self.assert_link_on_entry(entry,
                                  partials_by_rel=expected_rel_and_partial)

    def test_work_entry_includes_open_access_or_borrow_link(self):
        open_access_work = self._work(with_open_access_download=True)
        licensed_work = self._work(with_license_pool=True)
        licensed_work.license_pools[0].open_access = False

        feed = self.get_parsed_feed([open_access_work, licensed_work])
        [open_access_entry, licensed_entry] = feed.entries

        self.assert_link_on_entry(open_access_entry,
                                  rels=[OPDSFeed.BORROW_REL])
        self.assert_link_on_entry(licensed_entry, rels=[OPDSFeed.BORROW_REL])

    def test_language_and_audience_key_from_work(self):
        work = self._work(language='eng',
                          audience=Classifier.AUDIENCE_CHILDREN)
        result = self.annotator.language_and_audience_key_from_work(work)
        eq_(('eng', 'Children'), result)

        work = self._work(language='fre',
                          audience=Classifier.AUDIENCE_YOUNG_ADULT)
        result = self.annotator.language_and_audience_key_from_work(work)
        eq_(('fre', 'Children,Young+Adult'), result)

        work = self._work(language='spa', audience=Classifier.AUDIENCE_ADULT)
        result = self.annotator.language_and_audience_key_from_work(work)
        eq_(('spa', 'Adult,Adults+Only,Children,Young+Adult'), result)

        work = self._work(audience=Classifier.AUDIENCE_ADULTS_ONLY)
        result = self.annotator.language_and_audience_key_from_work(work)
        eq_(('eng', 'Adult,Adults+Only,Children,Young+Adult'), result)

    def test_work_entry_includes_contributor_links(self):
        """ContributorLane links are added to works with contributors"""
        work = self._work(with_open_access_download=True)
        contributor1 = work.presentation_edition.author_contributors[0]
        feed = self.get_parsed_feed([work])
        [entry] = feed.entries

        expected_rel_and_partial = dict(contributor='/contributor')
        self.assert_link_on_entry(
            entry,
            link_type=OPDSFeed.ACQUISITION_FEED_TYPE,
            partials_by_rel=expected_rel_and_partial,
        )

        # When there are two authors, they each get a contributor link.
        work.presentation_edition.add_contributor(u'Oprah',
                                                  Contributor.AUTHOR_ROLE)
        work.calculate_presentation(
            PresentationCalculationPolicy(regenerate_opds_entries=True),
            DummyExternalSearchIndex())
        [entry] = self.get_parsed_feed([work]).entries
        contributor_links = [l for l in entry.links if l.rel == 'contributor']
        eq_(2, len(contributor_links))
        contributor_links.sort(key=lambda l: l.href)
        for l in contributor_links:
            assert l.type == OPDSFeed.ACQUISITION_FEED_TYPE
            assert '/contributor' in l.href
        assert contributor1.sort_name in contributor_links[0].href
        assert 'Oprah' in contributor_links[1].href

        # When there's no author, there's no contributor link.
        self._db.delete(work.presentation_edition.contributions[0])
        self._db.delete(work.presentation_edition.contributions[1])
        self._db.commit()
        work.calculate_presentation(
            PresentationCalculationPolicy(regenerate_opds_entries=True),
            DummyExternalSearchIndex())
        feed = self.get_parsed_feed([work])
        [entry] = feed.entries
        eq_([], filter(lambda l: l.rel == 'contributor', entry.links))

    def test_work_entry_includes_series_link(self):
        """A series lane link is added to the work entry when its in a series
        """
        work = self._work(with_open_access_download=True,
                          series='Serious Cereals Series')
        feed = self.get_parsed_feed([work])
        [entry] = feed.entries
        expected_rel_and_partial = dict(series='/series')
        self.assert_link_on_entry(entry,
                                  link_type=OPDSFeed.ACQUISITION_FEED_TYPE,
                                  partials_by_rel=expected_rel_and_partial)

        # When there's no series, there's no series link.
        work = self._work(with_open_access_download=True)
        feed = self.get_parsed_feed([work])
        [entry] = feed.entries
        eq_([], filter(lambda l: l.rel == 'series', entry.links))

    def test_work_entry_includes_recommendations_link(self):
        work = self._work(with_open_access_download=True)

        # If NoveList Select isn't configured, there's no recommendations link.
        feed = self.get_parsed_feed([work])
        [entry] = feed.entries
        eq_([], filter(lambda l: l.rel == 'recommendations', entry.links))

        # There's a recommendation link when configuration is found, though!
        NoveListAPI.IS_CONFIGURED = None
        self._external_integration(ExternalIntegration.NOVELIST,
                                   goal=ExternalIntegration.METADATA_GOAL,
                                   username=u'library',
                                   password=u'sure',
                                   libraries=[self._default_library])

        feed = self.get_parsed_feed([work])
        [entry] = feed.entries
        expected_rel_and_partial = dict(recommendations='/recommendations')
        self.assert_link_on_entry(entry,
                                  link_type=OPDSFeed.ACQUISITION_FEED_TYPE,
                                  partials_by_rel=expected_rel_and_partial)

    def test_work_entry_includes_annotations_link(self):
        work = self._work(with_open_access_download=True)
        identifier_str = work.license_pools[0].identifier.identifier
        uri_parts = ['/annotations', identifier_str]
        rel_with_partials = {
            'http://www.w3.org/ns/oa#annotationservice': uri_parts
        }

        feed = self.get_parsed_feed([work])
        [entry] = feed.entries
        self.assert_link_on_entry(entry, partials_by_rel=rel_with_partials)

    def test_active_loan_feed(self):
        self.initialize_adobe(self._default_library)
        patron = self._patron()
        cls = CirculationManagerLoanAndHoldAnnotator
        raw = cls.active_loans_for(None, patron, test_mode=True)
        # No entries in the feed...
        raw = unicode(raw)
        feed = feedparser.parse(raw)
        eq_(0, len(feed['entries']))

        # ... but we have a link to the User Profile Management
        # Protocol endpoint...
        links = feed['feed']['links']
        [upmp_link] = [
            x for x in links if x['rel'] ==
            'http://librarysimplified.org/terms/rel/user-profile'
        ]
        annotator = cls(None, None, patron, test_mode=True)
        expect_url = annotator.url_for(
            'patron_profile',
            library_short_name=patron.library.short_name,
            _external=True)
        eq_(expect_url, upmp_link['href'])

        # ... and we have DRM licensing information.
        tree = etree.fromstring(raw)
        parser = OPDSXMLParser()
        licensor = parser._xpath1(tree, "//atom:feed/drm:licensor")

        adobe_patron_identifier = cls._adobe_patron_identifier(patron)

        # The DRM licensing information includes the Adobe vendor ID
        # and the patron's patron identifier for Adobe purposes.
        eq_(self.adobe_vendor_id.username,
            licensor.attrib['{http://librarysimplified.org/terms/drm}vendor'])
        [client_token, device_management_link] = licensor.getchildren()
        expected = ConfigurationSetting.for_library_and_externalintegration(
            self._db, ExternalIntegration.USERNAME, self._default_library,
            self.registry).value.upper()
        assert client_token.text.startswith(expected)
        assert adobe_patron_identifier in client_token.text
        eq_("{http://www.w3.org/2005/Atom}link", device_management_link.tag)
        eq_("http://librarysimplified.org/terms/drm/rel/devices",
            device_management_link.attrib['rel'])

        # Unlike other places this tag shows up, we use the
        # 'scheme' attribute to explicitly state that this
        # <drm:licensor> tag is talking about an ACS licensing
        # scheme. Since we're in a <feed> and not a <link> to a
        # specific book, that context would otherwise be lost.
        eq_('http://librarysimplified.org/terms/drm/scheme/ACS',
            licensor.attrib['{http://librarysimplified.org/terms/drm}scheme'])

        now = datetime.datetime.utcnow()
        tomorrow = now + datetime.timedelta(days=1)

        # A loan of an open-access book is open-ended.
        work1 = self._work(language="eng", with_open_access_download=True)
        loan1 = work1.license_pools[0].loan_to(patron, start=now)

        # A loan of some other kind of book
        work2 = self._work(language="eng", with_license_pool=True)
        loan2 = work2.license_pools[0].loan_to(patron, start=now, end=tomorrow)
        unused = self._work(language="eng", with_open_access_download=True)

        # Get the feed.
        feed_obj = CirculationManagerLoanAndHoldAnnotator.active_loans_for(
            None, patron, test_mode=True)
        raw = unicode(feed_obj)
        feed = feedparser.parse(raw)

        # The only entries in the feed is the work currently out on loan
        # to this patron.
        eq_(2, len(feed['entries']))
        e1, e2 = sorted(feed['entries'], key=lambda x: x['title'])
        eq_(work1.title, e1['title'])
        eq_(work2.title, e2['title'])

        # Make sure that the start and end dates from the loan are present
        # in an <opds:availability> child of the acquisition link.
        tree = etree.fromstring(raw)
        parser = OPDSXMLParser()
        acquisitions = parser._xpath(
            tree,
            "//atom:entry/atom:link[@rel='http://opds-spec.org/acquisition']")
        eq_(2, len(acquisitions))

        now_s = _strftime(now)
        tomorrow_s = _strftime(tomorrow)
        availabilities = [
            parser._xpath1(x, "opds:availability") for x in acquisitions
        ]

        # One of these availability tags has 'since' but not 'until'.
        # The other one has both.
        [no_until] = [x for x in availabilities if 'until' not in x.attrib]
        eq_(now_s, no_until.attrib['since'])

        [has_until] = [x for x in availabilities if 'until' in x.attrib]
        eq_(now_s, has_until.attrib['since'])
        eq_(tomorrow_s, has_until.attrib['until'])

    def test_loan_feed_includes_patron(self):
        patron = self._patron()

        patron.username = u'bellhooks'
        patron.authorization_identifier = u'987654321'
        feed_obj = CirculationManagerLoanAndHoldAnnotator.active_loans_for(
            None, patron, test_mode=True)
        raw = unicode(feed_obj)
        feed_details = feedparser.parse(raw)['feed']

        assert "simplified:authorizationIdentifier" in raw
        assert "simplified:username" in raw
        eq_(patron.username,
            feed_details['simplified_patron']['simplified:username'])
        eq_(
            u'987654321', feed_details['simplified_patron']
            ['simplified:authorizationidentifier'])

    def test_loans_feed_includes_annotations_link(self):
        patron = self._patron()
        feed_obj = CirculationManagerLoanAndHoldAnnotator.active_loans_for(
            None, patron, test_mode=True)
        raw = unicode(feed_obj)
        feed = feedparser.parse(raw)['feed']
        links = feed['links']

        [annotations_link] = [
            x for x in links if x['rel'].lower() ==
            "http://www.w3.org/ns/oa#annotationService".lower()
        ]
        assert '/annotations' in annotations_link['href']

    def test_active_loan_feed_ignores_inconsistent_local_data(self):
        patron = self._patron()

        work1 = self._work(language="eng", with_license_pool=True)
        loan, ignore = work1.license_pools[0].loan_to(patron)
        work2 = self._work(language="eng", with_license_pool=True)
        hold, ignore = work2.license_pools[0].on_hold_to(patron)

        # Uh-oh, our local loan data is bad.
        loan.license_pool.identifier = None

        # Our local hold data is also bad.
        hold.license_pool = None

        # We can still get a feed...
        feed_obj = CirculationManagerLoanAndHoldAnnotator.active_loans_for(
            None, patron, test_mode=True)

        # ...but it's empty.
        assert '<entry>' not in unicode(feed_obj)

    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

        feed = AcquisitionFeed(self._db, "title", "url", [work],
                               self.annotator)
        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

    def test_loans_feed_includes_fulfill_links_for_streaming(self):
        patron = self._patron()

        work = self._work(with_license_pool=True,
                          with_open_access_download=False)
        pool = work.license_pools[0]
        pool.open_access = False
        mech1 = pool.delivery_mechanisms[0]
        mech2 = pool.set_delivery_mechanism(Representation.PDF_MEDIA_TYPE,
                                            DeliveryMechanism.ADOBE_DRM,
                                            RightsStatus.IN_COPYRIGHT, None)
        streaming_mech = pool.set_delivery_mechanism(
            DeliveryMechanism.STREAMING_TEXT_CONTENT_TYPE,
            DeliveryMechanism.OVERDRIVE_DRM, RightsStatus.IN_COPYRIGHT, None)

        now = datetime.datetime.utcnow()
        loan, ignore = pool.loan_to(patron, start=now)

        feed_obj = CirculationManagerLoanAndHoldAnnotator.active_loans_for(
            None, patron, test_mode=True)
        raw = unicode(feed_obj)

        entries = feedparser.parse(raw)['entries']
        eq_(1, len(entries))

        links = entries[0]['links']

        # Before we fulfill the loan, there are fulfill links for all three mechanisms.
        fulfill_links = [
            link for link in links
            if link['rel'] == "http://opds-spec.org/acquisition"
        ]
        eq_(3, len(fulfill_links))

        eq_(
            set([
                mech1.delivery_mechanism.drm_scheme_media_type,
                mech2.delivery_mechanism.drm_scheme_media_type,
                OPDSFeed.ENTRY_TYPE
            ]), set([link['type'] for link in fulfill_links]))

        # When the loan is fulfilled, there are only fulfill links for that mechanism
        # and the streaming mechanism.
        loan.fulfillment = mech1

        feed_obj = CirculationManagerLoanAndHoldAnnotator.active_loans_for(
            None, patron, test_mode=True)
        raw = unicode(feed_obj)

        entries = feedparser.parse(raw)['entries']
        eq_(1, len(entries))

        links = entries[0]['links']

        fulfill_links = [
            link for link in links
            if link['rel'] == "http://opds-spec.org/acquisition"
        ]
        eq_(2, len(fulfill_links))

        eq_(
            set([
                mech1.delivery_mechanism.drm_scheme_media_type,
                OPDSFeed.ENTRY_TYPE
            ]), set([link['type'] for link in fulfill_links]))

    def test_fulfill_feed(self):
        patron = self._patron()

        work = self._work(with_license_pool=True,
                          with_open_access_download=False)
        pool = work.license_pools[0]
        pool.open_access = False
        streaming_mech = pool.set_delivery_mechanism(
            DeliveryMechanism.STREAMING_TEXT_CONTENT_TYPE,
            DeliveryMechanism.OVERDRIVE_DRM, RightsStatus.IN_COPYRIGHT, None)

        now = datetime.datetime.utcnow()
        loan, ignore = pool.loan_to(patron, start=now)
        fulfillment = FulfillmentInfo(
            pool.collection, pool.data_source.name, pool.identifier.type,
            pool.identifier.identifier, "http://streaming_link",
            Representation.TEXT_HTML_MEDIA_TYPE +
            DeliveryMechanism.STREAMING_PROFILE, None, None)

        feed_obj = CirculationManagerLoanAndHoldAnnotator.single_fulfillment_feed(
            None, loan, fulfillment, test_mode=True)
        raw = etree.tostring(feed_obj)

        entries = feedparser.parse(raw)['entries']
        eq_(1, len(entries))

        links = entries[0]['links']

        # The feed for a single fulfillment only includes one fulfill link.
        fulfill_links = [
            link for link in links
            if link['rel'] == "http://opds-spec.org/acquisition"
        ]
        eq_(1, len(fulfill_links))

        eq_(
            Representation.TEXT_HTML_MEDIA_TYPE +
            DeliveryMechanism.STREAMING_PROFILE, fulfill_links[0]['type'])
        eq_("http://streaming_link", fulfill_links[0]['href'])

    def test_drm_device_registration_feed_tags(self):
        """Check that drm_device_registration_feed_tags returns 
        a generic drm:licensor tag, except with the drm:scheme attribute 
        set.
        """
        self.initialize_adobe(self._default_library)
        annotator = CirculationManagerLoanAndHoldAnnotator(
            None, None, self._default_library, test_mode=True)
        patron = self._patron()
        [feed_tag] = annotator.drm_device_registration_feed_tags(patron)
        [generic_tag] = annotator.adobe_id_tags(patron)

        # The feed-level tag has the drm:scheme attribute set.
        key = '{http://librarysimplified.org/terms/drm}scheme'
        eq_("http://librarysimplified.org/terms/drm/scheme/ACS",
            feed_tag.attrib[key])

        # If we remove that attribute, the feed-level tag is the same as the
        # generic tag.
        del feed_tag.attrib[key]
        eq_(etree.tostring(feed_tag), etree.tostring(generic_tag))

    def test_borrow_link_raises_unfulfillable_work(self):
        edition, pool = self._edition(with_license_pool=True)
        kindle_mechanism = pool.set_delivery_mechanism(
            DeliveryMechanism.KINDLE_CONTENT_TYPE,
            DeliveryMechanism.KINDLE_DRM, RightsStatus.IN_COPYRIGHT, None)
        epub_mechanism = pool.set_delivery_mechanism(
            Representation.EPUB_MEDIA_TYPE, DeliveryMechanism.ADOBE_DRM,
            RightsStatus.IN_COPYRIGHT, None)
        data_source_name = pool.data_source.name
        identifier = pool.identifier

        annotator = CirculationManagerLoanAndHoldAnnotator(
            None, None, self._default_library, test_mode=True)

        # If there's no way to fulfill the book, borrow_link raises
        # UnfulfillableWork.
        assert_raises(UnfulfillableWork, annotator.borrow_link, identifier,
                      None, [])

        assert_raises(UnfulfillableWork, annotator.borrow_link, identifier,
                      None, [kindle_mechanism])

        # If there's a fulfillable mechanism, everything's fine.
        link = annotator.borrow_link(identifier, None, [epub_mechanism])
        assert link != None

        link = annotator.borrow_link(identifier, None,
                                     [epub_mechanism, kindle_mechanism])
        assert link != None