Exemplo n.º 1
0
    def test_search_info(self):
        # Searching this lane will use the language
        # and audience restrictions from the lane.
        lane = self._lane()
        lane.display_name = "Fiction"
        lane.languages = ["eng", "ger"]
        lane.audiences = [Classifier.AUDIENCE_YOUNG_ADULT]
        lane.fiction = True

        info = OpenSearchDocument.search_info(lane)
        assert "Search" == info["name"]
        assert "Search English/Deutsch Young Adult" == info["description"]
        assert "english/deutsch-young-adult" == info["tags"]

        # This lane is the root for a patron type, so searching
        # it will use all the lane's restrictions.
        root_lane = self._lane()
        root_lane.root_for_patron_type = ["A"]
        root_lane.display_name = "Science Fiction & Fantasy"
        sf, ignore = Genre.lookup(self._db, "Science Fiction")
        fantasy, ignore = Genre.lookup(self._db, "Fantasy")
        root_lane.add_genre(sf)
        root_lane.add_genre(fantasy)

        info = OpenSearchDocument.search_info(root_lane)
        assert "Search" == info["name"]
        assert "Search Science Fiction & Fantasy" == info["description"]
        assert "science-fiction-&-fantasy" == info["tags"]
Exemplo n.º 2
0
    def test_add_simplified_genres(self):
        work = self._work(with_license_pool=True)
        fantasy, ignore = Genre.lookup(self._db, "Fantasy", autocreate=True)
        romance, ignore = Genre.lookup(self._db, "Romance", autocreate=True)
        work.genres = [fantasy, romance]

        record = Record()
        Annotator.add_simplified_genres(record, work)
        fields = record.get_fields("650")
        [fantasy_field,
         romance_field] = sorted(fields, key=lambda x: x.get_subfields("a")[0])
        assert ["0", "7"] == fantasy_field.indicators
        assert "Fantasy" == fantasy_field.get_subfields("a")[0]
        assert "Library Simplified" == fantasy_field.get_subfields("2")[0]
        assert ["0", "7"] == romance_field.indicators
        assert "Romance" == romance_field.get_subfields("a")[0]
        assert "Library Simplified" == romance_field.get_subfields("2")[0]
Exemplo n.º 3
0
 def test_staff_genre_overrides_others(self):
     genre1, is_new = Genre.lookup(self._db, "Psychology")
     genre2, is_new = Genre.lookup(self._db, "Cooking")
     subject1 = self._subject(type="type1", identifier="subject1")
     subject1.genre = genre1
     subject2 = self._subject(type="type2", identifier="subject2")
     subject2.genre = genre2
     source = DataSource.lookup(self._db, DataSource.AXIS_360)
     staff_source = DataSource.lookup(self._db, DataSource.LIBRARY_STAFF)
     classification1 = self._classification(
         identifier=self.identifier, subject=subject1, data_source=source, weight=10
     )
     classification2 = self._classification(
         identifier=self.identifier,
         subject=subject2,
         data_source=staff_source,
         weight=1,
     )
     self.classifier.add(classification1)
     self.classifier.add(classification2)
     (genre_weights, fiction, audience, target_age) = self.classifier.classify()
     assert [genre2.name] == [genre.name for genre in list(genre_weights.keys())]
Exemplo n.º 4
0
 def test_staff_none_genre_overrides_others(self):
     source = DataSource.lookup(self._db, DataSource.AXIS_360)
     staff_source = DataSource.lookup(self._db, DataSource.LIBRARY_STAFF)
     genre1, is_new = Genre.lookup(self._db, "Poetry")
     subject1 = self._subject(type="type1", identifier="subject1")
     subject1.genre = genre1
     subject2 = self._subject(
         type=Subject.SIMPLIFIED_GENRE, identifier=SimplifiedGenreClassifier.NONE
     )
     classification1 = self._classification(
         identifier=self.identifier, subject=subject1, data_source=source, weight=10
     )
     classification2 = self._classification(
         identifier=self.identifier,
         subject=subject2,
         data_source=staff_source,
         weight=1,
     )
     self.classifier.add(classification1)
     self.classifier.add(classification2)
     (genre_weights, fiction, audience, target_age) = self.classifier.classify()
     assert 0 == len(list(genre_weights.keys()))
