From df16c85e52d47ea352f3bf1d8ad79e7843fe4ba3 Mon Sep 17 00:00:00 2001 From: Atte Moisio Date: Wed, 14 Feb 2024 20:23:42 +0200 Subject: [PATCH] Implement language facet for OPDS feed With a hard-coded list of languages: All, Finnish, Swedish, English, Others. When creating a library, this will be the default list of language facets. Default selection is 'All'. Instead of hard-coded language list, would also be possible to make the list fully customizable by library admins. --- api/app.py | 10 ++++++++- api/lanes.py | 1 + core/configuration/library.py | 28 ++++++++++++++++++++++++++ core/external_search.py | 24 +++++++++++++++++++++- core/facets.py | 26 ++++++++++++++++++++++++ core/lane.py | 38 +++++++++++++++++++++++++++++++++++ 6 files changed, 125 insertions(+), 2 deletions(-) diff --git a/api/app.py b/api/app.py index 9e3daf8a6..0f61c1cfa 100644 --- a/api/app.py +++ b/api/app.py @@ -155,4 +155,12 @@ def run(url=None): logging.info("Starting app on %s:%s", host, port) sslContext = "adhoc" if scheme == "https" else None - app.run(debug=debug, host=host, port=port, threaded=True, ssl_context=sslContext) + app.run( + debug=debug, + host=host, + port=port, + threaded=True, + ssl_context=sslContext, + use_reloader=False, + use_debugger=False, + ) diff --git a/api/lanes.py b/api/lanes.py index 13f269bde..4479426e4 100644 --- a/api/lanes.py +++ b/api/lanes.py @@ -1356,6 +1356,7 @@ class CrawlableFacets(Facets): Facets.COLLECTION_FACET_GROUP_NAME: Facets.COLLECTION_FULL, Facets.DISTRIBUTOR_FACETS_GROUP_NAME: Facets.DISTRIBUTOR_ALL, Facets.COLLECTION_NAME_FACETS_GROUP_NAME: Facets.COLLECTION_NAME_ALL, + Facets.LANGUAGE_FACET_GROUP_NAME: Facets.LANGUAGE_ALL, } @classmethod diff --git a/core/configuration/library.py b/core/configuration/library.py index eddf736b9..7e79effd0 100644 --- a/core/configuration/library.py +++ b/core/configuration/library.py @@ -264,6 +264,34 @@ class LibrarySettings(BaseSettings): skip=True, ), ) + # Finland + facets_enabled_language: list[str] = FormField( + FacetConstants.DEFAULT_ENABLED_FACETS[FacetConstants.LANGUAGE_FACET_GROUP_NAME], + form=LibraryConfFormItem( + label="Allow patrons to filter language to", + type=ConfigurationFormItemType.MENU, + options={ + facet: FacetConstants.FACET_DISPLAY_TITLES[facet] + for facet in FacetConstants.LANGUAGE_FACETS + }, + category="Lanes & Filters", + paired="facets_default_language", + level=Level.SYS_ADMIN_OR_MANAGER, + ), + ) + facets_default_language: str = FormField( + FacetConstants.LANGUAGE_ALL, + form=LibraryConfFormItem( + label="Default Language", + type=ConfigurationFormItemType.SELECT, + options={ + facet: FacetConstants.FACET_DISPLAY_TITLES[facet] + for facet in FacetConstants.LANGUAGE_FACETS + }, + category="Lanes & Filters", + skip=True, + ), + ) library_description: str | None = FormField( None, form=LibraryConfFormItem( diff --git a/core/external_search.py b/core/external_search.py index c3fe8af23..8479761c3 100644 --- a/core/external_search.py +++ b/core/external_search.py @@ -1768,8 +1768,10 @@ def from_worklist(cls, _db, worklist, facets): excluded_audiobook_data_sources = [DataSource.lookup(_db, x) for x in excluded] if library is None: allow_holds = True + facets_enabled_language = FacetConstants.LANGUAGE_ALL else: allow_holds = library.settings.allow_holds + facets_enabled_language = library.settings.facets_enabled_language return cls( collections, media, @@ -1784,6 +1786,7 @@ def from_worklist(cls, _db, worklist, facets): allow_holds=allow_holds, license_datasource=license_datasource_id, lane_building=True, + facets_enabled_language=facets_enabled_language, ) def __init__( @@ -1938,6 +1941,10 @@ def __init__( self.lane_building = kwargs.pop("lane_building", False) + self.facets_enabled_language = kwargs.pop( + "facets_enabled_language", FacetConstants.LANGUAGE_ALL + ) + # At this point there should be no keyword arguments -- you can't pass # whatever you want into this method. if kwargs: @@ -2065,7 +2072,22 @@ def build(self, _chain_filters=None): if self.media: f = chain(f, Terms(medium=scrub_list(self.media))) - if self.languages: + # Finland, logic for LANGUAGE_OTHERS + if self.languages == [FacetConstants.LANGUAGE_OTHERS]: + excluded_terms = [ + language + for language in self.facets_enabled_language + if language + not in { + FacetConstants.LANGUAGE_ALL, + FacetConstants.LANGUAGE_OTHERS, + } + ] + exclusion_query = Bool( + must_not=[Terms(language=scrub_list(excluded_terms))] + ) + f = chain(f, exclusion_query) + elif self.languages: f = chain(f, Terms(language=scrub_list(self.languages))) if self.fiction is not None: diff --git a/core/facets.py b/core/facets.py index 692967fca..652fdd450 100644 --- a/core/facets.py +++ b/core/facets.py @@ -30,6 +30,22 @@ class FacetConstants: AVAILABLE_OPEN_ACCESS, ] + # Finland + # Subset the collection by language. + LANGUAGE_FACET_GROUP_NAME = "language" + LANGUAGE_ALL = "all" + LANGUAGE_FINNISH = "fin" + LANGUAGE_SWEDISH = "swe" + LANGUAGE_ENGLISH = "eng" + LANGUAGE_OTHERS = "others" + LANGUAGE_FACETS: list[str] = [ + LANGUAGE_ALL, + LANGUAGE_FINNISH, + LANGUAGE_SWEDISH, + LANGUAGE_ENGLISH, + LANGUAGE_OTHERS, + ] + # The names of the order facets. ORDER_FACET_GROUP_NAME = "order" ORDER_TITLE = "title" @@ -66,6 +82,7 @@ class FacetConstants: COLLECTION_FACET_GROUP_NAME: COLLECTION_FACETS, AVAILABILITY_FACET_GROUP_NAME: AVAILABILITY_FACETS, ORDER_FACET_GROUP_NAME: ORDER_FACETS, + LANGUAGE_FACET_GROUP_NAME: LANGUAGE_FACETS, # Finland } GROUP_DISPLAY_TITLES = { @@ -74,6 +91,7 @@ class FacetConstants: COLLECTION_FACET_GROUP_NAME: _("Collection"), DISTRIBUTOR_FACETS_GROUP_NAME: _("Distributor"), COLLECTION_NAME_FACETS_GROUP_NAME: _("Collection Name"), + LANGUAGE_FACET_GROUP_NAME: _("Language"), # Finland } GROUP_DESCRIPTIONS = { @@ -84,6 +102,7 @@ class FacetConstants: COLLECTION_NAME_FACETS_GROUP_NAME: _( "Allow patrons to filter by collection name" ), + LANGUAGE_FACET_GROUP_NAME: _("Allow patrons to filter by language"), # Finland } FACET_DISPLAY_TITLES = { @@ -98,6 +117,11 @@ class FacetConstants: AVAILABLE_OPEN_ACCESS: _("Yours to keep"), COLLECTION_FULL: _("Everything"), COLLECTION_FEATURED: _("Popular Books"), + LANGUAGE_ALL: _("All"), + LANGUAGE_FINNISH: _("Finnish"), + LANGUAGE_SWEDISH: _("Swedish"), + LANGUAGE_ENGLISH: _("English"), + LANGUAGE_OTHERS: _("Others"), } # For titles generated based on some runtime value @@ -118,6 +142,7 @@ class FacetConstants: COLLECTION_FACET_GROUP_NAME: [COLLECTION_FULL, COLLECTION_FEATURED], DISTRIBUTOR_FACETS_GROUP_NAME: [DISTRIBUTOR_ALL], COLLECTION_NAME_FACETS_GROUP_NAME: [COLLECTION_NAME_ALL], + LANGUAGE_FACET_GROUP_NAME: LANGUAGE_FACETS, } # Unless a library offers an alternate configuration, these @@ -128,6 +153,7 @@ class FacetConstants: COLLECTION_FACET_GROUP_NAME: COLLECTION_FULL, DISTRIBUTOR_FACETS_GROUP_NAME: DISTRIBUTOR_ALL, COLLECTION_NAME_FACETS_GROUP_NAME: COLLECTION_NAME_ALL, + LANGUAGE_FACET_GROUP_NAME: LANGUAGE_ALL, } SORT_ORDER_TO_OPENSEARCH_FIELD_NAME = { diff --git a/core/lane.py b/core/lane.py index 4d04fcd3f..47706c2e9 100644 --- a/core/lane.py +++ b/core/lane.py @@ -436,12 +436,26 @@ def _values_from_request( 400, ) + # Finland + g = Facets.LANGUAGE_FACET_GROUP_NAME + language: str = get_argument(g, cls.default_facet(config, g)) + language_facets = cls.available_facets(config, g) + if language and not language in language_facets: + return INVALID_INPUT.detailed( + _( + "I don't understand what language '%(language)s' refers to.", + language=language, + ), + 400, + ) + enabled = { Facets.ORDER_FACET_GROUP_NAME: order_facets, Facets.AVAILABILITY_FACET_GROUP_NAME: availability_facets, Facets.COLLECTION_FACET_GROUP_NAME: collection_facets, Facets.DISTRIBUTOR_FACETS_GROUP_NAME: distributor_facets, Facets.COLLECTION_NAME_FACETS_GROUP_NAME: collection_name_facets, + Facets.LANGUAGE_FACET_GROUP_NAME: language_facets, # Finland } return dict( @@ -451,6 +465,7 @@ def _values_from_request( distributor=distributor, collection_name=collection_name, enabled_facets=enabled, + language=language, # Finland ) @classmethod @@ -484,6 +499,7 @@ def __init__( order, distributor, collection_name, + language, order_ascending=None, enabled_facets=None, entrypoint=None, @@ -536,6 +552,9 @@ def __init__( self.collection_name = collection_name or self.default_facet( library, self.COLLECTION_NAME_FACETS_GROUP_NAME ) + self.language: str = language or self.default_facet( + library, self.LANGUAGE_FACET_GROUP_NAME + ) if order_ascending == self.ORDER_ASCENDING: order_ascending = True elif order_ascending == self.ORDER_DESCENDING: @@ -551,6 +570,7 @@ def navigate( entrypoint=None, distributor=None, collection_name=None, + language=None, ): """Create a slightly different Facets object from this one.""" return self.__class__( @@ -560,6 +580,7 @@ def navigate( order=order or self.order, distributor=distributor or self.distributor, collection_name=collection_name or self.collection_name, + language=language or self.language, enabled_facets=self.facets_enabled_at_init, entrypoint=(entrypoint or self.entrypoint), entrypoint_is_default=False, @@ -577,6 +598,8 @@ def items(self): yield (self.DISTRIBUTOR_FACETS_GROUP_NAME, self.distributor) if self.collection_name: yield (self.COLLECTION_NAME_FACETS_GROUP_NAME, self.collection_name) + if self.language: + yield (self.LANGUAGE_FACET_GROUP_NAME, self.language) @property def enabled_facets(self): @@ -595,6 +618,7 @@ def enabled_facets(self): self.COLLECTION_FACET_GROUP_NAME, self.DISTRIBUTOR_FACETS_GROUP_NAME, self.COLLECTION_NAME_FACETS_GROUP_NAME, + self.LANGUAGE_FACET_GROUP_NAME, ] for facet_type in facet_types: yield self.facets_enabled_at_init.get(facet_type, []) @@ -606,6 +630,7 @@ def enabled_facets(self): Facets.COLLECTION_FACET_GROUP_NAME, Facets.DISTRIBUTOR_FACETS_GROUP_NAME, Facets.COLLECTION_NAME_FACETS_GROUP_NAME, + Facets.LANGUAGE_FACET_GROUP_NAME, ): yield self.available_facets(self.library, group_name) @@ -625,6 +650,7 @@ def facet_groups(self): collection_facets, distributor_facets, collection_name_facets, + language_facets, ) = self.enabled_facets def dy(new_value): @@ -674,6 +700,13 @@ def dy(new_value): facets = self.navigate(collection_name=facet) yield (group, facet, facets, facet == current_value) + if len(language_facets) > 1: + for facet in language_facets: + group = self.LANGUAGE_FACET_GROUP_NAME + current_value = self.language + facets = self.navigate(language=facet) + yield (group, facet, facets, facet == current_value) + def modify_search_filter(self, filter): """Modify the given external_search.Filter object so that it reflects the settings of this Facets object. @@ -692,6 +725,10 @@ def modify_search_filter(self, filter): filter.availability = self.availability filter.subcollection = self.collection + # Finland + if self.language and self.language != self.LANGUAGE_ALL: + filter.languages = [self.language] + # We can only have distributor and collection name facets if we have a library if self.library: _db = Session.object_session(self.library) @@ -2935,6 +2972,7 @@ def update_size(self, _db, search_engine=None): order=FacetConstants.ORDER_WORK_ID, distributor=FacetConstants.DISTRIBUTOR_ALL, collection_name=FacetConstants.COLLECTION_NAME_ALL, + language=FacetConstants.LANGUAGE_ALL, entrypoint=entrypoint, ) filter = self.filter(_db, facets)