def test_do_run(self):
        # Normally, do_run is only called by run() if the database has
        # not yet meen initialized. But we can test it by calling it
        # directly.
        timestamp = get_one(
            self._db, Timestamp, service=u"Database Migration",
            service_type=Timestamp.SCRIPT_TYPE
        )
        eq_(None, timestamp)

        # Remove all secret keys, should they exist, before running the
        # script.
        secret_keys = self._db.query(ConfigurationSetting).filter(
            ConfigurationSetting.key==Configuration.SECRET_KEY)
        [self._db.delete(secret_key) for secret_key in secret_keys]

        script = InstanceInitializationScript(_db=self._db)
        script.do_run(ignore_search=True)

        # It initializes the database.
        timestamp = get_one(
            self._db, Timestamp, service=u"Database Migration",
            service_type=Timestamp.SCRIPT_TYPE
        )
        assert timestamp

        # It creates a secret key.
        eq_(1, secret_keys.count())
        eq_(
            secret_keys.one().value,
            ConfigurationSetting.sitewide_secret(self._db, Configuration.SECRET_KEY)
        )
Example #2
0
    def test_process_item_creates_edition_for_series_info(self):
        work = self._work(with_license_pool=True)
        identifier = work.license_pools[0].identifier

        # Without series information, a NoveList-source edition is not
        # created for the original identifier.
        self.metadata.series = self.metadata.series_position = None
        self.novelist.api.setup(self.metadata)
        eq_(identifier, self.novelist.process_item(identifier))
        novelist_edition = get_one(
            self._db, Edition, data_source=self.novelist.source,
            primary_identifier=identifier
        )
        eq_(None, novelist_edition)

        # When series information exists, an edition is created for the
        # licensed identifier.
        self.metadata.series = "A Series of Unfortunate Events"
        self.metadata.series_position = 6
        self.novelist.api.setup(self.metadata)
        self.novelist.process_item(identifier)
        novelist_edition = get_one(
            self._db, Edition, data_source=self.novelist.source,
            primary_identifier=identifier
        )
        assert novelist_edition
        eq_(self.metadata.series, novelist_edition.series)
        eq_(self.metadata.series_position, novelist_edition.series_position)
        # Other basic metadata is also stored.
        eq_(self.metadata.title, novelist_edition.title)
    def test_patron_auth_services_post_create(self):
        mock_controller = self._get_mock()

        library, ignore = create(
            self._db, Library, name="Library", short_name="L",
        )

        with self.request_context_with_admin("/", method="POST"):
            flask.request.form = MultiDict([
                ("protocol", SimpleAuthenticationProvider.__module__),
                ("libraries", json.dumps([{
                    "short_name": library.short_name,
                    AuthenticationProvider.EXTERNAL_TYPE_REGULAR_EXPRESSION: "^(.)",
                    AuthenticationProvider.LIBRARY_IDENTIFIER_RESTRICTION_TYPE: AuthenticationProvider.LIBRARY_IDENTIFIER_RESTRICTION_TYPE_REGEX,
                    AuthenticationProvider.LIBRARY_IDENTIFIER_FIELD: AuthenticationProvider.LIBRARY_IDENTIFIER_RESTRICTION_BARCODE,
                    AuthenticationProvider.LIBRARY_IDENTIFIER_RESTRICTION: "^1234",
                }])),
            ] + self._common_basic_auth_arguments())

            response = mock_controller.process_patron_auth_services()
            eq_(response.status_code, 201)
            eq_(mock_controller.validate_formats_call_count, 1)

        auth_service = get_one(self._db, ExternalIntegration, goal=ExternalIntegration.PATRON_AUTH_GOAL)
        eq_(auth_service.id, int(response.response[0]))
        eq_(SimpleAuthenticationProvider.__module__, auth_service.protocol)
        eq_("user", auth_service.setting(BasicAuthenticationProvider.TEST_IDENTIFIER).value)
        eq_("pass", auth_service.setting(BasicAuthenticationProvider.TEST_PASSWORD).value)
        eq_([library], auth_service.libraries)
        eq_("^(.)", ConfigurationSetting.for_library_and_externalintegration(
                self._db, AuthenticationProvider.EXTERNAL_TYPE_REGULAR_EXPRESSION,
                library, auth_service).value)
        common_args = self._common_basic_auth_arguments()
        with self.request_context_with_admin("/", method="POST"):
            flask.request.form = MultiDict([
                ("protocol", MilleniumPatronAPI.__module__),
                (ExternalIntegration.URL, "url"),
                (MilleniumPatronAPI.VERIFY_CERTIFICATE, "true"),
                (MilleniumPatronAPI.AUTHENTICATION_MODE, MilleniumPatronAPI.PIN_AUTHENTICATION_MODE),
            ] + common_args)
            response = mock_controller.process_patron_auth_services()
            eq_(response.status_code, 201)
            eq_(mock_controller.validate_formats_call_count, 2)

        auth_service2 = get_one(self._db, ExternalIntegration,
                               goal=ExternalIntegration.PATRON_AUTH_GOAL,
                               protocol=MilleniumPatronAPI.__module__)
        assert auth_service2 != auth_service
        eq_(auth_service2.id, int(response.response[0]))
        eq_("url", auth_service2.url)
        eq_("user", auth_service2.setting(BasicAuthenticationProvider.TEST_IDENTIFIER).value)
        eq_("pass", auth_service2.setting(BasicAuthenticationProvider.TEST_PASSWORD).value)
        eq_("true",
            auth_service2.setting(MilleniumPatronAPI.VERIFY_CERTIFICATE).value)
        eq_(MilleniumPatronAPI.PIN_AUTHENTICATION_MODE,
            auth_service2.setting(MilleniumPatronAPI.AUTHENTICATION_MODE).value)
        eq_(None, auth_service2.setting(MilleniumPatronAPI.BLOCK_TYPES).value)
        eq_([], auth_service2.libraries)
 def check_short_name_unique(self, library, short_name):
     if not library or short_name != library.short_name:
         # If you're adding a new short_name, either by editing an
         # existing library or creating a new library, it must be unique.
         library_with_short_name = get_one(self._db, Library, short_name=short_name)
         if library_with_short_name:
             return LIBRARY_SHORT_NAME_ALREADY_IN_USE
    def test_analytics_services_post_create(self):
        library, ignore = create(
            self._db, Library, name="Library", short_name="L",
        )
        with self.request_context_with_admin("/", method="POST"):
            flask.request.form = MultiDict([
                ("name", "Google analytics name"),
                ("protocol", GoogleAnalyticsProvider.__module__),
                (ExternalIntegration.URL, "http://test"),
                ("libraries", json.dumps([{"short_name": "L", "tracking_id": "trackingid"}])),
            ])
            response = self.manager.admin_analytics_services_controller.process_analytics_services()
            eq_(response.status_code, 201)

        service = get_one(self._db, ExternalIntegration, goal=ExternalIntegration.ANALYTICS_GOAL)
        eq_(service.id, int(response.response[0]))
        eq_(GoogleAnalyticsProvider.__module__, service.protocol)
        eq_("http://test", service.url)
        eq_([library], service.libraries)
        eq_("trackingid", ConfigurationSetting.for_library_and_externalintegration(
                self._db, GoogleAnalyticsProvider.TRACKING_ID, library, service).value)

        # Creating a local analytics service doesn't require a URL.
        with self.request_context_with_admin("/", method="POST"):
            flask.request.form = MultiDict([
                ("name", "local analytics name"),
                ("protocol", LocalAnalyticsProvider.__module__),
                ("libraries", json.dumps([{"short_name": "L", "tracking_id": "trackingid"}])),
            ])
            response = self.manager.admin_analytics_services_controller.process_analytics_services()
            eq_(response.status_code, 201)
    def test_catalog_services_post_create(self):
        ME = MARCExporter

        s3, ignore = create(
            self._db, ExternalIntegration,
            protocol=ExternalIntegration.S3,
            goal=ExternalIntegration.STORAGE_GOAL,
        )
        s3.setting(S3Uploader.MARC_BUCKET_KEY).value = "marc-files"

        with self.request_context_with_admin("/", method="POST"):
            flask.request.form = MultiDict([
                ("name", "exporter name"),
                ("protocol", ME.NAME),
                (ME.STORAGE_PROTOCOL, ExternalIntegration.S3),
                ("libraries", json.dumps([{
                    "short_name": self._default_library.short_name,
                    ME.INCLUDE_SUMMARY: "false",
                    ME.INCLUDE_SIMPLIFIED_GENRES: "true",
                }])),
            ])
            response = self.manager.admin_catalog_services_controller.process_catalog_services()
            eq_(response.status_code, 201)

        service = get_one(self._db, ExternalIntegration, goal=ExternalIntegration.CATALOG_GOAL)
        eq_(service.id, int(response.response[0]))
        eq_(ME.NAME, service.protocol)
        eq_("exporter name", service.name)
        eq_(ExternalIntegration.S3, service.setting(ME.STORAGE_PROTOCOL).value)
        eq_([self._default_library], service.libraries)
        eq_("false", ConfigurationSetting.for_library_and_externalintegration(
                self._db, ME.INCLUDE_SUMMARY, self._default_library, service).value)
        eq_("true", ConfigurationSetting.for_library_and_externalintegration(
                self._db, ME.INCLUDE_SIMPLIFIED_GENRES, self._default_library, service).value)
