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"]
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]
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())]
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()))
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]
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)
def _genre(self, genre_data): expected_genre, ignore = Genre.lookup(self._db, genre_data.name) return expected_genre
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)