def test_visible_sublanes(self): fantasy, ig = Genre.lookup(self._db, classifier.Fantasy) urban_fantasy, ig = Genre.lookup(self._db, classifier.Urban_Fantasy) humorous, ig = Genre.lookup(self._db, classifier.Humorous_Fiction) visible_sublane = Lane(self._db, "Humorous Fiction", genres=humorous) visible_grandchild = Lane(self._db, "Urban Fantasy", genres=urban_fantasy) invisible_sublane = Lane(self._db, "Fantasy", invisible=True, genres=fantasy, sublanes=[visible_grandchild], subgenre_behavior=Lane.IN_SAME_LANE) lane = Lane(self._db, "English", sublanes=[visible_sublane, invisible_sublane], subgenre_behavior=Lane.IN_SAME_LANE) eq_(2, len(lane.visible_sublanes)) assert visible_sublane in lane.visible_sublanes assert visible_grandchild in lane.visible_sublanes
def test_custom_sublanes(self): fantasy, ig = Genre.lookup(self._db, classifier.Fantasy) urban_fantasy, ig = Genre.lookup(self._db, classifier.Urban_Fantasy) urban_fantasy_lane = Lane(self._db, "Urban Fantasy", genres=urban_fantasy) fantasy_lane = Lane(self._db, "Fantasy", fantasy, genres=fantasy, subgenre_behavior=Lane.IN_SAME_LANE, sublanes=[urban_fantasy_lane]) eq_([urban_fantasy_lane], fantasy_lane.sublanes.lanes) # You can just give the name of a genre as a sublane and it # will work. fantasy_lane = Lane(self._db, "Fantasy", fantasy, genres=fantasy, subgenre_behavior=Lane.IN_SAME_LANE, sublanes="Urban Fantasy") eq_([["Urban Fantasy"]], [x.genre_names for x in fantasy_lane.sublanes.lanes])
def test_gather_matching_genres(self): self.fantasy, ig = Genre.lookup(self._db, classifier.Fantasy) self.urban_fantasy, ig = Genre.lookup(self._db, classifier.Urban_Fantasy) self.cooking, ig = Genre.lookup(self._db, classifier.Cooking) self.history, ig = Genre.lookup(self._db, classifier.History) # Fantasy contains three subgenres and is restricted to fiction. fantasy, default = Lane.gather_matching_genres( [self.fantasy], Lane.FICTION_DEFAULT_FOR_GENRE) eq_(4, len(fantasy)) eq_(True, default) fantasy, default = Lane.gather_matching_genres([self.fantasy], True) eq_(4, len(fantasy)) eq_(True, default) fantasy, default = Lane.gather_matching_genres([self.fantasy], True, [self.urban_fantasy]) eq_(3, len(fantasy)) eq_(True, default) # Attempting to create a contradiction (like nonfiction fantasy) # will create a lane broad enough to actually contain books fantasy, default = Lane.gather_matching_genres([self.fantasy], False) eq_(4, len(fantasy)) eq_(Lane.BOTH_FICTION_AND_NONFICTION, default) # Fantasy and history have conflicting fiction defaults, so # although we can make a lane that contains both, we can't # have it use the default value. assert_raises(UndefinedLane, Lane.gather_matching_genres, [self.fantasy, self.history], Lane.FICTION_DEFAULT_FOR_GENRE)
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) eq_("Search", info['name']) eq_("Search English/Deutsch Young Adult", info['description']) eq_("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) eq_("Search", info['name']) eq_("Search Science Fiction & Fantasy", info['description']) eq_("science-fiction-&-fantasy", info['tags'])
def test_all_matching_genres(self): fantasy, ig = Genre.lookup(self._db, classifier.Fantasy) cooking, ig = Genre.lookup(self._db, classifier.Cooking) matches = Lane.all_matching_genres(self._db, [fantasy, cooking]) names = sorted([x.name for x in matches]) eq_([ u'Cooking', u'Epic Fantasy', u'Fantasy', u'Historical Fantasy', u'Urban Fantasy' ], names)
def test_from_description(self): """Create a LaneList from a simple description.""" lanes = LaneList.from_description( self._db, None, [dict( full_name="Fiction", fiction=True, audiences=Classifier.AUDIENCE_ADULT, ), classifier.Fantasy, dict( full_name="Young Adult", fiction=Lane.BOTH_FICTION_AND_NONFICTION, audiences=Classifier.AUDIENCE_YOUNG_ADULT, ), ] ) fantasy_genre, ignore = Genre.lookup(self._db, classifier.Fantasy.name) urban_fantasy_genre, ignore = Genre.lookup(self._db, classifier.Urban_Fantasy.name) fiction = lanes.by_languages['']['Fiction'] young_adult = lanes.by_languages['']['Young Adult'] fantasy = lanes.by_languages['']['Fantasy'] urban_fantasy = lanes.by_languages['']['Urban Fantasy'] eq_(set([fantasy, fiction, young_adult]), set(lanes.lanes)) eq_("Fiction", fiction.name) eq_(set([Classifier.AUDIENCE_ADULT]), fiction.audiences) eq_([], fiction.genre_ids) eq_(True, fiction.fiction) eq_("Fantasy", fantasy.name) eq_(set(), fantasy.audiences) expect = set(x.name for x in fantasy_genre.self_and_subgenres) eq_(expect, set(fantasy.genre_names)) eq_(True, fantasy.fiction) eq_("Urban Fantasy", urban_fantasy.name) eq_(set(), urban_fantasy.audiences) eq_([urban_fantasy_genre.id], urban_fantasy.genre_ids) eq_(True, urban_fantasy.fiction) eq_("Young Adult", young_adult.name) eq_(set([Classifier.AUDIENCE_YOUNG_ADULT]), young_adult.audiences) eq_([], young_adult.genre_ids) eq_(Lane.BOTH_FICTION_AND_NONFICTION, young_adult.fiction)
def test_custom_lanes_conflict_with_subgenre_sublanes(self): fantasy, ig = Genre.lookup(self._db, classifier.Fantasy) urban_fantasy, ig = Genre.lookup(self._db, classifier.Urban_Fantasy) urban_fantasy_lane = Lane( self._db, "Urban Fantasy", genres=urban_fantasy) assert_raises(UndefinedLane, Lane, self._db, "Fantasy", fantasy, genres=fantasy, audiences=Lane.AUDIENCE_YOUNG_ADULT, subgenre_behavior=Lane.IN_SUBLANES, sublanes=[urban_fantasy_lane] )
def test_get_search_target(self): fantasy, ig = Genre.lookup(self._db, classifier.Fantasy) lane = Lane( self._db, "YA Fantasy", genres=fantasy, languages='eng', audiences=Lane.AUDIENCE_YOUNG_ADULT, age_range=[15,16], subgenre_behavior=Lane.IN_SUBLANES ) sublanes = lane.sublanes.lanes names = sorted([x.name for x in sublanes]) eq_(["Epic Fantasy", "Historical Fantasy", "Urban Fantasy"], names) # To start with, none of the lanes are searchable. eq_(None, lane.search_target) eq_(None, sublanes[0].search_target) # If we make a lane searchable, suddenly there's a search target. lane.searchable = True eq_(lane, lane.search_target) # The searchable lane also becomes the search target for its # children. eq_(lane, sublanes[0].search_target)
def _lane(self, display_name=None, library=None, parent=None, genres=None, languages=None, fiction=None ): display_name = display_name or self._str library = library or self._default_library lane, is_new = create( self._db, Lane, library=library, parent=parent, display_name=display_name, fiction=fiction ) if is_new and parent: lane.priority = len(parent.sublanes)-1 if genres: if not isinstance(genres, list): genres = [genres] for genre in genres: if isinstance(genre, basestring): genre, ignore = Genre.lookup(self._db, genre) lane.genres.append(genre) if languages: if not isinstance(languages, list): languages = [languages] lane.languages = languages return lane
def _lane(self, display_name=None, library=None, parent=None, genres=None, languages=None, fiction=None): display_name = display_name or self._str library = library or self._default_library lane, is_new = get_one_or_create( self._db, Lane, library=library, parent=parent, display_name=display_name, create_method_kwargs=dict(fiction=fiction)) if is_new and parent: lane.priority = len(parent.sublanes) - 1 if genres: if not isinstance(genres, list): genres = [genres] for genre in genres: if isinstance(genre, basestring): genre, ignore = Genre.lookup(self._db, genre) lane.genres.append(genre) if languages: if not isinstance(languages, list): languages = [languages] lane.languages = languages return lane
def load_genre(cls, _db, descriptor): """Turn some kind of genre descriptor into a (Genre, GenreData) 2-tuple. The descriptor might be a 2-tuple, a 3-tuple, a Genre object or a GenreData object. """ if isinstance(descriptor, tuple): if len(descriptor) == 2: genre, subgenres = descriptor else: genre, subgenres, audience_restriction = descriptor else: genre = descriptor if isinstance(genre, GenreData): genredata = genre else: if isinstance(genre, Genre): genre_name = genre.name else: genre_name = genre # It's in the database--just make sure it's not an old entry # that shouldn't be in the database anymore. genredata = classifier.genres.get(genre_name) if not isinstance(genre, Genre): genre, ignore = Genre.lookup(_db, genre) return genre, genredata
def test_gather_matching_genres(self): self.fantasy, ig = Genre.lookup(self._db, classifier.Fantasy) self.urban_fantasy, ig = Genre.lookup(self._db, classifier.Urban_Fantasy) self.cooking, ig = Genre.lookup(self._db, classifier.Cooking) self.history, ig = Genre.lookup(self._db, classifier.History) # Fantasy contains three subgenres and is restricted to fiction. fantasy, default = Lane.gather_matching_genres( self._db, [self.fantasy], Lane.FICTION_DEFAULT_FOR_GENRE) eq_(4, len(fantasy)) eq_(True, default) fantasy, default = Lane.gather_matching_genres(self._db, [self.fantasy], True) eq_(4, len(fantasy)) eq_(True, default) fantasy, default = Lane.gather_matching_genres(self._db, [self.fantasy], True, [self.urban_fantasy]) eq_(3, len(fantasy)) eq_(True, default) # If there are only exclude_genres available, then it and its # subgenres are ignored while every OTHER genre is set. genres, default = Lane.gather_matching_genres(self._db, [], True, [self.fantasy]) eq_(False, any([g for g in self.fantasy.self_and_subgenres if g in genres])) # According to known fiction status, that is. eq_(True, all([g.default_fiction == True for g in genres])) # Attempting to create a contradiction (like nonfiction fantasy) # will create a lane broad enough to actually contain books fantasy, default = Lane.gather_matching_genres(self._db, [self.fantasy], False) eq_(4, len(fantasy)) eq_(Lane.BOTH_FICTION_AND_NONFICTION, default) # Fantasy and history have conflicting fiction defaults, so # although we can make a lane that contains both, we can't # have it use the default value. assert_raises(UndefinedLane, Lane.gather_matching_genres, self._db, [self.fantasy, self.history], Lane.FICTION_DEFAULT_FOR_GENRE)
def test_visible_parent(self): fantasy, ig = Genre.lookup(self._db, classifier.Fantasy) urban_fantasy, ig = Genre.lookup(self._db, classifier.Urban_Fantasy) sublane = Lane( self._db, "Urban Fantasy", genres=urban_fantasy) invisible_parent = Lane( self._db, "Fantasy", invisible=True, genres=fantasy, sublanes=[sublane], subgenre_behavior=Lane.IN_SAME_LANE) visible_grandparent = Lane( self._db, "English", sublanes=[invisible_parent], subgenre_behavior=Lane.IN_SAME_LANE) eq_(sublane.visible_parent(), visible_grandparent) eq_(invisible_parent.visible_parent(), visible_grandparent) eq_(visible_grandparent.visible_parent(), None)
def test_visible_ancestors(self): fantasy, ig = Genre.lookup(self._db, classifier.Fantasy) urban_fantasy, ig = Genre.lookup(self._db, classifier.Urban_Fantasy) lane = Lane( self._db, "Urban Fantasy", genres=urban_fantasy) visible_parent = Lane( self._db, "Fantasy", genres=fantasy, sublanes=[lane], subgenre_behavior=Lane.IN_SAME_LANE) invisible_grandparent = Lane( self._db, "English", invisible=True, sublanes=[visible_parent], subgenre_behavior=Lane.IN_SAME_LANE) visible_ancestor = Lane( self._db, "Books With Words", sublanes=[invisible_grandparent], subgenre_behavior=Lane.IN_SAME_LANE) eq_(lane.visible_ancestors(), [visible_parent, visible_ancestor])
def test_subgenres_become_sublanes(self): fantasy, ig = Genre.lookup(self._db, classifier.Fantasy) lane = Lane(self._db, "YA Fantasy", genres=fantasy, languages='eng', audiences=Lane.AUDIENCE_YOUNG_ADULT, age_range=[15, 16], subgenre_behavior=Lane.IN_SUBLANES) sublanes = lane.sublanes.lanes names = sorted([x.name for x in sublanes]) eq_(["Epic Fantasy", "Historical Fantasy", "Urban Fantasy"], names) # Sublanes inherit settings from their parent. assert all([x.languages == ['eng'] for x in sublanes]) assert all([x.age_range == [15, 16] for x in sublanes]) assert all([x.audiences == set(['Young Adult']) for x in sublanes])
def _work(self, title=None, authors=None, genre=None, language=None, audience=None, fiction=True, with_license_pool=False, with_open_access_download=False, quality=0.5, series=None, presentation_edition=None, collection=None, data_source_name=None): """Create a Work. For performance reasons, this method does not generate OPDS entries or calculate a presentation edition for the new Work. Tests that rely on this information being present should call _slow_work() instead, which takes more care to present the sort of Work that would be created in a real environment. """ pools = [] if with_open_access_download: with_license_pool = True language = language or "eng" title = unicode(title or self._str) audience = audience or Classifier.AUDIENCE_ADULT if audience == Classifier.AUDIENCE_CHILDREN and not data_source_name: # TODO: This is necessary because Gutenberg's childrens books # get filtered out at the moment. data_source_name = DataSource.OVERDRIVE elif not data_source_name: data_source_name = DataSource.GUTENBERG if fiction is None: fiction = True new_edition = False if not presentation_edition: new_edition = True presentation_edition = self._edition( title=title, language=language, authors=authors, with_license_pool=with_license_pool, with_open_access_download=with_open_access_download, data_source_name=data_source_name, series=series, collection=collection, ) if with_license_pool: presentation_edition, pool = presentation_edition if with_open_access_download: pool.open_access = True pools = [pool] else: pools = presentation_edition.license_pools work, ignore = get_one_or_create(self._db, Work, create_method_kwargs=dict( audience=audience, fiction=fiction, quality=quality), id=self._id) if genre: if not isinstance(genre, Genre): genre, ignore = Genre.lookup(self._db, genre, autocreate=True) work.genres = [genre] work.random = 0.5 work.set_presentation_edition(presentation_edition) if pools: # make sure the pool's presentation_edition is set, # bc loan tests assume that. if not work.license_pools: for pool in pools: work.license_pools.append(pool) for pool in pools: pool.set_presentation_edition() # This is probably going to be used in an OPDS feed, so # fake that the work is presentation ready. work.presentation_ready = True work.calculate_opds_entries(verbose=False) return work
def _work(self, title=None, authors=None, genre=None, language=None, audience=None, fiction=True, with_license_pool=False, with_open_access_download=False, quality=0.5, series=None, presentation_edition=None, collection=None): pool = None if with_open_access_download: with_license_pool = True language = language or "eng" title = unicode(title or self._str) audience = audience or Classifier.AUDIENCE_ADULT if audience == Classifier.AUDIENCE_CHILDREN: # TODO: This is necessary because Gutenberg's childrens books # get filtered out at the moment. data_source_name = DataSource.OVERDRIVE else: data_source_name = DataSource.GUTENBERG if fiction is None: fiction = True new_edition = False if not presentation_edition: new_edition = True presentation_edition = self._edition( title=title, language=language, authors=authors, with_license_pool=with_license_pool, with_open_access_download=with_open_access_download, data_source_name=data_source_name, series=series, collection=collection, ) if with_license_pool: presentation_edition, pool = presentation_edition else: pool = presentation_edition.license_pool if new_edition: presentation_edition.calculate_presentation() work, ignore = get_one_or_create(self._db, Work, create_method_kwargs=dict( audience=audience, fiction=fiction, quality=quality), id=self._id) if genre: if not isinstance(genre, Genre): genre, ignore = Genre.lookup(self._db, genre, autocreate=True) work.genres = [genre] work.random = 0.5 work.set_presentation_edition(presentation_edition) work.calculate_presentation_edition() if pool != None: # make sure the pool's presentation_edition is set, # bc loan tests assume that. if not work.license_pools: work.license_pools.append(pool) pool.set_presentation_edition() # This is probably going to be used in an OPDS feed, so # fake that the work is presentation ready. work.presentation_ready = True work.calculate_opds_entries(verbose=False) if with_open_access_download: pool.open_access = True return work
def setup(self): super(TestLanesQuery, self).setup() # Look up the Fantasy genre and some of its subgenres. self.fantasy, ig = Genre.lookup(self._db, classifier.Fantasy) self.epic_fantasy, ig = Genre.lookup(self._db, classifier.Epic_Fantasy) self.urban_fantasy, ig = Genre.lookup(self._db, classifier.Urban_Fantasy) # Look up the History genre and some of its subgenres. self.history, ig = Genre.lookup(self._db, classifier.History) self.african_history, ig = Genre.lookup(self._db, classifier.African_History) self.adult_works = {} self.ya_works = {} self.childrens_works = {} for genre in (self.fantasy, self.epic_fantasy, self.urban_fantasy, self.history, self.african_history): fiction = True if genre in (self.history, self.african_history): fiction = False # Create a number of books for each genre. adult_work = self._work( title="%s Adult" % genre.name, audience=Lane.AUDIENCE_ADULT, fiction=fiction, with_license_pool=True, genre=genre, ) self.adult_works[genre] = adult_work adult_work.simple_opds_entry = '<entry>' # Childrens and YA books need to be attached to a data # source other than Gutenberg, or they'll get filtered # out. ya_edition, lp = self._edition( title="%s YA" % genre.name, data_source_name=DataSource.OVERDRIVE, with_license_pool=True) ya_work = self._work( audience=Lane.AUDIENCE_YOUNG_ADULT, fiction=fiction, with_license_pool=True, presentation_edition=ya_edition, genre=genre, ) self.ya_works[genre] = ya_work ya_work.simple_opds_entry = '<entry>' childrens_edition, lp = self._edition( title="%s Childrens" % genre.name, data_source_name=DataSource.OVERDRIVE, with_license_pool=True) childrens_work = self._work( audience=Lane.AUDIENCE_CHILDREN, fiction=fiction, with_license_pool=True, presentation_edition=childrens_edition, genre=genre, ) if genre == self.epic_fantasy: childrens_work.target_age = NumericRange(7, 9, '[]') else: childrens_work.target_age = NumericRange(8, 10, '[]') self.childrens_works[genre] = childrens_work childrens_work.simple_opds_entry = '<entry>' # Create generic 'Adults Only' fiction and nonfiction books # that are not in any genre. self.nonfiction = self._work(title="Generic Nonfiction", fiction=False, audience=Lane.AUDIENCE_ADULTS_ONLY, with_license_pool=True) self.nonfiction.simple_opds_entry = '<entry>' self.fiction = self._work(title="Generic Fiction", fiction=True, audience=Lane.AUDIENCE_ADULTS_ONLY, with_license_pool=True) self.fiction.simple_opds_entry = '<entry>' # Create a work of music. self.music = self._work( title="Music", fiction=False, audience=Lane.AUDIENCE_ADULT, with_license_pool=True, ) self.music.presentation_edition.medium = Edition.MUSIC_MEDIUM self.music.simple_opds_entry = '<entry>' # Create a Spanish book. self.spanish = self._work(title="Spanish book", fiction=True, audience=Lane.AUDIENCE_ADULT, with_license_pool=True, language='spa') self.spanish.simple_opds_entry = '<entry>' # Refresh the materialized views so that all these books are present # in them. SessionManager.refresh_materialized_views(self._db)
def _work(self, title=None, authors=None, genre=None, language=None, audience=None, fiction=True, with_license_pool=False, with_open_access_download=False, quality=0.5, series=None, presentation_edition=None, collection=None, data_source_name=None): """Create a Work. For performance reasons, this method does not generate OPDS entries or calculate a presentation edition for the new Work. Tests that rely on this information being present should call _slow_work() instead, which takes more care to present the sort of Work that would be created in a real environment. """ pools = [] if with_open_access_download: with_license_pool = True language = language or "eng" title = unicode(title or self._str) audience = audience or Classifier.AUDIENCE_ADULT if audience == Classifier.AUDIENCE_CHILDREN and not data_source_name: # TODO: This is necessary because Gutenberg's childrens books # get filtered out at the moment. data_source_name = DataSource.OVERDRIVE elif not data_source_name: data_source_name = DataSource.GUTENBERG if fiction is None: fiction = True new_edition = False if not presentation_edition: new_edition = True presentation_edition = self._edition( title=title, language=language, authors=authors, with_license_pool=with_license_pool, with_open_access_download=with_open_access_download, data_source_name=data_source_name, series=series, collection=collection, ) if with_license_pool: presentation_edition, pool = presentation_edition if with_open_access_download: pool.open_access = True pools = [pool] else: pools = presentation_edition.license_pools work, ignore = get_one_or_create( self._db, Work, create_method_kwargs=dict( audience=audience, fiction=fiction, quality=quality), id=self._id) if genre: if not isinstance(genre, Genre): genre, ignore = Genre.lookup(self._db, genre, autocreate=True) work.genres = [genre] work.random = 0.5 work.set_presentation_edition(presentation_edition) if pools: # make sure the pool's presentation_edition is set, # bc loan tests assume that. if not work.license_pools: for pool in pools: work.license_pools.append(pool) for pool in pools: pool.set_presentation_edition() # This is probably going to be used in an OPDS feed, so # fake that the work is presentation ready. work.presentation_ready = True work.calculate_opds_entries(verbose=False) return work