def _navigation_feed(self, library, annotator, url_for=None):
        """Generate an OPDS feed for navigating the COPPA age gate."""
        url_for = url_for or cdn_url_for
        base_url = url_for("index", library_short_name=library.short_name)

        # An entry for grown-ups.
        feed = OPDSFeed(title=library.name, url=base_url)
        opds = feed.feed

        yes_url = url_for(
            "acquisition_groups",
            library_short_name=library.short_name,
            lane_identifier=self.yes_lane_id,
        )
        opds.append(self.navigation_entry(yes_url, self.YES_TITLE, self.YES_CONTENT))

        # An entry for children.
        no_url = url_for(
            "acquisition_groups",
            library_short_name=library.short_name,
            lane_identifier=self.no_lane_id,
        )
        opds.append(self.navigation_entry(no_url, self.NO_TITLE, self.NO_CONTENT))

        # The gate tag is the thing that the SimplyE client actually uses.
        opds.append(self.gate_tag(self.URI, yes_url, no_url))

        # Add any other links associated with this library, notably
        # the link to its authentication document.
        if annotator:
            annotator.annotate_feed(feed, None)

        now = utc_now()
        opds.append(OPDSFeed.E.updated(OPDSFeed._strftime(now)))
        return feed
Beispiel #2
0
    def adobe_id_tags(self, patron_identifier):
        """Construct tags using the DRM Extensions for OPDS standard that
        explain how to get an Adobe ID for this patron, and how to
        manage their list of device IDs.

        :param delivery_mechanism: A DeliveryMechanism

        :return: If Adobe Vendor ID delegation is configured, a list
        containing a <drm:licensor> tag. If not, an empty list.
        """
        # CirculationManagerAnnotators are created per request.
        # Within the context of a single request, we can cache the
        # tags that explain how the patron can get an Adobe ID, and
        # reuse them across <entry> tags. This saves a little time,
        # makes tests more reliable, and stops us from providing a
        # different Short Client Token for every <entry> tag.
        if isinstance(patron_identifier, Patron):
            cache_key = patron_identifier.id
        else:
            cache_key = patron_identifier
        cached = self._adobe_id_tags.get(cache_key)
        if cached is None:
            if isinstance(patron_identifier, Patron):
                # Find the patron's identifier for Adobe ID purposes.
                patron_identifier = self._adobe_patron_identifier(
                    patron_identifier
                )
            cached = []
            authdata = AuthdataUtility.from_config(self.library)
            if authdata:
                # TODO: We would like to call encode() here, and have
                # the client use a JWT as authdata, but we can't,
                # because there's no way to use authdata to deactivate
                # a device. So we've used this alternate technique
                # that's much smaller than a JWT and can be smuggled
                # into username/password.
                vendor_id, jwt = authdata.encode_short_client_token(patron_identifier)

                drm_licensor = OPDSFeed.makeelement("{%s}licensor" % OPDSFeed.DRM_NS)
                vendor_attr = "{%s}vendor" % OPDSFeed.DRM_NS
                drm_licensor.attrib[vendor_attr] = vendor_id
                patron_key = OPDSFeed.makeelement("{%s}clientToken" % OPDSFeed.DRM_NS)
                patron_key.text = jwt
                drm_licensor.append(patron_key)
                
                # Add the link to the DRM Device Management Protocol
                # endpoint. See:
                # https://github.com/NYPL-Simplified/Simplified/wiki/DRM-Device-Management
                device_list_link = OPDSFeed.makeelement("link")
                device_list_link.attrib['rel'] = 'http://librarysimplified.org/terms/drm/rel/devices'
                device_list_link.attrib['href'] = self.url_for(
                    "adobe_drm_devices", library_short_name=self.library.short_name, _external=True
                )
                drm_licensor.append(device_list_link)
                cached = [drm_licensor]

            self._adobe_id_tags[cache_key] = cached
        else:
            cached = copy.deepcopy(cached)
        return cached
 def navigation_entry(cls, href, title, content):
     """Create an <entry> that serves as navigation."""
     E = OPDSFeed.E
     content_tag = E.content(type="text")
     content_tag.text = unicode(content)
     now = datetime.datetime.utcnow()
     entry = E.entry(E.id(href), E.title(unicode(title)), content_tag,
                     E.updated(OPDSFeed._strftime(now)))
     OPDSFeed.add_link_to_entry(entry,
                                href=href,
                                rel="subsection",
                                type=OPDSFeed.ACQUISITION_FEED_TYPE)
     return entry
Beispiel #4
0
 def _add_link(l):
     if isinstance(feed, OPDSFeed):
         feed.add_link_to_feed(feed.feed, **l)
     else:
         # This is an ElementTree object.
         link = OPDSFeed.link(**l)
         feed.append(link)            
