diff --git a/geniza/corpus/forms.py b/geniza/corpus/forms.py index 11bd56d7f..f29528a21 100644 --- a/geniza/corpus/forms.py +++ b/geniza/corpus/forms.py @@ -216,6 +216,13 @@ class DocumentSearchForm(RangeForm): ), ) + exclude_inferred = forms.BooleanField( + # Translators: label for "exclude inferred dates" search form filter + label=_("Exclude inferred dates"), + required=False, + widget=forms.CheckboxInput, + ) + doctype = FacetChoiceField( # Translators: label for document type search form filter label=_("Document type"), diff --git a/geniza/corpus/models.py b/geniza/corpus/models.py index 940bb93f9..93a8551db 100644 --- a/geniza/corpus/models.py +++ b/geniza/corpus/models.py @@ -1220,6 +1220,10 @@ def index_data(self): ), # combined original/standard document date for display "document_date_t": strip_tags(self.document_date) or None, + # inferred document date for display + "document_dating_t": standard_date_display( + "/".join([d.isoformat() for d in self.dating_range() if d]) + ), # date range for filtering "document_date_dr": self.solr_date_range(), # date range for filtering, but including inferred datings if any exist @@ -1232,6 +1236,17 @@ def index_data(self): "end_date_i": ( self.end_date.numeric_format(mode="max") if self.end_date else None ), + # start/end of document date or date range, including inferred datings, for sort + "start_dating_i": ( + self.dating_range()[0].numeric_format() + if self.dating_range()[0] + else None + ), + "end_dating_i": ( + self.dating_range()[1].numeric_format(mode="max") + if self.dating_range()[1] + else None + ), # library/collection possibly redundant? "collection_ss": [str(f.collection) for f in fragments], "tags_ss_lower": [t.name for t in self.tags.all()], diff --git a/geniza/corpus/solr_queryset.py b/geniza/corpus/solr_queryset.py index 134e2ba93..79a80f3ac 100644 --- a/geniza/corpus/solr_queryset.py +++ b/geniza/corpus/solr_queryset.py @@ -48,6 +48,7 @@ class DocumentSolrQuerySet(AliasedSolrQuerySet): "shelfmark": "shelfmark_s", # string version for display "shelfmarks": "fragment_shelfmark_ss", "document_date": "document_date_t", # text version for search & display + "document_dating": "document_dating_t", # inferred date for display "original_date_t": "original_date", "collection": "collection_ss", "tags": "tags_ss_lower", diff --git a/geniza/corpus/templates/corpus/document_list.html b/geniza/corpus/templates/corpus/document_list.html index ccee49427..a973a68c1 100644 --- a/geniza/corpus/templates/corpus/document_list.html +++ b/geniza/corpus/templates/corpus/document_list.html @@ -56,6 +56,11 @@

{{ page_title }}

{# NOTE: stimulus action is configured via django widget attrs #} {{ form.docdate }} +
{% translate "Includes" %} diff --git a/geniza/corpus/templates/corpus/snippets/document_result.html b/geniza/corpus/templates/corpus/snippets/document_result.html index b96cc156e..dbec34d0e 100644 --- a/geniza/corpus/templates/corpus/snippets/document_result.html +++ b/geniza/corpus/templates/corpus/snippets/document_result.html @@ -21,6 +21,16 @@

{{ document.document_date.0 }} {# indexed as _t which is a multival field #} + {% elif document.document_dating %} +
+ {# Translators: label for inferred date on a document #} + {% translate "Inferred date" %} +
+
+ +
{% endif %} {% if document.languages|length %}
diff --git a/geniza/corpus/tests/test_corpus_views.py b/geniza/corpus/tests/test_corpus_views.py index 5c380ced9..0a08a117d 100644 --- a/geniza/corpus/tests/test_corpus_views.py +++ b/geniza/corpus/tests/test_corpus_views.py @@ -547,8 +547,8 @@ def test_get_range_stats(self, mock_solr_queryset): # mock_queryset_cls.return_value.stats.return_value.get_stats.return_value = { mock_queryset_cls.return_value.get_stats.return_value = { "stats_fields": { - "start_date_i": {"min": None}, - "end_date_i": {"max": None}, + "start_dating_i": {"min": None}, + "end_dating_i": {"max": None}, } } docsearch_view = DocumentSearchView() @@ -560,14 +560,14 @@ def test_get_range_stats(self, mock_solr_queryset): ) assert stats == {"docdate": (None, None)} mock_queryset_cls.return_value.stats.assert_called_with( - "start_date_i", "end_date_i" + "start_dating_i", "end_dating_i" ) # convert integer date to year mock_queryset_cls.return_value.get_stats.return_value = { "stats_fields": { - "start_date_i": {"min": 10380101.0}, - "end_date_i": {"max": 10421231.0}, + "start_dating_i": {"min": 10380101.0}, + "end_dating_i": {"max": 10421231.0}, } } stats = docsearch_view.get_range_stats( @@ -577,7 +577,7 @@ def test_get_range_stats(self, mock_solr_queryset): # test three-digit year mock_queryset_cls.return_value.get_stats.return_value["stats_fields"][ - "start_date_i" + "start_dating_i" ]["min"] = 8430101.0 stats = docsearch_view.get_range_stats( queryset_cls=mock_queryset_cls, field_name="docdate" @@ -979,6 +979,14 @@ def test_get_solr_sort(self): assert random_sort.startswith("random_") assert int(random_sort.split("_")[1]) + # doc dating without exclude_inferred: should include inferred + dating_sort = docsearch_view.get_solr_sort("docdate_asc") + assert dating_sort.startswith("start_dating_") + + # with exclude_inferred: should use start_dating, which is dates without inferred + dating_sort = docsearch_view.get_solr_sort("docdate_asc", "true") + assert dating_sort.startswith("start_date_") + def test_random_page_redirect(self, client): # any page of results other than one should redirect to the first page docsearch_url = reverse("corpus:document-search") diff --git a/geniza/corpus/views.py b/geniza/corpus/views.py index 4de2a17fc..59c3e90cd 100644 --- a/geniza/corpus/views.py +++ b/geniza/corpus/views.py @@ -53,13 +53,13 @@ def get_range_stats(self, queryset_cls, field_name): the key is not added to a dictionary. :rtype: dict """ - stats = queryset_cls().stats("start_date_i", "end_date_i").get_stats() + stats = queryset_cls().stats("start_dating_i", "end_dating_i").get_stats() if stats.get("stats_fields"): # use minimum from start date and max from end date # - we're storing YYYYMMDD as 8-digit number for this we only want year # convert to str, take first 4 digits, then convert back to int - min_val = stats["stats_fields"]["start_date_i"]["min"] - max_val = stats["stats_fields"]["end_date_i"]["max"] + min_val = stats["stats_fields"]["start_dating_i"]["min"] + max_val = stats["stats_fields"]["end_dating_i"]["max"] # trim from the end to handle 3-digit years; includes .0 at end min_year = int(str(min_val)[:-6]) if min_val else None @@ -96,6 +96,8 @@ class DocumentSearchView( "shelfmark": "shelfmark_natsort", "docdate_asc": "start_date_i", "docdate_desc": "-end_date_i", + "docdating_asc": "start_dating_i", + "docdating_desc": "-end_dating_i", } def dispatch(self, request, *args, **kwargs): @@ -116,13 +118,16 @@ def last_modified(self): return None return super().last_modified() - def get_solr_sort(self, sort_option): + def get_solr_sort(self, sort_option, exclude_inferred=False): """Return solr sort field for user-seleted sort option; generates random sort field using solr random dynamic field; otherwise uses solr sort field from :attr:`solr_sort`""" if sort_option == "random": # use solr's random dynamic field to sort randomly return "random_%s" % randint(1000, 9999) + elif "docdate" in sort_option and not exclude_inferred: + # use inferred datings if exclude_inferred is not true + sort_option = sort_option.replace("date", "dating") return self.solr_sort[sort_option] def get_form_kwargs(self): @@ -249,7 +254,11 @@ def get_queryset(self): ) # include relevance score in results # order by sort option - documents = documents.order_by(self.get_solr_sort(search_opts["sort"])) + documents = documents.order_by( + self.get_solr_sort( + search_opts["sort"], search_opts.get("exclude_inferred", False) + ) + ) # filter by type if specified if search_opts["doctype"]: @@ -286,9 +295,13 @@ def get_queryset(self): if search_opts["docdate"]: # date range filter; returns tuple of value or None for open-ended range start, end = search_opts["docdate"] - documents = documents.filter( - document_date_dr="[%s TO %s]" % (start or "*", end or "*") + date_filter = "[%s TO %s]" % (start or "*", end or "*") + date_field = ( + "document_date_dr" + if search_opts.get("exclude_inferred", False) + else "document_dating_dr" ) + documents = documents.filter(**{date_field: date_filter}) label = "%s–%s" % (start, end) if start and not end: label = _("After %s") % start diff --git a/sitemedia/scss/base/_colors.scss b/sitemedia/scss/base/_colors.scss index 11449a3d3..d7830006e 100644 --- a/sitemedia/scss/base/_colors.scss +++ b/sitemedia/scss/base/_colors.scss @@ -24,6 +24,7 @@ $off-white: #f7f7f7; --primary-80: rgba(67, 96, 67, 0.8); --secondary: #567856; --secondary-80: rgba(86, 120, 86, 0.8); + --secondary-40: rgba(86, 120, 86, 0.4); --tertiary: #7bac7b; --link-primary: #567856; --link-secondary: #436043; @@ -107,6 +108,7 @@ $off-white: #f7f7f7; --primary-80: rgba(183, 102, 128, 0.8); --secondary: #b76680; --secondary-80: rgba(195, 127, 151, 0.8); + --secondary-40: rgba(195, 127, 151, 0.4); --tertiary: #b05070; --link-primary: #c37f97; --link-secondary: #b76680; diff --git a/sitemedia/scss/components/_searchform.scss b/sitemedia/scss/components/_searchform.scss index 99feb4c5d..5454b8845 100644 --- a/sitemedia/scss/components/_searchform.scss +++ b/sitemedia/scss/components/_searchform.scss @@ -277,6 +277,61 @@ main.people { } } +main.search form fieldset#filters div.fieldset-left-column { + @include breakpoints.for-tablet-landscape-up { + gap: 1rem; + } + label[for="id_exclude_inferred"] { + position: relative; + cursor: pointer; + input[type="checkbox"] + span::before { + display: none; + } + input[type="checkbox"] + span { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + } + input[type="checkbox"] + span::after { + display: block; + content: ""; + border-radius: 7px; + width: 34px; + height: 14px; + background-color: var(--background-gray); + transition: background-color 0.1s ease-in-out; + } + input[type="checkbox"]:active + span::after, + input[type="checkbox"]:checked + span::after { + background-color: var(--secondary-40); + } + input[type="checkbox"]:checked:active + span::after { + background-color: var(--background-gray); + } + .thumb { + display: block; + width: 20px; + height: 20px; + position: absolute; + top: calc(50% - 10px); + right: 14px; + transition: right 0.1s ease-in-out, + background-color 0.1s ease-in-out; + border-radius: 50%; + background-color: var(--disabled-on-background-light); + box-shadow: 0px 1px 3px 0px #00000033; + box-shadow: 0px 2px 1px 0px #0000001f; + box-shadow: 0px 1px 1px 0px #00000024; + } + input[type="checkbox"]:active + span + .thumb, + input[type="checkbox"]:checked + span + .thumb { + right: 0; + background-color: var(--secondary); + } + } +} + // tweaks for RTL search form for hebrew, arabic html[dir="rtl"] main.search form { // search query box and button