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