Exemplo n.º 5
0
    def test_end_to_end(self):
        # Search mock
        class MockSearchIndex:
            removed = []

            def remove_work(self, work):
                self.removed.append(work)

        # First, create three works.

        # This work has a license pool.
        has_license_pool = self._work(with_license_pool=True)

        # This work had a license pool and then lost it.
        had_license_pool = self._work(with_license_pool=True)
        self._db.delete(had_license_pool.license_pools[0])

        # This work never had a license pool.
        never_had_license_pool = self._work(with_license_pool=False)

        # Each work has a presentation edition -- keep track of these
        # for later.
        works = self._db.query(Work)
        presentation_editions = [x.presentation_edition for x in works]

        # If and when Work gets database-level cascading deletes, this
        # is where they will all be triggered, with no chance that an
        # ORM-level delete is doing the work. So let's verify that all
        # of the cascades work.

        # First, set up some related items for each Work.

        # Each work is assigned to a genre.
        genre, ignore = Genre.lookup(self._db, "Science Fiction")
        for work in works:
            work.genres = [genre]

        # Each work is on the same CustomList.
        l, ignore = self._customlist("a list", num_entries=0)
        for work in works:
            l.add_entry(work)

        # Each work has a WorkCoverageRecord.
        for work in works:
            WorkCoverageRecord.add_for(work, operation="some operation")

        # Each work has a CachedFeed.
        for work in works:
            feed = CachedFeed(work=work,
                              type="page",
                              content="content",
                              pagination="",
                              facets="")
            self._db.add(feed)

        # Also create a CachedFeed that has no associated Work.
        workless_feed = CachedFeed(work=None,
                                   type="page",
                                   content="content",
                                   pagination="",
                                   facets="")
        self._db.add(workless_feed)

        self._db.commit()

        # Run the reaper.
        s = MockSearchIndex()
        m = WorkReaper(self._db, search_index_client=s)
        print(m.search_index_client)
        m.run_once()

        # Search index was updated
        assert 2 == len(s.removed)
        assert has_license_pool not in s.removed
        assert had_license_pool in s.removed
        assert never_had_license_pool in s.removed

        # Only the work with a license pool remains.
        assert [has_license_pool] == [x for x in works]

        # The presentation editions are still around, since they might
        # theoretically be used by other parts of the system.
        all_editions = self._db.query(Edition).all()
        for e in presentation_editions:
            assert e in all_editions

        # The surviving work is still assigned to the Genre, and still
        # has WorkCoverageRecords.
        assert [has_license_pool] == genre.works
        surviving_records = self._db.query(WorkCoverageRecord)
        assert surviving_records.count() > 0
        assert all(x.work == has_license_pool for x in surviving_records)

        # The CustomListEntries still exist, but two of them have lost
        # their work.
        assert 2 == len([x for x in l.entries if not x.work])
        assert [has_license_pool] == [x.work for x in l.entries if x.work]

        # The CachedFeeds associated with the reaped Works have been
        # deleted. The surviving Work still has one, and the
        # CachedFeed that didn't have a work in the first place is
        # unaffected.
        feeds = self._db.query(CachedFeed).all()
        assert [workless_feed] == [x for x in feeds if not x.work]
        assert [has_license_pool] == [x.work for x in feeds if x.work]