Example #7
0
    def create_default_start_time(self, _db, cli_date):
        """Sets the default start time if it's passed as an argument.

        The command line date argument should have the format YYYY-MM-DD.
        """
        initialized = get_one(_db, Timestamp, self.service_name)
        two_years_ago = datetime.datetime.utcnow() - self.TWO_YEARS_AGO

        if cli_date:
            try:
                date = cli_date[0]
                return datetime.datetime.strptime(date, "%Y-%m-%d")
            except ValueError as e:
                # Date argument wasn't in the proper format.
                self.log.warn(
                    "%r. Using default date instead: %s.", e,
                    two_years_ago.strftime("%B %d, %Y")
                )
                return two_years_ago
        if not initialized:
            self.log.info(
                "Initializing %s from date: %s.", self.service_name,
                two_years_ago.strftime("%B %d, %Y")
            )
            return two_years_ago
        return None
    def test_borrow_creates_hold_when_no_available_copies(self):
         threem_edition, pool = self._edition(
             with_open_access_download=False,
             data_source_name=DataSource.THREEM,
             identifier_type=Identifier.THREEM_ID,
             with_license_pool=True,
         )
         threem_book = self._work(
             primary_edition=threem_edition,
         )
         pool.licenses_available = 0
         pool.open_access = False

         with self.app.test_request_context(
                 "/", headers=dict(Authorization=self.valid_auth)):
             self.manager.loans.authenticated_patron_from_request()
             self.manager.circulation.queue_checkout(NoAvailableCopies())
             self.manager.circulation.queue_hold(HoldInfo(
                 pool.identifier.type,
                 pool.identifier.identifier,
                 datetime.datetime.utcnow(),
                 datetime.datetime.utcnow() + datetime.timedelta(seconds=3600),
                 1,
             ))
             response = self.manager.loans.borrow(
                 DataSource.THREEM, pool.identifier.identifier)
             eq_(201, response.status_code)

             # A hold has been created for this license pool.
             hold = get_one(self._db, Hold, license_pool=pool)
             assert hold != None
    def test_metadata_service_delete(self):
        l1, ignore = create(
            self._db, Library, name="Library 1", short_name="L1",
        )
        novelist_service, ignore = create(
            self._db, ExternalIntegration,
            protocol=ExternalIntegration.NOVELIST,
            goal=ExternalIntegration.METADATA_GOAL,
        )
        novelist_service.username = "******"
        novelist_service.password = "******"
        novelist_service.libraries = [l1]

        with self.request_context_with_admin("/", method="DELETE"):
            self.admin.remove_role(AdminRole.SYSTEM_ADMIN)
            assert_raises(AdminNotAuthorized,
                          self.manager.admin_metadata_services_controller.process_delete,
                          novelist_service.id)

            self.admin.add_role(AdminRole.SYSTEM_ADMIN)
            response = self.manager.admin_metadata_services_controller.process_delete(novelist_service.id)
            eq_(response.status_code, 200)

        service = get_one(self._db, ExternalIntegration, id=novelist_service.id)
        eq_(None, service)
    def test_metadata_services_post_create(self):
        controller = self.manager.admin_metadata_services_controller
        library, ignore = create(
            self._db, Library, name="Library", short_name="L",
        )
        with self.request_context_with_admin("/", method="POST"):
            flask.request.form = MultiDict([
                ("name", "Name"),
                ("protocol", ExternalIntegration.NOVELIST),
                (ExternalIntegration.USERNAME, "user"),
                (ExternalIntegration.PASSWORD, "pass"),
                ("libraries", json.dumps([{"short_name": "L"}])),
            ])
            response = controller.process_post()
            eq_(response.status_code, 201)

        # A new ExternalIntegration has been created based on the submitted
        # information.
        service = get_one(
            self._db, ExternalIntegration,
            goal=ExternalIntegration.METADATA_GOAL
        )
        eq_(service.id, int(response.response[0]))
        eq_(ExternalIntegration.NOVELIST, service.protocol)
        eq_("user", service.username)
        eq_("pass", service.password)
        eq_([library], service.libraries)
    def look_up_library(self, library_short_name):
        """Find the library the user is trying to register, and check that it actually exists."""

        library = get_one(self._db, Library, short_name=library_short_name)
        if not library:
            return NO_SUCH_LIBRARY
        return library
    def test_patron_auth_service_delete(self):
        l1, ignore = create(
            self._db, Library, name="Library 1", short_name="L1",
        )
        auth_service, ignore = create(
            self._db, ExternalIntegration,
            protocol=SimpleAuthenticationProvider.__module__,
            goal=ExternalIntegration.PATRON_AUTH_GOAL
        )
        auth_service.setting(BasicAuthenticationProvider.TEST_IDENTIFIER).value = "old_user"
        auth_service.setting(BasicAuthenticationProvider.TEST_PASSWORD).value = "old_password"
        auth_service.libraries = [l1]

        with self.request_context_with_admin("/", method="DELETE"):
            self.admin.remove_role(AdminRole.SYSTEM_ADMIN)
            assert_raises(AdminNotAuthorized,
                          self.manager.admin_patron_auth_services_controller.process_delete,
                          auth_service.id)

            self.admin.add_role(AdminRole.SYSTEM_ADMIN)
            response = self.manager.admin_patron_auth_services_controller.process_delete(auth_service.id)
            eq_(response.status_code, 200)

        service = get_one(self._db, ExternalIntegration, id=auth_service.id)
        eq_(None, service)
    def test_libraries_get_with_no_libraries(self):
        # Delete any existing library created by the controller test setup.
        library = get_one(self._db, Library)
        if library:
            self._db.delete(library)

        with self.app.test_request_context("/"):
            response = self.manager.admin_library_settings_controller.process_get()
            eq_(response.get("libraries"), [])
 def process_delete(self, email):
     self.require_sitewide_library_manager()
     admin = get_one(self._db, Admin, email=email)
     if admin.is_system_admin():
         self.require_system_admin()
     if not admin:
         return MISSING_ADMIN
     self._db.delete(admin)
     return Response(unicode(_("Deleted")), 200)
    def get_name(self, auth_service):
        """Check that there isn't already an auth service with this name"""

        name = flask.request.form.get("name")
        if name:
            if auth_service.name != name:
                service_with_name = get_one(self._db, ExternalIntegration, name=name)
                if service_with_name:
                    return INTEGRATION_NAME_ALREADY_IN_USE
            return name
 def look_up_by_id(self, identifier):
     service = get_one(
         self._db,
         ExternalIntegration,
         id=identifier,
         goal=ExternalIntegration.PATRON_AUTH_GOAL
     )
     if not service:
         return MISSING_SERVICE
     return service
    def look_up_collection(self, collection_id):
        """Find the collection that the user is trying to register the library with,
        and check that it actually exists."""

        collection = get_one(self._db, Collection, id=collection_id)
        if not collection:
            return MISSING_COLLECTION
        if collection.protocol not in [api.NAME for api in self.shared_collection_provider_apis]:
            return COLLECTION_DOES_NOT_SUPPORT_REGISTRATION
        return collection
    def process_get(self):
        services = self._get_integration_info(ExternalIntegration.PATRON_AUTH_GOAL, self.protocols)

        for service in services:
            service_object = get_one(self._db, ExternalIntegration, id=service.get("id"), goal=ExternalIntegration.PATRON_AUTH_GOAL)
            service["self_test_results"] = self._get_prior_test_results(service_object, self._find_protocol_class(service_object))
        return dict(
            patron_auth_services=services,
            protocols=self.protocols
        )
 def get_library_from_uuid(self, library_uuid):
     if library_uuid:
         # Library UUID is required when editing an existing library
         # from the admin interface, and isn't present for new libraries.
         library = get_one(
             self._db, Library, uuid=library_uuid,
         )
         if library:
             return library
         else:
             return LIBRARY_NOT_FOUND.detailed(_("The specified library uuid does not exist."))
    def staff_email(self, _db, email):
        # If the admin already exists in the database, they can log in regardless of
        # whether their domain has been whitelisted for a library.
        admin = get_one(_db, Admin, email=email)
        if admin:
            return True

        # Otherwise, their email must match one of the configured domains.
        staff_domains = self.domains.keys()
        domain = email[email.index('@')+1:]
        return domain.lower() in [staff_domain.lower() for staff_domain in staff_domains]
    def fulfill(self, patron, pin, licensepool, delivery_mechanism, sync_on_failure=True):
        """Fulfil a book that a patron has previously checked out.

        :param delivery_mechanism: An explanation of how the patron
        wants the book to be delivered. If the book has previously been
        delivered through some other mechanism, this parameter is ignored
        and the previously used mechanism takes precedence.

        :return: A FulfillmentInfo object.
        """
        fulfillment = None
        loan = get_one(
            self._db, Loan, patron=patron, license_pool=licensepool,
            on_multiple='interchangeable'
        )
        if not loan:
            if sync_on_failure:
                # Sync and try again.
                self.sync_bookshelf(patron, pin)
                return self.fulfill(
                    patron, pin, licensepool=licensepool,
                    delivery_mechanism=delivery_mechanism,
                    sync_on_failure=False
                )
            else:
                raise NoActiveLoan("Cannot find your active loan for this work.")
        if loan.fulfillment is not None and loan.fulfillment != delivery_mechanism:
            raise DeliveryMechanismConflict(
                "You already fulfilled this loan as %s, you can't also do it as %s" 
                % (loan.fulfillment.delivery_mechanism.name, 
                   delivery_mechanism.delivery_mechanism.name)
            )

        if licensepool.open_access:
            fulfillment = self.fulfill_open_access(
                licensepool, delivery_mechanism
            )
        else:
            api = self.api_for_license_pool(licensepool)
            internal_format = api.internal_format(delivery_mechanism)
            fulfillment = api.fulfill(
                patron, pin, licensepool, internal_format
            )
            if not fulfillment or not (
                    fulfillment.content_link or fulfillment.content
            ):
                raise NoAcceptableFormat()
        # Make sure the delivery mechanism we just used is associated
        # with the loan.
        if loan.fulfillment is None:
            __transaction = self._db.begin_nested()
            loan.fulfillment = delivery_mechanism
            __transaction.commit()
        return fulfillment
Example #22
0
    def for_integration_id(cls, _db, integration_id, goal):
        """Find a LibraryRegistry object configured
        by the given ExternalIntegration ID.

        :param goal: The ExternalIntegration's .goal must be this goal.
        """
        integration = get_one(_db, ExternalIntegration,
                              goal=goal,
                              id=integration_id)
        if not integration:
            return None
        return cls(integration)
Example #23
0
    def authenticated_patron(self, _db, token):
        bearer_headers = {
            'Authorization': 'Bearer %s' % token
        }

        result = requests.get(self.CLEVER_API_BASE_URL + '/me', headers=bearer_headers).json()
        data = result['data']

        identifier = data['id']

        patron = get_one(_db, Patron, authorization_identifier=identifier)
        return patron
 def checkin(self, patron, pin, licensepool):
     # Delete the patron's loan for this licensepool.
     _db = Session.object_session(patron)
     try:
         loan = get_one(
             _db, Loan,
             patron_id=patron.id,
             license_pool_id=licensepool.id,
         )
         _db.delete(loan)
     except Exception, e:
         # The patron didn't have this book checked out.
         pass
    def test_discovery_services_post_create(self):
        with self.request_context_with_admin("/", method="POST"):
            flask.request.form = MultiDict([
                ("name", "Name"),
                ("protocol", ExternalIntegration.OPDS_REGISTRATION),
                (ExternalIntegration.URL, "http://registry_url"),
            ])
            response = self.manager.admin_discovery_services_controller.process_discovery_services()
            eq_(response.status_code, 201)

        service = get_one(self._db, ExternalIntegration, goal=ExternalIntegration.DISCOVERY_GOAL)
        eq_(service.id, int(response.response[0]))
        eq_(ExternalIntegration.OPDS_REGISTRATION, service.protocol)
        eq_("http://registry_url", service.url)
 def release_hold(self, patron, pin, licensepool):
     """Remove a patron's hold on a book."""
     hold = get_one(
         self._db, Hold, patron=patron, license_pool=licensepool,
         on_multiple='interchangeable'
     )
     if not licensepool.open_access:
         api = self.api_for_license_pool(licensepool)
         try:
             api.release_hold(patron, pin, licensepool)
         except NotOnHold, e:
             # The book wasn't on hold in the first place. Everything's
             # fine.
             pass
