Skip to content

Commit

Permalink
Add language facet filter to document search (#1402)
Browse files Browse the repository at this point in the history
  • Loading branch information
blms committed Oct 10, 2024
1 parent 957daf0 commit da07d3e
Show file tree
Hide file tree
Showing 9 changed files with 178 additions and 10 deletions.
71 changes: 64 additions & 7 deletions geniza/corpus/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ class SelectWithDisabled(SelectDisabledMixin, forms.Select):
"""


class CheckboxSelectWithCount(forms.CheckboxSelectMultiple):
# extend default CheckboxSelectMultiple to add facet counts and
class WidgetCountMixin:
# extend default choice field widgets to add facet counts and
# include per-item count as a data attribute
facet_counts = {}

Expand All @@ -68,6 +68,18 @@ def get_context(self, name, value, attrs):
return context


class CheckboxSelectWithCount(WidgetCountMixin, forms.CheckboxSelectMultiple):
"""
Subclass of :class:`django.forms.CheckboxSelectMultiple` with support for facet counts.
"""


class SelectWithCount(WidgetCountMixin, forms.Select):
"""
Subclass of :class:`django.forms.Select` with support for facet counts.
"""


class FacetFieldMixin:
# Borrowed from ppa-django / mep-django
# - turn off choice validation (shouldn't fail if facets don't get loaded)
Expand All @@ -79,15 +91,16 @@ def __init__(self, *args, **kwargs):

# get custom kwarg and remove before passing to MultipleChoiceField
# super method, which would cause an error
self.widget.legend = None
if "legend" in kwargs:
self.widget.legend = kwargs["legend"]
del kwargs["legend"]
if hasattr(self.widget, "legend"):
self.widget.legend = None
if "legend" in kwargs:
self.widget.legend = kwargs["legend"]
del kwargs["legend"]

Check warning on line 98 in geniza/corpus/forms.py

View check run for this annotation

Codecov / codecov/patch

geniza/corpus/forms.py#L95-L98

Added lines #L95 - L98 were not covered by tests

super().__init__(*args, **kwargs)

# if no custom legend, set it from label
if not self.widget.legend:
if hasattr(self.widget, "legend") and not self.widget.legend:
self.widget.legend = self.label

def valid_value(self, value):
Expand Down Expand Up @@ -117,6 +130,40 @@ def populate_from_facets(self, facet_dict):
self.widget.facet_counts = facet_dict


class FacetChoiceSelectField(FacetFieldMixin, forms.ChoiceField):
"""Choice field where choices are set based on Solr facets"""

# use a custom widget so we can add facet count as a data attribute
widget = SelectWithCount
empty_label = None

def __init__(self, empty_label=None, *args, **kwargs):
if empty_label:
self.empty_label = empty_label
super().__init__(*args, **kwargs)

def populate_from_facets(self, facet_dict):
"""
Populate the field choices from the facets returned by solr.
"""
# generate the list of choice from the facets
self.choices = (
(
val,
mark_safe(
f'<span>{label}</span> (<span class="count">{count:,}</span>)'
),
)
for val, (label, count) in facet_dict.items()
)
# pass the counts to the widget so it can be set as a data attribute
self.widget.facet_counts = facet_dict

# add empty label if present (for <select> dropdowns)
if self.empty_label:
self.choices = [("", self.empty_label)] + self.choices


class CheckboxInputWithCount(forms.CheckboxInput):
# extend default CheckboxInput to add facet count as a data attribute
facet_counts = {}
Expand Down Expand Up @@ -252,6 +299,12 @@ class DocumentSearchForm(RangeForm):
# Translators: label for "has discussion" search form filter
label=_("Discussion"),
)
translation_language = FacetChoiceSelectField(
# Translators: label for document translation language search form filter
label=_("Translation language"),
widget=SelectWithCount,
empty_label=_("All languages"),
)

mode = forms.ChoiceField(
# Translators: label for "search mode" (general or regex)
Expand Down Expand Up @@ -282,6 +335,10 @@ def __init__(self, data=None, *args, **kwargs):
{"label": self.SORT_CHOICES[0][1], "disabled": True},
)

# if "has translation" is not selected, language dropdown is disabled
if not data or not data.get("has_translation", None):
self.fields["translation_language"].disabled = True

def get_translated_label(self, field, label):
"""Lookup translated label via db model object when applicable;
handle Person.gender as a special case; and otherwise just return the label"""
Expand Down
8 changes: 8 additions & 0 deletions geniza/corpus/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1300,6 +1300,8 @@ def index_data(self):
# keep track of translation language for RTL/LTR display
translation_langcode = ""
translation_langdir = "ltr"
# keep track of translation language names for faceted filtering
translation_languages = []

# dict of sets of relations; keys are each source attached to any footnote on this document
source_relations = defaultdict(set)
Expand All @@ -1322,6 +1324,11 @@ def index_data(self):
lang = fn.source.languages.first()
translation_langcode = lang.code
translation_langdir = lang.direction
translation_languages = [
l.name
for l in fn.source.languages.all()
if "Unspecified" not in l.name
]
# add any doc relations to this footnote's source's set in source_relations
source_relations[fn.source] = source_relations[fn.source].union(
fn.doc_relation
Expand Down Expand Up @@ -1354,6 +1361,7 @@ def index_data(self):
"text_transcription": transcription_texts,
# transcription content as plaintext
"transcription_regex": transcription_texts_plaintext,
"translation_languages_ss": translation_languages,
"translation_language_code_s": translation_langcode,
"translation_language_direction_s": translation_langdir,
# translation content as html
Expand Down
1 change: 1 addition & 0 deletions geniza/corpus/solr_queryset.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ class DocumentSolrQuerySet(AliasedSolrQuerySet):
"language_script": "language_script_s",
"languages": "language_name_ss",
"translation": "text_translation",
"translation_language": "translation_languages_ss",
"translation_language_code": "translation_language_code_s",
"translation_language_direction": "translation_language_direction_s",
"iiif_images": "iiif_images_ss",
Expand Down
4 changes: 4 additions & 0 deletions geniza/corpus/templates/corpus/document_list.html
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@ <h2>
{% render_field form.has_translation data-action="search#update" %}
{{ form.has_translation.label }}
</label>
<label for="{{ form.translation_language.auto_id }}">
<span class="sr-only">{{ form.translation_language.label }}</span>
{% render_field form.translation_language data-action="search#update" %}
</label>
</li>
<li>
<label for="{{ form.has_discussion.auto_id }}">
Expand Down
1 change: 1 addition & 0 deletions geniza/corpus/tests/test_corpus_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1144,6 +1144,7 @@ def test_index_data_footnotes(
assert index_data["scholarship_count_i"] == 3 # unique sources
assert index_data["text_transcription"] == ["transcrip[ti]on lines"]
assert index_data["text_translation"] == ["translation lines"]
assert index_data["translation_languages_ss"] == ["English"]
assert index_data["translation_language_code_s"] == "en"
assert index_data["translation_language_direction_s"] == "ltr"

Expand Down
39 changes: 38 additions & 1 deletion geniza/corpus/tests/test_forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
DocumentMergeForm,
DocumentSearchForm,
FacetChoiceField,
FacetChoiceSelectField,
SelectWithCount,
SelectWithDisabled,
TagChoiceField,
TagMergeForm,
Expand Down Expand Up @@ -45,7 +47,7 @@ class TestFacetChoiceField:
# test adapted from ppa-django

def test_init(self):
fcf = FacetChoiceField(legend="Document type")
fcf = FacetChoiceField()
# uses RadioSelectWithCount
fcf.widget == CheckboxSelectWithCount
# not required by default
Expand All @@ -60,6 +62,36 @@ def test_valid_value(self):
assert fcf.valid_value("foo")


class TestFacetChoiceSelectField:
# similar to FacetChoiceField, but slightly different formatting and using a Select widget

def test_init(self):
fcf = FacetChoiceSelectField()
assert not fcf.empty_label
fcf = FacetChoiceSelectField(empty_label="Select...")
assert fcf.empty_label == "Select..."
# uses SelectWithCount
assert fcf.widget.__class__ == SelectWithCount
# not required by default
assert not fcf.required
# still can override required with a kwarg
fcf = FacetChoiceField(required=True)
assert fcf.required

def test_populate_from_facets(self):
fcf = FacetChoiceSelectField()
# should format like "label (count)"
fcf.populate_from_facets({"example": ("label", 1)})
assert fcf.choices == [
("example", '<span>label</span> (<span class="count">1</span>)')
]
# should add a choice with the empty label if provided
fcf = FacetChoiceSelectField(empty_label="Select...")
fcf.populate_from_facets({"example": ("label", 1)})
assert len(fcf.choices) == 2
assert ("", "Select...") in fcf.choices


class TestDocumentSearchForm:
# test adapted from ppa-django

Expand All @@ -68,6 +100,7 @@ def test_init(self):
# has query, relevance enabled
form = DocumentSearchForm(data)
assert form.fields["sort"].widget.choices[0] == form.SORT_CHOICES[0]
assert form.fields["translation_language"].disabled == True

# empty query, relevance disabled
data["q"] = ""
Expand All @@ -85,6 +118,10 @@ def test_init(self):
{"label": "Relevance", "disabled": True},
)

data = {"q": "illness", "has_translation": True}
form = DocumentSearchForm(data)
assert form.fields["translation_language"].disabled == False

def test_choices_from_facets(self):
"""A facet dict should produce correct choice labels"""
fake_facets = {
Expand Down
9 changes: 9 additions & 0 deletions geniza/corpus/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ def get_queryset(self):
"has_digital_translation",
"has_discussion",
)
.facet_field("translation_language", sort="value")
.facet_field("type", exclude="type", sort="value")
)
self.applied_filter_labels = []
Expand Down Expand Up @@ -274,6 +275,14 @@ def get_queryset(self):
form, "doctype", typelist
)

# filter by translation language if specified
if search_opts["translation_language"]:
lang = search_opts["translation_language"]
documents = documents.filter(translation_language=lang)
self.applied_filter_labels += self.get_applied_filter_labels(

Check warning on line 282 in geniza/corpus/views.py

View check run for this annotation

Codecov / codecov/patch

geniza/corpus/views.py#L280-L282

Added lines #L280 - L282 were not covered by tests
form, "translation_language", [lang]
)

# image filter
if search_opts["has_image"] == True:
documents = documents.filter(has_image=True)
Expand Down
11 changes: 9 additions & 2 deletions sitemedia/js/controllers/search_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,17 @@ export default class extends Controller {
if (filterValue === "on") {
selector = "checked";
}
const appliedFilter = this.filterModalTarget.querySelector(
let appliedFilter = this.filterModalTarget.querySelector(
`label[for*="${filterName}"] input[${selector}]`
);
appliedFilter.checked = false;
if (appliedFilter) {
appliedFilter.checked = false;
} else {
appliedFilter = this.filterModalTarget.querySelector(
`label[for*="${filterName}"] option[${selector}]`
);
appliedFilter.selected = false;
}
} else if (
["date_range", "docdate"].includes(filterName) &&
(searchParams.has("date_range_0") ||
Expand Down
44 changes: 44 additions & 0 deletions sitemedia/scss/components/_searchform.scss
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,39 @@ main.search form {
}
}
}
// dropdown in facet filters
#filters label:has(select) {
position: relative;
select {
border-radius: 5px;
height: 40px;
width: 260px;
cursor: pointer;
background-color: var(--background-light);
border: 1px solid var(--background-gray);
font-family: fonts.$primary;
font-weight: 400;
font-size: typography.$text-size-md;
margin: 0.25rem 0 0.25rem 1.75rem;
padding: 0 1rem;
-webkit-appearance: none;
appearance: none;
&:disabled {
cursor: default;
}
}
&::after {
position: absolute;
@include typography.icon-button-sm;
content: "\f0c2"; // phosphor caret-down icon
top: 0.6rem;
right: 1rem;
pointer-events: none;
}
&:has(select:disabled)::after {
color: var(--disabled);
}
}
// error messages
ul#search-errors {
margin-top: spacing.$spacing-md;
Expand Down Expand Up @@ -501,6 +534,17 @@ html[dir="rtl"] main.search form {
margin: 0 1.5rem 0 3.25rem;
}
}
// dropdown in facet filters
#filters label:has(select) {
select {
margin-right: 1.75rem;
margin-left: 0;
}
&::after {
right: auto;
left: 1rem;
}
}
}

// Hebrew variant
Expand Down

0 comments on commit da07d3e

Please sign in to comment.