Exemplo n.º 6
0
    def edit_classifications(self, identifier_type, identifier):
        """Edit a work's audience, target age, fiction status, and genres."""
        self.require_librarian(flask.request.library)

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

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

        # Previous staff classifications
        primary_identifier = work.presentation_edition.primary_identifier
        old_classifications = self._db \
            .query(Classification) \
            .join(Subject) \
            .filter(
                Classification.identifier == primary_identifier,
                Classification.data_source == staff_data_source
            )
        old_genre_classifications = old_classifications \
            .filter(Subject.genre_id != None)
        old_staff_genres = [
            c.subject.genre.name for c in old_genre_classifications
            if c.subject.genre
        ]
        old_computed_genres = [
            work_genre.genre.name for work_genre in work.work_genres
        ]

        # New genres should be compared to previously computed genres
        new_genres = flask.request.form.getlist("genres")
        genres_changed = sorted(new_genres) != sorted(old_computed_genres)

        # Update audience
        new_audience = flask.request.form.get("audience")
        if new_audience != work.audience:
            # Delete all previous staff audience classifications
            for c in old_classifications:
                if c.subject.type == Subject.FREEFORM_AUDIENCE:
                    self._db.delete(c)

            # Create a new classification with a high weight
            primary_identifier.classify(
                data_source=staff_data_source,
                subject_type=Subject.FREEFORM_AUDIENCE,
                subject_identifier=new_audience,
                weight=WorkController.STAFF_WEIGHT,
            )

        # Update target age if present
        new_target_age_min = flask.request.form.get("target_age_min")
        new_target_age_min = int(
            new_target_age_min) if new_target_age_min else None
        new_target_age_max = flask.request.form.get("target_age_max")
        new_target_age_max = int(
            new_target_age_max) if new_target_age_max else None
        if new_target_age_max < new_target_age_min:
            return INVALID_EDIT.detailed(
                _("Minimum target age must be less than maximum target age."))

        if work.target_age:
            old_target_age_min = work.target_age.lower
            old_target_age_max = work.target_age.upper
        else:
            old_target_age_min = None
            old_target_age_max = None
        if new_target_age_min != old_target_age_min or new_target_age_max != old_target_age_max:
            # Delete all previous staff target age classifications
            for c in old_classifications:
                if c.subject.type == Subject.AGE_RANGE:
                    self._db.delete(c)

            # Create a new classification with a high weight - higher than audience
            if new_target_age_min and new_target_age_max:
                age_range_identifier = "%s-%s" % (new_target_age_min,
                                                  new_target_age_max)
                primary_identifier.classify(
                    data_source=staff_data_source,
                    subject_type=Subject.AGE_RANGE,
                    subject_identifier=age_range_identifier,
                    weight=WorkController.STAFF_WEIGHT * 100,
                )

        # Update fiction status
        # If fiction status hasn't changed but genres have changed,
        # we still want to ensure that there's a staff classification
        new_fiction = True if flask.request.form.get(
            "fiction") == "fiction" else False
        if new_fiction != work.fiction or genres_changed:
            # Delete previous staff fiction classifications
            for c in old_classifications:
                if c.subject.type == Subject.SIMPLIFIED_FICTION_STATUS:
                    self._db.delete(c)

            # Create a new classification with a high weight (higher than genre)
            fiction_term = "Fiction" if new_fiction else "Nonfiction"
            classification = primary_identifier.classify(
                data_source=staff_data_source,
                subject_type=Subject.SIMPLIFIED_FICTION_STATUS,
                subject_identifier=fiction_term,
                weight=WorkController.STAFF_WEIGHT,
            )
            classification.subject.fiction = new_fiction

        # Update genres
        # make sure all new genres are legit
        for name in new_genres:
            genre, is_new = Genre.lookup(self._db, name)
            if not isinstance(genre, Genre):
                return GENRE_NOT_FOUND
            if genres[name].is_fiction is not None and genres[
                    name].is_fiction != new_fiction:
                return INCOMPATIBLE_GENRE
            if name == "Erotica" and new_audience != "Adults Only":
                return EROTICA_FOR_ADULTS_ONLY

        if genres_changed:
            # delete existing staff classifications for genres that aren't being kept
            for c in old_genre_classifications:
                if c.subject.genre.name not in new_genres:
                    self._db.delete(c)

            # add new staff classifications for new genres
            for genre in new_genres:
                if genre not in old_staff_genres:
                    classification = primary_identifier.classify(
                        data_source=staff_data_source,
                        subject_type=Subject.SIMPLIFIED_GENRE,
                        subject_identifier=genre,
                        weight=WorkController.STAFF_WEIGHT)

            # add NONE genre classification if we aren't keeping any genres
            if len(new_genres) == 0:
                primary_identifier.classify(
                    data_source=staff_data_source,
                    subject_type=Subject.SIMPLIFIED_GENRE,
                    subject_identifier=SimplifiedGenreClassifier.NONE,
                    weight=WorkController.STAFF_WEIGHT)
            else:
                # otherwise delete existing NONE genre classification
                none_classifications = self._db \
                    .query(Classification) \
                    .join(Subject) \
                    .filter(
                        Classification.identifier == primary_identifier,
                        Subject.identifier == SimplifiedGenreClassifier.NONE
                    ) \
                    .all()
                for c in none_classifications:
                    self._db.delete(c)

        # Update presentation
        policy = PresentationCalculationPolicy(classify=True,
                                               regenerate_opds_entries=True,
                                               regenerate_marc_record=True,
                                               update_search_index=True)
        work.calculate_presentation(policy=policy)

        return Response("", 200)