Example #27
0
    def cached_representation(self, scrubbed_url):
        """Attempts to find a usable cached Representation for a given URL"""
        representation = get_one(
            self._db, Representation, 'interchangeable', url=scrubbed_url
        )

        if not representation:
            return None
        if not representation.is_fresher_than(self.MAX_REPRESENTATION_AGE):
            # The Representation is nonexistent or stale. Delete it, so it
            # can be replaced.
            self._db.delete(representation)
            return None
        return representation
 def _load_lane(cls, 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 process_post(self):
        protocol = flask.request.form.get("protocol")
        is_new = False
        protocol_error = self.validate_form_fields(protocol)
        if protocol_error:
            return protocol_error

        id = flask.request.form.get("id")
        if id:
            # Find an existing service to edit
            auth_service = get_one(self._db, ExternalIntegration, id=id, goal=ExternalIntegration.PATRON_AUTH_GOAL)
            if not auth_service:
                return MISSING_SERVICE
            if protocol != auth_service.protocol:
                return CANNOT_CHANGE_PROTOCOL
        else:
            # Create a new service
            auth_service, is_new = self._create_integration(
                self.protocols, protocol, ExternalIntegration.PATRON_AUTH_GOAL
            )
            if isinstance(auth_service, ProblemDetail):
                return auth_service

        format_error = self.validate_formats()
        if format_error:
            self._db.rollback()
            return format_error

        name = self.get_name(auth_service)
        if isinstance(name, ProblemDetail):
            self._db.rollback()
            return name
        elif name:
            auth_service.name = name

        [protocol] = [p for p in self.protocols if p.get("name") == protocol]
        result = self._set_integration_settings_and_libraries(auth_service, protocol)
        if isinstance(result, ProblemDetail):
            return result

        library_error = self.check_libraries(auth_service)
        if library_error:
            self._db.rollback()
            return library_error

        if is_new:
            return Response(unicode(auth_service.id), 201)
        else:
            return Response(unicode(auth_service.id), 200)
    def test_library_delete(self):
        library = self._library()

        with self.request_context_with_admin("/", method="DELETE"):
            self.admin.remove_role(AdminRole.SYSTEM_ADMIN)
            assert_raises(AdminNotAuthorized,
                          self.manager.admin_library_settings_controller.process_delete,
                          library.uuid)

            self.admin.add_role(AdminRole.SYSTEM_ADMIN)
            response = self.manager.admin_library_settings_controller.process_delete(library.uuid)
            eq_(response.status_code, 200)

        library = get_one(self._db, Library, uuid=library.uuid)
        eq_(None, library)
    def test_analytics_service_delete(self):
        l1, ignore = create(
            self._db, Library, name="Library 1", short_name="L1",
        )
        ga_service, ignore = create(
            self._db, ExternalIntegration,
            protocol=GoogleAnalyticsProvider.__module__,
            goal=ExternalIntegration.ANALYTICS_GOAL,
        )
        ga_service.url = "oldurl"
        ga_service.libraries = [l1]

        with self.request_context_with_admin("/", method="DELETE"):
            self.admin.remove_role(AdminRole.SYSTEM_ADMIN)
            assert_raises(AdminNotAuthorized,
                          self.manager.admin_analytics_services_controller.process_delete,
                          ga_service.id)

            self.admin.add_role(AdminRole.SYSTEM_ADMIN)
            response = self.manager.admin_analytics_services_controller.process_delete(ga_service.id)
            eq_(response.status_code, 200)

        service = get_one(self._db, ExternalIntegration, id=ga_service.id)
        eq_(None, service)
Example #32
0
    def test_libraries_get_with_geographic_info(self):
        # Delete any existing library created by the controller test setup.
        library = get_one(self._db, Library)
        if library:
            self._db.delete(library)

        test_library = self._library("Library 1", "L1")
        ConfigurationSetting.for_library(
            Configuration.LIBRARY_FOCUS_AREA,
            test_library).value = '{"CA": ["N3L"], "US": ["11235"]}'
        ConfigurationSetting.for_library(
            Configuration.LIBRARY_SERVICE_AREA,
            test_library).value = '{"CA": ["J2S"], "US": ["31415"]}'

        with self.request_context_with_admin("/"):
            response = self.manager.admin_library_settings_controller.process_get(
            )
            library_settings = response.get("libraries")[0].get("settings")
            eq_(
                library_settings.get("focus_area"), {
                    u'CA': [{
                        u'N3L': u'Paris, Ontario'
                    }],
                    u'US': [{
                        u'11235': u'Brooklyn, NY'
                    }]
                })
            eq_(
                library_settings.get("service_area"), {
                    u'CA': [{
                        u'J2S': u'Saint-Hyacinthe Southwest, Quebec'
                    }],
                    u'US': [{
                        u'31415': u'Savannah, GA'
                    }]
                })
    def process_libraries(self, protocol, collection):
        """Go through the libraries that the user is trying to associate with this collection;
        check that each library actually exists, and that the library-related configuration settings
        that the user has submitted are complete and valid.  If the library passes all of the validations,
        go ahead and associate it with this collection."""

        libraries = []
        if flask.request.form.get("libraries"):
            libraries = json.loads(flask.request.form.get("libraries"))

        for library_info in libraries:
            library = get_one(self._db,
                              Library,
                              short_name=library_info.get("short_name"))
            if not library:
                return NO_SUCH_LIBRARY.detailed(
                    _("You attempted to add the collection to %(library_short_name)s, but the library does not exist.",
                      library_short_name=library_info.get("short_name")))
            if collection not in library.collections:
                library.collections.append(collection)
            result = self._set_integration_library(
                collection.external_integration, library_info, protocol)
            if isinstance(result, ProblemDetail):
                return result
        for library in collection.libraries:
            if library.short_name not in [
                    l.get("short_name") for l in libraries
            ]:
                library.collections.remove(collection)
                for setting in protocol.get("library_settings", []):
                    ConfigurationSetting.for_library_and_externalintegration(
                        self._db,
                        setting.get("key"),
                        library,
                        collection.external_integration,
                    ).value = None
Example #34
0
    def test_libraries_get_with_announcements(self):
        # Delete any existing library created by the controller test setup.
        library = get_one(self._db, Library)
        if library:
            self._db.delete(library)

        # Set some announcements for this library.
        test_library = self._library("Library 1", "L1")
        ConfigurationSetting.for_library(Announcements.SETTING_NAME,
                                         test_library).value = json.dumps([
                                             self.active, self.expired,
                                             self.forthcoming
                                         ])

        # When we request information about this library...
        with self.request_context_with_admin("/"):
            response = self.manager.admin_library_settings_controller.process_get(
            )
            library_settings = response.get("libraries")[0].get("settings")

            # We find out about the library's announcements.
            announcements = library_settings.get(Announcements.SETTING_NAME)
            eq_([
                self.active['id'], self.expired['id'], self.forthcoming['id']
            ], [x.get('id') for x in json.loads(announcements)])

            # The objects found in `library_settings` aren't exactly
            # the same as what is stored in the database: string dates
            # can be parsed into datetime.date objects.
            for i in json.loads(announcements):
                assert isinstance(
                    datetime.datetime.strptime(i.get('start'), "%Y-%m-%d"),
                    datetime.date)
                assert isinstance(
                    datetime.datetime.strptime(i.get('finish'), "%Y-%m-%d"),
                    datetime.date)
Example #35
0
    def test_collection_delete(self):
        collection = self._collection()
        assert False == collection.marked_for_deletion

        with self.request_context_with_admin("/", method="DELETE"):
            self.admin.remove_role(AdminRole.SYSTEM_ADMIN)
            pytest.raises(
                AdminNotAuthorized,
                self.manager.admin_collection_settings_controller.process_delete,
                collection.id,
            )

            self.admin.add_role(AdminRole.SYSTEM_ADMIN)
            response = self.manager.admin_collection_settings_controller.process_delete(
                collection.id
            )
            assert response.status_code == 200

        # The collection should still be available because it is not immediately deleted.
        # The collection will be deleted in the background by a script, but it is
        # now marked for deletion
        fetchedCollection = get_one(self._db, Collection, id=collection.id)
        assert collection == fetchedCollection
        assert True == fetchedCollection.marked_for_deletion
                  library_url=start_url))

        public_key = auth_document.get("public_key")
        if not public_key or not public_key.get(
                "type") == "RSA" or not public_key.get("value"):
            raise RemoteInitiatedServerError(
                _("Authentication document at %(auth_document_url)s did not contain an RSA public key.",
                  auth_document_url=auth_document_url),
                _("Remote authentication document"))

        public_key = public_key.get("value")
        public_key = RSA.importKey(public_key)
        encryptor = PKCS1_OAEP.new(public_key)

        normalized_url = IntegrationClient.normalize_url(start_url)
        client = get_one(self._db, IntegrationClient, url=normalized_url)
        if not client:
            client, ignore = IntegrationClient.register(self._db, start_url)

        shared_secret = client.shared_secret
        encrypted_secret = encryptor.encrypt(str(shared_secret))
        return dict(metadata=dict(
            shared_secret=base64.b64encode(encrypted_secret)))

    def check_client_authorization(self, collection, client):
        """Verify that an IntegrationClient is whitelisted for access to the collection."""
        external_library_urls = ConfigurationSetting.for_externalintegration(
            BaseSharedCollectionAPI.EXTERNAL_LIBRARY_URLS,
            collection.external_integration).json_value
        if client.url not in [
                IntegrationClient.normalize_url(url)
Example #37
0
 def library(self):
     return get_one(self._db, Library, id=self.library_id)
    def test_analytics_services_post_create(self):
        library, ignore = create(
            self._db,
            Library,
            name="Library",
            short_name="L",
        )
        with self.request_context_with_admin("/", method="POST"):
            flask.request.form = MultiDict([
                ("name", "Google analytics name"),
                ("protocol", GoogleAnalyticsProvider.__module__),
                (ExternalIntegration.URL, "http://test"),
                (
                    "libraries",
                    json.dumps([{
                        "short_name": "L",
                        "tracking_id": "trackingid"
                    }]),
                ),
            ])
            response = (self.manager.admin_analytics_services_controller.
                        process_analytics_services())
            assert response.status_code == 201

        service = get_one(
            self._db,
            ExternalIntegration,
            goal=ExternalIntegration.ANALYTICS_GOAL,
            protocol=GoogleAnalyticsProvider.__module__,
        )
        assert service.id == int(response.get_data())
        assert GoogleAnalyticsProvider.__module__ == service.protocol
        assert "http://test" == service.url
        assert [library] == service.libraries
        assert ("trackingid" ==
                ConfigurationSetting.for_library_and_externalintegration(
                    self._db, GoogleAnalyticsProvider.TRACKING_ID, library,
                    service).value)

        local_analytics_default = get_one(
            self._db,
            ExternalIntegration,
            goal=ExternalIntegration.ANALYTICS_GOAL,
            protocol=LocalAnalyticsProvider.__module__,
        )
        self._db.delete(local_analytics_default)

        # Creating a local analytics service doesn't require a URL.
        with self.request_context_with_admin("/", method="POST"):
            flask.request.form = MultiDict([
                ("name", "local analytics name"),
                ("protocol", LocalAnalyticsProvider.__module__),
                (
                    "libraries",
                    json.dumps([{
                        "short_name": "L",
                        "tracking_id": "trackingid"
                    }]),
                ),
            ])
            response = (self.manager.admin_analytics_services_controller.
                        process_analytics_services())
            assert response.status_code == 201
    def test_analytics_services_get_with_one_service(self):
        # Delete the local analytics service that gets created by default.
        local_analytics_default = get_one(
            self._db,
            ExternalIntegration,
            protocol=LocalAnalyticsProvider.__module__)

        self._db.delete(local_analytics_default)

        ga_service, ignore = create(
            self._db,
            ExternalIntegration,
            protocol=GoogleAnalyticsProvider.__module__,
            goal=ExternalIntegration.ANALYTICS_GOAL,
        )
        ga_service.url = self._str

        with self.request_context_with_admin("/"):
            response = (self.manager.admin_analytics_services_controller.
                        process_analytics_services())
            [service] = response.get("analytics_services")

            assert ga_service.id == service.get("id")
            assert ga_service.protocol == service.get("protocol")
            assert ga_service.url == service.get("settings").get(
                ExternalIntegration.URL)

        ga_service.libraries += [self._default_library]
        ConfigurationSetting.for_library_and_externalintegration(
            self._db,
            GoogleAnalyticsProvider.TRACKING_ID,
            self._default_library,
            ga_service,
        ).value = "trackingid"
        with self.request_context_with_admin("/"):
            response = (self.manager.admin_analytics_services_controller.
                        process_analytics_services())
            [service] = response.get("analytics_services")

            [library] = service.get("libraries")
            assert self._default_library.short_name == library.get(
                "short_name")
            assert "trackingid" == library.get(
                GoogleAnalyticsProvider.TRACKING_ID)

        self._db.delete(ga_service)

        local_service, ignore = create(
            self._db,
            ExternalIntegration,
            protocol=LocalAnalyticsProvider.__module__,
            goal=ExternalIntegration.ANALYTICS_GOAL,
        )

        local_service.libraries += [self._default_library]
        with self.request_context_with_admin("/"):
            response = (self.manager.admin_analytics_services_controller.
                        process_analytics_services())
            [local_analytics] = response.get("analytics_services")

            assert local_service.id == local_analytics.get("id")
            assert local_service.protocol == local_analytics.get("protocol")
            assert local_analytics.get(
                "protocol") == LocalAnalyticsProvider.__module__
            [library] = local_analytics.get("libraries")
            assert self._default_library.short_name == library.get(
                "short_name")
Example #40
0
    def process_contribution_web(self,
                                 _db,
                                 contribution,
                                 redo_complaints=False,
                                 log=None):
        """
        If sort_name that got from VIAF is not too far off from sort_name we already have,
        then use it (auto-fix).  If it is far off, then it's possible we did not match
        the author very well.  Make a wrong-author complaint, and ask a human to fix it.

        Searches VIAF by contributor's display_name and contribution title.  If the
        contributor already has a viaf_id store in our database, ignore it.  It's possible
        that id was produced by an older, less precise matching algorithm and might want replacing.

        :param redo_complaints: Should try OCLC/VIAF on the names that already have Complaint objects lodged against them?
        Alternative is to require human review of all Complaints.
        """
        if not contribution or not contribution.edition:
            return

        contributor = contribution.contributor
        if not contributor.display_name:
            return

        identifier = contribution.edition.primary_identifier
        if not identifier:
            return

        known_titles = []
        if contribution.edition.title:
            known_titles.append(contribution.edition.title)

        # Searching viaf can be resource-expensive, so only do it if specifically asked
        # See if there are any complaints already lodged by a previous run of this script.
        pool = contribution.edition.is_presentation_for
        parent_source = super(CheckContributorNamesOnWeb,
                              self).COMPLAINT_SOURCE
        complaint = get_one(
            _db,
            Complaint,
            on_multiple='interchangeable',
            license_pool=pool,
            source=self.COMPLAINT_SOURCE,
            type=self.COMPLAINT_TYPE,
        )

        if not redo_complaints and complaint:
            # We already did some work on this contributor, and determined to
            # ask a human for help.  This method was called with the time-saving
            # redo_complaints=False flag.  Skip calling OCLC and VIAF.
            return

        # can we find an ISBN-type Identifier for this Contribution to send
        # a request to OCLC with?
        isbn_identifier = None
        if identifier.type == Identifier.ISBN:
            isbn_identifier = identifier
        else:
            equivalencies = identifier.equivalencies
            for equivalency in equivalencies:
                if equivalency.output.type == Identifier.ISBN:
                    isbn_identifier = equivalency.output
                    break

        if isbn_identifier:
            # we can ask OCLC Linked Data about this ISBN
            uris = None
            sort_name, uris = self.canonicalizer.sort_name_from_oclc_linked_data(
                isbn_identifier, contributor.display_name)
            if sort_name:
                # see it's in correct format and not too far off from display_name
                name_ok = self.verify_sort_name(sort_name, contributor)
                if name_ok:
                    self.resolve_local_complaints(contribution)
                    self.set_contributor_sort_name(sort_name, contribution)
                    return
            else:
                # Nope. If OCLC Linked Data gave us any VIAF IDs, look them up
                # and see if we can get a sort name out of them.
                if uris:
                    for uri in uris:
                        match_found = self.canonicalizer.VIAF_ID.search(uri)
                        if match_found:
                            viaf_id = match_found.groups()[0]
                            contributor_data = self.canonicalizer.viaf.lookup_by_viaf(
                                viaf_id,
                                working_display_name=contributor.display_name
                            )[0]
                            if contributor_data.sort_name:
                                # see it's in correct format and not too far off from display_name
                                name_ok = self.verify_sort_name(
                                    sort_name, contributor)
                                if name_ok:
                                    self.resolve_local_complaints(contribution)
                                    self.set_contributor_sort_name(
                                        sort_name, contribution)
                                    return

        # Nope. If we were given a display name, let's ask VIAF about it
        # and see what it says.
        sort_name = self.canonicalizer.sort_name_from_viaf(
            contributor.display_name, known_titles)
        if sort_name:
            # see it's in correct format and not too far off from display_name
            name_ok = self.verify_sort_name(sort_name, contributor)
            if name_ok:
                self.resolve_local_complaints(contribution)
                self.set_contributor_sort_name(sort_name, contribution)
                return

        # If we got to this point, we have not gotten a satisfying enough answer from
        # either OCLC or VIAF.  Now is the time to generate a Complaint, ask a human to
        # come fix this.
        error_message_detail = "Contributor[id=%s].sort_name cannot be resolved from outside web services, human intervention required." % contributor.id
        self.register_problem(source=self.COMPLAINT_SOURCE,
                              contribution=contribution,
                              computed_sort_name=sort_name,
                              error_message_detail=error_message_detail,
                              log=log)
 def external_integration(self, _db):
     return get_one(_db,
                    ExternalIntegration,
                    id=self.external_integration_id)
Example #42
0
 def default_collection(self):
     if getattr(self, '_default_collection_id', None) is None:
         default_collection, ignore = IdentifierResolutionCoverageProvider.unaffiliated_collection(
             self._db)
         self._default_collection_id = default_collection.id
     return get_one(self._db, Collection, id=self._default_collection_id)
Example #43
0
    def test_register(self):
        # An auth document URL is required to register.
        assert_raises(InvalidInputException, self.shared_collection.register,
                      self.collection, None)

        # If the url doesn't return a valid auth document, there's an exception.
        auth_response = "not json"

        def do_get(*args, **kwargs):
            return MockRequestsResponse(200, content=auth_response)

        assert_raises(RemoteInitiatedServerError,
                      self.shared_collection.register,
                      self.collection,
                      "http://library.org/auth",
                      do_get=do_get)

        # The auth document also must have a link to the library's catalog.
        auth_response = json.dumps({"links": []})
        assert_raises(RemoteInitiatedServerError,
                      self.shared_collection.register,
                      self.collection,
                      "http://library.org/auth",
                      do_get=do_get)

        # If no external library URLs are configured, no one can register.
        auth_response = json.dumps(
            {"links": [{
                "href": "http://library.org",
                "rel": "start"
            }]})
        ConfigurationSetting.for_externalintegration(
            BaseSharedCollectionAPI.EXTERNAL_LIBRARY_URLS,
            self.collection.external_integration).value = None
        assert_raises(AuthorizationFailedException,
                      self.shared_collection.register,
                      self.collection,
                      "http://library.org/auth",
                      do_get=do_get)

        # If the library's URL isn't in the configuration, it can't register.
        auth_response = json.dumps({
            "links": [{
                "href": "http://differentlibrary.org",
                "rel": "start"
            }]
        })
        ConfigurationSetting.for_externalintegration(
            BaseSharedCollectionAPI.EXTERNAL_LIBRARY_URLS,
            self.collection.external_integration).value = json.dumps(
                ["http://library.org"])
        assert_raises(AuthorizationFailedException,
                      self.shared_collection.register,
                      self.collection,
                      "http://differentlibrary.org/auth",
                      do_get=do_get)

        # Or if the public key is missing from the auth document.
        auth_response = json.dumps(
            {"links": [{
                "href": "http://library.org",
                "rel": "start"
            }]})
        assert_raises(RemoteInitiatedServerError,
                      self.shared_collection.register,
                      self.collection,
                      "http://library.org/auth",
                      do_get=do_get)

        auth_response = json.dumps({
            "public_key": {
                "type": "not RSA",
                "value": "123"
            },
            "links": [{
                "href": "http://library.org",
                "rel": "start"
            }]
        })
        assert_raises(RemoteInitiatedServerError,
                      self.shared_collection.register,
                      self.collection,
                      "http://library.org/auth",
                      do_get=do_get)

        auth_response = json.dumps({
            "public_key": {
                "type": "RSA"
            },
            "links": [{
                "href": "http://library.org",
                "rel": "start"
            }]
        })
        assert_raises(RemoteInitiatedServerError,
                      self.shared_collection.register,
                      self.collection,
                      "http://library.org/auth",
                      do_get=do_get)

        # Here's an auth document with a valid key.
        key = RSA.generate(2048)
        public_key = key.publickey().exportKey()
        encryptor = PKCS1_OAEP.new(key)
        auth_response = json.dumps({
            "public_key": {
                "type": "RSA",
                "value": public_key
            },
            "links": [{
                "href": "http://library.org",
                "rel": "start"
            }]
        })
        response = self.shared_collection.register(self.collection,
                                                   "http://library.org/auth",
                                                   do_get=do_get)

        # An IntegrationClient has been created.
        client = get_one(
            self._db,
            IntegrationClient,
            url=IntegrationClient.normalize_url("http://library.org/"))
        decrypted_secret = encryptor.decrypt(
            base64.b64decode(
                response.get("metadata", {}).get("shared_secret")))
        eq_(client.shared_secret, decrypted_secret)
    def collections(self):
        protocols = []
        
        protocols.append({
            "name": Collection.OPDS_IMPORT,
            "fields": [
                { "key": "external_account_id", "label": _("URL") },
            ],
        })

        protocols.append({
            "name": Collection.OVERDRIVE,
            "fields": [
                { "key": "external_account_id", "label": _("Library ID") },
                { "key": "website_id", "label": _("Website ID") },
                { "key": "username", "label": _("Client Key") },
                { "key": "password", "label": _("Client Secret") },
            ],
        })

        protocols.append({
            "name": Collection.BIBLIOTHECA,
            "fields": [
                { "key": "username", "label": _("Account ID") },
                { "key": "password", "label": _("Account Key") },
                { "key": "external_account_id", "label": _("Library ID") },
            ],
        })

        protocols.append({
            "name": Collection.AXIS_360,
            "fields": [
                { "key": "username", "label": _("Username") },
                { "key": "password", "label": _("Password") },
                { "key": "external_account_id", "label": _("Library ID") },
                { "key": "url", "label": _("Server") },
            ],
        })

        protocols.append({
            "name": Collection.ONE_CLICK,
            "fields": [
                { "key": "password", "label": _("Basic Token") },
                { "key": "external_account_id", "label": _("Library ID") },
                { "key": "url", "label": _("URL") },
                { "key": "ebook_loan_length", "label": _("eBook Loan Length") },
                { "key": "eaudio_loan_length", "label": _("eAudio Loan Length") },
            ],
        })

        if flask.request.method == 'GET':
            collections = []
            for c in self._db.query(Collection).order_by(Collection.name).all():
                collection = dict(
                    name=c.name,
                    protocol=c.protocol,
                    libraries=[library.short_name for library in c.libraries],
                    external_account_id=c.external_account_id,
                    url=c.external_integration.url,
                    username=c.external_integration.username,
                    password=c.external_integration.password,
                )
                if c.protocol in [p.get("name") for p in protocols]:
                    [protocol] = [p for p in protocols if p.get("name") == c.protocol]
                    for field in protocol.get("fields"):
                        key = field.get("key")
                        if key not in collection:
                            collection[key] = c.external_integration.setting(key).value
                collections.append(collection)

            return dict(
                collections=collections,
                protocols=protocols,
            )


        name = flask.request.form.get("name")
        if not name:
            return MISSING_COLLECTION_NAME

        protocol = flask.request.form.get("protocol")

        if protocol and protocol not in [p.get("name") for p in protocols]:
            return UNKNOWN_COLLECTION_PROTOCOL

        is_new = False
        collection = get_one(self._db, Collection, name=name)
        if collection:
            if protocol != collection.protocol:
                return CANNOT_CHANGE_COLLECTION_PROTOCOL

        else:
            if protocol:
                collection, is_new = get_one_or_create(
                    self._db, Collection, name=name, protocol=protocol
                )
            else:
                return NO_PROTOCOL_FOR_NEW_COLLECTION

        [protocol] = [p for p in protocols if p.get("name") == protocol]
        fields = protocol.get("fields")

        for field in fields:
            key = field.get("key")
            value = flask.request.form.get(key)
            if not value:
                # Roll back any changes to the collection that have already been made.
                self._db.rollback()
                return INCOMPLETE_COLLECTION_CONFIGURATION.detailed(
                    _("The collection configuration is missing a required field: %(field)s",
                      field=field.get("label")))

            if key == "external_account_id":
                collection.external_account_id = value
            elif key == "username":
                collection.external_integration.username = value
            elif key == "password":
                collection.external_integration.password = value
            elif key == "url":
                collection.external_integration.url = value
            else:
                collection.external_integration.setting(key).value = value

        libraries = []
        if flask.request.form.get("libraries"):
            libraries = json.loads(flask.request.form.get("libraries"))

        for short_name in libraries:
            library = get_one(self._db, Library, short_name=short_name)
            if not library:
                return NO_SUCH_LIBRARY.detailed(_("You attempted to add the collection to %(library_short_name)s, but it does not exist.", library_short_name=short_name))
            if collection not in library.collections:
                library.collections.append(collection)
        for library in collection.libraries:
            if library.short_name not in libraries:
                library.collections.remove(collection)

        if is_new:
            return Response(unicode(_("Success")), 201)
        else:
            return Response(unicode(_("Success")), 200)
Example #45
0
    def custom_lists(self, identifier_type, identifier):
        self.require_librarian(flask.request.library)

        library = flask.request.library
        work = self.load_work(library, identifier_type, identifier)
        if isinstance(work, ProblemDetail):
            return work

        staff_data_source = DataSource.lookup(self._db,
                                              DataSource.LIBRARY_STAFF)

        if flask.request.method == "GET":
            lists = []
            for entry in work.custom_list_entries:
                list = entry.customlist
                lists.append(dict(id=list.id, name=list.name))
            return dict(custom_lists=lists)

        if flask.request.method == "POST":
            lists = flask.request.form.get("lists")
            if lists:
                lists = json.loads(lists)
            else:
                lists = []

            affected_lanes = set()

            # Remove entries for lists that were not in the submitted form.
            submitted_ids = [l.get("id") for l in lists if l.get("id")]
            for entry in work.custom_list_entries:
                if entry.list_id not in submitted_ids:
                    list = entry.customlist
                    list.remove_entry(work)
                    for lane in Lane.affected_by_customlist(list):
                        affected_lanes.add(lane)

            # Add entries for any new lists.
            for list_info in lists:
                id = list_info.get("id")
                name = list_info.get("name")

                if id:
                    is_new = False
                    list = get_one(self._db,
                                   CustomList,
                                   id=int(id),
                                   name=name,
                                   library=library,
                                   data_source=staff_data_source)
                    if not list:
                        self._db.rollback()
                        return MISSING_CUSTOM_LIST.detailed(
                            _("Could not find list \"%(list_name)s\"",
                              list_name=name))
                else:
                    list, is_new = create(self._db,
                                          CustomList,
                                          name=name,
                                          data_source=staff_data_source,
                                          library=library)
                    list.created = datetime.now()
                entry, was_new = list.add_entry(work, featured=True)
                if was_new:
                    for lane in Lane.affected_by_customlist(list):
                        affected_lanes.add(lane)

            # If any list changes affected lanes, update their sizes.
            # NOTE: This may not make a difference until the
            # works are actually re-indexed.
            for lane in affected_lanes:
                lane.update_size(self._db, self.search_engine)

            return Response(unicode(_("Success")), 200)
        if loan_info:
            # We successfuly secured a loan.  Now create it in our
            # database.
            __transaction = self._db.begin_nested()
            loan, new_loan_record = licensepool.loan_to(
                patron,
                start=loan_info.start_date or now,
                end=loan_info.end_date,
                external_identifier=loan_info.external_identifier)

            if must_set_delivery_mechanism:
                loan.fulfillment = delivery_mechanism
            existing_hold = get_one(self._db,
                                    Hold,
                                    patron=patron,
                                    license_pool=licensepool,
                                    on_multiple='interchangeable')
            if existing_hold:
                # The book was on hold, and now we have a loan.
                # Delete the record of the hold.
                self._db.delete(existing_hold)
            __transaction.commit()

            if loan and new_loan:
                # Send out an analytics event to record the fact that
                # a loan was initiated through the circulation
                # manager.
                self._collect_checkout_event(patron, licensepool)
            return loan, None, new_loan_record
    def borrow(self,
               patron,
               pin,
               licensepool,
               delivery_mechanism,
               hold_notification_email=None):
        """Either borrow a book or put it on hold. Don't worry about fulfilling
        the loan yet.
        
        :return: A 3-tuple (`Loan`, `Hold`, `is_new`). Either `Loan`
        or `Hold` must be None, but not both.
        """
        # Short-circuit the request if the patron lacks borrowing
        # privileges.
        PatronUtility.assert_borrowing_privileges(patron)

        now = datetime.datetime.utcnow()
        if licensepool.open_access:
            # We can 'loan' open-access content ourselves just by
            # putting a row in the database.
            now = datetime.datetime.utcnow()
            __transaction = self._db.begin_nested()
            loan, is_new = licensepool.loan_to(patron, start=now, end=None)
            __transaction.commit()
            self._collect_checkout_event(patron, licensepool)
            return loan, None, is_new

        # Okay, it's not an open-access book. This means we need to go
        # to an external service to get the book.
        #
        # This also means that our internal model of whether this book
        # is currently on loan or on hold might be wrong.
        api = self.api_for_license_pool(licensepool)

        must_set_delivery_mechanism = (
            api.SET_DELIVERY_MECHANISM_AT == BaseCirculationAPI.BORROW_STEP)

        if must_set_delivery_mechanism and not delivery_mechanism:
            raise DeliveryMechanismMissing()

        content_link = content_expires = None

        internal_format = api.internal_format(delivery_mechanism)

        if patron.fines:
            max_fines = Configuration.max_outstanding_fines(patron.library)
            if patron.fines >= max_fines.amount:
                raise OutstandingFines()

        # Do we (think we) already have this book out on loan?
        existing_loan = get_one(self._db,
                                Loan,
                                patron=patron,
                                license_pool=licensepool,
                                on_multiple='interchangeable')

        loan_info = None
        hold_info = None
        if existing_loan:
            # Sync with the API to see if the loan still exists.  If
            # it does, we still want to perform a 'checkout' operation
            # on the API, because that's how loans are renewed, but
            # certain error conditions (like NoAvailableCopies) mean
            # something different if you already have a confirmed
            # active loan.

            # TODO: This would be a great place to pass in only the
            # single API that needs to be synced.
            self.sync_bookshelf(patron, pin)
            existing_loan = get_one(self._db,
                                    Loan,
                                    patron=patron,
                                    license_pool=licensepool,
                                    on_multiple='interchangeable')

        new_loan = False

        loan_limit = patron.library.setting(Configuration.LOAN_LIMIT).int_value
        non_open_access_loans_with_end_date = [
            loan for loan in patron.loans
            if loan.license_pool.open_access == False and loan.end
        ]
        at_loan_limit = (
            loan_limit
            and len(non_open_access_loans_with_end_date) >= loan_limit)

        # If we're at the loan limit, skip trying to check out the book and just try
        # to place a hold. Otherwise, try to check out the book even if we think it's
        # not available.
        if not at_loan_limit:
            try:
                loan_info = api.checkout(patron, pin, licensepool,
                                         internal_format)

                # We asked the API to create a loan and it gave us a
                # LoanInfo object, rather than raising an exception like
                # AlreadyCheckedOut.
                #
                # For record-keeping purposes we're going to treat this as
                # a newly transacted loan, although it's possible that the
                # API does something unusual like return LoanInfo instead
                # of raising AlreadyCheckedOut.
                new_loan = True
            except AlreadyCheckedOut:
                # This is good, but we didn't get the real loan info.
                # Just fake it.
                identifier = licensepool.identifier
                loan_info = LoanInfo(licensepool.collection,
                                     licensepool.data_source,
                                     identifier.type,
                                     identifier.identifier,
                                     start_date=None,
                                     end_date=now +
                                     datetime.timedelta(hours=1))
                if existing_loan:
                    loan_info.external_identifier = existing_loan.external_identifier
            except AlreadyOnHold:
                # We're trying to check out a book that we already have on hold.
                hold_info = HoldInfo(licensepool.collection,
                                     licensepool.data_source,
                                     licensepool.identifier.type,
                                     licensepool.identifier.identifier, None,
                                     None, None)
            except NoAvailableCopies:
                if existing_loan:
                    # The patron tried to renew a loan but there are
                    # people waiting in line for them to return the book,
                    # so renewals are not allowed.
                    raise CannotRenew(
                        _("You cannot renew a loan if other patrons have the work on hold."
                          ))
                else:
                    # That's fine, we'll just (try to) place a hold.
                    #
                    # Since the patron incorrectly believed there were
                    # copies available, update availability information
                    # immediately.
                    api.update_availability(licensepool)
            except NoLicenses, e:
                # Since the patron incorrectly believed there were
                # licenses available, update availability information
                # immediately.
                api.update_availability(licensepool)
                raise e
Example #48
0
 def run(self, cmd_args=None):
     existing_timestamp = get_one(self._db, Timestamp, service=self.name)
     if not existing_timestamp:
         super(InstanceInitializationScript, self).run(cmd_args=cmd_args)
    def test_metadata_services_post_calls_register_with_metadata_wrangler(
            self):
        """Verify that process_post() calls register_with_metadata_wrangler
        if the rest of the request is handled successfully.
        """
        class Mock(MetadataServicesController):
            RETURN_VALUE = INVALID_URL
            called_with = None

            def register_with_metadata_wrangler(self, do_get, do_post, is_new,
                                                service):
                self.called_with = (do_get, do_post, is_new, service)
                return self.RETURN_VALUE

        controller = Mock(self.manager)
        library, ignore = create(
            self._db,
            Library,
            name="Library",
            short_name="L",
        )
        do_get = object()
        do_post = object()
        with self.request_context_with_admin("/", method="POST"):
            flask.request.form = MultiDict([])
            controller.process_post(do_get, do_post)

            # Since there was an error condition,
            # register_with_metadata_wrangler was not called.
            eq_(None, controller.called_with)

        form = MultiDict([
            ("name", "Name"),
            ("protocol", ExternalIntegration.NOVELIST),
            (ExternalIntegration.USERNAME, "user"),
            (ExternalIntegration.PASSWORD, "pass"),
        ])

        with self.request_context_with_admin("/", method="POST"):
            flask.request.form = form
            response = controller.process_post(do_get=do_get, do_post=do_post)

            # register_with_metadata_wrangler was called, but it
            # returned a ProblemDetail, so the overall request
            # failed.
            eq_((do_get, do_post, True), controller.called_with[:-1])
            eq_(INVALID_URL, response)

            # We ended up not creating an ExternalIntegration.
            eq_(
                None,
                get_one(self._db,
                        ExternalIntegration,
                        goal=ExternalIntegration.METADATA_GOAL))

            # But the ExternalIntegration we _would_ have created was
            # passed in to register_with_metadata_wrangler.
            bad_integration = controller.called_with[-1]

            # We can tell it's bad because it was disconnected from
            # our database session.
            eq_(None, bad_integration._sa_instance_state.session)

        # Now try the same scenario, except that
        # register_with_metadata_wrangler does _not_ return a
        # ProblemDetail.
        Mock.RETURN_VALUE = "It's all good"
        Mock.called_with = None
        with self.request_context_with_admin("/", method="POST"):
            flask.request.form = form
            response = controller.process_post(do_get=do_get, do_post=do_post)

            # This time we successfully created an ExternalIntegration.
            integration = get_one(self._db,
                                  ExternalIntegration,
                                  goal=ExternalIntegration.METADATA_GOAL)
            assert integration != None

            # It was passed in to register_with_metadata_wrangler
            # along with the rest of the arguments we expect.
            eq_((do_get, do_post, True, integration), controller.called_with)
            eq_(integration, controller.called_with[-1])
            eq_(self._db, integration._sa_instance_state.session)
Example #50
0
    def test_patron_auth_services_post_create(self):
        mock_controller = self._get_mock()

        library, ignore = create(
            self._db,
            Library,
            name="Library",
            short_name="L",
        )

        with self.request_context_with_admin("/", method="POST"):
            flask.request.form = MultiDict([
                ("protocol", SimpleAuthenticationProvider.__module__),
                (
                    "libraries",
                    json.dumps([{
                        "short_name":
                        library.short_name,
                        AuthenticationProvider.EXTERNAL_TYPE_REGULAR_EXPRESSION:
                        "^(.)",
                        AuthenticationProvider.LIBRARY_IDENTIFIER_RESTRICTION_TYPE:
                        AuthenticationProvider.
                        LIBRARY_IDENTIFIER_RESTRICTION_TYPE_REGEX,
                        AuthenticationProvider.LIBRARY_IDENTIFIER_FIELD:
                        AuthenticationProvider.
                        LIBRARY_IDENTIFIER_RESTRICTION_BARCODE,
                        AuthenticationProvider.LIBRARY_IDENTIFIER_RESTRICTION:
                        "^1234",
                    }]),
                ),
            ] + self._common_basic_auth_arguments())

            response = mock_controller.process_patron_auth_services()
            assert response.status_code == 201
            assert mock_controller.validate_formats_call_count == 1

        auth_service = get_one(self._db,
                               ExternalIntegration,
                               goal=ExternalIntegration.PATRON_AUTH_GOAL)
        assert auth_service.id == int(response.response[0])
        assert SimpleAuthenticationProvider.__module__ == auth_service.protocol
        assert ("user" == auth_service.setting(
            BasicAuthenticationProvider.TEST_IDENTIFIER).value)
        assert ("pass" == auth_service.setting(
            BasicAuthenticationProvider.TEST_PASSWORD).value)
        assert [library] == auth_service.libraries
        assert (
            "^(.)" == ConfigurationSetting.for_library_and_externalintegration(
                self._db,
                AuthenticationProvider.EXTERNAL_TYPE_REGULAR_EXPRESSION,
                library,
                auth_service,
            ).value)
        common_args = self._common_basic_auth_arguments()
        with self.request_context_with_admin("/", method="POST"):
            flask.request.form = MultiDict([
                ("protocol", MilleniumPatronAPI.__module__),
                (ExternalIntegration.URL, "url"),
                (MilleniumPatronAPI.VERIFY_CERTIFICATE, "true"),
                (
                    MilleniumPatronAPI.AUTHENTICATION_MODE,
                    MilleniumPatronAPI.PIN_AUTHENTICATION_MODE,
                ),
            ] + common_args)
            response = mock_controller.process_patron_auth_services()
            assert response.status_code == 201
            assert mock_controller.validate_formats_call_count == 2

        auth_service2 = get_one(
            self._db,
            ExternalIntegration,
            goal=ExternalIntegration.PATRON_AUTH_GOAL,
            protocol=MilleniumPatronAPI.__module__,
        )
        assert auth_service2 != auth_service
        assert auth_service2.id == int(response.response[0])
        assert "url" == auth_service2.url
        assert ("user" == auth_service2.setting(
            BasicAuthenticationProvider.TEST_IDENTIFIER).value)
        assert ("pass" == auth_service2.setting(
            BasicAuthenticationProvider.TEST_PASSWORD).value)
        assert ("true" == auth_service2.setting(
            MilleniumPatronAPI.VERIFY_CERTIFICATE).value)
        assert (MilleniumPatronAPI.PIN_AUTHENTICATION_MODE ==
                auth_service2.setting(
                    MilleniumPatronAPI.AUTHENTICATION_MODE).value)
        assert None == auth_service2.setting(
            MilleniumPatronAPI.BLOCK_TYPES).value
        assert [] == auth_service2.libraries
Example #51
0
    def test_collections_post_edit_mirror_integration(self):
        # The collection exists.
        collection = self._collection(
            name="Collection 1", protocol=ExternalIntegration.AXIS_360
        )

        # There is a storage integration not associated with the collection.
        storage = self._external_integration(
            protocol=ExternalIntegration.S3, goal=ExternalIntegration.STORAGE_GOAL
        )

        # It's possible to associate the storage integration with the
        # collection for either a books or covers mirror.
        base_request = self._base_collections_post_request(collection)
        with self.request_context_with_admin("/", method="POST"):
            request = MultiDict(
                base_request + [("books_mirror_integration_id", storage.id)]
            )
            flask.request.form = request
            response = (
                self.manager.admin_collection_settings_controller.process_collections()
            )
            assert response.status_code == 200

            # There is an external integration link to associate the collection's
            # external integration with the storage integration for a books mirror.
            external_integration_link = get_one(
                self._db,
                ExternalIntegrationLink,
                external_integration_id=collection.external_integration.id,
            )
            assert storage.id == external_integration_link.other_integration_id

        # It's possible to unset the mirror integration.
        controller = self.manager.admin_collection_settings_controller
        with self.request_context_with_admin("/", method="POST"):
            request = MultiDict(
                base_request
                + [
                    (
                        "books_mirror_integration_id",
                        str(controller.NO_MIRROR_INTEGRATION),
                    )
                ]
            )
            flask.request.form = request
            response = controller.process_collections()
            assert response.status_code == 200
            external_integration_link = get_one(
                self._db,
                ExternalIntegrationLink,
                external_integration_id=collection.external_integration.id,
            )
            assert None == external_integration_link

        # Providing a nonexistent integration ID gives an error.
        with self.request_context_with_admin("/", method="POST"):
            request = MultiDict(base_request + [("books_mirror_integration_id", -200)])
            flask.request.form = request
            response = (
                self.manager.admin_collection_settings_controller.process_collections()
            )
            assert response == MISSING_SERVICE
    def register(self, collection, auth_document_url, do_get=HTTP.get_with_timeout):
        """Register a library on an external circulation manager for access to this
        collection. The library's auth document url must be whitelisted in the
        collection's settings."""
        if not auth_document_url:
            raise InvalidInputException(
                _("An authentication document URL is required to register a library.")
            )

        auth_response = do_get(auth_document_url, allowed_response_codes=["2xx", "3xx"])
        try:
            auth_document = json.loads(auth_response.content)
        except ValueError as e:
            raise RemoteInitiatedServerError(
                _(
                    "Authentication document at %(auth_document_url)s was not valid JSON.",
                    auth_document_url=auth_document_url,
                ),
                _("Remote authentication document"),
            )

        links = auth_document.get("links")
        start_url = None
        for link in links:
            if link.get("rel") == "start":
                start_url = link.get("href")
                break

        if not start_url:
            raise RemoteInitiatedServerError(
                _(
                    "Authentication document at %(auth_document_url)s did not contain a start link.",
                    auth_document_url=auth_document_url,
                ),
                _("Remote authentication document"),
            )

        external_library_urls = ConfigurationSetting.for_externalintegration(
            BaseSharedCollectionAPI.EXTERNAL_LIBRARY_URLS,
            collection.external_integration,
        ).json_value

        if not external_library_urls or start_url not in external_library_urls:
            raise AuthorizationFailedException(
                _(
                    "Your library's URL is not one of the allowed URLs for this collection. Ask the collection administrator to add %(library_url)s to the list of allowed URLs.",
                    library_url=start_url,
                )
            )

        public_key = auth_document.get("public_key")
        if (
            not public_key
            or not public_key.get("type") == "RSA"
            or not public_key.get("value")
        ):
            raise RemoteInitiatedServerError(
                _(
                    "Authentication document at %(auth_document_url)s did not contain an RSA public key.",
                    auth_document_url=auth_document_url,
                ),
                _("Remote authentication document"),
            )

        public_key = public_key.get("value")
        encryptor = Configuration.cipher(public_key)

        normalized_url = IntegrationClient.normalize_url(start_url)
        client = get_one(self._db, IntegrationClient, url=normalized_url)
        if not client:
            client, ignore = IntegrationClient.register(self._db, start_url)

        shared_secret = client.shared_secret.encode("utf-8")
        encrypted_secret = encryptor.encrypt(shared_secret)
        return dict(metadata=dict(shared_secret=base64.b64encode(encrypted_secret)))
Example #53
0
 def get_timestamp():
     return get_one(self._db, Timestamp, service=monitor.service_name)
class CirculationAPI(object):
    """Implement basic circulation logic and abstract away the details
    between different circulation APIs.
    """
    def __init__(self, _db, overdrive=None, threem=None, axis=None):
        self._db = _db
        self.overdrive = overdrive
        self.threem = threem
        self.axis = axis
        self.apis = [x for x in (overdrive, threem, axis) if x]
        self.log = logging.getLogger("Circulation API")

        # When we get our view of a patron's loans and holds, we need
        # to include loans from all licensed data sources.  We do not
        # need to include loans from open-access sources because we
        # are the authorities on those.
        data_sources_for_sync = []
        if self.overdrive:
            data_sources_for_sync.append(
                DataSource.lookup(_db, DataSource.OVERDRIVE))
        if self.threem:
            data_sources_for_sync.append(
                DataSource.lookup(_db, DataSource.THREEM))
        if self.axis:
            data_sources_for_sync.append(
                DataSource.lookup(_db, DataSource.AXIS_360))

        h = dict()
        for ds in data_sources_for_sync:
            type = ds.primary_identifier_type
            h[type] = ds.name
            if type in Identifier.DEPRECATED_NAMES:
                new_name = Identifier.DEPRECATED_NAMES[type]
                h[new_name] = ds.name
        self.identifier_type_to_data_source_name = h
        self.data_source_ids_for_sync = [x.id for x in data_sources_for_sync]

    def api_for_license_pool(self, licensepool):
        """Find the API to use for the given license pool."""
        if licensepool.data_source.name == DataSource.OVERDRIVE:
            api = self.overdrive
        elif licensepool.data_source.name == DataSource.THREEM:
            api = self.threem
        elif licensepool.data_source.name == DataSource.AXIS_360:
            api = self.axis
        else:
            return None

        return api

    def can_revoke_hold(self, licensepool, hold):
        """Some circulation providers allow you to cancel a hold
        when the book is reserved to you. Others only allow you to cancel
        a hole while you're in the hold queue.
        """
        if hold.position is None or hold.position > 0:
            return True
        api = self.api_for_license_pool(licensepool)
        if api.CAN_REVOKE_HOLD_WHEN_RESERVED:
            return True
        return False

    def borrow(self,
               patron,
               pin,
               licensepool,
               delivery_mechanism,
               hold_notification_email=None):
        """Either borrow a book or put it on hold. Don't worry about fulfilling
        the loan yet.
        
        :return: A 3-tuple (`Loan`, `Hold`, `is_new`). Either `Loan`
        or `Hold` must be None, but not both.
        """
        # Short-circuit the request if the patron lacks borrowing
        # privileges.
        PatronUtility.assert_borrowing_privileges(patron)

        now = datetime.datetime.utcnow()
        if licensepool.open_access:
            # We can 'loan' open-access content ourselves just by
            # putting a row in the database.
            now = datetime.datetime.utcnow()
            __transaction = self._db.begin_nested()
            loan, is_new = licensepool.loan_to(patron, start=now, end=None)
            __transaction.commit()
            self._collect_checkout_event(licensepool)
            return loan, None, is_new

        # Okay, it's not an open-access book. This means we need to go
        # to an external service to get the book.
        #
        # This also means that our internal model of whether this book
        # is currently on loan or on hold might be wrong.
        api = self.api_for_license_pool(licensepool)

        must_set_delivery_mechanism = (
            api.SET_DELIVERY_MECHANISM_AT == BaseCirculationAPI.BORROW_STEP)

        if must_set_delivery_mechanism and not delivery_mechanism:
            raise DeliveryMechanismMissing()

        content_link = content_expires = None

        internal_format = api.internal_format(delivery_mechanism)

        if patron.fines:
            max_fines = Configuration.max_outstanding_fines()
            if patron.fines >= max_fines.amount:
                raise OutstandingFines()

        # Do we (think we) already have this book out on loan?
        existing_loan = get_one(self._db,
                                Loan,
                                patron=patron,
                                license_pool=licensepool,
                                on_multiple='interchangeable')

        loan_info = None
        hold_info = None
        if existing_loan:
            # Sync with the API to see if the loan still exists.  If
            # it does, we still want to perform a 'checkout' operation
            # on the API, because that's how loans are renewed, but
            # certain error conditions (like NoAvailableCopies) mean
            # something different if you already have a confirmed
            # active loan.
            self.sync_bookshelf(patron, pin)
            existing_loan = get_one(self._db,
                                    Loan,
                                    patron=patron,
                                    license_pool=licensepool,
                                    on_multiple='interchangeable')

        new_loan = False
        try:
            loan_info = api.checkout(patron, pin, licensepool, internal_format)

            # We asked the API to create a loan and it gave us a
            # LoanInfo object, rather than raising an exception like
            # AlreadyCheckedOut.
            #
            # For record-keeping purposes we're going to treat this as
            # a newly transacted loan, although it's possible that the
            # API does something unusual like return LoanInfo instead
            # of raising AlreadyCheckedOut.
            new_loan = True
        except AlreadyCheckedOut:
            # This is good, but we didn't get the real loan info.
            # Just fake it.
            identifier = licensepool.identifier
            loan_info = LoanInfo(identifier.type,
                                 identifier,
                                 start_date=None,
                                 end_date=now + datetime.timedelta(hours=1))
        except AlreadyOnHold:
            # We're trying to check out a book that we already have on hold.
            hold_info = HoldInfo(licensepool.identifier.type,
                                 licensepool.identifier.identifier, None, None,
                                 None)
        except NoAvailableCopies:
            if existing_loan:
                # The patron tried to renew a loan but there are
                # people waiting in line for them to return the book,
                # so renewals are not allowed.
                raise CannotRenew(
                    _("You cannot renew a loan if other patrons have the work on hold."
                      ))
            else:
                # That's fine, we'll just (try to) place a hold.
                #
                # Since the patron incorrectly believed there were
                # copies available, update availability information
                # immediately.
                api.update_availability(licensepool)
        except NoLicenses, e:
            # Since the patron incorrectly believed there were
            # licenses available, update availability information
            # immediately.
            api.update_availability(licensepool)
            raise e

        if loan_info:
            # We successfuly secured a loan.  Now create it in our
            # database.
            __transaction = self._db.begin_nested()
            loan, new_loan_record = licensepool.loan_to(
                patron,
                start=loan_info.start_date or now,
                end=loan_info.end_date)

            if must_set_delivery_mechanism:
                loan.fulfillment = delivery_mechanism
            existing_hold = get_one(self._db,
                                    Hold,
                                    patron=patron,
                                    license_pool=licensepool,
                                    on_multiple='interchangeable')
            if existing_hold:
                # The book was on hold, and now we have a loan.
                # Delete the record of the hold.
                self._db.delete(existing_hold)
            __transaction.commit()

            if loan and new_loan:
                # Send out an analytics event to record the fact that
                # a loan was initiated through the circulation
                # manager.
                self._collect_checkout_event(licensepool)
            return loan, None, new_loan_record

        # At this point we know that we neither successfully
        # transacted a loan, nor discovered a preexisting loan.

        # Checking out a book didn't work, so let's try putting
        # the book on hold.
        if not hold_info:
            try:
                hold_info = api.place_hold(patron, pin, licensepool,
                                           hold_notification_email)
            except AlreadyOnHold, e:
                hold_info = HoldInfo(licensepool.identifier.type,
                                     licensepool.identifier.identifier, None,
                                     None, None)
Example #55
0
    def test_collections_post_create(self):
        l1, ignore = create(
            self._db, Library, name="Library 1", short_name="L1",
        )
        l2, ignore = create(
            self._db, Library, name="Library 2", short_name="L2",
        )
        l3, ignore = create(
            self._db, Library, name="Library 3", short_name="L3",
        )

        with self.request_context_with_admin("/", method="POST"):
            flask.request.form = MultiDict([
                ("name", "New Collection"),
                ("protocol", "Overdrive"),
                ("libraries", json.dumps([
                    {"short_name": "L1", "ils_name": "l1_ils"},
                    {"short_name":"L2", "ils_name": "l2_ils"}
                ])),
                ("external_account_id", "acctid"),
                ("username", "username"),
                ("password", "password"),
                ("website_id", "1234"),
            ])
            response = self.manager.admin_collection_settings_controller.process_collections()
            eq_(response.status_code, 201)

        # The collection was created and configured properly.
        collection = get_one(self._db, Collection, name="New Collection")
        eq_(collection.id, int(response.response[0]))
        eq_("New Collection", collection.name)
        eq_("acctid", collection.external_account_id)
        eq_("username", collection.external_integration.username)
        eq_("password", collection.external_integration.password)

        # Two libraries now have access to the collection.
        eq_([collection], l1.collections)
        eq_([collection], l2.collections)
        eq_([], l3.collections)

        # Additional settings were set on the collection.
        setting = collection.external_integration.setting("website_id")
        eq_("website_id", setting.key)
        eq_("1234", setting.value)

        eq_("l1_ils", ConfigurationSetting.for_library_and_externalintegration(
                self._db, "ils_name", l1, collection.external_integration).value)
        eq_("l2_ils", ConfigurationSetting.for_library_and_externalintegration(
                self._db, "ils_name", l2, collection.external_integration).value)

        # This collection will be a child of the first collection.
        with self.request_context_with_admin("/", method="POST"):
            flask.request.form = MultiDict([
                ("name", "Child Collection"),
                ("protocol", "Overdrive"),
                ("parent_id", collection.id),
                ("libraries", json.dumps([{"short_name": "L3", "ils_name": "l3_ils"}])),
                ("external_account_id", "child-acctid"),
            ])
            response = self.manager.admin_collection_settings_controller.process_collections()
            eq_(response.status_code, 201)

        # The collection was created and configured properly.
        child = get_one(self._db, Collection, name="Child Collection")
        eq_(child.id, int(response.response[0]))
        eq_("Child Collection", child.name)
        eq_("child-acctid", child.external_account_id)

        # The settings that are inherited from the parent weren't set.
        eq_(None, child.external_integration.username)
        eq_(None, child.external_integration.password)
        setting = child.external_integration.setting("website_id")
        eq_(None, setting.value)

        # One library has access to the collection.
        eq_([child], l3.collections)

        eq_("l3_ils", ConfigurationSetting.for_library_and_externalintegration(
                self._db, "ils_name", l3, child.external_integration).value)
    def fulfill(self,
                patron,
                pin,
                licensepool,
                delivery_mechanism,
                sync_on_failure=True):
        """Fulfil a book that a patron has previously checked out.

        :param delivery_mechanism: A LicensePoolDeliveryMechanism
        explaining how the patron wants the book to be delivered. If
        the book has previously been delivered through some other
        mechanism, this parameter is ignored and the previously used
        mechanism takes precedence.

        :return: A FulfillmentInfo object.
        """
        fulfillment = None
        loan = get_one(self._db,
                       Loan,
                       patron=patron,
                       license_pool=licensepool,
                       on_multiple='interchangeable')
        if not loan:
            if sync_on_failure:
                # Sync and try again.
                # TODO: Pass in only the single collection or LicensePool
                # that needs to be synced.
                self.sync_bookshelf(patron, pin)
                return self.fulfill(patron,
                                    pin,
                                    licensepool=licensepool,
                                    delivery_mechanism=delivery_mechanism,
                                    sync_on_failure=False)
            else:
                raise NoActiveLoan(
                    _("Cannot find your active loan for this work."))
        if loan.fulfillment is not None and loan.fulfillment != delivery_mechanism and not delivery_mechanism.delivery_mechanism.is_streaming:
            raise DeliveryMechanismConflict(
                _("You already fulfilled this loan as %(loan_delivery_mechanism)s, you can't also do it as %(requested_delivery_mechanism)s",
                  loan_delivery_mechanism=loan.fulfillment.delivery_mechanism.
                  name,
                  requested_delivery_mechanism=delivery_mechanism.
                  delivery_mechanism.name))

        if licensepool.open_access:
            fulfillment = self.fulfill_open_access(
                licensepool, delivery_mechanism.delivery_mechanism)
        else:
            api = self.api_for_license_pool(licensepool)
            internal_format = api.internal_format(delivery_mechanism)
            fulfillment = api.fulfill(patron, pin, licensepool,
                                      internal_format)
            if not fulfillment or not (fulfillment.content_link
                                       or fulfillment.content):
                raise NoAcceptableFormat()

        # Send out an analytics event to record the fact that
        # a fulfillment was initiated through the circulation
        # manager.
        if self.analytics:
            self.analytics.collect_event(
                patron.library,
                licensepool,
                CirculationEvent.CM_FULFILL,
            )

        # Make sure the delivery mechanism we just used is associated
        # with the loan.
        if loan.fulfillment is None and not delivery_mechanism.delivery_mechanism.is_streaming:
            __transaction = self._db.begin_nested()
            loan.fulfillment = delivery_mechanism
            __transaction.commit()
        return fulfillment
Example #57
0
def create_lanes_for_large_collection(_db, library, languages, priority=0):
    """Ensure that the lanes appropriate to a large collection are all
    present.

    This means:

    * A "%(language)s Adult Fiction" lane containing sublanes for each fiction
        genre.
    * A "%(language)s Adult Nonfiction" lane containing sublanes for
        each nonfiction genre.
    * A "%(language)s YA Fiction" lane containing sublanes for the
        most popular YA fiction genres.
    * A "%(language)s YA Nonfiction" lane containing sublanes for the
        most popular YA fiction genres.
    * A "%(language)s Children and Middle Grade" lane containing
        sublanes for childrens' books at different age levels.

    :param library: Newly created lanes will be associated with this
        library.
    :param languages: Newly created lanes will contain only books
        in these languages.
    :return: A list of top-level Lane objects.

    TODO: If there are multiple large collections, their top-level lanes do
    not have distinct display names.
    """
    if isinstance(languages, basestring):
        languages = [languages]

    ADULT = Classifier.AUDIENCES_ADULT
    YA = [Classifier.AUDIENCE_YOUNG_ADULT]
    CHILDREN = [Classifier.AUDIENCE_CHILDREN]

    common_args = dict(
        languages=languages,
        media=None
    )
    adult_common_args = dict(common_args)
    adult_common_args['audiences'] = ADULT

    include_best_sellers = False
    nyt_data_source = DataSource.lookup(_db, DataSource.NYT)
    nyt_integration = get_one(
        _db, ExternalIntegration,
        goal=ExternalIntegration.METADATA_GOAL,
        protocol=ExternalIntegration.NYT,
    )
    if nyt_integration:
        include_best_sellers = True

    sublanes = []
    if include_best_sellers:
        best_sellers, ignore = create(
            _db, Lane, library=library,
            display_name="Best Sellers",
            priority=priority,
            **common_args
        )
        priority += 1
        best_sellers.list_datasource = nyt_data_source
        sublanes.append(best_sellers)


    adult_fiction_sublanes = []
    adult_fiction_priority = 0
    if include_best_sellers:
        adult_fiction_best_sellers, ignore = create(
            _db, Lane, library=library,
            display_name="Best Sellers",
            fiction=True,
            priority=adult_fiction_priority,
            **adult_common_args
        )
        adult_fiction_priority += 1
        adult_fiction_best_sellers.list_datasource = nyt_data_source
        adult_fiction_sublanes.append(adult_fiction_best_sellers)

    for genre in fiction_genres:
        if isinstance(genre, basestring):
            genre_name = genre
        else:
            genre_name = genre.get("name")
        genre_lane = lane_from_genres(
            _db, library, [genre],
            priority=adult_fiction_priority,
            **adult_common_args)
        adult_fiction_priority += 1
        adult_fiction_sublanes.append(genre_lane)

    adult_fiction, ignore = create(
        _db, Lane, library=library,
        display_name="Fiction",
        genres=[],
        sublanes=adult_fiction_sublanes,
        fiction=True,
        priority=priority,
        **adult_common_args
    )
    priority += 1
    sublanes.append(adult_fiction)

    adult_nonfiction_sublanes = []
    adult_nonfiction_priority = 0
    if include_best_sellers:
        adult_nonfiction_best_sellers, ignore = create(
            _db, Lane, library=library,
            display_name="Best Sellers",
            fiction=False,
            priority=adult_nonfiction_priority,
            **adult_common_args
        )
        adult_nonfiction_priority += 1
        adult_nonfiction_best_sellers.list_datasource = nyt_data_source
        adult_nonfiction_sublanes.append(adult_nonfiction_best_sellers)

    for genre in nonfiction_genres:
        # "Life Strategies" is a YA-specific genre that should not be
        # included in the Adult Nonfiction lane.
        if genre != genres.Life_Strategies:
            if isinstance(genre, basestring):
                genre_name = genre
            else:
                genre_name = genre.get("name")
            genre_lane = lane_from_genres(
                _db, library, [genre],
                priority=adult_nonfiction_priority,
                **adult_common_args)
            adult_nonfiction_priority += 1
            adult_nonfiction_sublanes.append(genre_lane)

    adult_nonfiction, ignore = create(
        _db, Lane, library=library,
        display_name="Nonfiction",
        genres=[],
        sublanes=adult_nonfiction_sublanes,
        fiction=False,
        priority=priority,
        **adult_common_args
    )
    priority += 1
    sublanes.append(adult_nonfiction)

    ya_common_args = dict(common_args)
    ya_common_args['audiences'] = YA

    ya_fiction, ignore = create(
        _db, Lane, library=library,
        display_name="Young Adult Fiction",
        genres=[], fiction=True,
        sublanes=[],
        priority=priority,
        **ya_common_args
    )
    priority += 1
    sublanes.append(ya_fiction)

    ya_fiction_priority = 0
    if include_best_sellers:
        ya_fiction_best_sellers, ignore = create(
            _db, Lane, library=library,
            display_name="Best Sellers",
            fiction=True,
            priority=ya_fiction_priority,
            **ya_common_args
        )
        ya_fiction_priority += 1
        ya_fiction_best_sellers.list_datasource = nyt_data_source
        ya_fiction.sublanes.append(ya_fiction_best_sellers)

    ya_fiction.sublanes.append(
        lane_from_genres(_db, library, [genres.Dystopian_SF],
                         priority=ya_fiction_priority, **ya_common_args))
    ya_fiction_priority += 1
    ya_fiction.sublanes.append(
        lane_from_genres(_db, library, [genres.Fantasy],
                         priority=ya_fiction_priority, **ya_common_args))
    ya_fiction_priority += 1
    ya_fiction.sublanes.append(
        lane_from_genres(_db, library, [genres.Comics_Graphic_Novels],
                         priority=ya_fiction_priority, **ya_common_args))
    ya_fiction_priority += 1
    ya_fiction.sublanes.append(
        lane_from_genres(_db, library, [genres.Literary_Fiction],
                         display_name="Contemporary Fiction",
                         priority=ya_fiction_priority, **ya_common_args))
    ya_fiction_priority += 1
    ya_fiction.sublanes.append(
        lane_from_genres(_db, library, [genres.LGBTQ_Fiction],
                         priority=ya_fiction_priority, **ya_common_args))
    ya_fiction_priority += 1
    ya_fiction.sublanes.append(
        lane_from_genres(_db, library, [genres.Suspense_Thriller, genres.Mystery],
                         display_name="Mystery & Thriller",
                         priority=ya_fiction_priority, **ya_common_args))
    ya_fiction_priority += 1
    ya_fiction.sublanes.append(
        lane_from_genres(_db, library, [genres.Romance],
                         priority=ya_fiction_priority, **ya_common_args))
    ya_fiction_priority += 1
    ya_fiction.sublanes.append(
        lane_from_genres(_db, library, [genres.Science_Fiction],
                         exclude_genres=[genres.Dystopian_SF, genres.Steampunk],
                         priority=ya_fiction_priority, **ya_common_args))
    ya_fiction_priority += 1
    ya_fiction.sublanes.append(
        lane_from_genres(_db, library, [genres.Steampunk],
                         priority=ya_fiction_priority, **ya_common_args))
    ya_fiction_priority += 1

    ya_nonfiction, ignore = create(
        _db, Lane, library=library,
        display_name="Young Adult Nonfiction",
        genres=[], fiction=False,
        sublanes=[],
        priority=priority,
        **ya_common_args
    )
    priority += 1
    sublanes.append(ya_nonfiction)

    ya_nonfiction_priority = 0
    if include_best_sellers:
        ya_nonfiction_best_sellers, ignore = create(
            _db, Lane, library=library,
            display_name="Best Sellers",
            fiction=False,
            priority=ya_nonfiction_priority,
            **ya_common_args
        )
        ya_nonfiction_priority += 1
        ya_nonfiction_best_sellers.list_datasource = nyt_data_source
        ya_nonfiction.sublanes.append(ya_nonfiction_best_sellers)

    ya_nonfiction.sublanes.append(
        lane_from_genres(_db, library, [genres.Biography_Memoir],
                         display_name="Biography",
                         priority=ya_nonfiction_priority, **ya_common_args))
    ya_nonfiction_priority += 1
    ya_nonfiction.sublanes.append(
        lane_from_genres(_db, library, [genres.History, genres.Social_Sciences],
                         display_name="History & Sociology",
                         priority=ya_nonfiction_priority, **ya_common_args))
    ya_nonfiction_priority += 1
    ya_nonfiction.sublanes.append(
        lane_from_genres(_db, library, [genres.Life_Strategies],
                         priority=ya_nonfiction_priority, **ya_common_args))
    ya_nonfiction_priority += 1
    ya_nonfiction.sublanes.append(
        lane_from_genres(_db, library, [genres.Religion_Spirituality],
                         priority=ya_nonfiction_priority, **ya_common_args))
    ya_nonfiction_priority += 1


    children_common_args = dict(common_args)
    children_common_args['audiences'] = CHILDREN

    children, ignore = create(
        _db, Lane, library=library,
        display_name="Children and Middle Grade",
        genres=[], fiction=None,
        sublanes=[],
        priority=priority,
        **children_common_args
    )
    priority += 1
    sublanes.append(children)

    children_priority = 0
    if include_best_sellers:
        children_best_sellers, ignore = create(
            _db, Lane, library=library,
            display_name="Best Sellers",
            priority=children_priority,
            **children_common_args
        )
        children_priority += 1
        children_best_sellers.list_datasource = nyt_data_source
        children.sublanes.append(children_best_sellers)

    picture_books, ignore = create(
        _db, Lane, library=library,
        display_name="Picture Books",
        target_age=(0,4), genres=[], fiction=None,
        priority=children_priority,
        languages=languages,
    )
    children_priority += 1
    children.sublanes.append(picture_books)

    easy_readers, ignore = create(
        _db, Lane, library=library,
        display_name="Easy Readers",
        target_age=(5,8), genres=[], fiction=None,
        priority=children_priority,
        languages=languages,
    )
    children_priority += 1
    children.sublanes.append(easy_readers)

    chapter_books, ignore = create(
        _db, Lane, library=library,
        display_name="Chapter Books",
        target_age=(9,12), genres=[], fiction=None,
        priority=children_priority,
        languages=languages,
    )
    children_priority += 1
    children.sublanes.append(chapter_books)

    children_poetry, ignore = create(
        _db, Lane, library=library,
        display_name="Poetry Books",
        priority=children_priority,
        **children_common_args
    )
    children_priority += 1
    children_poetry.add_genre(genres.Poetry.name)
    children.sublanes.append(children_poetry)

    children_folklore, ignore = create(
        _db, Lane, library=library,
        display_name="Folklore",
        priority=children_priority,
        **children_common_args
    )
    children_priority += 1
    children_folklore.add_genre(genres.Folklore.name)
    children.sublanes.append(children_folklore)

    children_fantasy, ignore = create(
        _db, Lane, library=library,
        display_name="Fantasy",
        fiction=True,
        priority=children_priority,
        **children_common_args
    )
    children_priority += 1
    children_fantasy.add_genre(genres.Fantasy.name)
    children.sublanes.append(children_fantasy)

    children_sf, ignore = create(
        _db, Lane, library=library,
        display_name="Science Fiction",
        fiction=True,
        priority=children_priority,
        **children_common_args
    )
    children_priority += 1
    children_sf.add_genre(genres.Science_Fiction.name)
    children.sublanes.append(children_sf)

    realistic_fiction, ignore = create(
        _db, Lane, library=library,
        display_name="Realistic Fiction",
        fiction=True,
        priority=children_priority,
        **children_common_args
    )
    children_priority += 1
    realistic_fiction.add_genre(genres.Literary_Fiction.name)
    children.sublanes.append(realistic_fiction)

    children_graphic_novels, ignore = create(
        _db, Lane, library=library,
        display_name="Comics & Graphic Novels",
        priority=children_priority,
        **children_common_args
    )
    children_priority += 1
    children_graphic_novels.add_genre(genres.Comics_Graphic_Novels.name)
    children.sublanes.append(children_graphic_novels)

    children_biography, ignore = create(
        _db, Lane, library=library,
        display_name="Biography",
        priority=children_priority,
        **children_common_args
    )
    children_priority += 1
    children_biography.add_genre(genres.Biography_Memoir.name)
    children.sublanes.append(children_biography)

    children_historical_fiction, ignore = create(
        _db, Lane, library=library,
        display_name="Historical Fiction",
        priority=children_priority,
        **children_common_args
    )
    children_priority += 1
    children_historical_fiction.add_genre(genres.Historical_Fiction.name)
    children.sublanes.append(children_historical_fiction)

    informational, ignore = create(
        _db, Lane, library=library,
        display_name="Informational Books",
        fiction=False, genres=[],
        priority=children_priority,
        **children_common_args
    )
    children_priority += 1
    informational.add_genre(genres.Biography_Memoir.name, inclusive=False)
    children.sublanes.append(informational)

    return priority
Example #58
0
    def test_libraries_post_create(self):
        class TestFileUpload(StringIO):
            headers = {"Content-Type": "image/png"}

        image_data = '\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x01\x03\x00\x00\x00%\xdbV\xca\x00\x00\x00\x06PLTE\xffM\x00\x01\x01\x01\x8e\x1e\xe5\x1b\x00\x00\x00\x01tRNS\xcc\xd24V\xfd\x00\x00\x00\nIDATx\x9cc`\x00\x00\x00\x02\x00\x01H\xaf\xa4q\x00\x00\x00\x00IEND\xaeB`\x82'

        original_validate = GeographicValidator().validate_geographic_areas

        class MockValidator(GeographicValidator):
            def __init__(self):
                self.was_called = False

            def validate_geographic_areas(self, values, db):
                self.was_called = True
                return original_validate(values, db)

        with self.request_context_with_admin("/", method="POST"):
            flask.request.form = MultiDict([
                ("name", "The New York Public Library"),
                ("short_name", "nypl"),
                ("library_description", "Short description of library"),
                (Configuration.WEBSITE_URL, "https://library.library/"),
                (Configuration.TINY_COLLECTION_LANGUAGES, ['ger']),
                (Configuration.LIBRARY_SERVICE_AREA,
                 ['06759', 'everywhere', 'MD', 'Boston, MA']),
                (Configuration.LIBRARY_FOCUS_AREA,
                 ['Manitoba', 'Broward County, FL', 'QC']),
                (Configuration.DEFAULT_NOTIFICATION_EMAIL_ADDRESS,
                 "*****@*****.**"),
                (Configuration.HELP_EMAIL, "*****@*****.**"),
                (Configuration.FEATURED_LANE_SIZE, "5"),
                (Configuration.DEFAULT_FACET_KEY_PREFIX +
                 FacetConstants.ORDER_FACET_GROUP_NAME,
                 FacetConstants.ORDER_RANDOM),
                (Configuration.ENABLED_FACETS_KEY_PREFIX +
                 FacetConstants.ORDER_FACET_GROUP_NAME + "_" +
                 FacetConstants.ORDER_TITLE, ''),
                (Configuration.ENABLED_FACETS_KEY_PREFIX +
                 FacetConstants.ORDER_FACET_GROUP_NAME + "_" +
                 FacetConstants.ORDER_RANDOM, ''),
            ])
            flask.request.files = MultiDict([
                (Configuration.LOGO, TestFileUpload(image_data)),
            ])
            validator = MockValidator()
            response = self.manager.admin_library_settings_controller.process_post(
                validator)
            eq_(response.status_code, 201)

        library = get_one(self._db, Library, short_name="nypl")
        eq_(library.uuid, response.response[0])
        eq_(library.name, "The New York Public Library")
        eq_(library.short_name, "nypl")
        eq_(
            "5",
            ConfigurationSetting.for_library(Configuration.FEATURED_LANE_SIZE,
                                             library).value)
        eq_(
            FacetConstants.ORDER_RANDOM,
            ConfigurationSetting.for_library(
                Configuration.DEFAULT_FACET_KEY_PREFIX +
                FacetConstants.ORDER_FACET_GROUP_NAME, library).value)
        eq_(
            json.dumps(
                [FacetConstants.ORDER_TITLE, FacetConstants.ORDER_RANDOM]),
            ConfigurationSetting.for_library(
                Configuration.ENABLED_FACETS_KEY_PREFIX +
                FacetConstants.ORDER_FACET_GROUP_NAME, library).value)
        eq_(
            "data:image/png;base64,%s" % base64.b64encode(image_data),
            ConfigurationSetting.for_library(Configuration.LOGO,
                                             library).value)
        eq_(validator.was_called, True)
        eq_(
            '{"CA": [], "US": ["06759", "everywhere", "MD", "Boston, MA"]}',
            ConfigurationSetting.for_library(
                Configuration.LIBRARY_SERVICE_AREA, library).value)
        eq_(
            '{"CA": ["Manitoba", "Quebec"], "US": ["Broward County, FL"]}',
            ConfigurationSetting.for_library(Configuration.LIBRARY_FOCUS_AREA,
                                             library).value)

        # When the library was created, default lanes were also created
        # according to its language setup. This library has one tiny
        # collection (not a good choice for a real library), so only
        # two lanes were created: "Other Languages" and then "German"
        # underneath it.
        [german, other_languages] = sorted(library.lanes,
                                           key=lambda x: x.display_name)
        eq_(None, other_languages.parent)
        eq_(['ger'], other_languages.languages)
        eq_(other_languages, german.parent)
        eq_(['ger'], german.languages)
Example #59
0
    def test_libraries_post_edit(self):
        # A library already exists.
        library = self._library("New York Public Library", "nypl")

        ConfigurationSetting.for_library(Configuration.FEATURED_LANE_SIZE,
                                         library).value = 5
        ConfigurationSetting.for_library(
            Configuration.DEFAULT_FACET_KEY_PREFIX +
            FacetConstants.ORDER_FACET_GROUP_NAME,
            library).value = FacetConstants.ORDER_RANDOM
        ConfigurationSetting.for_library(
            Configuration.ENABLED_FACETS_KEY_PREFIX +
            FacetConstants.ORDER_FACET_GROUP_NAME, library).value = json.dumps(
                [FacetConstants.ORDER_TITLE, FacetConstants.ORDER_RANDOM])
        ConfigurationSetting.for_library(Configuration.LOGO,
                                         library).value = "A tiny image"

        with self.request_context_with_admin("/", method="POST"):
            flask.request.form = MultiDict([
                ("uuid", library.uuid),
                ("name", "The New York Public Library"),
                ("short_name", "nypl"),
                (Configuration.FEATURED_LANE_SIZE, "20"),
                (Configuration.MINIMUM_FEATURED_QUALITY, "0.9"),
                (Configuration.WEBSITE_URL, "https://library.library/"),
                (Configuration.DEFAULT_NOTIFICATION_EMAIL_ADDRESS,
                 "*****@*****.**"),
                (Configuration.HELP_EMAIL, "*****@*****.**"),
                (Configuration.DEFAULT_FACET_KEY_PREFIX +
                 FacetConstants.ORDER_FACET_GROUP_NAME,
                 FacetConstants.ORDER_AUTHOR),
                (Configuration.ENABLED_FACETS_KEY_PREFIX +
                 FacetConstants.ORDER_FACET_GROUP_NAME + "_" +
                 FacetConstants.ORDER_AUTHOR, ''),
                (Configuration.ENABLED_FACETS_KEY_PREFIX +
                 FacetConstants.ORDER_FACET_GROUP_NAME + "_" +
                 FacetConstants.ORDER_RANDOM, ''),
            ])
            flask.request.files = MultiDict([])
            response = self.manager.admin_library_settings_controller.process_post(
            )
            eq_(response.status_code, 200)

        library = get_one(self._db, Library, uuid=library.uuid)

        eq_(library.uuid, response.response[0])
        eq_(library.name, "The New York Public Library")
        eq_(library.short_name, "nypl")

        # The library-wide settings were updated.
        def val(x):
            return ConfigurationSetting.for_library(x, library).value

        eq_("https://library.library/", val(Configuration.WEBSITE_URL))
        eq_("*****@*****.**",
            val(Configuration.DEFAULT_NOTIFICATION_EMAIL_ADDRESS))
        eq_("*****@*****.**", val(Configuration.HELP_EMAIL))
        eq_("20", val(Configuration.FEATURED_LANE_SIZE))
        eq_("0.9", val(Configuration.MINIMUM_FEATURED_QUALITY))
        eq_(
            FacetConstants.ORDER_AUTHOR,
            val(Configuration.DEFAULT_FACET_KEY_PREFIX +
                FacetConstants.ORDER_FACET_GROUP_NAME))
        eq_(
            json.dumps(
                [FacetConstants.ORDER_AUTHOR, FacetConstants.ORDER_RANDOM]),
            val(Configuration.ENABLED_FACETS_KEY_PREFIX +
                FacetConstants.ORDER_FACET_GROUP_NAME))

        # The library-wide logo was not updated and has been left alone.
        eq_(
            "A tiny image",
            ConfigurationSetting.for_library(Configuration.LOGO,
                                             library).value)
    def admin_auth_services(self):
        if flask.request.method == 'GET':
            auth_services = []
            auth_service = get_one(self._db, AdminAuthenticationService)
            if auth_service and auth_service.provider == AdminAuthenticationService.GOOGLE_OAUTH:
                auth_services = [
                    dict(
                        name=auth_service.name,
                        provider=auth_service.provider,
                        url=auth_service.external_integration.url,
                        username=auth_service.external_integration.username,
                        password=auth_service.external_integration.password,
                        domains=json.loads(auth_service.external_integration.setting("domains").value),
                    )
                ]

            return dict(
                admin_auth_services=auth_services,
                providers=AdminAuthenticationService.PROVIDERS,
            )

        name = flask.request.form.get("name")
        if not name:
            return MISSING_ADMIN_AUTH_SERVICE_NAME

        provider = flask.request.form.get("provider")

        if provider and provider not in AdminAuthenticationService.PROVIDERS:
            return UNKNOWN_ADMIN_AUTH_SERVICE_PROVIDER

        is_new = False
        auth_service = get_one(self._db, AdminAuthenticationService)
        if auth_service:
            # Currently there can only be one admin auth service, and one already exists.
            if name != auth_service.name:
                return ADMIN_AUTH_SERVICE_NOT_FOUND

            if provider != auth_service.provider:
                return CANNOT_CHANGE_ADMIN_AUTH_SERVICE_PROVIDER

        else:
            if provider:
                auth_service, is_new = get_one_or_create(
                    self._db, AdminAuthenticationService, name=name, provider=provider
                )
            else:
                return NO_PROVIDER_FOR_NEW_ADMIN_AUTH_SERVICE

        # Only Google OAuth is supported for now.
        url = flask.request.form.get("url")
        username = flask.request.form.get("username")
        password = flask.request.form.get("password")
        domains = flask.request.form.get("domains")
        
        if not url or not username or not password or not domains:
            # If an admin auth service was created, make sure it
            # isn't saved in a incomplete state.
            self._db.rollback()
            return INCOMPLETE_ADMIN_AUTH_SERVICE_CONFIGURATION

        # Also make sure the domain list is valid JSON.
        try:
            json.loads(domains)
        except Exception:
            self._db.rollback()
            return INVALID_ADMIN_AUTH_DOMAIN_LIST

        integration = auth_service.external_integration
        integration.url = url
        integration.username = username
        integration.password = password
        integration.set_setting("domains", domains)

        if is_new:
            return Response(unicode(_("Success")), 201)
        else:
            return Response(unicode(_("Success")), 200)