From 782815a5a61e1da43a00bb825fb3edb35fa51158 Mon Sep 17 00:00:00 2001 From: YishaiGlasner Date: Wed, 4 Feb 2026 15:22:00 +0200 Subject: [PATCH 01/32] chore: remove load_by_title from terms_api, as it should be based on name. --- reader/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/reader/views.py b/reader/views.py index d481a2be7d..4d88b8309d 100644 --- a/reader/views.py +++ b/reader/views.py @@ -2743,7 +2743,7 @@ def terms_api(request, name): This is mainly to be used for adding hebrew internationalization language for section names, categories and commentators """ if request.method == "GET": - term = Term().load({'name': name}) or Term().load_by_title(name) + term = Term().load({'name': name}) if term is None: return jsonResponse({"error": "Term does not exist."}) else: @@ -2751,7 +2751,7 @@ def terms_api(request, name): if request.method in ("POST", "DELETE"): def _internal_do_post(request, uid): - t = Term().load({'name': name}) or Term().load_by_title(name) + t = Term().load({'name': name}) if request.method == "POST": if "json" in request.POST: term = request.POST.get("json") From f2ef5e5fc62d867040316ae847d81a33cee54165 Mon Sep 17 00:00:00 2001 From: YishaiGlasner Date: Wed, 4 Feb 2026 15:44:36 +0200 Subject: [PATCH 02/32] chore: change term load in hok_leyisrael to rely on term name. --- sefaria/utils/calendars.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sefaria/utils/calendars.py b/sefaria/utils/calendars.py index 7f3a596e90..95a347e6db 100644 --- a/sefaria/utils/calendars.py +++ b/sefaria/utils/calendars.py @@ -364,7 +364,7 @@ def get_hok_parasha(datetime_obj, diaspora=diaspora): parasha = parasha.split('-')[0] if parasha == 'Shmini Atzeret': parasha = "V'Zot HaBerachah" - parasha_term = Term().load({'category': 'Torah Portions', 'titles': {'$elemMatch': {'text': parasha}}}) + parasha_term = Term().load({'category': 'Torah Portions', 'name': parasha}) if not parasha_term: parasha_term = get_hok_parasha(datetime_obj + datetime.timedelta(7), diaspora=diaspora) return parasha_term From 467ac49daa1d69e81ba5b5d2e43e19e0a6c8fb6d Mon Sep 17 00:00:00 2001 From: YishaiGlasner Date: Wed, 4 Feb 2026 16:19:28 +0200 Subject: [PATCH 03/32] chore: change term normalize to rely on name (checked the only occurrence). --- sefaria/model/schema.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sefaria/model/schema.py b/sefaria/model/schema.py index 4ced240d8c..7262d401e9 100644 --- a/sefaria/model/schema.py +++ b/sefaria/model/schema.py @@ -286,7 +286,7 @@ def _validate(self): def normalize(term, lang="en"): """ Returns the primary title for of 'term' if it exists in the terms collection otherwise return 'term' unchanged """ - t = Term().load_by_title(term) + t = Term().load({'name': term}) return t.get_primary_title(lang=lang) if t else term From 2213e8af8fc14f65e3ed536d693a10645274782d Mon Sep 17 00:00:00 2001 From: YishaiGlasner Date: Wed, 4 Feb 2026 17:01:53 +0200 Subject: [PATCH 04/32] chore: remove unused check_term function (was used in the category editor before being replaced with different logic). --- sefaria/helper/category.py | 29 ----------------------------- 1 file changed, 29 deletions(-) diff --git a/sefaria/helper/category.py b/sefaria/helper/category.py index 68e9abfd14..bb5ba25582 100644 --- a/sefaria/helper/category.py +++ b/sefaria/helper/category.py @@ -197,32 +197,3 @@ def update_order_of_category_children(cat, uid, subcategoriesAndBooks): result = tracker.update(uid, Category, cat) results.append(result.contents()) return results - - - - - -def check_term(last_path, he_last_path): - """ - if Category Editor is used, make sure English and Hebrew titles correspond to the same term. - if neither of the titles correspond to a term, create the appropriate term - :param last_path: (str) Corresponds to lastPath of Category and english title of Term - :param he_last_path: (str) Corresponds to a hebrew title of Term - """ - - error_msg = "" - en_term = Term().load_by_title(last_path) - he_term = Term().load_by_title(he_last_path) - - if en_term == he_term: - pass - if (en_term and he_term != en_term) or (he_term and he_term != en_term): - # they do not correspond, either because both terms exist but are not the same, or one term already - # exists but the other one doesn't exist - error_msg = f"English and Hebrew titles, {last_path} and {he_last_path}, do not correspond to the same term. Please use the term editor." - elif en_term is None and he_term is None: - t = Term() - t.name = last_path - t.add_primary_titles(last_path, he_last_path) - t.save() - return error_msg From eceed095b7371f539b9f1c70aa4ccd1ee1e4464c Mon Sep 17 00:00:00 2001 From: YishaiGlasner Date: Thu, 5 Feb 2026 08:49:23 +0200 Subject: [PATCH 05/32] fix: check if 'path' is a key in j BEFORE using it. --- reader/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reader/views.py b/reader/views.py index 4d88b8309d..9ec37123e9 100644 --- a/reader/views.py +++ b/reader/views.py @@ -2631,9 +2631,9 @@ def _internal_do_post(request, update, cat, uid, **kwargs): return jsonResponse({"error": "Missing data in POST request."}) j = json.loads(j) update = int(request.GET.get("update", False)) - new_category = Category().load({"path": j["path"]}) if "path" not in j: return jsonResponse({"error": "'path' is a required attribute"}) + new_category = Category().load({"path": j["path"]}) if not update and new_category is not None: return jsonResponse({"error": "Category {} already exists.".format(", ".join(j["path"]))}) From b033a07ec93ae37b3639d5bb3852ea31bea1897f Mon Sep 17 00:00:00 2001 From: YishaiGlasner Date: Thu, 5 Feb 2026 09:21:04 +0200 Subject: [PATCH 06/32] feat(Term): 1. add `load_by_primary_title`. 2. remove validations regarding titles. rather add validation for new term's name doesn't exist. --- sefaria/model/schema.py | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/sefaria/model/schema.py b/sefaria/model/schema.py index 7262d401e9..11e9185a98 100644 --- a/sefaria/model/schema.py +++ b/sefaria/model/schema.py @@ -262,6 +262,13 @@ def load_by_title(self, title): query = {'titles.text': title} return self.load(query=query) + def load_by_primary_title(self, title): + query = {'titles': {'$elemMatch:': { + 'text': title, + 'primary': True + }}} + return self.load(query=query) + def _set_derived_attributes(self): self.set_titles(getattr(self, "titles", None)) @@ -270,17 +277,19 @@ def set_titles(self, titles): def _normalize(self): self.titles = self.title_group.titles + if not hasattr(self, 'name'): + name = self.get_primary_title() + dupes = len(TermSet({'name': {'$regex': fr'^{name}\d*$'}})) + if dupes: + name = f'{name}{dupes}' + setattr(self, 'name', name) def _validate(self): super(Term, self)._validate() - # do not allow duplicates: - for title in self.get_titles(): - other_term = Term().load_by_title(title) - if other_term and not self.same_record(other_term): - raise InputError("A Term with the title {} in it already exists".format(title)) + # do not allow duplicate names: + if self.is_new() and Term().load({'name': self.name}): + raise InputError(f"A Term with the name {self.name} already exists") self.title_group.validate() - if self.name != self.get_primary_title(): - raise InputError("Term name {} does not match primary title {}".format(self.name, self.get_primary_title())) @staticmethod def normalize(term, lang="en"): From 98ca0678f8f12920c3c4afb2ccd7c8b556a0d71e Mon Sep 17 00:00:00 2001 From: YishaiGlasner Date: Thu, 5 Feb 2026 09:22:40 +0200 Subject: [PATCH 07/32] refactor(category api): load terms by primary titles, which ensure the category will get the required titles. remove term's name setting, leaving it to be determined by the term saving. --- reader/views.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/reader/views.py b/reader/views.py index 9ec37123e9..a8128790ad 100644 --- a/reader/views.py +++ b/reader/views.py @@ -2652,14 +2652,13 @@ def _internal_do_post(request, update, cat, uid, **kwargs): return {"error": f"Merging two categories named {last_path} is not supported."} elif "heSharedTitle" in j: # if heSharedTitle provided, make sure sharedTitle and heSharedTitle correspond to same Term - en_term = Term().load_by_title(last_path) - he_term = Term().load_by_title(he_last_path) + en_term = Term().load_by_primary_title(last_path) + he_term = Term().load_by_primary_title(he_last_path) if en_term and en_term == he_term: pass # both titles are found in an existing Term object else: # titles weren't found in same Term object, so try to create a new Term t = Term() - t.name = last_path t.add_primary_titles(last_path, he_last_path) t.save() From ef5804bd4706d8afb11ccb17fce2e8a9b8680cb2 Mon Sep 17 00:00:00 2001 From: YishaiGlasner Date: Thu, 5 Feb 2026 09:29:27 +0200 Subject: [PATCH 08/32] chore(TitledTreeNode): remove dead code (if False) and commented-out code. --- sefaria/model/schema.py | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/sefaria/model/schema.py b/sefaria/model/schema.py index 11e9185a98..e065a69e7f 100644 --- a/sefaria/model/schema.py +++ b/sefaria/model/schema.py @@ -870,23 +870,6 @@ def validate(self): if self.sharedTitle and Term().load({"name": self.sharedTitle}).titles != self.get_titles_object(): raise IndexSchemaError("Schema node {} with sharedTitle can not have explicit titles".format(self)) - # disable this check while data is still not conforming to validation - if not self.sharedTitle and False: - special_book_cases = ["Genesis", "Exodus", "Leviticus", "Numbers", "Deuteronomy", "Judges"] - for title in self.title_group.titles: - title = title["text"] - if self.get_primary_title() in special_book_cases: - break - term = Term().load_by_title(title) - if term: - if "scheme" in list(vars(term).keys()): - if vars(term)["scheme"] == "Parasha": - raise InputError( - "Nodes that represent Parashot must contain the corresponding sharedTitles.") - - # if not self.default and not self.primary_title("he"): - # raise IndexSchemaError("Schema node {} missing primary Hebrew title".format(self.key)) - def serialize(self, **kwargs): d = super(TitledTreeNode, self).serialize(**kwargs) if self.default: From 6aeba1c331d027395a854a19ebc6ce0908f7de82 Mon Sep 17 00:00:00 2001 From: YishaiGlasner Date: Thu, 5 Feb 2026 10:02:55 +0200 Subject: [PATCH 09/32] refactor(Term): add validation name is not changed --- sefaria/model/schema.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/sefaria/model/schema.py b/sefaria/model/schema.py index e065a69e7f..35301daa88 100644 --- a/sefaria/model/schema.py +++ b/sefaria/model/schema.py @@ -7,6 +7,7 @@ from functools import reduce import re2 as re from sefaria.system.decorators import conditional_graceful_exception +from django.core.exceptions import ValidationError logger = structlog.get_logger(__name__) @@ -15,7 +16,8 @@ from sefaria.system.database import db from sefaria.model.lexicon import LexiconEntrySet from sefaria.model.linker.has_match_template import MatchTemplateMixin -from sefaria.system.exceptions import InputError, IndexSchemaError, DictionaryEntryNotFoundError, SheetNotFoundError +from sefaria.system.exceptions import InputError, IndexSchemaError, DictionaryEntryNotFoundError, SheetNotFoundError, \ + DuplicateRecordError from sefaria.utils.hebrew import decode_hebrew_numeral, encode_small_hebrew_numeral, encode_hebrew_numeral, encode_hebrew_daf, hebrew_term, sanitize from sefaria.utils.talmud import daf_to_section @@ -288,7 +290,9 @@ def _validate(self): super(Term, self)._validate() # do not allow duplicate names: if self.is_new() and Term().load({'name': self.name}): - raise InputError(f"A Term with the name {self.name} already exists") + raise DuplicateRecordError(f"A Term with the name {self.name} already exists") + elif not self.is_new() and self.is_key_changed('name'): + raise ValidationError({"name": "This field cannot be changed."}) self.title_group.validate() @staticmethod From c4dc101a89b5c822f600ba186b955e4431923839 Mon Sep 17 00:00:00 2001 From: YishaiGlasner Date: Thu, 5 Feb 2026 10:09:23 +0200 Subject: [PATCH 10/32] refactor(terms api): change `term` into `name`. --- reader/views.py | 10 +++++----- sefaria/urls_library.py | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/reader/views.py b/reader/views.py index a8128790ad..a0082eb61c 100644 --- a/reader/views.py +++ b/reader/views.py @@ -1409,13 +1409,13 @@ def edit_text_info(request, title=None, new_title=None): @ensure_csrf_cookie @staff_member_required -def terms_editor(request, term=None): +def terms_editor(request, name=None): """ Add/Editor a term using the JSON Editor. """ - if term is not None: - existing_term = Term().load_by_title(term) - data = existing_term.contents() if existing_term else {"name": term, "titles": []} + if name is not None: + existing_term = Term().load({'name': name}) + data = existing_term.contents() if existing_term else {} else: return render_template(request,'static/generic.html', None, { "title": "Terms Editor", @@ -1424,7 +1424,7 @@ def terms_editor(request, term=None): dataJSON = json.dumps(data) return render_template(request,'edit_term.html', None, { - 'term': term, + 'term': name, 'dataJSON': dataJSON, 'is_update': "true" if existing_term else "false" }) diff --git a/sefaria/urls_library.py b/sefaria/urls_library.py index bc797786cc..9a3ad66631 100644 --- a/sefaria/urls_library.py +++ b/sefaria/urls_library.py @@ -55,8 +55,8 @@ url(r'^add/new/?$', reader_views.edit_text), url(r'^add/(?P.+)$', reader_views.edit_text), url(r'^translate/(?P.+)$', reader_views.edit_text), - url(r'^edit/terms/(?P.+)$', reader_views.terms_editor), - url(r'^add/terms/(?P.+)$', reader_views.terms_editor), + url(r'^edit/terms/(?P.+)$', reader_views.terms_editor), + url(r'^add/terms/(?P.+)$', reader_views.terms_editor), url(r'^edit/(?P.+)/(?P\w\w)/(?P.+)$', reader_views.edit_text), url(r'^edit/(?P.+)$', reader_views.edit_text), From 275f71d2c153c3f4755198f71a041810f68324b8 Mon Sep 17 00:00:00 2001 From: YishaiGlasner Date: Thu, 5 Feb 2026 10:10:25 +0200 Subject: [PATCH 11/32] refactor(terms api): remove checking if name exists as it's guaranteed by url. --- reader/views.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/reader/views.py b/reader/views.py index a0082eb61c..4cef031d2a 100644 --- a/reader/views.py +++ b/reader/views.py @@ -1409,18 +1409,12 @@ def edit_text_info(request, title=None, new_title=None): @ensure_csrf_cookie @staff_member_required -def terms_editor(request, name=None): +def terms_editor(request, name): """ Add/Editor a term using the JSON Editor. """ - if name is not None: - existing_term = Term().load({'name': name}) - data = existing_term.contents() if existing_term else {} - else: - return render_template(request,'static/generic.html', None, { - "title": "Terms Editor", - "content": "Please include the primary Term name in the URL to uses the Terms Editor." - }) + existing_term = Term().load({'name': name}) + data = existing_term.contents() if existing_term else {} dataJSON = json.dumps(data) return render_template(request,'edit_term.html', None, { From aa182b486b701126bc3c7bc8f8c80b2818b0bdb1 Mon Sep 17 00:00:00 2001 From: YishaiGlasner Date: Thu, 5 Feb 2026 10:13:11 +0200 Subject: [PATCH 12/32] refactor(terms api): remove add/terms endpoint - it's unreachable (caught by only `add`), and have the same view as edit/terms. --- sefaria/urls_library.py | 1 - 1 file changed, 1 deletion(-) diff --git a/sefaria/urls_library.py b/sefaria/urls_library.py index 9a3ad66631..8b4f5857da 100644 --- a/sefaria/urls_library.py +++ b/sefaria/urls_library.py @@ -56,7 +56,6 @@ url(r'^add/(?P.+)$', reader_views.edit_text), url(r'^translate/(?P.+)$', reader_views.edit_text), url(r'^edit/terms/(?P.+)$', reader_views.terms_editor), - url(r'^add/terms/(?P.+)$', reader_views.terms_editor), url(r'^edit/(?P.+)/(?P\w\w)/(?P.+)$', reader_views.edit_text), url(r'^edit/(?P.+)$', reader_views.edit_text), From 4ad5dcf88b4492f204195adf9d585a113c6f343f Mon Sep 17 00:00:00 2001 From: YishaiGlasner Date: Thu, 5 Feb 2026 11:10:13 +0200 Subject: [PATCH 13/32] refactor(Term): remove `load_by_title` since now more than one term can have the same title. --- sefaria/model/schema.py | 4 ---- sefaria/model/tests/terms_test.py | 4 ---- 2 files changed, 8 deletions(-) diff --git a/sefaria/model/schema.py b/sefaria/model/schema.py index 35301daa88..f8fa473b7c 100644 --- a/sefaria/model/schema.py +++ b/sefaria/model/schema.py @@ -260,10 +260,6 @@ class Term(abst.AbstractMongoRecord, AbstractTitledObject): "description" ] - def load_by_title(self, title): - query = {'titles.text': title} - return self.load(query=query) - def load_by_primary_title(self, title): query = {'titles': {'$elemMatch:': { 'text': title, diff --git a/sefaria/model/tests/terms_test.py b/sefaria/model/tests/terms_test.py index 90cfe368af..e2ed7784da 100644 --- a/sefaria/model/tests/terms_test.py +++ b/sefaria/model/tests/terms_test.py @@ -19,10 +19,6 @@ def test_existing_term(self): Term().load({"name": 'Torah'}).title_group.validate() Term().load({"name": 'Verse'}).title_group.validate() - def test_load_by_non_primary_title(self): - assert Term().load_by_title('Nachmanides') is not None - assert Term().load_by_title('פרשת לך לך') is not None - def test_add_duplicate_primary(self): with pytest.raises(InputError): term = Term({ From aea84ca5593a95e3268e52f7e8e2bfbfcfc294d8 Mon Sep 17 00:00:00 2001 From: YishaiGlasner Date: Thu, 5 Feb 2026 11:20:13 +0200 Subject: [PATCH 14/32] refactor(Term): refactor `load_by_primary_title` to `load_by_primary_titles` since different terms can have one equal primary title. --- reader/views.py | 7 ++----- sefaria/model/schema.py | 11 ++++++----- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/reader/views.py b/reader/views.py index 4cef031d2a..fa88bde713 100644 --- a/reader/views.py +++ b/reader/views.py @@ -2646,11 +2646,8 @@ def _internal_do_post(request, update, cat, uid, **kwargs): return {"error": f"Merging two categories named {last_path} is not supported."} elif "heSharedTitle" in j: # if heSharedTitle provided, make sure sharedTitle and heSharedTitle correspond to same Term - en_term = Term().load_by_primary_title(last_path) - he_term = Term().load_by_primary_title(he_last_path) - if en_term and en_term == he_term: - pass # both titles are found in an existing Term object - else: + existing_term = Term().load_by_primary_titles(last_path, he_last_path) + if not existing_term: # titles weren't found in same Term object, so try to create a new Term t = Term() t.add_primary_titles(last_path, he_last_path) diff --git a/sefaria/model/schema.py b/sefaria/model/schema.py index f8fa473b7c..5aa146a3b2 100644 --- a/sefaria/model/schema.py +++ b/sefaria/model/schema.py @@ -260,11 +260,12 @@ class Term(abst.AbstractMongoRecord, AbstractTitledObject): "description" ] - def load_by_primary_title(self, title): - query = {'titles': {'$elemMatch:': { - 'text': title, - 'primary': True - }}} + def load_by_primary_titles(self, en_title, he_title): + query = { + 'titles': { + '$all': [{'$elemMatch': {'text': t, 'primary': True}} for t in [en_title, he_title]] + } + } return self.load(query=query) def _set_derived_attributes(self): From df3eb12e3c6e1a547a8bc7265afd241b21463313 Mon Sep 17 00:00:00 2001 From: YishaiGlasner Date: Thu, 5 Feb 2026 11:22:28 +0200 Subject: [PATCH 15/32] chore(Term): newlines and indentation. --- sefaria/model/schema.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sefaria/model/schema.py b/sefaria/model/schema.py index 5aa146a3b2..c8d6939246 100644 --- a/sefaria/model/schema.py +++ b/sefaria/model/schema.py @@ -263,7 +263,9 @@ class Term(abst.AbstractMongoRecord, AbstractTitledObject): def load_by_primary_titles(self, en_title, he_title): query = { 'titles': { - '$all': [{'$elemMatch': {'text': t, 'primary': True}} for t in [en_title, he_title]] + '$all': [{'$elemMatch': { + 'text': t, 'primary': True + }} for t in [en_title, he_title]] } } return self.load(query=query) From 3198f75a8c59b4446f16496a7851795147a7e198 Mon Sep 17 00:00:00 2001 From: YishaiGlasner Date: Thu, 5 Feb 2026 11:28:11 +0200 Subject: [PATCH 16/32] refactor(Term): add validation ensuing uniqueness of primary titles together. --- sefaria/model/schema.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sefaria/model/schema.py b/sefaria/model/schema.py index c8d6939246..b479c4af72 100644 --- a/sefaria/model/schema.py +++ b/sefaria/model/schema.py @@ -287,7 +287,10 @@ def _normalize(self): def _validate(self): super(Term, self)._validate() - # do not allow duplicate names: + # ensue uniqueness of primary titles together + if self.is_new() and Term().load_by_primary_titles(self.get_primary_title(), self.get_primary_title('he')): + raise DuplicateRecordError(f"A Term with the primary titles {self.get_primary_title()} and {self.get_primary_title('he')} already exists") + # do not allow duplicate names if self.is_new() and Term().load({'name': self.name}): raise DuplicateRecordError(f"A Term with the name {self.name} already exists") elif not self.is_new() and self.is_key_changed('name'): From abe7562389a3680dbd41835bd813d8afdeb1d790 Mon Sep 17 00:00:00 2001 From: YishaiGlasner Date: Thu, 5 Feb 2026 13:56:44 +0200 Subject: [PATCH 17/32] chore(text.py): remove unused imports --- sefaria/model/text.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/sefaria/model/text.py b/sefaria/model/text.py index 7771bb649b..e62dd9f403 100644 --- a/sefaria/model/text.py +++ b/sefaria/model/text.py @@ -6,7 +6,7 @@ import time import structlog from functools import reduce, partial -from typing import Optional, Union +from typing import Union from remote_config.keys import REF_CACHE_LIMIT_KEY logger = structlog.get_logger(__name__) @@ -18,17 +18,17 @@ import json import itertools from collections import defaultdict, OrderedDict -from bs4 import BeautifulSoup, Tag +from bs4 import BeautifulSoup import re2 as re from . import abstract as abst -from django_topics.models.topic import Topic as DjangoTopic -from .schema import deserialize_tree, AltStructNode, VirtualNode, DictionaryNode, JaggedArrayNode, TitledTreeNode, DictionaryEntryNode, SheetNode, AddressTalmud, Term, TermSet, TitleGroup, AddressType +from .schema import deserialize_tree, AltStructNode, VirtualNode, JaggedArrayNode, TitledTreeNode, DictionaryEntryNode, SheetNode, AddressTalmud, Term, TermSet, \ + AddressType from sefaria.system.database import db import sefaria.system.cache as scache from sefaria.system.cache import in_memory_cache from sefaria.system.exceptions import InputError, BookNameError, PartialRefInputError, IndexSchemaError, \ - NoVersionFoundError, DictionaryEntryNotFoundError, MissingKeyError, ComplexBookLevelRefError + NoVersionFoundError, MissingKeyError, ComplexBookLevelRefError from sefaria.utils.hebrew import has_hebrew, is_all_hebrew, hebrew_term from sefaria.utils.util import list_depth, truncate_string from sefaria.datatype.jagged_array import JaggedTextArray, JaggedArray @@ -5671,7 +5671,7 @@ def _build_topic_mapping(self): value of the topic's Hebrew primary title. :returns: topic map for the given slug Dictionary """ - from .topic import Topic, TopicSet + from .topic import TopicSet self._topic_mapping = {t.slug: {"en": t.get_primary_title("en"), "he": t.get_primary_title("he")} for t in TopicSet()} return self._topic_mapping From 28d87ea561a7660a04354d8f506520b0516046bf Mon Sep 17 00:00:00 2001 From: YishaiGlasner Date: Sun, 8 Feb 2026 12:41:38 +0200 Subject: [PATCH 18/32] feat(Term): dependencies for term changing. --- sefaria/helper/schema.py | 51 ++++++++++++++++++++++++++++++----- sefaria/model/dependencies.py | 16 +++++------ sefaria/model/schema.py | 2 +- 3 files changed, 53 insertions(+), 16 deletions(-) diff --git a/sefaria/helper/schema.py b/sefaria/helper/schema.py index 76ee3f9230..5af2a2edb8 100644 --- a/sefaria/helper/schema.py +++ b/sefaria/helper/schema.py @@ -1,12 +1,14 @@ # -*- coding: utf-8 -*- - +from sefaria.local_settings_example import MULTISERVER_ENABLED from sefaria.model import * +from sefaria.model import library from sefaria.model.abstract import AbstractMongoRecord from sefaria.model.marked_up_text_chunk import MarkedUpTextChunkSet from sefaria.model.schema import DictionaryNode from sefaria.system.exceptions import InputError from sefaria.system.database import db from sefaria.sheets import save_sheet +from sefaria.system.multiserver.coordinator import server_coordinator from sefaria.utils.util import list_depth, traverse_dict_tree import re @@ -705,7 +707,7 @@ def construct_query(attribute, queries): generic_rewrite(RefDataSet(construct_query('ref', identifier))) print('Updating Topic Links') generic_rewrite(RefTopicLinkSet(construct_query('ref', identifier))) - generic_rewrite(RefTopicLinkSet(construct_query('expandedRefs', identifier))) + generic_rewrite(RefTopicLinkSet(construct_query('expandedRefs', identifier)), attr_name='expandedRefs') print('Updating Garden Stops') generic_rewrite(GardenStopSet(construct_query('ref', identifier))) print('Updating Sheets') @@ -715,11 +717,11 @@ def construct_query(attribute, queries): print('Updating Marked Up Text Chunks') generic_rewrite(MarkedUpTextChunkSet(construct_query('ref', identifier))) print('Updating Manuscripts') - generic_rewrite(ManuscriptSet(construct_query('contained_refs', identifier))) - generic_rewrite(ManuscriptSet(construct_query('expanded_refs', identifier))) + generic_rewrite(ManuscriptPageSet(construct_query('contained_refs', identifier)), attr_name='contained_refs') + generic_rewrite(ManuscriptPageSet(construct_query('expanded_refs', identifier)), attr_name='expanded_refs') print('Updating WebPages') - generic_rewrite(WebPageSet(construct_query('refs', identifier))) - generic_rewrite(ManuscriptSet(construct_query('expandedRefs', identifier))) + generic_rewrite(WebPageSet(construct_query('refs', identifier)), attr_name='refs') + generic_rewrite(WebPageSet(construct_query('expandedRefs', identifier)), attr_name='expandedRefs') if not skip_history: print('Updating History') generic_rewrite(HistorySet(construct_query('ref', identifier), sort=[('ref', 1)])) @@ -1119,3 +1121,40 @@ def update_headwords_map(dictionary_node): if quoted: print(f'Other entries in this lexicon with this old headword as ref: {", ".join(quoted)}') print('Warning: old ref can appear as wrapped ref in other places in the library.') + + +def cascade_node_shared_title_change(node, old): + old_address = node.address()[:-1] + [old] + old_pattern = f"^{re.escape(', '.join(old_address))}(?=$|, |:| \d)" + new_replacement = node.full_title() + + needs_rewrite = lambda ref_str, *args: re.search(old_pattern, ref_str) + rewriter = lambda ref_str: re.sub(old_pattern, new_replacement, ref_str) + + print(f'Cascading from {old_pattern} to {new_replacement}') + cascade(node.index.title, rewriter, needs_rewrite) + + +def process_term_primary_title_change(term, **kwargs): + """ + When a Term's primary title (en or he) changes, rebuild library caches. + This updates term mapping, categories, and indexes that reference this term. + """ + old_titles = kwargs.get("old", []) + old_primary_en = next((t["text"] for t in old_titles if t.get("primary") and t["lang"] == "en")) + old_primary_he = next((t["text"] for t in old_titles if t.get("primary") and t["lang"] == "he")) + new_primary_en = term.get_primary_title("en") + new_primary_he = term.get_primary_title("he") + cause_ref_change = old_primary_en != new_primary_en + + if old_primary_en != new_primary_en or old_primary_he != new_primary_he: # Rebuild terms, categories indexes and refs + library.rebuild(include_toc=True) + + if MULTISERVER_ENABLED: + server_coordinator.publish_event("library", "rebuild", [True]) + + if cause_ref_change: # Now new refs are available and can be cascaded + for index in library.all_index_records(): + for node in [index.nodes] + index.nodes.all_children(): + if getattr(node, "sharedTitle", None) == old_primary_en: + cascade_node_shared_title_change(node, old_primary_en) diff --git a/sefaria/model/dependencies.py b/sefaria/model/dependencies.py index 7e95115119..45bc95db9f 100644 --- a/sefaria/model/dependencies.py +++ b/sefaria/model/dependencies.py @@ -1,7 +1,7 @@ """ dependencies.py -- list cross model dependencies and subscribe listeners to changes. """ - +import sefaria.helper.schema from . import abstract, link, note, history, schema, text, layer, version_state, timeperiod, garden, notification, collection, library, category, ref_data, user_profile, manuscript, topic, place, marked_up_text_chunk from .abstract import subscribe, cascade, cascade_to_list, cascade_delete, cascade_delete_to_list @@ -80,17 +80,15 @@ def process_version_title_change_in_search(ver, **kwargs): # Terms -# TODO cascade change to Term.name. -# TODO Current locations where we know terms are used [Index, Categories] -# TODO Use Sefaria-Project/scripts/search_for_indexes_that_use_terms.py for now +# TermScheme name change cascades to Term.scheme field subscribe(cascade(schema.TermSet, "scheme"), schema.TermScheme, "attributeChange", "name") + +# Term save/delete rebuilds the term mapping cache subscribe(text.reset_simple_term_mapping, schema.Term, "delete") subscribe(text.reset_simple_term_mapping, schema.Term, "save") -""" -Notes on where Terms are used -Index (alt structs and schema) -Category -""" + +# Term primary title change rebuilds library (term mapping, categories, indexes) +subscribe(sefaria.helper.schema.process_term_primary_title_change, schema.Term, "attributeChange", "titles") # Time subscribe(cascade(topic.PersonTopicSet, "properties.era.value"), timeperiod.TimePeriod, "attributeChange", "symbol") diff --git a/sefaria/model/schema.py b/sefaria/model/schema.py index b479c4af72..25c6d55b59 100644 --- a/sefaria/model/schema.py +++ b/sefaria/model/schema.py @@ -243,7 +243,7 @@ class Term(abst.AbstractMongoRecord, AbstractTitledObject): """ collection = 'term' track_pkeys = True - pkeys = ["name"] + pkeys = ["name", "titles"] title_group = None history_noun = "term" From c275e5170d7d0f71a37b28f6dfe2c11d21e180cf Mon Sep 17 00:00:00 2001 From: YishaiGlasner <60393023+YishaiGlasner@users.noreply.github.com> Date: Sun, 8 Feb 2026 14:23:41 +0200 Subject: [PATCH 19/32] fix: import form settings Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- sefaria/helper/schema.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sefaria/helper/schema.py b/sefaria/helper/schema.py index 5af2a2edb8..6366950f15 100644 --- a/sefaria/helper/schema.py +++ b/sefaria/helper/schema.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from sefaria.local_settings_example import MULTISERVER_ENABLED +from sefaria.settings import MULTISERVER_ENABLED from sefaria.model import * from sefaria.model import library from sefaria.model.abstract import AbstractMongoRecord From c5b045a79c0f33ad47c1acdd137d7d34ca21e8e9 Mon Sep 17 00:00:00 2001 From: YishaiGlasner Date: Sun, 8 Feb 2026 14:27:23 +0200 Subject: [PATCH 20/32] fix(term api): add empty list titles to data when term doesnt exist. --- reader/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reader/views.py b/reader/views.py index fa88bde713..7e4d2241c5 100644 --- a/reader/views.py +++ b/reader/views.py @@ -1414,7 +1414,7 @@ def terms_editor(request, name): Add/Editor a term using the JSON Editor. """ existing_term = Term().load({'name': name}) - data = existing_term.contents() if existing_term else {} + data = existing_term.contents() if existing_term else {"titles": []} dataJSON = json.dumps(data) return render_template(request,'edit_term.html', None, { From ff5dec9ce29338a02b6b8743ef946f8aa9b2aa55 Mon Sep 17 00:00:00 2001 From: YishaiGlasner Date: Sun, 8 Feb 2026 14:30:17 +0200 Subject: [PATCH 21/32] fix(term tests): remove test for name doesn't equal primary en. --- sefaria/model/tests/terms_test.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/sefaria/model/tests/terms_test.py b/sefaria/model/tests/terms_test.py index e2ed7784da..a13f10bfed 100644 --- a/sefaria/model/tests/terms_test.py +++ b/sefaria/model/tests/terms_test.py @@ -211,24 +211,6 @@ def test_add_invalid_terms(self): ] }).save() - with pytest.raises(InputError): - Term({ - "name": "Test Fail Five", # name not the same as primary - "scheme": "testing_terms", - "titles" : [ - { - "lang": "en", - "text": "alalalalalala", - "primary": True - }, - { - "lang": "he", - "text": "גלדכחשדף", - "primary": True - } - ] - }).save() - # for ascii validation with pytest.raises(InputError): Term({ From 4a62c226a741b8d1a2f7eca0189a58d445aada50 Mon Sep 17 00:00:00 2001 From: YishaiGlasner Date: Sun, 8 Feb 2026 16:32:06 +0200 Subject: [PATCH 22/32] refactor(term): change setting name logic not to count docs. --- sefaria/model/schema.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/sefaria/model/schema.py b/sefaria/model/schema.py index 25c6d55b59..98510c66f3 100644 --- a/sefaria/model/schema.py +++ b/sefaria/model/schema.py @@ -276,14 +276,20 @@ def _set_derived_attributes(self): def set_titles(self, titles): self.title_group = TitleGroup(titles) + def _set_name(self): + name = base_name = self.get_primary_title() + terms = TermSet({'name': {'$regex': f'^{re.escape(name)}\d*$'}}) + existing_names = {t.name for t in terms} + i = 1 + while name in existing_names: + name = base_name + i + i += 1 + self.name = name + def _normalize(self): self.titles = self.title_group.titles if not hasattr(self, 'name'): - name = self.get_primary_title() - dupes = len(TermSet({'name': {'$regex': fr'^{name}\d*$'}})) - if dupes: - name = f'{name}{dupes}' - setattr(self, 'name', name) + self._set_name() def _validate(self): super(Term, self)._validate() From ecdae5c022113b744d4d814716795f9e2251a8b9 Mon Sep 17 00:00:00 2001 From: YishaiGlasner Date: Sun, 8 Feb 2026 16:40:12 +0200 Subject: [PATCH 23/32] refactor(term): validate terms with same titles doesn't exist also for old term. --- sefaria/model/schema.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sefaria/model/schema.py b/sefaria/model/schema.py index 98510c66f3..0d0a574f63 100644 --- a/sefaria/model/schema.py +++ b/sefaria/model/schema.py @@ -294,7 +294,8 @@ def _normalize(self): def _validate(self): super(Term, self)._validate() # ensue uniqueness of primary titles together - if self.is_new() and Term().load_by_primary_titles(self.get_primary_title(), self.get_primary_title('he')): + same_titles_term = Term().load_by_primary_titles(self.get_primary_title(), self.get_primary_title('he')) + if same_titles_term and not self.same_record(same_titles_term): raise DuplicateRecordError(f"A Term with the primary titles {self.get_primary_title()} and {self.get_primary_title('he')} already exists") # do not allow duplicate names if self.is_new() and Term().load({'name': self.name}): From 7f5838484f8a52fd5e4a9c2e19b9a4864e8b0451 Mon Sep 17 00:00:00 2001 From: YishaiGlasner Date: Sun, 8 Feb 2026 18:50:28 +0200 Subject: [PATCH 24/32] refactor(term): track primary titles as scalars instead of mutable list in pkeys. The titles list was stored by reference in pkeys_orig_values, so in-place mutations bypassed dependency tracking. Replace with `_primary_en`/`_primary_he` string attrs that reliably detect changes. --- sefaria/helper/schema.py | 21 ++++++++------------- sefaria/model/dependencies.py | 3 ++- sefaria/model/schema.py | 12 +++++++++++- 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/sefaria/helper/schema.py b/sefaria/helper/schema.py index 6366950f15..a41a49c91f 100644 --- a/sefaria/helper/schema.py +++ b/sefaria/helper/schema.py @@ -1140,21 +1140,16 @@ def process_term_primary_title_change(term, **kwargs): When a Term's primary title (en or he) changes, rebuild library caches. This updates term mapping, categories, and indexes that reference this term. """ - old_titles = kwargs.get("old", []) - old_primary_en = next((t["text"] for t in old_titles if t.get("primary") and t["lang"] == "en")) - old_primary_he = next((t["text"] for t in old_titles if t.get("primary") and t["lang"] == "he")) - new_primary_en = term.get_primary_title("en") - new_primary_he = term.get_primary_title("he") - cause_ref_change = old_primary_en != new_primary_en + old = kwargs.get("old") + attr = kwargs.get("attr") - if old_primary_en != new_primary_en or old_primary_he != new_primary_he: # Rebuild terms, categories indexes and refs - library.rebuild(include_toc=True) + library.rebuild(include_toc=True) - if MULTISERVER_ENABLED: - server_coordinator.publish_event("library", "rebuild", [True]) + if MULTISERVER_ENABLED: + server_coordinator.publish_event("library", "rebuild", [True]) - if cause_ref_change: # Now new refs are available and can be cascaded + if attr == "_primary_en": # Now new refs are available and can be cascaded for index in library.all_index_records(): for node in [index.nodes] + index.nodes.all_children(): - if getattr(node, "sharedTitle", None) == old_primary_en: - cascade_node_shared_title_change(node, old_primary_en) + if getattr(node, "sharedTitle", None) == old: + cascade_node_shared_title_change(node, old) diff --git a/sefaria/model/dependencies.py b/sefaria/model/dependencies.py index 45bc95db9f..a84e06a60f 100644 --- a/sefaria/model/dependencies.py +++ b/sefaria/model/dependencies.py @@ -88,7 +88,8 @@ def process_version_title_change_in_search(ver, **kwargs): subscribe(text.reset_simple_term_mapping, schema.Term, "save") # Term primary title change rebuilds library (term mapping, categories, indexes) -subscribe(sefaria.helper.schema.process_term_primary_title_change, schema.Term, "attributeChange", "titles") +subscribe(sefaria.helper.schema.process_term_primary_title_change, schema.Term, "attributeChange", "_primary_en") +subscribe(sefaria.helper.schema.process_term_primary_title_change, schema.Term, "attributeChange", "_primary_he") # Time subscribe(cascade(topic.PersonTopicSet, "properties.era.value"), timeperiod.TimePeriod, "attributeChange", "symbol") diff --git a/sefaria/model/schema.py b/sefaria/model/schema.py index 0d0a574f63..fc11f7dfaa 100644 --- a/sefaria/model/schema.py +++ b/sefaria/model/schema.py @@ -243,7 +243,7 @@ class Term(abst.AbstractMongoRecord, AbstractTitledObject): """ collection = 'term' track_pkeys = True - pkeys = ["name", "titles"] + pkeys = ["name", "_primary_en", "_primary_he"] title_group = None history_noun = "term" @@ -270,6 +270,15 @@ def load_by_primary_titles(self, en_title, he_title): } return self.load(query=query) + def _update_tracked_primary_titles(self): + self._primary_en = self.get_primary_title("en") + self._primary_he = self.get_primary_title("he") + + def _set_pkeys(self): + self.set_titles(getattr(self, "titles", None)) + self._update_tracked_primary_titles() + super()._set_pkeys() + def _set_derived_attributes(self): self.set_titles(getattr(self, "titles", None)) @@ -288,6 +297,7 @@ def _set_name(self): def _normalize(self): self.titles = self.title_group.titles + self._update_tracked_primary_titles() if not hasattr(self, 'name'): self._set_name() From d375f940bc5a15c0c88800f0519104cf997c1889 Mon Sep 17 00:00:00 2001 From: YishaiGlasner Date: Sun, 8 Feb 2026 19:02:59 +0200 Subject: [PATCH 25/32] refactor(term): use InputError rather than ValidationError. --- sefaria/model/schema.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sefaria/model/schema.py b/sefaria/model/schema.py index fc11f7dfaa..fab30ca3fc 100644 --- a/sefaria/model/schema.py +++ b/sefaria/model/schema.py @@ -7,7 +7,6 @@ from functools import reduce import re2 as re from sefaria.system.decorators import conditional_graceful_exception -from django.core.exceptions import ValidationError logger = structlog.get_logger(__name__) @@ -311,7 +310,7 @@ def _validate(self): if self.is_new() and Term().load({'name': self.name}): raise DuplicateRecordError(f"A Term with the name {self.name} already exists") elif not self.is_new() and self.is_key_changed('name'): - raise ValidationError({"name": "This field cannot be changed."}) + raise InputError("The 'name' field of a Term cannot be changed.") self.title_group.validate() @staticmethod From b183ad6bc9c5c23eae6522f1b7607929eab3c32b Mon Sep 17 00:00:00 2001 From: YishaiGlasner Date: Sun, 8 Feb 2026 19:28:21 +0200 Subject: [PATCH 26/32] test(term): fix tests to allow dupliacte title but not of primary titles together. --- sefaria/model/tests/terms_test.py | 70 +++++++++++-------------------- 1 file changed, 24 insertions(+), 46 deletions(-) diff --git a/sefaria/model/tests/terms_test.py b/sefaria/model/tests/terms_test.py index a13f10bfed..8a41b7ea52 100644 --- a/sefaria/model/tests/terms_test.py +++ b/sefaria/model/tests/terms_test.py @@ -2,7 +2,8 @@ import pytest from sefaria.model import * -from sefaria.system.exceptions import InputError +from sefaria.system.exceptions import InputError, DuplicateRecordError + class Test_Terms_Validation(object): @classmethod @@ -86,7 +87,7 @@ def test_add_new_term(self): }).save() def test_duplicate_terms(self): - with pytest.raises(InputError): + with pytest.raises(DuplicateRecordError): Term({ "scheme": "commentary_works", "titles": [ @@ -104,51 +105,28 @@ def test_duplicate_terms(self): "name": "Ramban" }).save() - with pytest.raises(InputError): - Term({ - "scheme": "commentary_works", - "titles": [ - { - "lang": "en", - "text": "New Ramban", - "primary": True - }, - { - "lang": "en", - "text": "Ramban", - }, - { - "lang": "he", - "text": "רמב\"ן חדש", - "primary": True - }, - ], - "name": "New Ramban" - }).save() - with pytest.raises(InputError): - Term({"name" : "Parashat Nitzavim", - "titles" : [ - { - "lang" : "en", - "text" : "Parashat Nitzavim", - "primary" : True - }, - { - "lang" : "he", - "text" : "נצבים", - "primary" : True - }, - { - "lang" : "en", - "text" : "Nitzavim" - }, - { - "lang" : "he", - "text" : "פרשת נצבים" - } - ], - "scheme" : "Parasha"}).save() + def test_valid_duplicate_title(self): + Term({ + "scheme": "commentary_works", + "titles": [ + { + "lang": "en", + "text": "New Ramban", + "primary": True + }, + { + "lang": "en", + "text": "Ramban", + }, + { + "lang": "he", + "text": "רמב\"ן חדש", + "primary": True + }, + ], + "name": "New Ramban" + }).save() def test_add_invalid_terms(self): with pytest.raises(InputError): # no heb title at all From 3692fe52225cf23bfa1b96c0a7557e1b0b4e6d56 Mon Sep 17 00:00:00 2001 From: YishaiGlasner Date: Sun, 8 Feb 2026 19:28:58 +0200 Subject: [PATCH 27/32] test(mongo indexes): remove unique from term's titles index. --- sefaria/system/database.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sefaria/system/database.py b/sefaria/system/database.py index c0d7cdf15e..9cc5e393a4 100644 --- a/sefaria/system/database.py +++ b/sefaria/system/database.py @@ -149,7 +149,8 @@ def ensure_indices(active_db=None): ('word_form', ["form"],{}), ('word_form', ["c_form"],{}), ('word_form', ["refs"], {}), - ('term', ["titles.text"], {'unique': True}), + ('term', ["titles.text"]), + ('term', ["titles.name"]), ('term', ["category"],{}), ('lexicon_entry', [[("headword", pymongo.ASCENDING), ("parent_lexicon", pymongo.ASCENDING)]],{}), ('user_story', ["uid"],{}), From 4c4f352cefa4450f4b8dd283971afec00f10e68e Mon Sep 17 00:00:00 2001 From: YishaiGlasner <60393023+YishaiGlasner@users.noreply.github.com> Date: Mon, 9 Feb 2026 08:31:01 +0200 Subject: [PATCH 28/32] fix(term): use f string rather than plus to concaterate int to str Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- sefaria/model/schema.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sefaria/model/schema.py b/sefaria/model/schema.py index fab30ca3fc..ced5345e76 100644 --- a/sefaria/model/schema.py +++ b/sefaria/model/schema.py @@ -290,7 +290,7 @@ def _set_name(self): existing_names = {t.name for t in terms} i = 1 while name in existing_names: - name = base_name + i + name = f"{base_name}{i}" i += 1 self.name = name From 51ca59a7c47d2bbcc27c9a5ac8d7dd267d44a64d Mon Sep 17 00:00:00 2001 From: nsantacruz Date: Mon, 9 Feb 2026 14:35:19 +0200 Subject: [PATCH 29/32] chore: update gunicorn version to 25.0.3 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 18ed1cf5f1..7bd6bb6341 100644 --- a/requirements.txt +++ b/requirements.txt @@ -45,7 +45,7 @@ google-auth==1.24.0 google-cloud-logging==1.15.1 google-cloud-storage==1.32.0 google-re2 -gunicorn==20.0.4 +gunicorn==25.0.3 html5lib==0.9999999 httplib2==0.18.1 ipython==7.34.* From 0e9b37b9e03686c796d92ae904b25e10f2e817e5 Mon Sep 17 00:00:00 2001 From: nsantacruz Date: Mon, 9 Feb 2026 15:28:08 +0200 Subject: [PATCH 30/32] chore: downgrade gunicorn version to 23.0.0 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 7bd6bb6341..466551fed5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -45,7 +45,7 @@ google-auth==1.24.0 google-cloud-logging==1.15.1 google-cloud-storage==1.32.0 google-re2 -gunicorn==25.0.3 +gunicorn==23.0.0 html5lib==0.9999999 httplib2==0.18.1 ipython==7.34.* From cfd4bab035ce6b4f112e6b1863af4048e38fecb4 Mon Sep 17 00:00:00 2001 From: nsantacruz Date: Mon, 9 Feb 2026 15:40:38 +0200 Subject: [PATCH 31/32] chore: downgrade gunicorn version to 23.0.0 --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 466551fed5..8d02bfc30f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -46,6 +46,7 @@ google-cloud-logging==1.15.1 google-cloud-storage==1.32.0 google-re2 gunicorn==23.0.0 +setuptools==69.5.1 html5lib==0.9999999 httplib2==0.18.1 ipython==7.34.* From 5484258fa4bfe56ee9f915884e67526c86e67136 Mon Sep 17 00:00:00 2001 From: YishaiGlasner Date: Tue, 10 Feb 2026 16:29:27 +0200 Subject: [PATCH 32/32] fix(links api): change english collective title to be the terms' primary english title rather than its name. --- sefaria/client/wrapper.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/sefaria/client/wrapper.py b/sefaria/client/wrapper.py index 1f29cab4d2..b69adb4ff5 100644 --- a/sefaria/client/wrapper.py +++ b/sefaria/client/wrapper.py @@ -72,9 +72,10 @@ def format_link_object_for_client(link, with_text, ref, pos=None): # if the link is commentary, strip redundant info (e.g. "Rashi on Genesis 4:2" -> "Rashi") # this is now simpler, and there is explicit data on the index record for it. if com["type"] == "commentary": + collective_title = getattr(linkRef.index, 'collective_title', None) + term = library.get_simple_term_mapping().get(collective_title) or {} com["collectiveTitle"] = { - 'en': getattr(linkRef.index, 'collective_title', linkRef.index.title), - 'he': hebrew_term(getattr(linkRef.index, 'collective_title', linkRef.index.get_title("he"))) + lang: term.get(lang) or linkRef.index.get_title(lang) for lang in ('en', 'he') } else: com["collectiveTitle"] = {'en': linkRef.index.title, 'he': linkRef.index.get_title("he")}