Beispiel #5
0
    def add_series_link(self, work, feed, entry):
        series_tag = OPDSFeed.schema_('Series')
        series_entry = entry.find(series_tag)

        if series_entry is None:
            # There is no <series> tag, and thus nothing to annotate.
            # This probably indicates an out-of-date OPDS entry.
            if isinstance(work, Work):
                work_id = work.id
                work_title = work.title
            else:
                work_id = work.works_id
                work_title = work.sort_title
            self.log.error(
                'add_series_link() called on work %s ("%s"), which has no <schema:Series> tag in its OPDS entry.',
                work_id, work_title)
            return

        series_name = work.series
        languages, audiences = self.language_and_audience_key_from_work(work)
        href = self.url_for(
            'series',
            series_name=series_name,
            languages=languages,
            audiences=audiences,
            library_short_name=self.library.short_name,
            _external=True,
        )
        feed.add_link_to_entry(series_entry,
                               rel='series',
                               type=OPDSFeed.ACQUISITION_FEED_TYPE,
                               title=series_name,
                               href=href)
 def navigation_entry(cls, href, title, content):
     """Create an <entry> that serves as navigation."""
     E = OPDSFeed.E
     content_tag = E.content(type="text")
     content_tag.text = unicode(content)
     now = datetime.datetime.utcnow()
     entry = E.entry(
         E.id(href),
         E.title(unicode(title)),
         content_tag,
         E.updated(OPDSFeed._strftime(now))
     )
     OPDSFeed.add_link_to_entry(
         entry, href=href, rel="subsection",
         type=OPDSFeed.ACQUISITION_FEED_TYPE
     )
     return entry
Beispiel #7
0
    def add_patron(self, feed_obj):
        patron_details = {}
        if self.patron.username:
            patron_details["{%s}username" % OPDSFeed.SIMPLIFIED_NS] = self.patron.username
        if self.patron.authorization_identifier:
            patron_details["{%s}authorizationIdentifier" % OPDSFeed.SIMPLIFIED_NS] = self.patron.authorization_identifier

        patron_tag = OPDSFeed.makeelement("{%s}patron" % OPDSFeed.SIMPLIFIED_NS, patron_details)
        feed_obj.feed.append(patron_tag)
Beispiel #8
0
 def user_profile_management_protocol_link(self):
     """Create a <link> tag that points to the circulation
     manager's User Profile Management Protocol endpoint
     for the current patron.
     """
     link = OPDSFeed.makeelement("link")
     link.attrib[
         'rel'] = 'http://librarysimplified.org/terms/rel/user-profile'
     link.attrib['href'] = self.url_for('patron_profile', _external=True)
     return link
Beispiel #9
0
 def open_access_link(self, lpdm):
     url = cdnify(lpdm.resource.url, Configuration.cdns())
     kw = dict(rel=OPDSFeed.OPEN_ACCESS_REL, href=url)
     rep = lpdm.resource.representation
     if rep and rep.media_type:
         kw['type'] = rep.media_type
     link_tag = AcquisitionFeed.link(**kw)
     always_available = OPDSFeed.makeelement("{%s}availability" %
                                             OPDSFeed.OPDS_NS,
                                             status="available")
     link_tag.append(always_available)
     return link_tag
Beispiel #10
0
 def add_configuration_links(cls, feed):
     for rel, value in (
         ("terms-of-service", Configuration.terms_of_service_url()),
         ("privacy-policy", Configuration.privacy_policy_url()),
         ("copyright", Configuration.acknowledgements_url()),
         ("about", Configuration.about_url()),
         ("license", Configuration.license_url()),
     ):
         if value:
             d = dict(href=value, type="text/html", rel=rel)
             if isinstance(feed, OPDSFeed):
                 feed.add_link_to_feed(feed.feed, **d)
             else:
                 # This is an ElementTree object.
                 link = OPDSFeed.link(**d)
                 feed.append(link)
    def _navigation_feed(self, library, annotator, url_for=None):
        """Generate an OPDS feed for navigating the COPPA age gate."""
        url_for = url_for or cdn_url_for
        base_url = url_for('index', library_short_name=library.short_name)

        # An entry for grown-ups.
        feed = OPDSFeed(title=library.name, url=base_url)
        opds = feed.feed

        yes_url = url_for(
            'acquisition_groups',
            library_short_name=library.short_name,
            lane_identifier=self.yes_lane_id
        )
        opds.append(
            self.navigation_entry(yes_url, self.YES_TITLE, self.YES_CONTENT)
        )

        # An entry for children.
        no_url = url_for(
            'acquisition_groups',
            library_short_name=library.short_name,
            lane_identifier=self.no_lane_id
        )
        opds.append(
            self.navigation_entry(no_url, self.NO_TITLE, self.NO_CONTENT)
        )

        # The gate tag is the thing that the SimplyE client actually uses.
        opds.append(self.gate_tag(self.URI, yes_url, no_url))

        # Add any other links associated with this library, notably
        # the link to its authentication document.
        if annotator:
            annotator.annotate_feed(feed, None)

        now = datetime.datetime.utcnow()
        opds.append(OPDSFeed.E.updated(OPDSFeed._strftime(now)))
        return feed
