Source code for api.custom_index

"""A custom index view customizes a library's 'front page' to serve
something other than the default.

This code is DEPRECATED; you probably want a CustomPatronCatalog instead.
We're keeping it around because existing iOS versions of SimplyE need the
OPDS navigation feed it generates.
"""

from flask import Response
from flask_babel import lazy_gettext as _

from sqlalchemy.orm.session import Session

from .config import CannotLoadConfiguration
from core.app_server import cdn_url_for
from core.model import (
    get_one,
)
from core.lane import Lane
from core.model import (
    ConfigurationSetting,
    ExternalIntegration,
)
from core.util.datetime_helpers import utc_now
from core.util.opds_writer import OPDSFeed

[docs]class CustomIndexView(object): """A custom view that replaces the default OPDS view for a library. Any subclass of this class must define PROTOCOL and must be passed into a CustomIndexView.register() call after the class definition is complete. Subclasses of this class are loaded into the CirculationManager, so they should not store any objects obtained from the database without disconnecting them from their session. """ BY_PROTOCOL = {} GOAL = "custom_index"
[docs] @classmethod def register(self, view_class): protocol = view_class.PROTOCOL if protocol in self.BY_PROTOCOL: raise ValueError("Duplicate index view for protocol: %s" % protocol) self.BY_PROTOCOL[protocol] = view_class
[docs] @classmethod def unregister(self, view_class): """Remove a CustomIndexView from consideration. Only used in tests. """ del self.BY_PROTOCOL[view_class.PROTOCOL]
[docs] @classmethod def for_library(cls, library): """Find the appropriate CustomIndexView for the given library.""" _db = Session.object_session(library) integration = ExternalIntegration.one_for_library_and_goal( _db, library, cls.GOAL ) if not integration: return None protocol = integration.protocol if not protocol in cls.BY_PROTOCOL: raise CannotLoadConfiguration( "Unregistered custom index protocol: %s" % protocol ) view_class = cls.BY_PROTOCOL[protocol] return view_class(library, integration)
def __init__(self, library, integration): raise NotImplementedError() def __call__(self, library, annotator): """Render the custom index view. :return: A Response or ProblemDetail :param library: A Library :param annoatator: An Annotator for annotating OPDS feeds. """ raise NotImplementedError()
[docs]class COPPAGate(CustomIndexView): PROTOCOL = "COPPA Age Gate" URI = "http://librarysimplified.org/terms/restrictions/coppa" NO_TITLE = _("I'm Under 13") NO_CONTENT = _("Read children's books") YES_TITLE = _("I'm 13 or Older") YES_CONTENT = _("See the full collection") REQUIREMENT_MET_LANE = "requirement_met_lane" REQUIREMENT_NOT_MET_LANE = "requirement_not_met_lane" SETTINGS = [ { "key": REQUIREMENT_MET_LANE, "label": _("ID of lane for patrons who are 13 or older"), }, { "key": REQUIREMENT_NOT_MET_LANE, "label": _("ID of lane for patrons who are under 13"), }, ] def __init__(self, library, integration): _db = Session.object_session(library) m = ConfigurationSetting.for_library_and_externalintegration yes_lane_id = m(_db, self.REQUIREMENT_MET_LANE, library, integration) no_lane_id = m(_db, self.REQUIREMENT_NOT_MET_LANE, library, integration) # We don't want to store the Lane objects long-term, but we do need # to make sure the lane IDs correspond to real lanes for the # right library. self.yes_lane_id = yes_lane_id.int_value self.no_lane_id = no_lane_id.int_value yes_lane = self._load_lane(library, self.yes_lane_id) no_lane = self._load_lane(library, self.no_lane_id) def _load_lane(self, library, lane_id): """Make sure the Lane with the given ID actually exists and is associated with the given Library. """ _db = Session.object_session(library) lane = get_one(_db, Lane, id=lane_id) if not lane: raise CannotLoadConfiguration("No lane with ID: %s" % lane_id) if lane.library != library: raise CannotLoadConfiguration( "Lane %d is for the wrong library (%s, I need %s)" % (lane.id, lane.library.name, library.name) ) return lane def __call__(self, library, annotator, url_for=None): """Render an OPDS navigation feed that lets the patron choose a root lane on their own, without providing any credentials. """ if not hasattr(self, 'navigation_feed'): self.navigation_feed = self._navigation_feed( library, annotator, url_for ) headers = { "Content-Type": OPDSFeed.NAVIGATION_FEED_TYPE } return Response(str(self.navigation_feed), 200, headers) 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
[docs] @classmethod 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 = str(content) now = utc_now() entry = E.entry( E.id(href), E.title(str(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
[docs] @classmethod def gate_tag(cls, restriction, met_url, not_met_url): """Create a simplified:gate tag explaining the boolean option the client is faced with. """ tag = OPDSFeed.SIMPLIFIED.gate() tag.attrib['restriction-met'] = met_url tag.attrib['restriction-not-met'] = not_met_url tag.attrib['restriction'] = restriction return tag
CustomIndexView.register(COPPAGate)