Skip to content

Commit

Permalink
Merge pull request #13 from NatLibFi/feature/simplye-209/language-filter
Browse files Browse the repository at this point in the history
Implement language facet for OPDS feed
  • Loading branch information
attemoi authored Feb 20, 2024
2 parents 1a58ee4 + 012a35f commit 7e3e9e0
Show file tree
Hide file tree
Showing 11 changed files with 193 additions and 20 deletions.
1 change: 1 addition & 0 deletions api/lanes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
28 changes: 28 additions & 0 deletions core/configuration/library.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
24 changes: 23 additions & 1 deletion core/external_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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__(
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
26 changes: 26 additions & 0 deletions core/facets.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 = {
Expand All @@ -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 = {
Expand All @@ -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 = {
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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 = {
Expand Down
36 changes: 36 additions & 0 deletions core/lane.py
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,7 @@ def default(
entrypoint=None,
distributor=None,
collection_name=None,
language=None,
):
return cls(
library,
Expand All @@ -332,6 +333,7 @@ def default(
order=order,
distributor=distributor,
collection_name=collection_name,
language=language,
entrypoint=entrypoint,
)

Expand Down Expand Up @@ -436,12 +438,21 @@ 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)
# In the case of language, don't want to limit the input to only the
# available facet values as we might get searches from elsewhere with
# other languages.

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(
Expand All @@ -451,6 +462,7 @@ def _values_from_request(
distributor=distributor,
collection_name=collection_name,
enabled_facets=enabled,
language=language, # Finland
)

@classmethod
Expand Down Expand Up @@ -484,6 +496,7 @@ def __init__(
order,
distributor,
collection_name,
language,
order_ascending=None,
enabled_facets=None,
entrypoint=None,
Expand Down Expand Up @@ -536,6 +549,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:
Expand All @@ -551,6 +567,7 @@ def navigate(
entrypoint=None,
distributor=None,
collection_name=None,
language=None,
):
"""Create a slightly different Facets object from this one."""
return self.__class__(
Expand All @@ -560,6 +577,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,
Expand All @@ -577,6 +595,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):
Expand All @@ -595,6 +615,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, [])
Expand All @@ -606,6 +627,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)

Expand All @@ -625,6 +647,7 @@ def facet_groups(self):
collection_facets,
distributor_facets,
collection_name_facets,
language_facets,
) = self.enabled_facets

def dy(new_value):
Expand Down Expand Up @@ -674,6 +697,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.
Expand All @@ -692,6 +722,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)
Expand Down Expand Up @@ -973,6 +1007,7 @@ def __init__(self, **kwargs):
kwargs.setdefault("availability", None)
kwargs.setdefault("distributor", None)
kwargs.setdefault("collection_name", None)
kwargs.setdefault("language", None)
order = kwargs.setdefault("order", None)

if order in (None, self.ORDER_BY_RELEVANCE):
Expand Down Expand Up @@ -2935,6 +2970,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)
Expand Down
5 changes: 3 additions & 2 deletions tests/api/controller/test_work.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from core.classifier import Classifier
from core.entrypoint import AudiobooksEntryPoint
from core.external_search import SortKeyPagination, mock_search_index
from core.facets import FacetConstants
from core.feed.acquisition import OPDSAcquisitionFeed
from core.feed.annotator.circulation import LibraryAnnotator
from core.feed.types import WorkEntry
Expand Down Expand Up @@ -139,7 +140,7 @@ def test_contributor(self, work_fixture: WorkFixture):
facet_links = [
link for link in links if link["rel"] == "http://opds-spec.org/facet"
]
assert 10 == len(facet_links)
assert 10 + len(FacetConstants.LANGUAGE_FACETS) == len(facet_links)

# At this point we don't want to generate real feeds anymore.
# We can't do a real end-to-end test without setting up a real
Expand Down Expand Up @@ -905,7 +906,7 @@ def test_series(self, work_fixture: WorkFixture):
facet_links = [
link for link in links if link["rel"] == "http://opds-spec.org/facet"
]
assert 11 == len(facet_links)
assert 11 + len(FacetConstants.LANGUAGE_FACETS) == len(facet_links)

# The facet link we care most about is the default sort order,
# put into place by SeriesFacets.
Expand Down
2 changes: 1 addition & 1 deletion tests/api/feed/test_opds_acquisition_feed.py
Original file line number Diff line number Diff line change
Expand Up @@ -1151,7 +1151,7 @@ def run(wl=None, facets=None, pagination=None):
# The make_link function that was passed in calls
# TestAnnotator.feed_url() when passed an EntryPoint. The
# Facets object's other facet groups are propagated in this URL.
first_page_url = "http://wl/?available=all&collection=full&collectionName=All&distributor=All&entrypoint=Book&order=author"
first_page_url = "http://wl/?available=all&collection=full&collectionName=All&distributor=All&entrypoint=Book&language=all&order=author"
assert first_page_url == make_link(EbooksEntryPoint)

# Pagination information is not propagated through entry point links
Expand Down
4 changes: 3 additions & 1 deletion tests/api/test_lanes.py
Original file line number Diff line number Diff line change
Expand Up @@ -918,6 +918,7 @@ def test_default(self, db: DatabaseTransactionFixture):
assert facets.collection == CrawlableFacets.COLLECTION_FULL
assert facets.availability == CrawlableFacets.AVAILABLE_ALL
assert facets.order == CrawlableFacets.ORDER_LAST_UPDATE
assert facets.language == CrawlableFacets.LANGUAGE_ALL
assert facets.order_ascending is False

[
Expand All @@ -926,10 +927,11 @@ def test_default(self, db: DatabaseTransactionFixture):
collection,
distributor,
collectionName,
language,
] = facets.enabled_facets

# The default facets are the only ones enabled.
for facet in [order, availability, collection]:
for facet in [order, availability, collection, language]:
assert len(facet) == 1

# Except for distributor and collectionName, which have the default
Expand Down
2 changes: 2 additions & 0 deletions tests/core/models/test_library.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,8 @@ def test_explain(self, db: DatabaseTransactionFixture):
facets_default_available='all'
facets_enabled_collection='['full', 'featured']'
facets_default_collection='full'
facets_enabled_language='['all', 'fin', 'swe', 'eng', 'others']'
facets_default_language='all'
help_web='http://library.com/support'
default_notification_email_address='noreply@thepalaceproject.org'
color_scheme='blue'
Expand Down
Loading

0 comments on commit 7e3e9e0

Please sign in to comment.