Beispiel #12
0
    def acquisition_links(self, active_license_pool, active_loan, active_hold, active_fulfillment,
                          feed, identifier):
        """Generate a number of <link> tags that enumerate all acquisition methods."""

        can_borrow = False
        can_fulfill = False
        can_revoke = False
        can_hold = self.library.allow_holds

        if active_loan:
            can_fulfill = True
            can_revoke = True
        elif active_hold:
            # We display the borrow link even if the patron can't
            # borrow the book right this minute.
            can_borrow = True

            can_revoke = (
                not self.circulation or 
                self.circulation.can_revoke_hold(
                    active_license_pool, active_hold)
            )
        elif active_fulfillment:
            can_fulfill = True
            can_revoke = True
        else:
            # The patron has no existing relationship with this
            # work. Give them the opportunity to check out the work
            # or put it on hold.
            can_borrow = True

        # If there is something to be revoked for this book,
        # add a link to revoke it.
        revoke_links = []
        if can_revoke:
            url = self.url_for(
                'revoke_loan_or_hold',
                license_pool_id=active_license_pool.id,
                library_short_name=self.library.short_name,
                _external=True)

            kw = dict(href=url, rel=OPDSFeed.REVOKE_LOAN_REL)
            revoke_link_tag = OPDSFeed.makeelement("link", **kw)
            revoke_links.append(revoke_link_tag)

        # Add next-step information for every useful delivery
        # mechanism.
        borrow_links = []
        api = None
        if self.circulation:
            api = self.circulation.api_for_license_pool(active_license_pool)
        if api:
            set_mechanism_at_borrow = (
                api.SET_DELIVERY_MECHANISM_AT == BaseCirculationAPI.BORROW_STEP)
        else:
            # This is most likely an open-access book. Just put one
            # borrow link and figure out the rest later.
            set_mechanism_at_borrow = False
        if can_borrow:
            # Borrowing a book gives you an OPDS entry that gives you
            # fulfillment links.
            if set_mechanism_at_borrow:
                # The ebook distributor requires that the delivery
                # mechanism be set at the point of checkout. This means
                # a separate borrow link for each mechanism.
                for mechanism in active_license_pool.delivery_mechanisms:
                    borrow_links.append(
                        self.borrow_link(
                            identifier,
                            mechanism, [mechanism]
                        )
                    )
            else:
                # The ebook distributor does not require that the
                # delivery mechanism be set at the point of
                # checkout. This means a single borrow link with
                # indirectAcquisition tags for every delivery
                # mechanism. If a delivery mechanism must be set, it
                # will be set at the point of fulfillment.
                borrow_links.append(
                    self.borrow_link(
                        identifier,
                        None, active_license_pool.delivery_mechanisms
                    )
                )

            # Generate the licensing tags that tell you whether the book
            # is available.
            license_tags = feed.license_tags(
                active_license_pool, active_loan, active_hold
            )
            for link in borrow_links:
                for t in license_tags:
                    link.append(t)

        # Add links for fulfilling an active loan.
        fulfill_links = []
        if can_fulfill:
            if active_fulfillment:
                # We're making an entry for a specific fulfill link.
                type = active_fulfillment.content_type
                url = active_fulfillment.content_link
                rel = OPDSFeed.ACQUISITION_REL
                link_tag = AcquisitionFeed.acquisition_link(
                    rel=rel, href=url, types=[type])
                fulfill_links.append(link_tag)

            elif active_loan.fulfillment:
                # The delivery mechanism for this loan has been
                # set. There is one link for the delivery mechanism
                # that was locked in, and links for any streaming
                # delivery mechanisms.
                for lpdm in active_license_pool.delivery_mechanisms:
                    if lpdm is active_loan.fulfillment or lpdm.delivery_mechanism.is_streaming:
                        fulfill_links.append(
                            self.fulfill_link(
                                active_license_pool,
                                active_loan,
                                lpdm.delivery_mechanism
                            )
                        )
            else:
                # The delivery mechanism for this loan has not been
                # set. There is one fulfill link for every delivery
                # mechanism.
                for lpdm in active_license_pool.delivery_mechanisms:
                    fulfill_links.append(
                        self.fulfill_link(
                            active_license_pool,
                            active_loan,
                            lpdm.delivery_mechanism
                        )
                    )
                                               
        # If this is an open-access book, add an open-access link for
        # every delivery mechanism with an associated resource.
        open_access_links = []
        if active_license_pool.open_access:
            for lpdm in active_license_pool.delivery_mechanisms:
                if lpdm.resource:
                    open_access_links.append(self.open_access_link(lpdm))

        return [x for x in borrow_links + fulfill_links + open_access_links + revoke_links
                if x is not None]