Exemplo n.º 7
0
 def _genre(self, genre_data):
     expected_genre, ignore = Genre.lookup(self._db, genre_data.name)
     return expected_genre
Exemplo n.º 8
0
    def test_edit_classifications(self):
        # start with a couple genres based on BISAC classifications from Axis 360
        work = self.english_1
        [lp] = work.license_pools
        primary_identifier = work.presentation_edition.primary_identifier
        work.audience = "Adult"
        work.fiction = True
        axis_360 = DataSource.lookup(self._db, DataSource.AXIS_360)
        classification1 = primary_identifier.classify(
            data_source=axis_360,
            subject_type=Subject.BISAC,
            subject_identifier="FICTION / Horror",
            weight=1
        )
        classification2 = primary_identifier.classify(
            data_source=axis_360,
            subject_type=Subject.BISAC,
            subject_identifier="FICTION / Science Fiction / Time Travel",
            weight=1
        )
        genre1, ignore = Genre.lookup(self._db, "Horror")
        genre2, ignore = Genre.lookup(self._db, "Science Fiction")
        work.genres = [genre1, genre2]

        # make no changes
        with self.app.test_request_context("/"):
            flask.request.form = MultiDict([
                ("audience", "Adult"),
                ("fiction", "fiction"),
                ("genres", "Horror"),
                ("genres", "Science Fiction")
            ])
            requested_genres = flask.request.form.getlist("genres")
            response = self.manager.admin_work_controller.edit_classifications(lp.data_source.name, lp.identifier.type, lp.identifier.identifier)
            eq_(response.status_code, 200)

        staff_data_source = DataSource.lookup(self._db, DataSource.LIBRARY_STAFF)
        genre_classifications = self._db \
            .query(Classification) \
            .join(Subject) \
            .filter(
                Classification.identifier == primary_identifier,
                Classification.data_source == staff_data_source,
                Subject.genre_id != None
            )
        staff_genres = [
            c.subject.genre.name 
            for c in genre_classifications 
            if c.subject.genre
        ]
        eq_(staff_genres, [])
        eq_("Adult", work.audience)
        eq_(18, work.target_age.lower)
        eq_(None, work.target_age.upper)
        eq_(True, work.fiction)

        # remove all genres
        with self.app.test_request_context("/"):
            flask.request.form = MultiDict([
                ("audience", "Adult"),
                ("fiction", "fiction")
            ])
            response = self.manager.admin_work_controller.edit_classifications(lp.data_source.name, lp.identifier.type, lp.identifier.identifier)
            eq_(response.status_code, 200)

        primary_identifier = work.presentation_edition.primary_identifier
        staff_data_source = DataSource.lookup(self._db, DataSource.LIBRARY_STAFF)
        none_classification_count = self._db \
            .query(Classification) \
            .join(Subject) \
            .filter(
                Classification.identifier == primary_identifier,
                Classification.data_source == staff_data_source,
                Subject.identifier == SimplifiedGenreClassifier.NONE
            ) \
            .all()
        eq_(1, len(none_classification_count))
        eq_("Adult", work.audience)
        eq_(18, work.target_age.lower)
        eq_(None, work.target_age.upper)
        eq_(True, work.fiction)

        # completely change genres
        with self.app.test_request_context("/"):
            flask.request.form = MultiDict([
                ("audience", "Adult"),
                ("fiction", "fiction"),
                ("genres", "Drama"),
                ("genres", "Urban Fantasy"),
                ("genres", "Women's Fiction")
            ])
            requested_genres = flask.request.form.getlist("genres")
            response = self.manager.admin_work_controller.edit_classifications(lp.data_source.name, lp.identifier.type, lp.identifier.identifier)
            eq_(response.status_code, 200)
            
        new_genre_names = [work_genre.genre.name for work_genre in work.work_genres]
        eq_(sorted(new_genre_names), sorted(requested_genres))
        eq_("Adult", work.audience)
        eq_(18, work.target_age.lower)
        eq_(None, work.target_age.upper)
        eq_(True, work.fiction)

        # remove some genres and change audience and target age
        with self.app.test_request_context("/"):
            flask.request.form = MultiDict([
                ("audience", "Young Adult"),
                ("target_age_min", 16),
                ("target_age_max", 18),
                ("fiction", "fiction"),
                ("genres", "Urban Fantasy")
            ])
            requested_genres = flask.request.form.getlist("genres")
            response = self.manager.admin_work_controller.edit_classifications(lp.data_source.name, lp.identifier.type, lp.identifier.identifier)
            eq_(response.status_code, 200)

        # new_genre_names = self._db.query(WorkGenre).filter(WorkGenre.work_id == work.id).all()
        new_genre_names = [work_genre.genre.name for work_genre in work.work_genres]
        eq_(sorted(new_genre_names), sorted(requested_genres))
        eq_("Young Adult", work.audience)
        eq_(16, work.target_age.lower)
        eq_(19, work.target_age.upper)
        eq_(True, work.fiction)

        previous_genres = new_genre_names

        # try to add a nonfiction genre
        with self.app.test_request_context("/"):
            flask.request.form = MultiDict([
                ("audience", "Young Adult"),
                ("target_age_min", 16),
                ("target_age_max", 18),
                ("fiction", "fiction"),
                ("genres", "Cooking"),
                ("genres", "Urban Fantasy")
            ])
            response = self.manager.admin_work_controller.edit_classifications(lp.data_source.name, lp.identifier.type, lp.identifier.identifier)

        eq_(response, INCOMPATIBLE_GENRE)
        new_genre_names = [work_genre.genre.name for work_genre in work.work_genres]
        eq_(sorted(new_genre_names), sorted(previous_genres))
        eq_("Young Adult", work.audience)
        eq_(16, work.target_age.lower)
        eq_(19, work.target_age.upper)
        eq_(True, work.fiction)

        # try to add Erotica
        with self.app.test_request_context("/"):
            flask.request.form = MultiDict([
                ("audience", "Young Adult"),
                ("target_age_min", 16),
                ("target_age_max", 18),
                ("fiction", "fiction"),
                ("genres", "Erotica"),
                ("genres", "Urban Fantasy")
            ])
            response = self.manager.admin_work_controller.edit_classifications(lp.data_source.name, lp.identifier.type, lp.identifier.identifier)
            eq_(response, EROTICA_FOR_ADULTS_ONLY)

        new_genre_names = [work_genre.genre.name for work_genre in work.work_genres]
        eq_(sorted(new_genre_names), sorted(previous_genres))
        eq_("Young Adult", work.audience)
        eq_(16, work.target_age.lower)
        eq_(19, work.target_age.upper)
        eq_(True, work.fiction)

        # try to set min target age greater than max target age
        # othe edits should not go through
        with self.app.test_request_context("/"):
            flask.request.form = MultiDict([
                ("audience", "Young Adult"),
                ("target_age_min", 16),
                ("target_age_max", 14),
                ("fiction", "nonfiction"),
                ("genres", "Cooking")
            ])
            response = self.manager.admin_work_controller.edit_classifications(lp.data_source.name, lp.identifier.type, lp.identifier.identifier)        
            eq_(400, response.status_code)
            eq_(INVALID_EDIT.uri, response.uri)

        new_genre_names = [work_genre.genre.name for work_genre in work.work_genres]
        eq_(sorted(new_genre_names), sorted(previous_genres))
        eq_(True, work.fiction)        

        # change to nonfiction with nonfiction genres and new target age
        with self.app.test_request_context("/"):
            flask.request.form = MultiDict([
                ("audience", "Young Adult"),
                ("target_age_min", 15),
                ("target_age_max", 17),
                ("fiction", "nonfiction"),
                ("genres", "Cooking")
            ])
            requested_genres = flask.request.form.getlist("genres")
            response = self.manager.admin_work_controller.edit_classifications(lp.data_source.name, lp.identifier.type, lp.identifier.identifier)

        new_genre_names = [work_genre.genre.name for work_genre in lp.work.work_genres]
        eq_(sorted(new_genre_names), sorted(requested_genres))
        eq_("Young Adult", work.audience)
        eq_(15, work.target_age.lower)
        eq_(18, work.target_age.upper)
        eq_(False, work.fiction)

        # set to Adult and make sure that target ages is set automatically
        with self.app.test_request_context("/"):
            flask.request.form = MultiDict([
                ("audience", "Adult"),
                ("fiction", "nonfiction"),
                ("genres", "Cooking")
            ])
            requested_genres = flask.request.form.getlist("genres")
            response = self.manager.admin_work_controller.edit_classifications(lp.data_source.name, lp.identifier.type, lp.identifier.identifier)

        eq_("Adult", work.audience)
        eq_(18, work.target_age.lower)
        eq_(None, work.target_age.upper)
    def test_update_genres(self):
        # start with a couple genres
        [lp] = self.english_1.license_pools
        genre, ignore = Genre.lookup(self._db, "Occult Horror")
        lp.work.genres = [genre]

        # change genres
        with self.app.test_request_context("/"):
            requested_genres = ["Drama", "Urban Fantasy", "Women's Fiction"]
            form = MultiDict()
            for genre in requested_genres:
                form.add("genres", genre)
            flask.request.form = form
            response = self.manager.admin_work_controller.update_genres(lp.data_source.name, lp.identifier.identifier)

        new_genre_names = [work_genre.genre.name for work_genre in lp.work.work_genres]
        eq_(len(new_genre_names), len(requested_genres))
        for genre in requested_genres:
            eq_(True, genre in new_genre_names)

        # remove a genre
        with self.app.test_request_context("/"):
            requested_genres = ["Drama", "Women's Fiction"]
            form = MultiDict()
            for genre in requested_genres:
                form.add("genres", genre)
            flask.request.form = form
            response = self.manager.admin_work_controller.update_genres(lp.data_source.name, lp.identifier.identifier)

        new_genre_names = [work_genre.genre.name for work_genre in lp.work.work_genres]
        eq_(len(new_genre_names), len(requested_genres))
        for genre in requested_genres:
            eq_(True, genre in new_genre_names)

        previous_genres = requested_genres

        # try to add a nonfiction genre
        with self.app.test_request_context("/"):
            requested_genres = ["Drama", "Women's Fiction", "Cooking"]
            form = MultiDict()
            for genre in requested_genres:
                form.add("genres", genre)
            flask.request.form = form
            response = self.manager.admin_work_controller.update_genres(lp.data_source.name, lp.identifier.identifier)

        eq_(response, INCOMPATIBLE_GENRE)
        new_genre_names = [work_genre.genre.name for work_genre in lp.work.work_genres]
        eq_(len(new_genre_names), len(previous_genres))
        for genre in previous_genres:
            eq_(True, genre in new_genre_names)

        # try to add a nonexistent genre
        with self.app.test_request_context("/"):
            requested_genres = ["Drama", "Women's Fiction", "Epic Military Memoirs"]
            form = MultiDict()
            for genre in requested_genres:
                form.add("genres", genre)
            flask.request.form = form
            response = self.manager.admin_work_controller.update_genres(lp.data_source.name, lp.identifier.identifier)

        eq_(response, GENRE_NOT_FOUND)
        new_genre_names = [work_genre.genre.name for work_genre in lp.work.work_genres]
        eq_(len(new_genre_names), len(previous_genres))
        for genre in previous_genres:
            eq_(True, genre in new_genre_names)