From 62ec0caf8f3d2957d6b4c0809727331633fedd65 Mon Sep 17 00:00:00 2001
From: YishaiGlasner
Date: Wed, 3 Jul 2024 18:14:15 +0300
Subject: [PATCH 001/125] feat(Topic): add pools to optional_attrs.
---
sefaria/model/topic.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/sefaria/model/topic.py b/sefaria/model/topic.py
index a5dfe56568..48bbfd52f2 100644
--- a/sefaria/model/topic.py
+++ b/sefaria/model/topic.py
@@ -49,6 +49,7 @@ class Topic(abst.SluggedAbstractMongoRecord, AbstractTitledObject):
"data_source", #any topic edited manually should display automatically in the TOC and this flag ensures this
'image',
"portal_slug", # slug to relevant Portal object
+ 'pools', # list of strings, any of them represents a pool that this topic is member of
]
attr_schemas = {
From 849cb8ae483724f54027834dbfc7d3d75689bd83 Mon Sep 17 00:00:00 2001
From: YishaiGlasner
Date: Wed, 3 Jul 2024 18:18:32 +0300
Subject: [PATCH 002/125] feat(Topic): normalize pools - remove duplicates,
sort, and set to an empty array if missing.
---
sefaria/model/topic.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/sefaria/model/topic.py b/sefaria/model/topic.py
index 48bbfd52f2..0d796c7121 100644
--- a/sefaria/model/topic.py
+++ b/sefaria/model/topic.py
@@ -115,6 +115,7 @@ def _normalize(self):
displays_under_link = IntraTopicLink().load({"fromTopic": slug, "linkType": "displays-under"})
if getattr(displays_under_link, "toTopic", "") == "authors":
self.subclass = "author"
+ self.pools = sorted(set(getattr(self, 'pools', [])))
def _sanitize(self):
super()._sanitize()
From 2c42b7680183ab3a50834b275bd3f9d50234e5bf Mon Sep 17 00:00:00 2001
From: YishaiGlasner
Date: Wed, 3 Jul 2024 18:22:37 +0300
Subject: [PATCH 003/125] feat(Topic): function for adding pool.
---
sefaria/model/topic.py | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/sefaria/model/topic.py b/sefaria/model/topic.py
index 0d796c7121..03868e9b95 100644
--- a/sefaria/model/topic.py
+++ b/sefaria/model/topic.py
@@ -427,6 +427,10 @@ def __str__(self):
def __repr__(self):
return "{}.init('{}')".format(self.__class__.__name__, self.slug)
+ def add_pool(self, pool_name):
+ if pool_name not in self.pools:
+ self.pools.append(pool_name)
+ self.save()
class PersonTopic(Topic):
"""
From eec5b9ac0e67fa8e58ed253e7e83792a258d2218 Mon Sep 17 00:00:00 2001
From: YishaiGlasner
Date: Wed, 3 Jul 2024 18:31:18 +0300
Subject: [PATCH 004/125] feat(Topic): add optional_pools as class attribute.
validate pools are in optional_pools.
---
sefaria/model/topic.py | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/sefaria/model/topic.py b/sefaria/model/topic.py
index 03868e9b95..78d06a4acf 100644
--- a/sefaria/model/topic.py
+++ b/sefaria/model/topic.py
@@ -52,6 +52,8 @@ class Topic(abst.SluggedAbstractMongoRecord, AbstractTitledObject):
'pools', # list of strings, any of them represents a pool that this topic is member of
]
+ optional_pools = {'sheets', 'textual', 'torahtab'}
+
attr_schemas = {
"image": {
"image_uri": {
@@ -104,6 +106,7 @@ def _validate(self):
if getattr(self, "image", False):
img_url = self.image.get("image_uri")
if img_url: validate_url(img_url)
+ assert all(pool in self.optional_pools for pool in self.pools), f'Pools {[pool for pool in self.pools if pool not in self.optional_pools]} is not an optional pool'
def _normalize(self):
super()._normalize()
@@ -432,6 +435,8 @@ def add_pool(self, pool_name):
self.pools.append(pool_name)
self.save()
+ def update_sheets_pool(self):
+
class PersonTopic(Topic):
"""
Represents a topic which is a person. Not necessarily an author of a book.
From e63842e13cd63740049ad5a52ec8fee915a30ff0 Mon Sep 17 00:00:00 2001
From: YishaiGlasner
Date: Wed, 3 Jul 2024 18:51:45 +0300
Subject: [PATCH 005/125] feat(Topic): add function for updating 'sheets' in
pool, in accordance with refLinks to sheet existence.
---
sefaria/model/topic.py | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/sefaria/model/topic.py b/sefaria/model/topic.py
index 78d06a4acf..2833ab8f4f 100644
--- a/sefaria/model/topic.py
+++ b/sefaria/model/topic.py
@@ -367,6 +367,10 @@ def link_set(self, _class='intraTopic', query_kwargs: dict = None, **kwargs):
kwargs['record_kwargs'] = {'context_slug': self.slug}
return TopicLinkSetHelper.find(intra_link_query, **kwargs)
+ def get_sheets_links(self, query_kwargs: dict = None, **kwargs):
+ query_kwargs['is_sheet'] = True
+ return self.link_set('refTopic', query_kwargs, **kwargs)
+
def contents(self, **kwargs):
mini = kwargs.get('minify', False)
d = {'slug': self.slug} if mini else super(Topic, self).contents(**kwargs)
@@ -436,6 +440,11 @@ def add_pool(self, pool_name):
self.save()
def update_sheets_pool(self):
+ sheets_links = self.get_sheets_links()
+ if bool(sheets_links) != 'sheets' in self.pools:
+ self.pools.remove('sheets') if 'sheets' in self.pools else self.pools.append('sheets')
+ self.save()
+
class PersonTopic(Topic):
"""
From 3c377cfa15f94b599918833e468d61f1b92b1df7 Mon Sep 17 00:00:00 2001
From: YishaiGlasner
Date: Wed, 3 Jul 2024 19:21:37 +0300
Subject: [PATCH 006/125] refactor(Topic): remove magic string from
update_sheets_pool.
---
sefaria/model/topic.py | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/sefaria/model/topic.py b/sefaria/model/topic.py
index 2833ab8f4f..b8b28e2aa5 100644
--- a/sefaria/model/topic.py
+++ b/sefaria/model/topic.py
@@ -440,9 +440,10 @@ def add_pool(self, pool_name):
self.save()
def update_sheets_pool(self):
+ pool = 'sheets'
sheets_links = self.get_sheets_links()
- if bool(sheets_links) != 'sheets' in self.pools:
- self.pools.remove('sheets') if 'sheets' in self.pools else self.pools.append('sheets')
+ if bool(sheets_links) != pool in self.pools:
+ self.pools.remove(pool) if pool in self.pools else self.pools.append(pool)
self.save()
From 91d9e6c6d0e8e209b4e6f493d6d13e23747ced19 Mon Sep 17 00:00:00 2001
From: YishaiGlasner
Date: Wed, 3 Jul 2024 19:30:17 +0300
Subject: [PATCH 007/125] refactor(Topic): use pools['torahtab'] rather than
good_to_promote'.
---
sefaria/helper/topic.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/sefaria/helper/topic.py b/sefaria/helper/topic.py
index ae3337d0c7..3119d0ff2a 100644
--- a/sefaria/helper/topic.py
+++ b/sefaria/helper/topic.py
@@ -281,7 +281,7 @@ def curated_primacy(order_dict, lang):
def get_random_topic(good_to_promote=True) -> Optional[Topic]:
- query = {"good_to_promote": True} if good_to_promote else {}
+ query = {"pools": 'sheets'} if good_to_promote else {}
random_topic_dict = list(db.topics.aggregate([
{"$match": query},
{"$sample": {"size": 1}}
From c24313d17b86096f86eabde465549a87c4e2a112 Mon Sep 17 00:00:00 2001
From: YishaiGlasner
Date: Wed, 3 Jul 2024 19:43:14 +0300
Subject: [PATCH 008/125] doc(Topic): remove good_to_promote from openAPI.jsom.
---
docs/openAPI.json | 18 +-----------------
1 file changed, 1 insertion(+), 17 deletions(-)
diff --git a/docs/openAPI.json b/docs/openAPI.json
index fe97c22686..62c5aa4d1e 100644
--- a/docs/openAPI.json
+++ b/docs/openAPI.json
@@ -4187,7 +4187,6 @@
},
"categoryDescription": {},
"numSources": 1767,
- "good_to_promote": true,
"description_published": true,
"data_source": "sefaria",
"primaryTitle": {
@@ -4591,7 +4590,6 @@
"isTopLevelDisplay": true,
"displayOrder": 30,
"numSources": 2937,
- "good_to_promote": true,
"primaryTitle": {
"en": "Prayer",
"he": "תפילה"
@@ -4623,7 +4621,6 @@
"_temp_id": "תורה"
},
"numSources": 2333,
- "good_to_promote": true,
"primaryTitle": {
"en": "Torah",
"he": "תורה"
@@ -4766,7 +4763,6 @@
},
"categoryDescription": {},
"numSources": 1967,
- "good_to_promote": true,
"description_published": true,
"data_source": "sefaria",
"primaryTitle": {
@@ -4869,7 +4865,6 @@
"categoryDescription": {},
"displayOrder": 0,
"numSources": 1662,
- "good_to_promote": true,
"description_published": true,
"data_source": "sefaria",
"image": {
@@ -5162,7 +5157,6 @@
}
},
"numSources": 7,
- "good_to_promote": true,
"primaryTitle": {
"en": "Metushelach",
"he": "מתושלח"
@@ -9761,7 +9755,6 @@
"categoryDescription": {},
"displayOrder": 0,
"numSources": 1662,
- "good_to_promote": true,
"description_published": true,
"data_source": "sefaria",
"image": {
@@ -10071,9 +10064,6 @@
"format": "int32",
"type": "integer"
},
- "good_to_promote": {
- "type": "boolean"
- },
"description_published": {
"type": "boolean"
},
@@ -10089,7 +10079,7 @@
}
}
},
- "example": "{\n\"slug\": \"metushelach\",\n\"titles\": [\n{\n\"text\": \"Metushelach\",\n\"lang\": \"en\",\n\"primary\": true,\n\"transliteration\": true\n},\n{\n\"text\": \"מתושלח\",\n\"lang\": \"he\",\n\"primary\": true\n},\n{\n\"text\": \"Methuselah\",\n\"lang\": \"en\"\n},\n{\n\"text\": \"Methushelach\",\n\"lang\": \"en\"\n}\n],\n\"subclass\": \"person\",\n\"alt_ids\": {\n\"_temp_id\": \"מתושלח\",\n\"wikidata\": \"Q156290\"\n},\n\"properties\": {\n\"enWikiLink\": {\n\"value\": \"https://en.wikipedia.org/wiki/Methuselah\",\n\"dataSource\": \"wikidata\"\n},\n\"heWikiLink\": {\n\"value\": \"https://he.wikipedia.org/wiki/מתושלח\",\n\"dataSource\": \"wikidata\"\n},\n\"deWikiLink\": {\n\"value\": \"https://de.wikipedia.org/wiki/Methusalem\",\n\"dataSource\": \"wikidata\"\n},\n\"esWikiLink\": {\n\"value\": \"https://es.wikipedia.org/wiki/Matusalén\",\n\"dataSource\": \"wikidata\"\n},\n\"frWikiLink\": {\n\"value\": \"https://fr.wikipedia.org/wiki/Mathusalem\",\n\"dataSource\": \"wikidata\"\n},\n\"ruWikiLink\": {\n\"value\": \"https://ru.wikipedia.org/wiki/Мафусал_(потомок_Сифа)\",\n\"dataSource\": \"wikidata\"\n}\n},\n\"numSources\": 7,\n\"good_to_promote\": true,\n\"primaryTitle\": {\n\"en\": \"Metushelach\",\n\"he\": \"מתושלח\"\n}\n}"
+ "example": "{\n\"slug\": \"metushelach\",\n\"titles\": [\n{\n\"text\": \"Metushelach\",\n\"lang\": \"en\",\n\"primary\": true,\n\"transliteration\": true\n},\n{\n\"text\": \"מתושלח\",\n\"lang\": \"he\",\n\"primary\": true\n},\n{\n\"text\": \"Methuselah\",\n\"lang\": \"en\"\n},\n{\n\"text\": \"Methushelach\",\n\"lang\": \"en\"\n}\n],\n\"subclass\": \"person\",\n\"alt_ids\": {\n\"_temp_id\": \"מתושלח\",\n\"wikidata\": \"Q156290\"\n},\n\"properties\": {\n\"enWikiLink\": {\n\"value\": \"https://en.wikipedia.org/wiki/Methuselah\",\n\"dataSource\": \"wikidata\"\n},\n\"heWikiLink\": {\n\"value\": \"https://he.wikipedia.org/wiki/מתושלח\",\n\"dataSource\": \"wikidata\"\n},\n\"deWikiLink\": {\n\"value\": \"https://de.wikipedia.org/wiki/Methusalem\",\n\"dataSource\": \"wikidata\"\n},\n\"esWikiLink\": {\n\"value\": \"https://es.wikipedia.org/wiki/Matusalén\",\n\"dataSource\": \"wikidata\"\n},\n\"frWikiLink\": {\n\"value\": \"https://fr.wikipedia.org/wiki/Mathusalem\",\n\"dataSource\": \"wikidata\"\n},\n\"ruWikiLink\": {\n\"value\": \"https://ru.wikipedia.org/wiki/Мафусал_(потомок_Сифа)\",\n\"dataSource\": \"wikidata\"\n}\n},\n\"numSources\": 7,\n\"primaryTitle\": {\n\"en\": \"Metushelach\",\n\"he\": \"מתושלח\"\n}\n}"
},
"url": {
"description": "The `Ref` in a format appropriate for a URL, with spaces replaced with `.` etc. ",
@@ -10135,7 +10125,6 @@
},
"categoryDescription": {},
"numSources": 217,
- "good_to_promote": true,
"description_published": true,
"primaryTitle": {
"en": "Hillel",
@@ -10170,10 +10159,6 @@
"description": "A description of the category of this topic",
"type": "string"
},
- "good_to_promote": {
- "description": "A topic which will be included in our results from the `random-by-topic` endpoint. ",
- "type": "boolean"
- },
"numSources": {
"description": "The number of text sources associated with a topic. ",
"type": "integer",
@@ -10222,7 +10207,6 @@
},
"categoryDescription": {},
"numSources": 120,
- "good_to_promote": true,
"description_published": true,
"data_source": "sefaria",
"primaryTitle": {
From 796661db79067a9e7c9ab2919f15e5d9633464c8 Mon Sep 17 00:00:00 2001
From: YishaiGlasner
Date: Thu, 4 Jul 2024 11:59:34 +0300
Subject: [PATCH 009/125] feat(Topic): remove pools from contents() as default.
---
sefaria/model/topic.py | 2 ++
1 file changed, 2 insertions(+)
diff --git a/sefaria/model/topic.py b/sefaria/model/topic.py
index b8b28e2aa5..ea6331995c 100644
--- a/sefaria/model/topic.py
+++ b/sefaria/model/topic.py
@@ -374,6 +374,8 @@ def get_sheets_links(self, query_kwargs: dict = None, **kwargs):
def contents(self, **kwargs):
mini = kwargs.get('minify', False)
d = {'slug': self.slug} if mini else super(Topic, self).contents(**kwargs)
+ if kwargs.get('remove_pools', True):
+ d.pop('pools', None)
d['primaryTitle'] = {}
for lang in ('en', 'he'):
d['primaryTitle'][lang] = self.get_primary_title(lang=lang, with_disambiguation=kwargs.get('with_disambiguation', True))
From 604bca7939b09d0e378e1a6f0bee5ebd99f99b05 Mon Sep 17 00:00:00 2001
From: YishaiGlasner
Date: Thu, 4 Jul 2024 12:48:12 +0300
Subject: [PATCH 010/125] refactor(Topic): change get_random_topic param
good_to_promote to a param pool that defaults to 'torahtab'.
---
reader/views.py | 2 +-
sefaria/helper/topic.py | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/reader/views.py b/reader/views.py
index ef8682dd38..e0fbebf308 100644
--- a/reader/views.py
+++ b/reader/views.py
@@ -4195,7 +4195,7 @@ def random_by_topic_api(request):
Returns Texts API data for a random text taken from popular topic tags
"""
cb = request.GET.get("callback", None)
- random_topic = get_random_topic(good_to_promote=True)
+ random_topic = get_random_topic()
if random_topic is None:
return random_by_topic_api(request)
random_source = get_random_topic_source(random_topic)
diff --git a/sefaria/helper/topic.py b/sefaria/helper/topic.py
index 3119d0ff2a..43a1847c3d 100644
--- a/sefaria/helper/topic.py
+++ b/sefaria/helper/topic.py
@@ -280,8 +280,8 @@ def curated_primacy(order_dict, lang):
return (bord.get('numDatasource', 0) * bord.get('tfidf', 0)) - (aord.get('numDatasource', 0) * aord.get('tfidf', 0))
-def get_random_topic(good_to_promote=True) -> Optional[Topic]:
- query = {"pools": 'sheets'} if good_to_promote else {}
+def get_random_topic(pool='torahtab') -> Optional[Topic]:
+ query = {"pools": pool} if pool else {}
random_topic_dict = list(db.topics.aggregate([
{"$match": query},
{"$sample": {"size": 1}}
From f90e5ad175dfa33f5b5be6a7e7b724b818a4636c Mon Sep 17 00:00:00 2001
From: YishaiGlasner
Date: Mon, 8 Jul 2024 08:53:06 +0300
Subject: [PATCH 011/125] refactor(Topic): add pool to Topic when saving
RefTopicLink.
---
sefaria/model/topic.py | 11 +++++++++++
1 file changed, 11 insertions(+)
diff --git a/sefaria/model/topic.py b/sefaria/model/topic.py
index ea6331995c..11f5b4164c 100644
--- a/sefaria/model/topic.py
+++ b/sefaria/model/topic.py
@@ -837,6 +837,17 @@ def set_description(self, lang, title, prompt):
self.descriptions = d
return self
+ def get_pool(self):
+ return 'sheets' if self.is_sheet else 'textual'
+
+ def get_topic(self):
+ return Topic().load({'slug': self.toTopic})
+
+ def save(self, override_dependencies=False):
+ super(RefTopicLink, self).save()
+ topic = self.get_topic()
+ topic.add_pool(self.get_pool())
+
def _sanitize(self):
super()._sanitize()
for lang, d in getattr(self, "descriptions", {}).items():
From 12569a528e569808331b3d6311a3c12662136019 Mon Sep 17 00:00:00 2001
From: YishaiGlasner
Date: Mon, 8 Jul 2024 09:05:32 +0300
Subject: [PATCH 012/125] refactor(Topic): refactor the functions for getting
sheet links and removeing sheets pool, to apply also for textual links and
pool.
---
sefaria/model/topic.py | 15 +++++++++------
1 file changed, 9 insertions(+), 6 deletions(-)
diff --git a/sefaria/model/topic.py b/sefaria/model/topic.py
index 11f5b4164c..ca8f747dd8 100644
--- a/sefaria/model/topic.py
+++ b/sefaria/model/topic.py
@@ -367,8 +367,8 @@ def link_set(self, _class='intraTopic', query_kwargs: dict = None, **kwargs):
kwargs['record_kwargs'] = {'context_slug': self.slug}
return TopicLinkSetHelper.find(intra_link_query, **kwargs)
- def get_sheets_links(self, query_kwargs: dict = None, **kwargs):
- query_kwargs['is_sheet'] = True
+ def get_ref_links(self, is_sheet, query_kwargs: dict = None, **kwargs):
+ query_kwargs['is_sheet'] = is_sheet
return self.link_set('refTopic', query_kwargs, **kwargs)
def contents(self, **kwargs):
@@ -441,10 +441,13 @@ def add_pool(self, pool_name):
self.pools.append(pool_name)
self.save()
- def update_sheets_pool(self):
- pool = 'sheets'
- sheets_links = self.get_sheets_links()
- if bool(sheets_links) != pool in self.pools:
+ def update_pool_by_links(self, pool):
+ """
+ updating the pools 'sheets' or 'textual' according to the existence of links
+ :param pool: 'sheets' or 'textual'
+ """
+ links = self.get_ref_links(pool == 'sheets')
+ if bool(links) != pool in self.pools:
self.pools.remove(pool) if pool in self.pools else self.pools.append(pool)
self.save()
From 454375aeb0e29c9bb0eb1f1b74b6170258d13f0f Mon Sep 17 00:00:00 2001
From: YishaiGlasner
Date: Mon, 8 Jul 2024 09:12:45 +0300
Subject: [PATCH 013/125] feat(Topic): update pool when deleting a
RefTopicLink.
---
sefaria/model/topic.py | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/sefaria/model/topic.py b/sefaria/model/topic.py
index ca8f747dd8..b0ea2e4156 100644
--- a/sefaria/model/topic.py
+++ b/sefaria/model/topic.py
@@ -851,6 +851,13 @@ def save(self, override_dependencies=False):
topic = self.get_topic()
topic.add_pool(self.get_pool())
+ def delete(self, force=False, override_dependencies=False):
+ topic = self.get_topic()
+ pool = self.get_pool()
+ super(RefTopicLink, self).delete()
+ if topic:
+ topic.update_pool_by_links(pool)
+
def _sanitize(self):
super()._sanitize()
for lang, d in getattr(self, "descriptions", {}).items():
From 10ed04218ff509e32578ae3aaa8358bcb6b57e25 Mon Sep 17 00:00:00 2001
From: YishaiGlasner
Date: Mon, 8 Jul 2024 09:14:28 +0300
Subject: [PATCH 014/125] chore(Topic): add params to the supers.
---
sefaria/model/topic.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/sefaria/model/topic.py b/sefaria/model/topic.py
index b0ea2e4156..5a61f359ff 100644
--- a/sefaria/model/topic.py
+++ b/sefaria/model/topic.py
@@ -847,14 +847,14 @@ def get_topic(self):
return Topic().load({'slug': self.toTopic})
def save(self, override_dependencies=False):
- super(RefTopicLink, self).save()
+ super(RefTopicLink, self).save(override_dependencies)
topic = self.get_topic()
topic.add_pool(self.get_pool())
def delete(self, force=False, override_dependencies=False):
topic = self.get_topic()
pool = self.get_pool()
- super(RefTopicLink, self).delete()
+ super(RefTopicLink, self).delete(force, override_dependencies)
if topic:
topic.update_pool_by_links(pool)
From 8488f2f5d4cb0d8e990ca81c750182be841121b9 Mon Sep 17 00:00:00 2001
From: YishaiGlasner
Date: Mon, 8 Jul 2024 09:15:37 +0300
Subject: [PATCH 015/125] refactor(Topic): rename get_pool to get_related_pool.
---
sefaria/model/topic.py | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/sefaria/model/topic.py b/sefaria/model/topic.py
index 5a61f359ff..0efb1272f6 100644
--- a/sefaria/model/topic.py
+++ b/sefaria/model/topic.py
@@ -840,7 +840,7 @@ def set_description(self, lang, title, prompt):
self.descriptions = d
return self
- def get_pool(self):
+ def get_related_pool(self):
return 'sheets' if self.is_sheet else 'textual'
def get_topic(self):
@@ -849,11 +849,11 @@ def get_topic(self):
def save(self, override_dependencies=False):
super(RefTopicLink, self).save(override_dependencies)
topic = self.get_topic()
- topic.add_pool(self.get_pool())
+ topic.add_pool(self.get_related_pool())
def delete(self, force=False, override_dependencies=False):
topic = self.get_topic()
- pool = self.get_pool()
+ pool = self.get_related_pool()
super(RefTopicLink, self).delete(force, override_dependencies)
if topic:
topic.update_pool_by_links(pool)
From 740d31f057262f4f3f742e68d109b068a2d373ee Mon Sep 17 00:00:00 2001
From: YishaiGlasner
Date: Mon, 15 Jul 2024 10:55:37 +0300
Subject: [PATCH 016/125] refactor(Topic): one function for updating topic
after saving a refLink. numSources is the total of both kinds of refLinks, to
texts and sheets.
---
sefaria/model/topic.py | 18 +++++++-----------
1 file changed, 7 insertions(+), 11 deletions(-)
diff --git a/sefaria/model/topic.py b/sefaria/model/topic.py
index 0efb1272f6..6903cf2a41 100644
--- a/sefaria/model/topic.py
+++ b/sefaria/model/topic.py
@@ -39,7 +39,7 @@ class Topic(abst.SluggedAbstractMongoRecord, AbstractTitledObject):
'categoryDescription', # dictionary, keys are 2-letter language codes
'isTopLevelDisplay',
'displayOrder',
- 'numSources',
+ 'numSources', # total number of refLinks, to texts and sheets.
'shouldDisplay',
'parasha', # name of parsha as it appears in `parshiot` collection
'ref', # dictionary for topics with refs associated with them (e.g. parashah) containing strings `en`, `he`, and `url`.
@@ -436,20 +436,16 @@ def __str__(self):
def __repr__(self):
return "{}.init('{}')".format(self.__class__.__name__, self.slug)
- def add_pool(self, pool_name):
- if pool_name not in self.pools:
- self.pools.append(pool_name)
- self.save()
-
- def update_pool_by_links(self, pool):
+ def update_after_link_change(self, pool):
"""
- updating the pools 'sheets' or 'textual' according to the existence of links
+ updating the pools 'sheets' or 'textual' according to the existence of links and the numSources
:param pool: 'sheets' or 'textual'
"""
links = self.get_ref_links(pool == 'sheets')
if bool(links) != pool in self.pools:
self.pools.remove(pool) if pool in self.pools else self.pools.append(pool)
- self.save()
+ self.numSources = self.link_set('refTopic').count()
+ self.save()
class PersonTopic(Topic):
@@ -849,14 +845,14 @@ def get_topic(self):
def save(self, override_dependencies=False):
super(RefTopicLink, self).save(override_dependencies)
topic = self.get_topic()
- topic.add_pool(self.get_related_pool())
+ topic.update_after_link_change(self.get_related_pool())
def delete(self, force=False, override_dependencies=False):
topic = self.get_topic()
pool = self.get_related_pool()
super(RefTopicLink, self).delete(force, override_dependencies)
if topic:
- topic.update_pool_by_links(pool)
+ topic.update_after_link_change(pool)
def _sanitize(self):
super()._sanitize()
From a1efe9336a570db5086864a8645314bb285a892a Mon Sep 17 00:00:00 2001
From: YishaiGlasner
Date: Mon, 15 Jul 2024 15:41:01 +0300
Subject: [PATCH 017/125] feat(topics): in recalculate_secondary_topic_data the
only thing that done is reordering.
---
sefaria/helper/topic.py | 23 ++++-------------------
1 file changed, 4 insertions(+), 19 deletions(-)
diff --git a/sefaria/helper/topic.py b/sefaria/helper/topic.py
index 43a1847c3d..a84b1bb68a 100644
--- a/sefaria/helper/topic.py
+++ b/sefaria/helper/topic.py
@@ -960,33 +960,18 @@ def calculate_popular_writings_for_authors(top_n, min_pr):
"order": {"custom_order": rd['pagesheetrank']}
}).save()
-
def recalculate_secondary_topic_data():
- # run before everything else because this creates new links
- calculate_popular_writings_for_authors(100, 300)
+ sheet_source_links = RefTopicLinkSet({'pools': 'textual'})
+ sheet_topic_links = RefTopicLinkSet({'pools': 'sheets'})
+ sheet_related_links = IntraTopicLinkSet()
- sheet_source_links, sheet_related_links, sheet_topic_links = generate_all_topic_links_from_sheets()
related_links = update_intra_topic_link_orders(sheet_related_links)
- all_ref_links = update_ref_topic_link_orders(sheet_source_links, sheet_topic_links)
-
- # now that we've gathered all the new links, delete old ones and insert new ones
- RefTopicLinkSet({"generatedBy": TopicLinkHelper.generated_by_sheets}).delete()
- RefTopicLinkSet({"is_sheet": True}).delete()
- IntraTopicLinkSet({"generatedBy": TopicLinkHelper.generated_by_sheets}).delete()
- print(f"Num Ref Links {len(all_ref_links)}")
- print(f"Num Intra Links {len(related_links)}")
- print(f"Num to Update {len(list(filter(lambda x: getattr(x, '_id', False), all_ref_links + related_links)))}")
- print(f"Num to Insert {len(list(filter(lambda x: not getattr(x, '_id', False), all_ref_links + related_links)))}")
+ all_ref_links = update_ref_topic_link_orders(sheet_source_links.array(), sheet_topic_links.array())
db.topic_links.bulk_write([
UpdateOne({"_id": l._id}, {"$set": {"order": l.order}})
- if getattr(l, "_id", False) else
- InsertOne(l.contents(for_db=True))
for l in (all_ref_links + related_links)
])
- add_num_sources_to_topics()
- make_titles_unique()
-
def set_all_slugs_to_primary_title():
# reset all slugs to their primary titles, if they have drifted away
From 6fe3ec85b25eb61c3cda38b95585aaa023d49312 Mon Sep 17 00:00:00 2001
From: YishaiGlasner
Date: Tue, 16 Jul 2024 09:14:30 +0300
Subject: [PATCH 018/125] fix(Topic): init query_kwargs in get_ref_links.
---
sefaria/model/topic.py | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/sefaria/model/topic.py b/sefaria/model/topic.py
index 6903cf2a41..c558d505b4 100644
--- a/sefaria/model/topic.py
+++ b/sefaria/model/topic.py
@@ -367,7 +367,9 @@ def link_set(self, _class='intraTopic', query_kwargs: dict = None, **kwargs):
kwargs['record_kwargs'] = {'context_slug': self.slug}
return TopicLinkSetHelper.find(intra_link_query, **kwargs)
- def get_ref_links(self, is_sheet, query_kwargs: dict = None, **kwargs):
+ def get_ref_links(self, is_sheet, query_kwargs=None, **kwargs):
+ if query_kwargs is None:
+ query_kwargs = {}
query_kwargs['is_sheet'] = is_sheet
return self.link_set('refTopic', query_kwargs, **kwargs)
From ea881b87d45dcfea62f071b653ec323b758057db Mon Sep 17 00:00:00 2001
From: YishaiGlasner
Date: Wed, 17 Jul 2024 15:13:36 +0300
Subject: [PATCH 019/125] refactor(Topic): changes for 'pools' to be an
optional attribute - add the functions get_pools, has_pool, add_pool and
remove_pool, and using them in the class.
---
sefaria/model/topic.py | 25 +++++++++++++++++++++----
1 file changed, 21 insertions(+), 4 deletions(-)
diff --git a/sefaria/model/topic.py b/sefaria/model/topic.py
index c558d505b4..bb4b6590e9 100644
--- a/sefaria/model/topic.py
+++ b/sefaria/model/topic.py
@@ -106,7 +106,7 @@ def _validate(self):
if getattr(self, "image", False):
img_url = self.image.get("image_uri")
if img_url: validate_url(img_url)
- assert all(pool in self.optional_pools for pool in self.pools), f'Pools {[pool for pool in self.pools if pool not in self.optional_pools]} is not an optional pool'
+ assert all(pool in self.optional_pools for pool in self.get_pools()), f'Pools {[pool for pool in self.get_pools() if pool not in self.optional_pools]} is not an optional pool'
def _normalize(self):
super()._normalize()
@@ -118,7 +118,10 @@ def _normalize(self):
displays_under_link = IntraTopicLink().load({"fromTopic": slug, "linkType": "displays-under"})
if getattr(displays_under_link, "toTopic", "") == "authors":
self.subclass = "author"
- self.pools = sorted(set(getattr(self, 'pools', [])))
+ if self.get_pools():
+ self.pools = sorted(set(self.get_pools()))
+ elif hasattr(self, 'pools'):
+ delattr(self, 'pools')
def _sanitize(self):
super()._sanitize()
@@ -128,6 +131,20 @@ def _sanitize(self):
p[k] = bleach.clean(v, tags=[], strip=True)
setattr(self, attr, p)
+ def get_pools(self):
+ return getattr(self, 'pools', [])
+
+ def has_pool(self, pool):
+ return pool in self.get_pools()
+
+ def add_pool(self, pool): #does not save!
+ self.pools = self.get_pools()
+ self.pools.append(pool)
+
+ def remove_pool(self, pool): #does not save!
+ pools = self.get_pools()
+ pools.remove(pool)
+
def set_titles(self, titles):
self.title_group = TitleGroup(titles)
@@ -444,8 +461,8 @@ def update_after_link_change(self, pool):
:param pool: 'sheets' or 'textual'
"""
links = self.get_ref_links(pool == 'sheets')
- if bool(links) != pool in self.pools:
- self.pools.remove(pool) if pool in self.pools else self.pools.append(pool)
+ if bool(links) != pool in self.get_pools():
+ self.remove_pool(pool) if pool in self.get_pools() else self.add_pool(pool)
self.numSources = self.link_set('refTopic').count()
self.save()
From 972a8024e9b691ca11142676400925080f27a124 Mon Sep 17 00:00:00 2001
From: YishaiGlasner
Date: Tue, 30 Jul 2024 11:45:17 +0300
Subject: [PATCH 020/125] refactor(Topic): change assigning by condition to
self assigning with or.
---
sefaria/model/topic.py | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/sefaria/model/topic.py b/sefaria/model/topic.py
index bb4b6590e9..e5b432f638 100644
--- a/sefaria/model/topic.py
+++ b/sefaria/model/topic.py
@@ -385,8 +385,7 @@ def link_set(self, _class='intraTopic', query_kwargs: dict = None, **kwargs):
return TopicLinkSetHelper.find(intra_link_query, **kwargs)
def get_ref_links(self, is_sheet, query_kwargs=None, **kwargs):
- if query_kwargs is None:
- query_kwargs = {}
+ query_kwargs = query_kwargs or {}
query_kwargs['is_sheet'] = is_sheet
return self.link_set('refTopic', query_kwargs, **kwargs)
From 377ea5e00c8132042a724f6ef48e46eba624392b Mon Sep 17 00:00:00 2001
From: YishaiGlasner
Date: Tue, 30 Jul 2024 12:11:57 +0300
Subject: [PATCH 021/125] refactor(Topic): change conditions for updating the
pools after link change to be clearer.
---
sefaria/model/topic.py | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/sefaria/model/topic.py b/sefaria/model/topic.py
index e5b432f638..a0fee457ff 100644
--- a/sefaria/model/topic.py
+++ b/sefaria/model/topic.py
@@ -460,8 +460,10 @@ def update_after_link_change(self, pool):
:param pool: 'sheets' or 'textual'
"""
links = self.get_ref_links(pool == 'sheets')
- if bool(links) != pool in self.get_pools():
- self.remove_pool(pool) if pool in self.get_pools() else self.add_pool(pool)
+ if self.has_pool(pool) and not links:
+ self.remove_pool(pool)
+ elif not self.has_pool(pool) and links:
+ self.add_pool(pool)
self.numSources = self.link_set('refTopic').count()
self.save()
From 9f46f430e073443850dd7908029b4ebae2161f6b Mon Sep 17 00:00:00 2001
From: YishaiGlasner
Date: Tue, 30 Jul 2024 12:18:49 +0300
Subject: [PATCH 022/125] refactor(Topic): change default of get_random_topic
to None.
---
reader/views.py | 2 +-
sefaria/helper/topic.py | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/reader/views.py b/reader/views.py
index eb5e2cb75b..141114e8b8 100644
--- a/reader/views.py
+++ b/reader/views.py
@@ -4196,7 +4196,7 @@ def random_by_topic_api(request):
Returns Texts API data for a random text taken from popular topic tags
"""
cb = request.GET.get("callback", None)
- random_topic = get_random_topic()
+ random_topic = get_random_topic('torahtab')
if random_topic is None:
return random_by_topic_api(request)
random_source = get_random_topic_source(random_topic)
diff --git a/sefaria/helper/topic.py b/sefaria/helper/topic.py
index a84b1bb68a..d8d0dfc15a 100644
--- a/sefaria/helper/topic.py
+++ b/sefaria/helper/topic.py
@@ -280,7 +280,7 @@ def curated_primacy(order_dict, lang):
return (bord.get('numDatasource', 0) * bord.get('tfidf', 0)) - (aord.get('numDatasource', 0) * aord.get('tfidf', 0))
-def get_random_topic(pool='torahtab') -> Optional[Topic]:
+def get_random_topic(pool=None) -> Optional[Topic]:
query = {"pools": pool} if pool else {}
random_topic_dict = list(db.topics.aggregate([
{"$match": query},
From 4e9210fb71ee84de9d0f5f40d93cdf176f40dbd8 Mon Sep 17 00:00:00 2001
From: YishaiGlasner
Date: Tue, 30 Jul 2024 13:23:56 +0300
Subject: [PATCH 023/125] refactor(Topic): use enum for pools.
---
sefaria/model/topic.py | 12 +++++++++---
1 file changed, 9 insertions(+), 3 deletions(-)
diff --git a/sefaria/model/topic.py b/sefaria/model/topic.py
index a0fee457ff..b7fb5198e2 100644
--- a/sefaria/model/topic.py
+++ b/sefaria/model/topic.py
@@ -1,3 +1,4 @@
+from enum import Enum
from typing import Union, Optional
from . import abstract as abst
from .schema import AbstractTitledObject, TitleGroup
@@ -15,6 +16,11 @@
logger = structlog.get_logger(__name__)
+class Pool(Enum):
+ TEXTUAL = "textual"
+ SHEETS = "sheets"
+
+
class Topic(abst.SluggedAbstractMongoRecord, AbstractTitledObject):
collection = 'topics'
history_noun = 'topic'
@@ -52,7 +58,7 @@ class Topic(abst.SluggedAbstractMongoRecord, AbstractTitledObject):
'pools', # list of strings, any of them represents a pool that this topic is member of
]
- optional_pools = {'sheets', 'textual', 'torahtab'}
+ optional_pools = {pool.value for pool in Pool} | {'torahtab'}
attr_schemas = {
"image": {
@@ -459,7 +465,7 @@ def update_after_link_change(self, pool):
updating the pools 'sheets' or 'textual' according to the existence of links and the numSources
:param pool: 'sheets' or 'textual'
"""
- links = self.get_ref_links(pool == 'sheets')
+ links = self.get_ref_links(pool == Pool.SHEETS.value)
if self.has_pool(pool) and not links:
self.remove_pool(pool)
elif not self.has_pool(pool) and links:
@@ -857,7 +863,7 @@ def set_description(self, lang, title, prompt):
return self
def get_related_pool(self):
- return 'sheets' if self.is_sheet else 'textual'
+ return Pool.SHEETS.value if self.is_sheet else Pool.TEXTUAL.value
def get_topic(self):
return Topic().load({'slug': self.toTopic})
From 6e42995126393ff4f10642e727ccf1e64f85575f Mon Sep 17 00:00:00 2001
From: YishaiGlasner
Date: Sun, 4 Aug 2024 11:54:27 +0300
Subject: [PATCH 024/125] refactor(Topic): use attr_schemas for validation of
pools rather than explicit assertion.
---
sefaria/model/topic.py | 10 ++++++++--
1 file changed, 8 insertions(+), 2 deletions(-)
diff --git a/sefaria/model/topic.py b/sefaria/model/topic.py
index b7fb5198e2..5b55cdccd9 100644
--- a/sefaria/model/topic.py
+++ b/sefaria/model/topic.py
@@ -58,7 +58,7 @@ class Topic(abst.SluggedAbstractMongoRecord, AbstractTitledObject):
'pools', # list of strings, any of them represents a pool that this topic is member of
]
- optional_pools = {pool.value for pool in Pool} | {'torahtab'}
+ allowed_pools = {pool.value for pool in Pool} | {'torahtab'}
attr_schemas = {
"image": {
@@ -81,6 +81,13 @@ class Topic(abst.SluggedAbstractMongoRecord, AbstractTitledObject):
}
}
}
+ },
+ 'pools': {
+ 'type': 'list',
+ 'schema': {
+ 'type': 'string',
+ 'allowed': allowed_pools
+ }
}
}
@@ -112,7 +119,6 @@ def _validate(self):
if getattr(self, "image", False):
img_url = self.image.get("image_uri")
if img_url: validate_url(img_url)
- assert all(pool in self.optional_pools for pool in self.get_pools()), f'Pools {[pool for pool in self.get_pools() if pool not in self.optional_pools]} is not an optional pool'
def _normalize(self):
super()._normalize()
From c7039c9efd372d711440923e7a847b7c37807006 Mon Sep 17 00:00:00 2001
From: YishaiGlasner
Date: Sun, 4 Aug 2024 12:01:53 +0300
Subject: [PATCH 025/125] doc(Topic): documentation for get_random_topic.
---
sefaria/helper/topic.py | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/sefaria/helper/topic.py b/sefaria/helper/topic.py
index d8d0dfc15a..baaab3499c 100644
--- a/sefaria/helper/topic.py
+++ b/sefaria/helper/topic.py
@@ -281,6 +281,10 @@ def curated_primacy(order_dict, lang):
def get_random_topic(pool=None) -> Optional[Topic]:
+ """
+ :param pool: name of th requested pool
+ :return: Returns a random topic from the database. If you provide pool, then the selection is limited to topics in that pool.
+ """
query = {"pools": pool} if pool else {}
random_topic_dict = list(db.topics.aggregate([
{"$match": query},
From d93244940590597c71888c956f17c6fd3b463064 Mon Sep 17 00:00:00 2001
From: Noah Santacruz
Date: Mon, 5 Aug 2024 10:08:27 +0300
Subject: [PATCH 026/125] docs(Topic): be more specific about pool param
---
sefaria/helper/topic.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/sefaria/helper/topic.py b/sefaria/helper/topic.py
index baaab3499c..572b9fc58a 100644
--- a/sefaria/helper/topic.py
+++ b/sefaria/helper/topic.py
@@ -282,7 +282,7 @@ def curated_primacy(order_dict, lang):
def get_random_topic(pool=None) -> Optional[Topic]:
"""
- :param pool: name of th requested pool
+ :param pool: name of the pool from which to select the topic. If `None`, all topics are considered.
:return: Returns a random topic from the database. If you provide pool, then the selection is limited to topics in that pool.
"""
query = {"pools": pool} if pool else {}
From d276589fc449b890388db875cfb46160320920e7 Mon Sep 17 00:00:00 2001
From: YishaiGlasner
Date: Mon, 5 Aug 2024 19:13:59 +0300
Subject: [PATCH 027/125] refactor(cerberus): validate whole document with
allow_unknown=True rather than only validate some attributes (which allows
only validate dicts). change existing attr_schemas to fit.
---
sefaria/model/abstract.py | 13 +---
sefaria/model/portal.py | 139 ++++++++++++--------------------------
sefaria/model/topic.py | 29 +++-----
3 files changed, 57 insertions(+), 124 deletions(-)
diff --git a/sefaria/model/abstract.py b/sefaria/model/abstract.py
index 2057c0e91a..f6d93a8726 100644
--- a/sefaria/model/abstract.py
+++ b/sefaria/model/abstract.py
@@ -244,16 +244,9 @@ def _validate(self):
" not in " + ",".join(self.required_attrs) + " or " + ",".join(self.optional_attrs))
return False
"""
- for attr, schema in self.attr_schemas.items():
- v = Validator(schema)
- try:
- value = getattr(self, attr)
- if not v.validate(value):
- raise InputError(v.errors)
- except AttributeError:
- # not checking here if value exists, that is done above.
- # assumption is if value doesn't exist, it's optional
- pass
+ v = Validator(self.attr_schemas, allow_unknown=True)
+ if not v.validate(self._saveable_attrs()):
+ raise InputError(v.errors)
return True
def _normalize(self):
diff --git a/sefaria/model/portal.py b/sefaria/model/portal.py
index 36984ceaad..c10e6dde34 100644
--- a/sefaria/model/portal.py
+++ b/sefaria/model/portal.py
@@ -20,100 +20,51 @@ class Portal(abst.SluggedAbstractMongoRecord):
"organization"
]
attr_schemas = {
- "about": {
- "title": {
- "type": "dict",
- "required": True,
- "schema": {
- "en": {"type": "string", "required": True},
- "he": {"type": "string", "required": True}
- }
- },
- "title_url": {"type": "string"},
- "image_uri": {"type": "string"},
- "image_caption": {
- "type": "dict",
- "schema": {
- "en": {"type": "string"},
- "he": {"type": "string"}
- }
- },
- "description": {
- "type": "dict",
- "schema": {
- "en": {"type": "string", "required": True},
- "he": {"type": "string", "required": True}
- }
- },
- },
- "mobile": {
- "title": {
- "type": "dict",
- "required": True,
- "schema": {
- "en": {"type": "string", "required": True},
- "he": {"type": "string", "required": True}
- }
- },
- "description": {
- "type": "dict",
- "schema": {
- "en": {"type": "string"},
- "he": {"type": "string"}
- }
- },
- "android_link": {"type": "string"},
- "ios_link": {"type": "string"}
- },
- "organization": {
- "title": {
- "type": "dict",
- "required": True,
- "schema": {
- "en": {"type": "string", "required": True},
- "he": {"type": "string", "required": True}
- }
- },
- "description": {
- "type": "dict",
- "schema": {
- "en": {"type": "string", "required": True},
- "he": {"type": "string", "required": True}
- }
- },
- },
- "newsletter": {
- "title": {
- "type": "dict",
- "required": True,
- "schema": {
- "en": {"type": "string", "required": True},
- "he": {"type": "string", "required": True}
- }
- },
- "description": {
- "type": "dict",
- "schema": {
- "en": {"type": "string", "required": True},
- "he": {"type": "string", "required": True}
- }
- },
- "title_url": {"type": "string"},
- "api_schema": {
- "type": "dict",
- "schema": {
- "http_method": {"type": "string", "required": True},
- "payload": {
- "type": "dict",
- "schema": {
- "first_name_key": {"type": "string"},
- "last_name_key": {"type": "string"},
- "email_key": {"type": "string"}
- }
- },
- }
- }
- }
+ 'about': {'type': 'dict',
+ 'schema': {'title': {'type': 'dict',
+ 'required': True,
+ 'schema': {'en': {'type': 'string', 'required': True},
+ 'he': {'type': 'string', 'required': True}}},
+ 'title_url': {'type': 'string'},
+ 'image_uri': {'type': 'string'},
+ 'image_caption': {'type': 'dict',
+ 'schema': {'en': {'type': 'string'}, 'he': {'type': 'string'}}},
+ 'description': {'type': 'dict',
+ 'schema': {'en': {'type': 'string', 'required': True},
+ 'he': {'type': 'string', 'required': True}}}}},
+ 'mobile': {'type': 'dict',
+ 'schema': {'title': {'type': 'dict',
+ 'required': True,
+ 'schema': {'en': {'type': 'string', 'required': True},
+ 'he': {'type': 'string', 'required': True}}},
+ 'description': {'type': 'dict',
+ 'schema': {'en': {'type': 'string'}, 'he': {'type': 'string'}}},
+ 'android_link': {'type': 'string'},
+ 'ios_link': {'type': 'string'}}},
+ 'organization': {'type': 'dict',
+ 'schema': {'title': {'type': 'dict',
+ 'required': True,
+ 'schema': {'en': {'type': 'string', 'required': True},
+ 'he': {'type': 'string', 'required': True}}},
+ 'description': {'type': 'dict',
+ 'schema': {'en': {'type': 'string', 'required': True},
+ 'he': {'type': 'string', 'required': True}}}}},
+ 'newsletter': {'type': 'dict',
+ 'schema': {'title': {'type': 'dict',
+ 'required': True,
+ 'schema': {'en': {'type': 'string', 'required': True},
+ 'he': {'type': 'string', 'required': True}}},
+ 'description': {'type': 'dict',
+ 'schema': {'en': {'type': 'string', 'required': True},
+ 'he': {'type': 'string', 'required': True}}},
+ 'title_url': {'type': 'string'},
+ 'api_schema': {'type': 'dict',
+ 'schema': {'http_method': {'type': 'string', 'required': True},
+ 'payload': {'type': 'dict',
+ 'schema': {
+ 'first_name_key': {'type': 'string'},
+ 'last_name_key': {'type': 'string'},
+ 'email_key': {'type': 'string'}}}}}}}
}
def _validate(self):
diff --git a/sefaria/model/topic.py b/sefaria/model/topic.py
index 5b55cdccd9..3a44e30f3c 100644
--- a/sefaria/model/topic.py
+++ b/sefaria/model/topic.py
@@ -58,29 +58,18 @@ class Topic(abst.SluggedAbstractMongoRecord, AbstractTitledObject):
'pools', # list of strings, any of them represents a pool that this topic is member of
]
- allowed_pools = {pool.value for pool in Pool} | {'torahtab'}
+ allowed_pools = [pool.value for pool in Pool] + ['torahtab']
attr_schemas = {
"image": {
- "image_uri": {
- "type": "string",
- "required": True,
- "regex": "^https://storage\.googleapis\.com/img\.sefaria\.org/topics/.*?"
- },
- "image_caption": {
- "type": "dict",
- "required": True,
- "schema": {
- "en": {
- "type": "string",
- "required": True
- },
- "he": {
- "type": "string",
- "required": True
- }
- }
- }
+ 'type': 'dict',
+ 'schema': {'image_uri': {'type': 'string',
+ 'required': True,
+ 'regex': '^https://storage\\.googleapis\\.com/img\\.sefaria\\.org/topics/.*?'},
+ 'image_caption': {'type': 'dict',
+ 'required': True,
+ 'schema': {'en': {'type': 'string', 'required': True},
+ 'he': {'type': 'string', 'required': True}}}}
},
'pools': {
'type': 'list',
From 450a44e113e79756d720ac07ecb8a883dd088dfd Mon Sep 17 00:00:00 2001
From: YishaiGlasner
Date: Tue, 6 Aug 2024 10:17:27 +0300
Subject: [PATCH 028/125] fix(cerberus): allow unknown only in root level
(unless the attr_schemas explicitly allow it).
---
sefaria/model/abstract.py | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/sefaria/model/abstract.py b/sefaria/model/abstract.py
index f6d93a8726..1195b086be 100644
--- a/sefaria/model/abstract.py
+++ b/sefaria/model/abstract.py
@@ -244,7 +244,10 @@ def _validate(self):
" not in " + ",".join(self.required_attrs) + " or " + ",".join(self.optional_attrs))
return False
"""
- v = Validator(self.attr_schemas, allow_unknown=True)
+ schema = self.attr_schemas
+ for key in schema:
+ schema[key]['allow_unknown'] = schema[key].get('allow_unknown', False) # allow unknowns only in the root
+ v = Validator(schema, allow_unknown=True)
if not v.validate(self._saveable_attrs()):
raise InputError(v.errors)
return True
From eefd205c1d105339122637d1459a5dd1f3882b06 Mon Sep 17 00:00:00 2001
From: nsantacruz
Date: Wed, 21 Aug 2024 10:36:35 +0300
Subject: [PATCH 029/125] chore(Topic): add back ticks to docs
---
sefaria/helper/topic.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/sefaria/helper/topic.py b/sefaria/helper/topic.py
index 572b9fc58a..03707b8841 100644
--- a/sefaria/helper/topic.py
+++ b/sefaria/helper/topic.py
@@ -283,7 +283,7 @@ def curated_primacy(order_dict, lang):
def get_random_topic(pool=None) -> Optional[Topic]:
"""
:param pool: name of the pool from which to select the topic. If `None`, all topics are considered.
- :return: Returns a random topic from the database. If you provide pool, then the selection is limited to topics in that pool.
+ :return: Returns a random topic from the database. If you provide `pool`, then the selection is limited to topics in that pool.
"""
query = {"pools": pool} if pool else {}
random_topic_dict = list(db.topics.aggregate([
From 6bea38ee9b70c7f6905b831c805d8f79b57ebb40 Mon Sep 17 00:00:00 2001
From: nsantacruz
Date: Mon, 11 Nov 2024 22:11:30 +0200
Subject: [PATCH 030/125] feat(topics): add topic_pool_link model
---
admin_tools/__init__.py | 0
admin_tools/migrations/0001_initial.py | 24 ++++++++++++++++++
.../migrations/0002_delete_topicpoollink.py | 18 +++++++++++++
admin_tools/migrations/0003_topicpoollink.py | 25 +++++++++++++++++++
admin_tools/migrations/__init__.py | 0
admin_tools/models/__init__.py | 1 +
admin_tools/models/topic_pool_link.py | 25 +++++++++++++++++++
sefaria/settings.py | 1 +
8 files changed, 94 insertions(+)
create mode 100644 admin_tools/__init__.py
create mode 100644 admin_tools/migrations/0001_initial.py
create mode 100644 admin_tools/migrations/0002_delete_topicpoollink.py
create mode 100644 admin_tools/migrations/0003_topicpoollink.py
create mode 100644 admin_tools/migrations/__init__.py
create mode 100644 admin_tools/models/__init__.py
create mode 100644 admin_tools/models/topic_pool_link.py
diff --git a/admin_tools/__init__.py b/admin_tools/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/admin_tools/migrations/0001_initial.py b/admin_tools/migrations/0001_initial.py
new file mode 100644
index 0000000000..ec43fcb95a
--- /dev/null
+++ b/admin_tools/migrations/0001_initial.py
@@ -0,0 +1,24 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.29 on 2024-11-11 17:45
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='TopicPoolLink',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('pool', models.CharField(max_length=255)),
+ ('topic_slug', models.CharField(max_length=255)),
+ ],
+ ),
+ ]
diff --git a/admin_tools/migrations/0002_delete_topicpoollink.py b/admin_tools/migrations/0002_delete_topicpoollink.py
new file mode 100644
index 0000000000..98e95d6eef
--- /dev/null
+++ b/admin_tools/migrations/0002_delete_topicpoollink.py
@@ -0,0 +1,18 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.29 on 2024-11-11 18:42
+from __future__ import unicode_literals
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('admin_tools', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.DeleteModel(
+ name='TopicPoolLink',
+ ),
+ ]
diff --git a/admin_tools/migrations/0003_topicpoollink.py b/admin_tools/migrations/0003_topicpoollink.py
new file mode 100644
index 0000000000..95558d20a4
--- /dev/null
+++ b/admin_tools/migrations/0003_topicpoollink.py
@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.29 on 2024-11-11 18:43
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ('admin_tools', '0002_delete_topicpoollink'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='TopicPoolLink',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('pool', models.CharField(max_length=255)),
+ ('topic_slug', models.CharField(max_length=255)),
+ ],
+ ),
+ ]
diff --git a/admin_tools/migrations/__init__.py b/admin_tools/migrations/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/admin_tools/models/__init__.py b/admin_tools/models/__init__.py
new file mode 100644
index 0000000000..6eaf38f2f7
--- /dev/null
+++ b/admin_tools/models/__init__.py
@@ -0,0 +1 @@
+from .topic_pool_link import TopicPoolLink
diff --git a/admin_tools/models/topic_pool_link.py b/admin_tools/models/topic_pool_link.py
new file mode 100644
index 0000000000..17a64a726b
--- /dev/null
+++ b/admin_tools/models/topic_pool_link.py
@@ -0,0 +1,25 @@
+from django.db import models
+
+
+class TopicPoolLinkManager(models.Manager):
+ def get_random_topic_slugs(self, pool=None, limit=10) -> list[str]:
+ query_set = self.get_queryset()
+ if pool:
+ query_set = query_set.filter(pool=pool)
+ query_set = query_set.values('topic_slug').distinct().order_by('?')[:limit]
+ return [x['topic_slug'] for x in query_set]
+
+
+class TopicPoolLink(models.Model):
+ pool = models.CharField(max_length=255)
+ topic_slug = models.CharField(max_length=255)
+ objects = TopicPoolLinkManager()
+
+ def __str__(self):
+ return f"{self.pool} <> {self.topic_slug}"
+
+
+
+
+
+
diff --git a/sefaria/settings.py b/sefaria/settings.py
index 7eea25ff70..1a425e5392 100644
--- a/sefaria/settings.py
+++ b/sefaria/settings.py
@@ -144,6 +144,7 @@
'reader',
'sourcesheets',
'sefaria.gauth',
+ 'admin_tools',
'captcha',
'django.contrib.admin',
'anymail',
From ac1fa70e41a40603601056722936ba45bebc9f22 Mon Sep 17 00:00:00 2001
From: nsantacruz
Date: Mon, 11 Nov 2024 22:11:50 +0200
Subject: [PATCH 031/125] refactor(topics): modify random topic api to use
topic pool link model
---
sefaria/helper/topic.py | 11 ++++-------
1 file changed, 4 insertions(+), 7 deletions(-)
diff --git a/sefaria/helper/topic.py b/sefaria/helper/topic.py
index 8203a06b31..d69e3aa0cd 100644
--- a/sefaria/helper/topic.py
+++ b/sefaria/helper/topic.py
@@ -285,15 +285,12 @@ def get_random_topic(pool=None) -> Optional[Topic]:
:param pool: name of the pool from which to select the topic. If `None`, all topics are considered.
:return: Returns a random topic from the database. If you provide `pool`, then the selection is limited to topics in that pool.
"""
- query = {"pools": pool} if pool else {}
- random_topic_dict = list(db.topics.aggregate([
- {"$match": query},
- {"$sample": {"size": 1}}
- ]))
- if len(random_topic_dict) == 0:
+ from admin_tools.models import TopicPoolLink
+ random_topic_slugs = TopicPoolLink.objects.get_random_topic_slugs(pool=pool, limit=1)
+ if len(random_topic_slugs) == 0:
return None
- return Topic(random_topic_dict[0])
+ return Topic.init(random_topic_slugs[0])
def get_random_topic_source(topic:Topic) -> Optional[Ref]:
From 110b05104043f56aed6808fa097105eca930917a Mon Sep 17 00:00:00 2001
From: nsantacruz
Date: Mon, 11 Nov 2024 22:19:00 +0200
Subject: [PATCH 032/125] refactor(topics): change pool to 'promoted'
---
reader/views.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/reader/views.py b/reader/views.py
index aafc905ef2..35f191eea4 100644
--- a/reader/views.py
+++ b/reader/views.py
@@ -4230,7 +4230,7 @@ def random_by_topic_api(request):
Returns Texts API data for a random text taken from popular topic tags
"""
cb = request.GET.get("callback", None)
- random_topic = get_random_topic('torahtab')
+ random_topic = get_random_topic('promoted')
if random_topic is None:
return random_by_topic_api(request)
random_source = get_random_topic_source(random_topic)
From cce7daf4c4a59fec42bc1386d42f993ee706209b Mon Sep 17 00:00:00 2001
From: nsantacruz
Date: Tue, 12 Nov 2024 13:04:31 +0200
Subject: [PATCH 033/125] refactor(topics): move management of pools to use
TopicLinkPool model
---
admin_tools/models/topic_pool_link.py | 15 ++++++++++
reader/views.py | 3 +-
sefaria/model/topic.py | 43 ++++++++++-----------------
3 files changed, 33 insertions(+), 28 deletions(-)
diff --git a/admin_tools/models/topic_pool_link.py b/admin_tools/models/topic_pool_link.py
index 17a64a726b..f7426cf872 100644
--- a/admin_tools/models/topic_pool_link.py
+++ b/admin_tools/models/topic_pool_link.py
@@ -1,4 +1,11 @@
from django.db import models
+from enum import Enum
+
+
+class PoolType(Enum):
+ TEXTUAL = "textual"
+ SHEETS = "sheets"
+ PROMOTED = "promoted"
class TopicPoolLinkManager(models.Manager):
@@ -9,12 +16,20 @@ def get_random_topic_slugs(self, pool=None, limit=10) -> list[str]:
query_set = query_set.values('topic_slug').distinct().order_by('?')[:limit]
return [x['topic_slug'] for x in query_set]
+ @staticmethod
+ def get_pools_by_topic_slug(topic_slug) -> list[str]:
+ query_set = TopicPoolLink.objects.filter(topic_slug=topic_slug).values('pool').distinct()
+ return [x['pool'] for x in query_set]
+
class TopicPoolLink(models.Model):
pool = models.CharField(max_length=255)
topic_slug = models.CharField(max_length=255)
objects = TopicPoolLinkManager()
+ class Meta:
+ unique_together = ('pool', 'topic_slug')
+
def __str__(self):
return f"{self.pool} <> {self.topic_slug}"
diff --git a/reader/views.py b/reader/views.py
index 35f191eea4..1395cf1ac2 100644
--- a/reader/views.py
+++ b/reader/views.py
@@ -4229,8 +4229,9 @@ def random_by_topic_api(request):
"""
Returns Texts API data for a random text taken from popular topic tags
"""
+ from admin_tools.models.topic_pool_link import PoolType
cb = request.GET.get("callback", None)
- random_topic = get_random_topic('promoted')
+ random_topic = get_random_topic(PoolType.PROMOTED.value)
if random_topic is None:
return random_by_topic_api(request)
random_source = get_random_topic_source(random_topic)
diff --git a/sefaria/model/topic.py b/sefaria/model/topic.py
index ef48716027..c126c8b2d0 100644
--- a/sefaria/model/topic.py
+++ b/sefaria/model/topic.py
@@ -1,9 +1,11 @@
from enum import Enum
from typing import Union, Optional
+from django.db.utils import IntegrityError
from . import abstract as abst
from .schema import AbstractTitledObject, TitleGroup
from .text import Ref, IndexSet, AbstractTextRecord, Index, Term
from .category import Category
+from admin_tools.models.topic_pool_link import TopicPoolLink, PoolType
from sefaria.system.exceptions import InputError, DuplicateRecordError
from sefaria.model.timeperiod import TimePeriod, LifePeriod
from sefaria.system.validators import validate_url
@@ -121,11 +123,6 @@ def __hash__(self):
return hash((self.collective_title, self.base_cat_path))
-class Pool(Enum):
- TEXTUAL = "textual"
- SHEETS = "sheets"
-
-
class Topic(abst.SluggedAbstractMongoRecord, AbstractTitledObject):
collection = 'topics'
history_noun = 'topic'
@@ -163,8 +160,6 @@ class Topic(abst.SluggedAbstractMongoRecord, AbstractTitledObject):
'pools', # list of strings, any of them represents a pool that this topic is member of
]
- allowed_pools = [pool.value for pool in Pool] + ['torahtab']
-
attr_schemas = {
"image": {
'type': 'dict',
@@ -176,14 +171,7 @@ class Topic(abst.SluggedAbstractMongoRecord, AbstractTitledObject):
'schema': {'en': {'type': 'string', 'required': True},
'he': {'type': 'string', 'required': True}}}}
},
- 'pools': {
- 'type': 'list',
- 'schema': {
- 'type': 'string',
- 'allowed': allowed_pools
- }
- }
- }
+ }
ROOT = "Main Menu" # the root of topic TOC is not a topic, so this is a fake slug. we know it's fake because it's not in normal form
# this constant is helpful in the topic editor tool functions in this file
@@ -200,6 +188,7 @@ def load(self, query, proj=None):
def _set_derived_attributes(self):
self.set_titles(getattr(self, "titles", None))
+ self.pools = TopicPoolLink.objects.get_pools_by_topic_slug(getattr(self, "slug", None))
if self.__class__ != Topic and not getattr(self, "subclass", False):
# in a subclass. set appropriate "subclass" attribute
setattr(self, "subclass", self.reverse_subclass_map[self.__class__.__name__])
@@ -224,10 +213,6 @@ def _normalize(self):
displays_under_link = IntraTopicLink().load({"fromTopic": slug, "linkType": "displays-under"})
if getattr(displays_under_link, "toTopic", "") == "authors":
self.subclass = "author"
- if self.get_pools():
- self.pools = sorted(set(self.get_pools()))
- elif hasattr(self, 'pools'):
- delattr(self, 'pools')
def _sanitize(self):
super()._sanitize()
@@ -237,17 +222,23 @@ def _sanitize(self):
p[k] = bleach.clean(v, tags=[], strip=True)
setattr(self, attr, p)
- def get_pools(self):
+ def get_pools(self) -> list[str]:
return getattr(self, 'pools', [])
- def has_pool(self, pool):
+ def has_pool(self, pool: str) -> bool:
return pool in self.get_pools()
- def add_pool(self, pool): #does not save!
+ def add_pool(self, pool: str) -> None:
+ try:
+ link = TopicPoolLink(pool=pool, topic_slug=self.slug)
+ link.save()
+ except IntegrityError:
+ raise DuplicateRecordError(f"'{pool}'<>'{self.slug}' link already exists in TopicPoolLink table.")
self.pools = self.get_pools()
self.pools.append(pool)
- def remove_pool(self, pool): #does not save!
+ def remove_pool(self, pool) -> None:
+ TopicPoolLink.objects.filter(pool=pool, topic_slug=self.slug).delete()
pools = self.get_pools()
pools.remove(pool)
@@ -498,8 +489,6 @@ def get_ref_links(self, is_sheet, query_kwargs=None, **kwargs):
def contents(self, **kwargs):
mini = kwargs.get('minify', False)
d = {'slug': self.slug} if mini else super(Topic, self).contents(**kwargs)
- if kwargs.get('remove_pools', True):
- d.pop('pools', None)
d['primaryTitle'] = {}
for lang in ('en', 'he'):
d['primaryTitle'][lang] = self.get_primary_title(lang=lang, with_disambiguation=kwargs.get('with_disambiguation', True))
@@ -565,7 +554,7 @@ def update_after_link_change(self, pool):
updating the pools 'sheets' or 'textual' according to the existence of links and the numSources
:param pool: 'sheets' or 'textual'
"""
- links = self.get_ref_links(pool == Pool.SHEETS.value)
+ links = self.get_ref_links(pool == PoolType.SHEETS.value)
if self.has_pool(pool) and not links:
self.remove_pool(pool)
elif not self.has_pool(pool) and links:
@@ -970,7 +959,7 @@ def set_description(self, lang, title, prompt):
return self
def get_related_pool(self):
- return Pool.SHEETS.value if self.is_sheet else Pool.TEXTUAL.value
+ return PoolType.SHEETS.value if self.is_sheet else PoolType.TEXTUAL.value
def get_topic(self):
return Topic().load({'slug': self.toTopic})
From 94dee446314d355ed6adabba8259b1b1495ca6a5 Mon Sep 17 00:00:00 2001
From: nsantacruz
Date: Tue, 12 Nov 2024 13:04:51 +0200
Subject: [PATCH 034/125] chore(topics): add uniqueness constraint on
topicpoollink
---
.../migrations/0004_auto_20241111_2328.py | 19 +++++++++++++++++++
1 file changed, 19 insertions(+)
create mode 100644 admin_tools/migrations/0004_auto_20241111_2328.py
diff --git a/admin_tools/migrations/0004_auto_20241111_2328.py b/admin_tools/migrations/0004_auto_20241111_2328.py
new file mode 100644
index 0000000000..866e648b61
--- /dev/null
+++ b/admin_tools/migrations/0004_auto_20241111_2328.py
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.29 on 2024-11-12 03:28
+from __future__ import unicode_literals
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('admin_tools', '0003_topicpoollink'),
+ ]
+
+ operations = [
+ migrations.AlterUniqueTogether(
+ name='topicpoollink',
+ unique_together=set([('pool', 'topic_slug')]),
+ ),
+ ]
From 129529bf456f3fb5c35cf58db31410b939b4c94c Mon Sep 17 00:00:00 2001
From: nsantacruz
Date: Tue, 12 Nov 2024 13:31:52 +0200
Subject: [PATCH 035/125] chore(topics): add
migrate_good_to_promote_to_topic_pools.py
---
.../migrate_good_to_promote_to_topic_pools.py | 15 +++++++++++++++
1 file changed, 15 insertions(+)
create mode 100644 scripts/migrations/migrate_good_to_promote_to_topic_pools.py
diff --git a/scripts/migrations/migrate_good_to_promote_to_topic_pools.py b/scripts/migrations/migrate_good_to_promote_to_topic_pools.py
new file mode 100644
index 0000000000..1259dfa4df
--- /dev/null
+++ b/scripts/migrations/migrate_good_to_promote_to_topic_pools.py
@@ -0,0 +1,15 @@
+import django
+django.setup()
+from sefaria.model import *
+from admin_tools.models.topic_pool_link import PoolType, TopicPoolLink
+
+
+def run():
+ ts = TopicSet({'good_to_promote': True})
+ for topic in ts:
+ link = TopicPoolLink(topic_slug=topic.slug, pool=PoolType.PROMOTED.value)
+ link.save()
+
+
+if __name__ == "__main__":
+ run()
From 1827d9db338d221cfe56ee073452ddd06ed43190 Mon Sep 17 00:00:00 2001
From: nsantacruz
Date: Wed, 13 Nov 2024 14:04:15 +0200
Subject: [PATCH 036/125] refactor(topics): Refactor to use two models, Topic
and TopicPool to represent many to many relationship
---
admin_tools/migrations/0001_initial.py | 24 -----------
.../migrations/0002_delete_topicpoollink.py | 18 ---------
admin_tools/migrations/0003_topicpoollink.py | 25 ------------
.../migrations/0004_auto_20241111_2328.py | 19 ---------
admin_tools/models/__init__.py | 1 -
admin_tools/models/topic_pool_link.py | 40 -------------------
sefaria/settings.py | 2 +-
{admin_tools => topics}/__init__.py | 0
.../migrations/__init__.py | 0
topics/models/__init__.py | 2 +
topics/models/pool.py | 15 +++++++
topics/models/topic.py | 12 ++++++
12 files changed, 30 insertions(+), 128 deletions(-)
delete mode 100644 admin_tools/migrations/0001_initial.py
delete mode 100644 admin_tools/migrations/0002_delete_topicpoollink.py
delete mode 100644 admin_tools/migrations/0003_topicpoollink.py
delete mode 100644 admin_tools/migrations/0004_auto_20241111_2328.py
delete mode 100644 admin_tools/models/__init__.py
delete mode 100644 admin_tools/models/topic_pool_link.py
rename {admin_tools => topics}/__init__.py (100%)
rename {admin_tools => topics}/migrations/__init__.py (100%)
create mode 100644 topics/models/__init__.py
create mode 100644 topics/models/pool.py
create mode 100644 topics/models/topic.py
diff --git a/admin_tools/migrations/0001_initial.py b/admin_tools/migrations/0001_initial.py
deleted file mode 100644
index ec43fcb95a..0000000000
--- a/admin_tools/migrations/0001_initial.py
+++ /dev/null
@@ -1,24 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.11.29 on 2024-11-11 17:45
-from __future__ import unicode_literals
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- initial = True
-
- dependencies = [
- ]
-
- operations = [
- migrations.CreateModel(
- name='TopicPoolLink',
- fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('pool', models.CharField(max_length=255)),
- ('topic_slug', models.CharField(max_length=255)),
- ],
- ),
- ]
diff --git a/admin_tools/migrations/0002_delete_topicpoollink.py b/admin_tools/migrations/0002_delete_topicpoollink.py
deleted file mode 100644
index 98e95d6eef..0000000000
--- a/admin_tools/migrations/0002_delete_topicpoollink.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.11.29 on 2024-11-11 18:42
-from __future__ import unicode_literals
-
-from django.db import migrations
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('admin_tools', '0001_initial'),
- ]
-
- operations = [
- migrations.DeleteModel(
- name='TopicPoolLink',
- ),
- ]
diff --git a/admin_tools/migrations/0003_topicpoollink.py b/admin_tools/migrations/0003_topicpoollink.py
deleted file mode 100644
index 95558d20a4..0000000000
--- a/admin_tools/migrations/0003_topicpoollink.py
+++ /dev/null
@@ -1,25 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.11.29 on 2024-11-11 18:43
-from __future__ import unicode_literals
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- initial = True
-
- dependencies = [
- ('admin_tools', '0002_delete_topicpoollink'),
- ]
-
- operations = [
- migrations.CreateModel(
- name='TopicPoolLink',
- fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('pool', models.CharField(max_length=255)),
- ('topic_slug', models.CharField(max_length=255)),
- ],
- ),
- ]
diff --git a/admin_tools/migrations/0004_auto_20241111_2328.py b/admin_tools/migrations/0004_auto_20241111_2328.py
deleted file mode 100644
index 866e648b61..0000000000
--- a/admin_tools/migrations/0004_auto_20241111_2328.py
+++ /dev/null
@@ -1,19 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.11.29 on 2024-11-12 03:28
-from __future__ import unicode_literals
-
-from django.db import migrations
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('admin_tools', '0003_topicpoollink'),
- ]
-
- operations = [
- migrations.AlterUniqueTogether(
- name='topicpoollink',
- unique_together=set([('pool', 'topic_slug')]),
- ),
- ]
diff --git a/admin_tools/models/__init__.py b/admin_tools/models/__init__.py
deleted file mode 100644
index 6eaf38f2f7..0000000000
--- a/admin_tools/models/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-from .topic_pool_link import TopicPoolLink
diff --git a/admin_tools/models/topic_pool_link.py b/admin_tools/models/topic_pool_link.py
deleted file mode 100644
index f7426cf872..0000000000
--- a/admin_tools/models/topic_pool_link.py
+++ /dev/null
@@ -1,40 +0,0 @@
-from django.db import models
-from enum import Enum
-
-
-class PoolType(Enum):
- TEXTUAL = "textual"
- SHEETS = "sheets"
- PROMOTED = "promoted"
-
-
-class TopicPoolLinkManager(models.Manager):
- def get_random_topic_slugs(self, pool=None, limit=10) -> list[str]:
- query_set = self.get_queryset()
- if pool:
- query_set = query_set.filter(pool=pool)
- query_set = query_set.values('topic_slug').distinct().order_by('?')[:limit]
- return [x['topic_slug'] for x in query_set]
-
- @staticmethod
- def get_pools_by_topic_slug(topic_slug) -> list[str]:
- query_set = TopicPoolLink.objects.filter(topic_slug=topic_slug).values('pool').distinct()
- return [x['pool'] for x in query_set]
-
-
-class TopicPoolLink(models.Model):
- pool = models.CharField(max_length=255)
- topic_slug = models.CharField(max_length=255)
- objects = TopicPoolLinkManager()
-
- class Meta:
- unique_together = ('pool', 'topic_slug')
-
- def __str__(self):
- return f"{self.pool} <> {self.topic_slug}"
-
-
-
-
-
-
diff --git a/sefaria/settings.py b/sefaria/settings.py
index 1a425e5392..bdb6dd7460 100644
--- a/sefaria/settings.py
+++ b/sefaria/settings.py
@@ -144,7 +144,7 @@
'reader',
'sourcesheets',
'sefaria.gauth',
- 'admin_tools',
+ 'topics',
'captcha',
'django.contrib.admin',
'anymail',
diff --git a/admin_tools/__init__.py b/topics/__init__.py
similarity index 100%
rename from admin_tools/__init__.py
rename to topics/__init__.py
diff --git a/admin_tools/migrations/__init__.py b/topics/migrations/__init__.py
similarity index 100%
rename from admin_tools/migrations/__init__.py
rename to topics/migrations/__init__.py
diff --git a/topics/models/__init__.py b/topics/models/__init__.py
new file mode 100644
index 0000000000..3c756d8991
--- /dev/null
+++ b/topics/models/__init__.py
@@ -0,0 +1,2 @@
+from .topic import Topic
+from .pool import TopicPool
diff --git a/topics/models/pool.py b/topics/models/pool.py
new file mode 100644
index 0000000000..3facbb6b90
--- /dev/null
+++ b/topics/models/pool.py
@@ -0,0 +1,15 @@
+from django.db import models
+from enum import Enum
+
+
+class PoolType(Enum):
+ TEXTUAL = "textual"
+ SHEETS = "sheets"
+ PROMOTED = "promoted"
+
+
+class TopicPool(models.Model):
+ name = models.CharField(max_length=255, unique=True)
+
+ def __str__(self):
+ return f"TopicPool('{self.name}')"
diff --git a/topics/models/topic.py b/topics/models/topic.py
new file mode 100644
index 0000000000..6bba6523d4
--- /dev/null
+++ b/topics/models/topic.py
@@ -0,0 +1,12 @@
+from django.db import models
+from topics.models.pool import TopicPool
+
+
+class Topic(models.Model):
+ slug = models.CharField(max_length=255, unique=True)
+ en_title = models.CharField(max_length=255, blank=True, default="")
+ he_title = models.CharField(max_length=255, blank=True, default="")
+ pools = models.ManyToManyField(TopicPool, related_name="topics")
+
+ def __str__(self):
+ return f"Topic('{self.slug}')"
From ad18ba188bb3d8a90e425f5ad19cecfdc0e93e32 Mon Sep 17 00:00:00 2001
From: nsantacruz
Date: Thu, 14 Nov 2024 12:12:42 +0200
Subject: [PATCH 037/125] feat(topics): admin interface for topics and topic
pools
---
topics/admin.py | 72 +++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 72 insertions(+)
create mode 100644 topics/admin.py
diff --git a/topics/admin.py b/topics/admin.py
new file mode 100644
index 0000000000..189c98cfe7
--- /dev/null
+++ b/topics/admin.py
@@ -0,0 +1,72 @@
+from django.contrib import admin, messages
+from django.db.models import BooleanField, Case, When
+from topics.models import Topic, TopicPool
+from topics.models.pool import PoolType
+
+
+def create_add_to_specific_pool_action(pool_name):
+ def add_to_specific_pool(modeladmin, request, queryset):
+ try:
+ pool = TopicPool.objects.get(name=pool_name)
+ for topic in queryset:
+ topic.pools.add(pool)
+ modeladmin.message_user(request, f"Added {queryset.count()} topics to {pool.name}", messages.SUCCESS)
+
+ except TopicPool.DoesNotExist:
+ modeladmin.message_user(request, "The specified pool does not exist.", messages.ERROR)
+
+ add_to_specific_pool.short_description = f"Add selected topics to '{pool_name}' pool"
+ return add_to_specific_pool
+
+
+class TopicAdmin(admin.ModelAdmin):
+ list_display = ('slug', 'en_title', 'he_title', 'is_in_pool_general', 'is_in_pool_torah_tab')
+ filter_horizontal = ('pools',)
+ readonly_fields = ('slug', 'en_title', 'he_title')
+ actions = [create_add_to_specific_pool_action(pool_name) for pool_name in (PoolType.GENERAL.value, PoolType.TORAH_TAB.value)]
+
+ def get_queryset(self, request):
+ queryset = super().get_queryset(request)
+ return queryset.annotate(
+ in_pool_general=Case(
+ When(pools__name=PoolType.GENERAL.value, then=True),
+ default=False,
+ output_field=BooleanField()
+ ),
+ in_pool_torah_tab=Case(
+ When(pools__name=PoolType.TORAH_TAB.value, then=True),
+ default=False,
+ output_field=BooleanField()
+ )
+ )
+
+ def is_in_pool_general(self, obj):
+ return obj.in_pool_general
+ is_in_pool_general.boolean = True
+ is_in_pool_general.short_description = "General?"
+ is_in_pool_general.admin_order_field = 'in_pool_general'
+
+ def is_in_pool_torah_tab(self, obj):
+ return obj.in_pool_torah_tab
+ is_in_pool_torah_tab.boolean = True
+ is_in_pool_torah_tab.short_description = "TorahTab?"
+ is_in_pool_torah_tab.admin_order_field = 'in_pool_torah_tab'
+
+
+class TopicPoolAdmin(admin.ModelAdmin):
+ list_display = ('name', 'topic_names')
+ filter_horizontal = ('topics',)
+ readonly_fields = ('name',)
+
+ def topic_names(self, obj):
+ topic_slugs = obj.topics.all().values_list('slug', flat=True)
+ str_rep = ', '.join(topic_slugs[:30])
+ if len(topic_slugs) > 30:
+ str_rep = str_rep + '...'
+ return str_rep
+
+
+admin.site.register(Topic, TopicAdmin)
+admin.site.register(TopicPool, TopicPoolAdmin)
+
+
From 9725b6162e3defb2ced57a9f6cd417992a733ed1 Mon Sep 17 00:00:00 2001
From: nsantacruz
Date: Thu, 14 Nov 2024 12:15:47 +0200
Subject: [PATCH 038/125] feat(topics): only show library topics in topic admin
view
---
topics/admin.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/topics/admin.py b/topics/admin.py
index 189c98cfe7..8b8e2ce685 100644
--- a/topics/admin.py
+++ b/topics/admin.py
@@ -38,7 +38,7 @@ def get_queryset(self, request):
default=False,
output_field=BooleanField()
)
- )
+ ).filter(pools__name=PoolType.LIBRARY.value)
def is_in_pool_general(self, obj):
return obj.in_pool_general
From 544df751865d4d38271f16698985ceb7055f6583 Mon Sep 17 00:00:00 2001
From: nsantacruz
Date: Thu, 14 Nov 2024 14:42:43 +0200
Subject: [PATCH 039/125] chore(topics): update pools migration to fully
migrate
---
.../migrate_good_to_promote_to_topic_pools.py | 76 +++++++++++++++++--
1 file changed, 71 insertions(+), 5 deletions(-)
diff --git a/scripts/migrations/migrate_good_to_promote_to_topic_pools.py b/scripts/migrations/migrate_good_to_promote_to_topic_pools.py
index 1259dfa4df..c74e2b9ec3 100644
--- a/scripts/migrations/migrate_good_to_promote_to_topic_pools.py
+++ b/scripts/migrations/migrate_good_to_promote_to_topic_pools.py
@@ -1,14 +1,80 @@
import django
+from django.db import IntegrityError
+
django.setup()
-from sefaria.model import *
-from admin_tools.models.topic_pool_link import PoolType, TopicPoolLink
+from sefaria.model import TopicSet, RefTopicLinkSet
+from topics.models.topic import Topic
+from topics.models.pool import TopicPool, PoolType
-def run():
+def add_to_torah_tab_pool():
+ print('Adding topics to torah tab pool')
+ pool = TopicPool.objects.get(name=PoolType.TORAH_TAB.value)
ts = TopicSet({'good_to_promote': True})
for topic in ts:
- link = TopicPoolLink(topic_slug=topic.slug, pool=PoolType.PROMOTED.value)
- link.save()
+ t = Topic.objects.get(slug=topic.slug)
+ t.pools.add(pool)
+
+
+def add_to_library_pool():
+ print('Adding topics to library pool')
+ pool = TopicPool.objects.get(name=PoolType.LIBRARY.value)
+ ts = TopicSet({'subclass': 'author'})
+ for topic in ts:
+ t = Topic.objects.get(slug=topic.slug)
+ t.pools.add(pool)
+ links = RefTopicLinkSet({'is_sheet': False, 'linkType': 'about'})
+ topic_slugs = {link.toTopic for link in links}
+ for slug in topic_slugs:
+ try:
+ t = Topic.objects.get(slug=slug)
+ t.pools.add(pool)
+ except Topic.DoesNotExist:
+ print('Could not find topic with slug {}'.format(slug))
+
+
+def add_to_sheets_pool():
+ print('Adding topics to sheets pool')
+ pool = TopicPool.objects.get(name=PoolType.SHEETS.value)
+ links = RefTopicLinkSet({'is_sheet': True, 'linkType': 'about'})
+ topic_slugs = {link.toTopic for link in links}
+ for slug in topic_slugs:
+ try:
+ t = Topic.objects.get(slug=slug)
+ t.pools.add(pool)
+ except Topic.DoesNotExist:
+ print('Could not find topic with slug {}'.format(slug))
+
+
+def delete_all_data():
+ print("Delete data")
+ Topic.pools.through.objects.all().delete()
+ Topic.objects.all().delete()
+ TopicPool.objects.all().delete()
+
+
+def add_topics():
+ print('Adding topics')
+ for topic in TopicSet({}):
+ try:
+ Topic.objects.create(slug=topic.slug, en_title=topic.get_primary_title('en'), he_title=topic.get_primary_title('he'))
+ except IntegrityError:
+ print('Duplicate topic', topic.slug)
+
+
+def add_pools():
+ print('Adding pools')
+ for pool_name in [PoolType.LIBRARY.value, PoolType.SHEETS.value, PoolType.GENERAL.value, PoolType.TORAH_TAB.value]:
+ TopicPool.objects.create(name=pool_name)
+
+
+def run():
+ delete_all_data()
+ add_topics()
+ add_pools()
+ add_to_torah_tab_pool()
+ add_to_library_pool()
+ add_to_sheets_pool()
if __name__ == "__main__":
From eed87c609768125f43e34c1fedf38080e4dd664b Mon Sep 17 00:00:00 2001
From: nsantacruz
Date: Thu, 14 Nov 2024 14:43:02 +0200
Subject: [PATCH 040/125] feat(topics): add filters and boolean columns
---
topics/admin.py | 71 ++++++++++++++++++++++++++++++++++---------------
1 file changed, 49 insertions(+), 22 deletions(-)
diff --git a/topics/admin.py b/topics/admin.py
index 8b8e2ce685..a0e765ecba 100644
--- a/topics/admin.py
+++ b/topics/admin.py
@@ -1,11 +1,10 @@
from django.contrib import admin, messages
-from django.db.models import BooleanField, Case, When
from topics.models import Topic, TopicPool
from topics.models.pool import PoolType
-def create_add_to_specific_pool_action(pool_name):
- def add_to_specific_pool(modeladmin, request, queryset):
+def create_add_to_pool_action(pool_name):
+ def add_to_pool(modeladmin, request, queryset):
try:
pool = TopicPool.objects.get(name=pool_name)
for topic in queryset:
@@ -15,42 +14,70 @@ def add_to_specific_pool(modeladmin, request, queryset):
except TopicPool.DoesNotExist:
modeladmin.message_user(request, "The specified pool does not exist.", messages.ERROR)
- add_to_specific_pool.short_description = f"Add selected topics to '{pool_name}' pool"
- return add_to_specific_pool
+ add_to_pool.short_description = f"Add selected topics to '{pool_name}' pool"
+ add_to_pool.__name__ = f"add_to_specific_pool_{pool_name}"
+ return add_to_pool
+
+
+def create_remove_from_pool_action(pool_name):
+ def remove_from_pool(modeladmin, request, queryset):
+ try:
+ pool = TopicPool.objects.get(name=pool_name)
+ for topic in queryset:
+ topic.pools.remove(pool)
+ modeladmin.message_user(request, f"Removed {queryset.count()} topics from {pool.name}", messages.SUCCESS)
+
+ except TopicPool.DoesNotExist:
+ modeladmin.message_user(request, "The specified pool does not exist.", messages.ERROR)
+
+ remove_from_pool.short_description = f"Remove selected topics from '{pool_name}' pool"
+ remove_from_pool.__name__ = f"remove_from_pool_{pool_name}"
+ return remove_from_pool
+
+
+class PoolFilter(admin.SimpleListFilter):
+ title = 'Pool Filter'
+ parameter_name = 'pool'
+
+ def lookups(self, request, model_admin):
+ return [
+ (PoolType.GENERAL.value, 'General Pool'),
+ (PoolType.TORAH_TAB.value, 'TorahTab Pool'),
+ ]
+
+ def queryset(self, request, queryset):
+ pool_name = self.value()
+ if pool_name:
+ pool = TopicPool.objects.get(name=pool_name)
+ return queryset.filter(pools=pool)
+ return queryset
class TopicAdmin(admin.ModelAdmin):
list_display = ('slug', 'en_title', 'he_title', 'is_in_pool_general', 'is_in_pool_torah_tab')
+ list_filter = (PoolFilter,)
filter_horizontal = ('pools',)
readonly_fields = ('slug', 'en_title', 'he_title')
- actions = [create_add_to_specific_pool_action(pool_name) for pool_name in (PoolType.GENERAL.value, PoolType.TORAH_TAB.value)]
+ actions = [
+ create_add_to_pool_action(PoolType.GENERAL.value),
+ create_add_to_pool_action(PoolType.TORAH_TAB.value),
+ create_remove_from_pool_action(PoolType.GENERAL.value),
+ create_remove_from_pool_action(PoolType.TORAH_TAB.value),
+ ]
def get_queryset(self, request):
queryset = super().get_queryset(request)
- return queryset.annotate(
- in_pool_general=Case(
- When(pools__name=PoolType.GENERAL.value, then=True),
- default=False,
- output_field=BooleanField()
- ),
- in_pool_torah_tab=Case(
- When(pools__name=PoolType.TORAH_TAB.value, then=True),
- default=False,
- output_field=BooleanField()
- )
- ).filter(pools__name=PoolType.LIBRARY.value)
+ return queryset.filter(pools__name=PoolType.LIBRARY.value)
def is_in_pool_general(self, obj):
- return obj.in_pool_general
+ return obj.pools.filter(name=PoolType.GENERAL.value).exists()
is_in_pool_general.boolean = True
is_in_pool_general.short_description = "General?"
- is_in_pool_general.admin_order_field = 'in_pool_general'
def is_in_pool_torah_tab(self, obj):
- return obj.in_pool_torah_tab
+ return obj.pools.filter(name=PoolType.TORAH_TAB.value).exists()
is_in_pool_torah_tab.boolean = True
is_in_pool_torah_tab.short_description = "TorahTab?"
- is_in_pool_torah_tab.admin_order_field = 'in_pool_torah_tab'
class TopicPoolAdmin(admin.ModelAdmin):
From e85ecf1f117a4b30e277cb6f11998d58a94e5310 Mon Sep 17 00:00:00 2001
From: nsantacruz
Date: Thu, 14 Nov 2024 14:51:59 +0200
Subject: [PATCH 041/125] refactor(topics): refactor sefaria functions to use
new django models
---
reader/views.py | 2 +-
sefaria/helper/topic.py | 4 ++--
sefaria/model/topic.py | 21 ++++++++++-----------
3 files changed, 13 insertions(+), 14 deletions(-)
diff --git a/reader/views.py b/reader/views.py
index 1395cf1ac2..dff1551aa5 100644
--- a/reader/views.py
+++ b/reader/views.py
@@ -4229,7 +4229,7 @@ def random_by_topic_api(request):
"""
Returns Texts API data for a random text taken from popular topic tags
"""
- from admin_tools.models.topic_pool_link import PoolType
+ from topics.models.topic_pool_link import PoolType
cb = request.GET.get("callback", None)
random_topic = get_random_topic(PoolType.PROMOTED.value)
if random_topic is None:
diff --git a/sefaria/helper/topic.py b/sefaria/helper/topic.py
index d69e3aa0cd..e7af0f6836 100644
--- a/sefaria/helper/topic.py
+++ b/sefaria/helper/topic.py
@@ -285,8 +285,8 @@ def get_random_topic(pool=None) -> Optional[Topic]:
:param pool: name of the pool from which to select the topic. If `None`, all topics are considered.
:return: Returns a random topic from the database. If you provide `pool`, then the selection is limited to topics in that pool.
"""
- from admin_tools.models import TopicPoolLink
- random_topic_slugs = TopicPoolLink.objects.get_random_topic_slugs(pool=pool, limit=1)
+ from topics.models import Topic as DjangoTopic
+ random_topic_slugs = DjangoTopic.objects.sample_topic_slugs('random', pool, limit=1)
if len(random_topic_slugs) == 0:
return None
diff --git a/sefaria/model/topic.py b/sefaria/model/topic.py
index c126c8b2d0..d28ca42e45 100644
--- a/sefaria/model/topic.py
+++ b/sefaria/model/topic.py
@@ -5,7 +5,8 @@
from .schema import AbstractTitledObject, TitleGroup
from .text import Ref, IndexSet, AbstractTextRecord, Index, Term
from .category import Category
-from admin_tools.models.topic_pool_link import TopicPoolLink, PoolType
+from topics.models import Topic as DjangoTopic
+from topics.models import TopicPool, PoolType
from sefaria.system.exceptions import InputError, DuplicateRecordError
from sefaria.model.timeperiod import TimePeriod, LifePeriod
from sefaria.system.validators import validate_url
@@ -188,7 +189,7 @@ def load(self, query, proj=None):
def _set_derived_attributes(self):
self.set_titles(getattr(self, "titles", None))
- self.pools = TopicPoolLink.objects.get_pools_by_topic_slug(getattr(self, "slug", None))
+ self.pools = DjangoTopic.objects.get_pools_by_topic_slug(getattr(self, "slug", None))
if self.__class__ != Topic and not getattr(self, "subclass", False):
# in a subclass. set appropriate "subclass" attribute
setattr(self, "subclass", self.reverse_subclass_map[self.__class__.__name__])
@@ -228,17 +229,15 @@ def get_pools(self) -> list[str]:
def has_pool(self, pool: str) -> bool:
return pool in self.get_pools()
- def add_pool(self, pool: str) -> None:
- try:
- link = TopicPoolLink(pool=pool, topic_slug=self.slug)
- link.save()
- except IntegrityError:
- raise DuplicateRecordError(f"'{pool}'<>'{self.slug}' link already exists in TopicPoolLink table.")
+ def add_pool(self, pool_name: str) -> None:
+ pool = TopicPool.objects.get(name=pool_name)
+ DjangoTopic.objects.get(slug=self.slug).pools.add(pool)
self.pools = self.get_pools()
- self.pools.append(pool)
+ self.pools.append(pool_name)
def remove_pool(self, pool) -> None:
- TopicPoolLink.objects.filter(pool=pool, topic_slug=self.slug).delete()
+ pool = TopicPool.objects.get(name=pool)
+ DjangoTopic.objects.get(slug=self.slug).pools.remove(pool)
pools = self.get_pools()
pools.remove(pool)
@@ -959,7 +958,7 @@ def set_description(self, lang, title, prompt):
return self
def get_related_pool(self):
- return PoolType.SHEETS.value if self.is_sheet else PoolType.TEXTUAL.value
+ return PoolType.SHEETS.value if self.is_sheet else PoolType.LIBRARY.value
def get_topic(self):
return Topic().load({'slug': self.toTopic})
From b4837142dcda2963d4f8ac6dfa9d781027879848 Mon Sep 17 00:00:00 2001
From: nsantacruz
Date: Thu, 14 Nov 2024 14:54:23 +0200
Subject: [PATCH 042/125] chore(topics): add topic migrations
---
topics/migrations/0001_initial.py | 37 ++++++++++++++++++++
topics/migrations/0002_auto_20241113_0809.py | 20 +++++++++++
2 files changed, 57 insertions(+)
create mode 100644 topics/migrations/0001_initial.py
create mode 100644 topics/migrations/0002_auto_20241113_0809.py
diff --git a/topics/migrations/0001_initial.py b/topics/migrations/0001_initial.py
new file mode 100644
index 0000000000..86d8cb24f2
--- /dev/null
+++ b/topics/migrations/0001_initial.py
@@ -0,0 +1,37 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.29 on 2024-11-13 12:02
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Topic',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('slug', models.CharField(max_length=255, unique=True)),
+ ('en_title', models.CharField(blank=True, default='', max_length=255)),
+ ('he_title', models.CharField(blank=True, default='', max_length=255)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='TopicPool',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(max_length=255, unique=True)),
+ ],
+ ),
+ migrations.AddField(
+ model_name='topic',
+ name='pools',
+ field=models.ManyToManyField(related_name='topics', to='topics.TopicPool'),
+ ),
+ ]
diff --git a/topics/migrations/0002_auto_20241113_0809.py b/topics/migrations/0002_auto_20241113_0809.py
new file mode 100644
index 0000000000..4fff2f2c79
--- /dev/null
+++ b/topics/migrations/0002_auto_20241113_0809.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.29 on 2024-11-13 12:09
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('topics', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='topic',
+ name='pools',
+ field=models.ManyToManyField(blank=True, related_name='topics', to='topics.TopicPool'),
+ ),
+ ]
From fb18fcd1e5570a63584799c5c5e0b57ed7ab3ddc Mon Sep 17 00:00:00 2001
From: nsantacruz
Date: Thu, 14 Nov 2024 14:54:35 +0200
Subject: [PATCH 043/125] chore(topics): add PoolType to model export
---
topics/models/__init__.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/topics/models/__init__.py b/topics/models/__init__.py
index 3c756d8991..4c01d93533 100644
--- a/topics/models/__init__.py
+++ b/topics/models/__init__.py
@@ -1,2 +1,2 @@
from .topic import Topic
-from .pool import TopicPool
+from .pool import TopicPool, PoolType
From f67db0858bfaae82afc0f325a22882ed2bfa9e65 Mon Sep 17 00:00:00 2001
From: nsantacruz
Date: Thu, 14 Nov 2024 14:54:54 +0200
Subject: [PATCH 044/125] refactor(topics): rename pools
---
topics/models/pool.py | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/topics/models/pool.py b/topics/models/pool.py
index 3facbb6b90..b84df46fec 100644
--- a/topics/models/pool.py
+++ b/topics/models/pool.py
@@ -3,9 +3,10 @@
class PoolType(Enum):
- TEXTUAL = "textual"
+ LIBRARY = "library"
SHEETS = "sheets"
- PROMOTED = "promoted"
+ TORAH_TAB = "torah_tab"
+ GENERAL = "general"
class TopicPool(models.Model):
From 67dec73208b81ce51ca30c7bd7f192b59dbc8c03 Mon Sep 17 00:00:00 2001
From: nsantacruz
Date: Thu, 14 Nov 2024 14:55:21 +0200
Subject: [PATCH 045/125] feat(topics): add utility funcs to topic model
---
topics/models/topic.py | 22 +++++++++++++++++++++-
1 file changed, 21 insertions(+), 1 deletion(-)
diff --git a/topics/models/topic.py b/topics/models/topic.py
index 6bba6523d4..9613518ace 100644
--- a/topics/models/topic.py
+++ b/topics/models/topic.py
@@ -1,12 +1,32 @@
from django.db import models
+import random
from topics.models.pool import TopicPool
+class TopicManager(models.Manager):
+ def sample_topic_slugs(self, order, pool: str = None, limit=10) -> list[str]:
+ if pool:
+ topics = self.get_topic_slugs_by_pool(pool)
+ else:
+ topics = self.all().values_list('slug', flat=True)
+ if order == 'random':
+ return random.sample(list(topics), min(limit, len(topics)))
+ else:
+ raise Exception("Invalid order: '{}'".format(order))
+
+ def get_pools_by_topic_slug(self, topic_slug: str) -> list[str]:
+ return self.filter(topic_slug=topic_slug).values_list("pools__name", flat=True)
+
+ def get_topic_slugs_by_pool(self, pool: str) -> list[str]:
+ return self.filter(pools__name=pool).values_list("slug", flat=True)
+
+
class Topic(models.Model):
slug = models.CharField(max_length=255, unique=True)
en_title = models.CharField(max_length=255, blank=True, default="")
he_title = models.CharField(max_length=255, blank=True, default="")
- pools = models.ManyToManyField(TopicPool, related_name="topics")
+ pools = models.ManyToManyField(TopicPool, related_name="topics", blank=True)
+ objects = TopicManager()
def __str__(self):
return f"Topic('{self.slug}')"
From 86804eb2ca6130fbf8b268ec1020b490a98a33bc Mon Sep 17 00:00:00 2001
From: nsantacruz
Date: Thu, 14 Nov 2024 15:51:48 +0200
Subject: [PATCH 046/125] fix(topics): remove pools from mongo topics model
---
sefaria/model/topic.py | 1 -
1 file changed, 1 deletion(-)
diff --git a/sefaria/model/topic.py b/sefaria/model/topic.py
index d28ca42e45..c729d83a02 100644
--- a/sefaria/model/topic.py
+++ b/sefaria/model/topic.py
@@ -158,7 +158,6 @@ class Topic(abst.SluggedAbstractMongoRecord, AbstractTitledObject):
"data_source", #any topic edited manually should display automatically in the TOC and this flag ensures this
'image',
"portal_slug", # slug to relevant Portal object
- 'pools', # list of strings, any of them represents a pool that this topic is member of
]
attr_schemas = {
From 17c6a31fc50417f14728d7f370df4eb9ab30bcd9 Mon Sep 17 00:00:00 2001
From: nsantacruz
Date: Thu, 14 Nov 2024 15:52:11 +0200
Subject: [PATCH 047/125] fix(topics): fix query
---
topics/models/topic.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/topics/models/topic.py b/topics/models/topic.py
index 9613518ace..b0211a5def 100644
--- a/topics/models/topic.py
+++ b/topics/models/topic.py
@@ -15,7 +15,7 @@ def sample_topic_slugs(self, order, pool: str = None, limit=10) -> list[str]:
raise Exception("Invalid order: '{}'".format(order))
def get_pools_by_topic_slug(self, topic_slug: str) -> list[str]:
- return self.filter(topic_slug=topic_slug).values_list("pools__name", flat=True)
+ return self.filter(slug=topic_slug).values_list("pools__name", flat=True)
def get_topic_slugs_by_pool(self, pool: str) -> list[str]:
return self.filter(pools__name=pool).values_list("slug", flat=True)
From b2682468cbfb02e49e2c6697a8122dcd9b1d8767 Mon Sep 17 00:00:00 2001
From: nsantacruz
Date: Thu, 14 Nov 2024 15:53:06 +0200
Subject: [PATCH 048/125] refactor(topics): import and pool name
---
reader/views.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/reader/views.py b/reader/views.py
index dff1551aa5..e4ca937670 100644
--- a/reader/views.py
+++ b/reader/views.py
@@ -4229,9 +4229,9 @@ def random_by_topic_api(request):
"""
Returns Texts API data for a random text taken from popular topic tags
"""
- from topics.models.topic_pool_link import PoolType
+ from topics.models import PoolType
cb = request.GET.get("callback", None)
- random_topic = get_random_topic(PoolType.PROMOTED.value)
+ random_topic = get_random_topic(PoolType.TORAH_TAB.value)
if random_topic is None:
return random_by_topic_api(request)
random_source = get_random_topic_source(random_topic)
From 30736ee4ee1e426920594de1df73427fdb421aea Mon Sep 17 00:00:00 2001
From: nsantacruz
Date: Thu, 14 Nov 2024 16:05:02 +0200
Subject: [PATCH 049/125] chore(topics): update django topic model on mongo
topic save
---
sefaria/model/topic.py | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/sefaria/model/topic.py b/sefaria/model/topic.py
index c729d83a02..1faaea7a06 100644
--- a/sefaria/model/topic.py
+++ b/sefaria/model/topic.py
@@ -193,6 +193,13 @@ def _set_derived_attributes(self):
# in a subclass. set appropriate "subclass" attribute
setattr(self, "subclass", self.reverse_subclass_map[self.__class__.__name__])
+ def _pre_save(self):
+ super()._pre_save()
+ django_topic, created = DjangoTopic.objects.get_or_create(slug=self.slug)
+ django_topic.en_title = self.get_primary_title('en')
+ django_topic.he_title = self.get_primary_title('he')
+ django_topic.save()
+
def _validate(self):
super(Topic, self)._validate()
if getattr(self, 'subclass', False):
From 53affe9e19e055853014f344b4d4f8dd510bff9c Mon Sep 17 00:00:00 2001
From: nsantacruz
Date: Thu, 14 Nov 2024 16:16:56 +0200
Subject: [PATCH 050/125] chore(topics): update django topic when mongo topic
slug changes
---
sefaria/model/topic.py | 2 ++
1 file changed, 2 insertions(+)
diff --git a/sefaria/model/topic.py b/sefaria/model/topic.py
index 1faaea7a06..edf4c8411c 100644
--- a/sefaria/model/topic.py
+++ b/sefaria/model/topic.py
@@ -389,6 +389,7 @@ def set_slug(self, new_slug) -> None:
old_slug = getattr(self, slug_field)
setattr(self, slug_field, new_slug)
setattr(self, slug_field, self.normalize_slug_field(slug_field))
+ DjangoTopic.objects.filter(slug=old_slug).update(slug=new_slug)
self.save() # so that topic with this slug exists when saving links to it
self.merge(old_slug)
@@ -464,6 +465,7 @@ def merge(self, other: Union['Topic', str]) -> None:
setattr(self, attr, getattr(other, attr))
self.save()
other.delete()
+ DjangoTopic.objects.get(slug=other_slug).delete()
def link_set(self, _class='intraTopic', query_kwargs: dict = None, **kwargs):
"""
From f754481f342a528701df2347f4243966ab0fcf84 Mon Sep 17 00:00:00 2001
From: nsantacruz
Date: Thu, 14 Nov 2024 16:17:27 +0200
Subject: [PATCH 051/125] chore(topics): remove extra newline
---
sefaria/model/topic.py | 1 -
1 file changed, 1 deletion(-)
diff --git a/sefaria/model/topic.py b/sefaria/model/topic.py
index edf4c8411c..e211379e37 100644
--- a/sefaria/model/topic.py
+++ b/sefaria/model/topic.py
@@ -393,7 +393,6 @@ def set_slug(self, new_slug) -> None:
self.save() # so that topic with this slug exists when saving links to it
self.merge(old_slug)
-
def merge(self, other: Union['Topic', str]) -> None:
"""
Merge `other` into `self`. This means that all data from `other` will be merged into self.
From d787bf66b1037b54756a528eeae3fa3b772f6ce0 Mon Sep 17 00:00:00 2001
From: nsantacruz
Date: Thu, 14 Nov 2024 16:22:00 +0200
Subject: [PATCH 052/125] refactor(topics): move delete to Topic delete
dependency
---
sefaria/model/topic.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/sefaria/model/topic.py b/sefaria/model/topic.py
index e211379e37..5d23144241 100644
--- a/sefaria/model/topic.py
+++ b/sefaria/model/topic.py
@@ -464,7 +464,6 @@ def merge(self, other: Union['Topic', str]) -> None:
setattr(self, attr, getattr(other, attr))
self.save()
other.delete()
- DjangoTopic.objects.get(slug=other_slug).delete()
def link_set(self, _class='intraTopic', query_kwargs: dict = None, **kwargs):
"""
@@ -1170,6 +1169,7 @@ def process_topic_delete(topic):
for sheet in db.sheets.find({"topics.slug": topic.slug}):
sheet["topics"] = [t for t in sheet["topics"] if t["slug"] != topic.slug]
db.sheets.save(sheet)
+ DjangoTopic.objects.get(slug=topic.slug).delete()
def process_topic_description_change(topic, **kwargs):
"""
From af9f31d0b831676803d0712605526539fd8f396c Mon Sep 17 00:00:00 2001
From: nsantacruz
Date: Thu, 14 Nov 2024 16:44:58 +0200
Subject: [PATCH 053/125] test(topics): add tests to make sure django topic
remains in sync with mongo topic
---
sefaria/model/tests/topic_test.py | 17 +++++++++++++++++
1 file changed, 17 insertions(+)
diff --git a/sefaria/model/tests/topic_test.py b/sefaria/model/tests/topic_test.py
index 56345624f1..4d61ccab5c 100644
--- a/sefaria/model/tests/topic_test.py
+++ b/sefaria/model/tests/topic_test.py
@@ -3,6 +3,7 @@
from sefaria.model.text import Ref
from sefaria.system.database import db
from sefaria.system.exceptions import SluggedMongoRecordMissingError
+from topics.models import Topic as DjangoTopic
from sefaria.helper.topic import update_topic
@@ -155,6 +156,22 @@ def test_merge(self, topic_graph_to_merge):
{"slug": '30', 'asTyped': 'thirty'}
]
+ t40 = Topic.init('40')
+ assert t40 is None
+ DjangoTopic.objects.get(slug='20')
+ with pytest.raises(DjangoTopic.DoesNotExist):
+ DjangoTopic.objects.get(slug='40')
+
+ def test_change_title(self, topic_graph):
+ ts = topic_graph['topics']
+ dt1 = DjangoTopic.objects.get(slug=ts['1'].slug)
+ assert dt1.en_title == ts['1'].get_primary_title('en')
+ ts['1'].title_group.add_title('new title', 'en', True, True)
+ ts['1'].save()
+ dt1 = DjangoTopic.objects.get(slug=ts['1'].slug)
+ assert dt1.en_title == ts['1'].get_primary_title('en')
+
+
def test_sanitize(self):
t = Topic()
t.slug = "sdfsdg"
From c9a0c4398747fde6795dda5c26aa12adf6db9eb1 Mon Sep 17 00:00:00 2001
From: nsantacruz
Date: Thu, 14 Nov 2024 16:45:14 +0200
Subject: [PATCH 054/125] fix(topics): cast queryset to list
---
sefaria/model/topic.py | 13 ++++++++-----
1 file changed, 8 insertions(+), 5 deletions(-)
diff --git a/sefaria/model/topic.py b/sefaria/model/topic.py
index 5d23144241..4f94f27449 100644
--- a/sefaria/model/topic.py
+++ b/sefaria/model/topic.py
@@ -188,7 +188,7 @@ def load(self, query, proj=None):
def _set_derived_attributes(self):
self.set_titles(getattr(self, "titles", None))
- self.pools = DjangoTopic.objects.get_pools_by_topic_slug(getattr(self, "slug", None))
+ self.pools = list(DjangoTopic.objects.get_pools_by_topic_slug(getattr(self, "slug", None)))
if self.__class__ != Topic and not getattr(self, "subclass", False):
# in a subclass. set appropriate "subclass" attribute
setattr(self, "subclass", self.reverse_subclass_map[self.__class__.__name__])
@@ -241,11 +241,11 @@ def add_pool(self, pool_name: str) -> None:
self.pools = self.get_pools()
self.pools.append(pool_name)
- def remove_pool(self, pool) -> None:
- pool = TopicPool.objects.get(name=pool)
+ def remove_pool(self, pool_name) -> None:
+ pool = TopicPool.objects.get(name=pool_name)
DjangoTopic.objects.get(slug=self.slug).pools.remove(pool)
pools = self.get_pools()
- pools.remove(pool)
+ pools.remove(pool_name)
def set_titles(self, titles):
self.title_group = TitleGroup(titles)
@@ -1169,7 +1169,10 @@ def process_topic_delete(topic):
for sheet in db.sheets.find({"topics.slug": topic.slug}):
sheet["topics"] = [t for t in sheet["topics"] if t["slug"] != topic.slug]
db.sheets.save(sheet)
- DjangoTopic.objects.get(slug=topic.slug).delete()
+ try:
+ DjangoTopic.objects.get(slug=topic.slug).delete()
+ except DjangoTopic.DoesNotExist:
+ print('Topic {} does not exist in django'.format(topic.slug))
def process_topic_description_change(topic, **kwargs):
"""
From c99c0e5d791d470b8b887447434755c1d928d302 Mon Sep 17 00:00:00 2001
From: nsantacruz
Date: Thu, 14 Nov 2024 17:03:03 +0200
Subject: [PATCH 055/125] chore(topics): remove unused imports
---
sefaria/model/topic.py | 2 --
1 file changed, 2 deletions(-)
diff --git a/sefaria/model/topic.py b/sefaria/model/topic.py
index 4f94f27449..7a75c7d582 100644
--- a/sefaria/model/topic.py
+++ b/sefaria/model/topic.py
@@ -1,6 +1,4 @@
-from enum import Enum
from typing import Union, Optional
-from django.db.utils import IntegrityError
from . import abstract as abst
from .schema import AbstractTitledObject, TitleGroup
from .text import Ref, IndexSet, AbstractTextRecord, Index, Term
From fb805a88abf1a1d319b8a8a5870d5b7b90821731 Mon Sep 17 00:00:00 2001
From: nsantacruz
Date: Thu, 14 Nov 2024 21:42:50 +0200
Subject: [PATCH 056/125] chore(topics): improve functionality of adding and
removing pools
---
sefaria/model/topic.py | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/sefaria/model/topic.py b/sefaria/model/topic.py
index 7a75c7d582..8273300bbc 100644
--- a/sefaria/model/topic.py
+++ b/sefaria/model/topic.py
@@ -236,14 +236,14 @@ def has_pool(self, pool: str) -> bool:
def add_pool(self, pool_name: str) -> None:
pool = TopicPool.objects.get(name=pool_name)
DjangoTopic.objects.get(slug=self.slug).pools.add(pool)
- self.pools = self.get_pools()
- self.pools.append(pool_name)
+ if not self.has_pool(pool_name):
+ self.get_pools().append(pool_name)
def remove_pool(self, pool_name) -> None:
pool = TopicPool.objects.get(name=pool_name)
DjangoTopic.objects.get(slug=self.slug).pools.remove(pool)
- pools = self.get_pools()
- pools.remove(pool_name)
+ if self.has_pool(pool_name):
+ self.get_pools().remove(pool_name)
def set_titles(self, titles):
self.title_group = TitleGroup(titles)
From bf1a99b8d63ad349e106236b40a540c98b05fa49 Mon Sep 17 00:00:00 2001
From: nsantacruz
Date: Thu, 14 Nov 2024 21:43:04 +0200
Subject: [PATCH 057/125] test(topics): add topic pool tests
---
sefaria/model/tests/topic_test.py | 26 ++++++++++++++++++++++++--
1 file changed, 24 insertions(+), 2 deletions(-)
diff --git a/sefaria/model/tests/topic_test.py b/sefaria/model/tests/topic_test.py
index 4d61ccab5c..2f49ff234a 100644
--- a/sefaria/model/tests/topic_test.py
+++ b/sefaria/model/tests/topic_test.py
@@ -3,8 +3,7 @@
from sefaria.model.text import Ref
from sefaria.system.database import db
from sefaria.system.exceptions import SluggedMongoRecordMissingError
-from topics.models import Topic as DjangoTopic
-from sefaria.helper.topic import update_topic
+from topics.models import Topic as DjangoTopic, TopicPool
def make_topic(slug):
@@ -106,6 +105,13 @@ def topic_graph_to_merge():
db.sheets.delete_one({"id": 1234567890})
+@pytest.fixture(scope='module')
+def topic_pool():
+ pool = TopicPool.objects.create(name='test-pool')
+ yield pool
+ pool.delete()
+
+
class TestTopics(object):
def test_graph_funcs(self, topic_graph):
@@ -171,6 +177,22 @@ def test_change_title(self, topic_graph):
dt1 = DjangoTopic.objects.get(slug=ts['1'].slug)
assert dt1.en_title == ts['1'].get_primary_title('en')
+ def test_pools(self, topic_graph, topic_pool):
+ ts = topic_graph['topics']
+ t1 = ts['1']
+ assert len(t1.pools) == 0
+ t1.add_pool(topic_pool.name)
+ assert t1.pools == [topic_pool.name]
+
+ # dont add duplicates
+ t1.add_pool(topic_pool.name)
+ assert t1.pools == [topic_pool.name]
+
+ assert t1.has_pool(topic_pool.name)
+ t1.remove_pool(topic_pool.name)
+ assert len(t1.pools) == 0
+ # dont error when removing non-existant pool
+ t1.remove_pool(topic_pool.name)
def test_sanitize(self):
t = Topic()
From 2b7d79d976b6bb0a0aac3ca0b141646789457ee0 Mon Sep 17 00:00:00 2001
From: nsantacruz
Date: Thu, 14 Nov 2024 22:07:49 +0200
Subject: [PATCH 058/125] feat(topics): add topic pools api
---
reader/views.py | 10 ++++++++++
sefaria/urls.py | 1 +
2 files changed, 11 insertions(+)
diff --git a/reader/views.py b/reader/views.py
index e4ca937670..c8ad2b5e5a 100644
--- a/reader/views.py
+++ b/reader/views.py
@@ -3234,6 +3234,16 @@ def topic_graph_api(request, topic):
return jsonResponse(response, callback=request.GET.get("callback", None))
+@catch_error_as_json
+def topic_pool_api(request, pool_name):
+ from topics.models import Topic as DjangoTopic
+ n_samples = int(request.GET.get("n"))
+ order = request.GET.get("order", "random")
+ topic_slugs = DjangoTopic.objects.sample_topic_slugs(order, pool_name, n_samples)
+ response = [Topic.init(slug).contents() for slug in topic_slugs]
+ return jsonResponse(response, callback=request.GET.get("callback", None))
+
+
@staff_member_required
def reorder_topics(request):
topics = json.loads(request.POST["json"]).get("topics", [])
diff --git a/sefaria/urls.py b/sefaria/urls.py
index b73ae60733..fb95509e47 100644
--- a/sefaria/urls.py
+++ b/sefaria/urls.py
@@ -264,6 +264,7 @@
url(r'^api/topics$', reader_views.topics_list_api),
url(r'^api/topics/generate-prompts/(?P.+)$', reader_views.generate_topic_prompts_api),
url(r'^api/topics-graph/(?P.+)$', reader_views.topic_graph_api),
+ url(r'^api/topics/pools/(?P.+)$', reader_views.topic_pool_api),
url(r'^api/ref-topic-links/bulk$', reader_views.topic_ref_bulk_api),
url(r'^api/ref-topic-links/(?P.+)$', reader_views.topic_ref_api),
url(r'^api/v2/topics/(?P.+)$', reader_views.topics_api, {'v2': True}),
From d88994247ed4e7bfb000d64ecfb559aad7f08ee8 Mon Sep 17 00:00:00 2001
From: nsantacruz
Date: Mon, 18 Nov 2024 13:09:28 +0200
Subject: [PATCH 059/125] refactor(topics): move topics folder to django_topics
---
django_topics/README.md | 8 ++++++++
{topics => django_topics}/__init__.py | 0
{topics => django_topics}/admin.py | 4 ++--
{topics => django_topics}/migrations/0001_initial.py | 0
.../migrations/0002_auto_20241113_0809.py | 0
{topics => django_topics}/migrations/__init__.py | 0
{topics => django_topics}/models/__init__.py | 0
{topics => django_topics}/models/pool.py | 0
{topics => django_topics}/models/topic.py | 2 +-
reader/views.py | 4 ++--
.../migrations/migrate_good_to_promote_to_topic_pools.py | 4 ++--
sefaria/helper/topic.py | 2 +-
sefaria/model/tests/topic_test.py | 2 +-
sefaria/model/topic.py | 4 ++--
sefaria/settings.py | 2 +-
static/js/Header.jsx | 4 ++--
static/js/NavSidebar.jsx | 2 +-
static/js/SourceEditor.jsx | 4 ++--
static/js/TopicEditor.jsx | 2 +-
static/js/sheets.js | 2 +-
templates/static/nash-bravmann-collection.html | 2 +-
21 files changed, 28 insertions(+), 20 deletions(-)
create mode 100644 django_topics/README.md
rename {topics => django_topics}/__init__.py (100%)
rename {topics => django_topics}/admin.py (97%)
rename {topics => django_topics}/migrations/0001_initial.py (100%)
rename {topics => django_topics}/migrations/0002_auto_20241113_0809.py (100%)
rename {topics => django_topics}/migrations/__init__.py (100%)
rename {topics => django_topics}/models/__init__.py (100%)
rename {topics => django_topics}/models/pool.py (100%)
rename {topics => django_topics}/models/topic.py (96%)
diff --git a/django_topics/README.md b/django_topics/README.md
new file mode 100644
index 0000000000..d84868dd9a
--- /dev/null
+++ b/django_topics/README.md
@@ -0,0 +1,8 @@
+# Django Topics app
+
+Django app that defines models and admin interfaces for editing certain aspects of topics that are unique to Sefaria's product and not needed for general usage of Sefaria's data.
+
+Currently contains methods to:
+- Edit which topics are in which pools
+- Define topic of the day schedule
+- Define seasonal topic schedule
\ No newline at end of file
diff --git a/topics/__init__.py b/django_topics/__init__.py
similarity index 100%
rename from topics/__init__.py
rename to django_topics/__init__.py
diff --git a/topics/admin.py b/django_topics/admin.py
similarity index 97%
rename from topics/admin.py
rename to django_topics/admin.py
index a0e765ecba..229c3b3ed3 100644
--- a/topics/admin.py
+++ b/django_topics/admin.py
@@ -1,6 +1,6 @@
from django.contrib import admin, messages
-from topics.models import Topic, TopicPool
-from topics.models.pool import PoolType
+from django_topics.models import Topic, TopicPool
+from django_topics.models.pool import PoolType
def create_add_to_pool_action(pool_name):
diff --git a/topics/migrations/0001_initial.py b/django_topics/migrations/0001_initial.py
similarity index 100%
rename from topics/migrations/0001_initial.py
rename to django_topics/migrations/0001_initial.py
diff --git a/topics/migrations/0002_auto_20241113_0809.py b/django_topics/migrations/0002_auto_20241113_0809.py
similarity index 100%
rename from topics/migrations/0002_auto_20241113_0809.py
rename to django_topics/migrations/0002_auto_20241113_0809.py
diff --git a/topics/migrations/__init__.py b/django_topics/migrations/__init__.py
similarity index 100%
rename from topics/migrations/__init__.py
rename to django_topics/migrations/__init__.py
diff --git a/topics/models/__init__.py b/django_topics/models/__init__.py
similarity index 100%
rename from topics/models/__init__.py
rename to django_topics/models/__init__.py
diff --git a/topics/models/pool.py b/django_topics/models/pool.py
similarity index 100%
rename from topics/models/pool.py
rename to django_topics/models/pool.py
diff --git a/topics/models/topic.py b/django_topics/models/topic.py
similarity index 96%
rename from topics/models/topic.py
rename to django_topics/models/topic.py
index b0211a5def..43e9db67ec 100644
--- a/topics/models/topic.py
+++ b/django_topics/models/topic.py
@@ -1,6 +1,6 @@
from django.db import models
import random
-from topics.models.pool import TopicPool
+from django_topics.models.pool import TopicPool
class TopicManager(models.Manager):
diff --git a/reader/views.py b/reader/views.py
index c8ad2b5e5a..abcdb36277 100644
--- a/reader/views.py
+++ b/reader/views.py
@@ -3236,7 +3236,7 @@ def topic_graph_api(request, topic):
@catch_error_as_json
def topic_pool_api(request, pool_name):
- from topics.models import Topic as DjangoTopic
+ from django_topics.models import Topic as DjangoTopic
n_samples = int(request.GET.get("n"))
order = request.GET.get("order", "random")
topic_slugs = DjangoTopic.objects.sample_topic_slugs(order, pool_name, n_samples)
@@ -4239,7 +4239,7 @@ def random_by_topic_api(request):
"""
Returns Texts API data for a random text taken from popular topic tags
"""
- from topics.models import PoolType
+ from django_topics.models import PoolType
cb = request.GET.get("callback", None)
random_topic = get_random_topic(PoolType.TORAH_TAB.value)
if random_topic is None:
diff --git a/scripts/migrations/migrate_good_to_promote_to_topic_pools.py b/scripts/migrations/migrate_good_to_promote_to_topic_pools.py
index c74e2b9ec3..0dcba95c52 100644
--- a/scripts/migrations/migrate_good_to_promote_to_topic_pools.py
+++ b/scripts/migrations/migrate_good_to_promote_to_topic_pools.py
@@ -3,8 +3,8 @@
django.setup()
from sefaria.model import TopicSet, RefTopicLinkSet
-from topics.models.topic import Topic
-from topics.models.pool import TopicPool, PoolType
+from django_topics.models.topic import Topic
+from django_topics.models.pool import TopicPool, PoolType
def add_to_torah_tab_pool():
diff --git a/sefaria/helper/topic.py b/sefaria/helper/topic.py
index e7af0f6836..c54bdbf25f 100644
--- a/sefaria/helper/topic.py
+++ b/sefaria/helper/topic.py
@@ -285,7 +285,7 @@ def get_random_topic(pool=None) -> Optional[Topic]:
:param pool: name of the pool from which to select the topic. If `None`, all topics are considered.
:return: Returns a random topic from the database. If you provide `pool`, then the selection is limited to topics in that pool.
"""
- from topics.models import Topic as DjangoTopic
+ from django_topics.models import Topic as DjangoTopic
random_topic_slugs = DjangoTopic.objects.sample_topic_slugs('random', pool, limit=1)
if len(random_topic_slugs) == 0:
return None
diff --git a/sefaria/model/tests/topic_test.py b/sefaria/model/tests/topic_test.py
index 2f49ff234a..005d42db4a 100644
--- a/sefaria/model/tests/topic_test.py
+++ b/sefaria/model/tests/topic_test.py
@@ -3,7 +3,7 @@
from sefaria.model.text import Ref
from sefaria.system.database import db
from sefaria.system.exceptions import SluggedMongoRecordMissingError
-from topics.models import Topic as DjangoTopic, TopicPool
+from django_topics.models import Topic as DjangoTopic, TopicPool
def make_topic(slug):
diff --git a/sefaria/model/topic.py b/sefaria/model/topic.py
index 8273300bbc..3881ca1b9e 100644
--- a/sefaria/model/topic.py
+++ b/sefaria/model/topic.py
@@ -3,8 +3,8 @@
from .schema import AbstractTitledObject, TitleGroup
from .text import Ref, IndexSet, AbstractTextRecord, Index, Term
from .category import Category
-from topics.models import Topic as DjangoTopic
-from topics.models import TopicPool, PoolType
+from django_topics.models import Topic as DjangoTopic
+from django_topics.models import TopicPool, PoolType
from sefaria.system.exceptions import InputError, DuplicateRecordError
from sefaria.model.timeperiod import TimePeriod, LifePeriod
from sefaria.system.validators import validate_url
diff --git a/sefaria/settings.py b/sefaria/settings.py
index bdb6dd7460..4a2c939b45 100644
--- a/sefaria/settings.py
+++ b/sefaria/settings.py
@@ -144,7 +144,7 @@
'reader',
'sourcesheets',
'sefaria.gauth',
- 'topics',
+ 'django_topics',
'captcha',
'django.contrib.admin',
'anymail',
diff --git a/static/js/Header.jsx b/static/js/Header.jsx
index d097d85f90..182f2d12e4 100644
--- a/static/js/Header.jsx
+++ b/static/js/Header.jsx
@@ -50,7 +50,7 @@ class Header extends Component {
{ Sefaria._siteSettings.TORAH_SPECIFIC ?
{logo} : null }
Texts
- Topics
+ Topics
Community
Donate
@@ -211,7 +211,7 @@ const MobileNavMenu = ({onRefClick, showSearch, openTopic, openURL, close, visib
Texts
-
+
Topics
diff --git a/static/js/NavSidebar.jsx b/static/js/NavSidebar.jsx
index 1aaadeaee4..066a2047d6 100644
--- a/static/js/NavSidebar.jsx
+++ b/static/js/NavSidebar.jsx
@@ -479,7 +479,7 @@ const WeeklyTorahPortion = () => {
-
+
All Portions ›
פרשות השבוע ›
diff --git a/static/js/SourceEditor.jsx b/static/js/SourceEditor.jsx
index 6efa15b9bc..dbf7b2d0c3 100644
--- a/static/js/SourceEditor.jsx
+++ b/static/js/SourceEditor.jsx
@@ -56,7 +56,7 @@ const SourceEditor = ({topic, close, origData={}}) => {
const currentUrlObj = new URL(window.location.href);
const tabName = currentUrlObj.searchParams.get('tab');
Sefaria.postRefTopicLink(refInUrl, payload)
- .then(() => window.location.href = `/topics/${topic}?sort=Relevance&tab=${tabName}`)
+ .then(() => window.location.href = `../../django_topics/${topic}?sort=Relevance&tab=${tabName}`)
.finally(() => setSavingStatus(false));
}
@@ -96,7 +96,7 @@ const SourceEditor = ({topic, close, origData={}}) => {
const deleteTopicSource = function() {
const url = `/api/ref-topic-links/${Sefaria.normRef(origData.ref)}?topic=${topic}&interface_lang=${Sefaria.interfaceLang}`;
Sefaria.adminEditorApiRequest(url, null, null, "DELETE")
- .then(() => window.location.href = `/topics/${topic}`);
+ .then(() => window.location.href = `../../django_topics/${topic}`);
}
const previousTitleItemRef = useRef(data.enTitle ? "Previous Title" : null); //use useRef to make value null even if component re-renders
const previousPromptItemRef = useRef(data.prompt ? "Previous Prompt" : null);
diff --git a/static/js/TopicEditor.jsx b/static/js/TopicEditor.jsx
index 9680617df1..392d3ce0e8 100644
--- a/static/js/TopicEditor.jsx
+++ b/static/js/TopicEditor.jsx
@@ -206,7 +206,7 @@ const TopicEditor = ({origData, onCreateSuccess, close, origWasCat}) => {
onCreateSuccess(newSlug);
}
else {
- window.location.href = `/topics/${newSlug}`;
+ window.location.href = `../../django_topics/${newSlug}`;
}
}
}).fail(function (xhr, status, errorThrown) {
diff --git a/static/js/sheets.js b/static/js/sheets.js
index b7ed8d13c9..ca955eff1b 100755
--- a/static/js/sheets.js
+++ b/static/js/sheets.js
@@ -2172,7 +2172,7 @@ sjs.sheetTagger = {
}
var html = "";
for (var i = 0; i < topics.length; i++) {
- html = html + ''+topics[i].asTyped+'';
+ html = html + ''+topics[i].asTyped+'';
}
$("#sheetTags").html(html);
},
diff --git a/templates/static/nash-bravmann-collection.html b/templates/static/nash-bravmann-collection.html
index bec3974a0d..7a3d79abac 100644
--- a/templates/static/nash-bravmann-collection.html
+++ b/templates/static/nash-bravmann-collection.html
@@ -110,7 +110,7 @@
The Jack Nash and Ludwig Bravmann Collection is a free, online library of Rabbi Adin Even-Israel Steinsaltz's major commentaries in Hebrew and English. Interlinked with Sefaria’s vast and ever-growing corpus of Jewish text, the Nash-Bravmann Collection further integrates Rabbi Steinsaltz’s Torah into the Jewish library, while also bringing these already-renowned commentaries to an even wider global audience.
האוסף על שם ג׳ק נאש ולודוויג ברוומן הינו
-
+
ספריה מקוונת חינמית הכוללת את הפרשנויות המרכזיות של הרב עדין אבן-ישראל שטיינזלץ
בעברית ובאנגלית. בעצם שילובם של כתבי הרב שטיינזלץ באופן שוטף בספרייה ההולכת ומתרחבת של ספריא, אוסף נאש-ברוומן משמש כאמצעי להטמעה מעמיקה של תורתו בתוך שאר עולם המקורות היהודי, תוך כדי הנגשת הפרשנויות הנודעות האלו לקהילתנו העולמית.
From 1c7f3bec25746026ae8141015b3a666a057858fb Mon Sep 17 00:00:00 2001
From: nsantacruz
Date: Mon, 18 Nov 2024 16:32:03 +0200
Subject: [PATCH 060/125] feat(topics): add TopicOfTheDay model with admin view
---
django_topics/admin.py | 19 ++++++++--
django_topics/migrations/0001_initial.py | 22 ++++++++++-
.../migrations/0002_auto_20241113_0809.py | 20 ----------
django_topics/models/__init__.py | 1 +
django_topics/models/topic.py | 2 +-
django_topics/models/topic_of_the_day.py | 38 +++++++++++++++++++
6 files changed, 76 insertions(+), 26 deletions(-)
delete mode 100644 django_topics/migrations/0002_auto_20241113_0809.py
create mode 100644 django_topics/models/topic_of_the_day.py
diff --git a/django_topics/admin.py b/django_topics/admin.py
index 229c3b3ed3..94d8ff8d29 100644
--- a/django_topics/admin.py
+++ b/django_topics/admin.py
@@ -1,5 +1,5 @@
from django.contrib import admin, messages
-from django_topics.models import Topic, TopicPool
+from django_topics.models import Topic, TopicPool, TopicOfTheDay
from django_topics.models.pool import PoolType
@@ -53,10 +53,12 @@ def queryset(self, request, queryset):
return queryset
+@admin.register(Topic)
class TopicAdmin(admin.ModelAdmin):
list_display = ('slug', 'en_title', 'he_title', 'is_in_pool_general', 'is_in_pool_torah_tab')
list_filter = (PoolFilter,)
filter_horizontal = ('pools',)
+ search_fields = ('slug', 'en_title', 'he_title')
readonly_fields = ('slug', 'en_title', 'he_title')
actions = [
create_add_to_pool_action(PoolType.GENERAL.value),
@@ -80,6 +82,7 @@ def is_in_pool_torah_tab(self, obj):
is_in_pool_torah_tab.short_description = "TorahTab?"
+@admin.register(TopicPool)
class TopicPoolAdmin(admin.ModelAdmin):
list_display = ('name', 'topic_names')
filter_horizontal = ('topics',)
@@ -93,7 +96,17 @@ def topic_names(self, obj):
return str_rep
-admin.site.register(Topic, TopicAdmin)
-admin.site.register(TopicPool, TopicPoolAdmin)
+@admin.register(TopicOfTheDay)
+class TopicOfTheDayAdmin(admin.ModelAdmin):
+ list_display = ('topic', 'start_date', 'end_date')
+ list_filter = ('start_date',)
+ raw_id_fields = ('topic',)
+ search_fields = ('topic__slug', 'topic__en_title', 'topic__he_title')
+ date_hierarchy = 'start_date'
+ fieldsets = (
+ (None, {
+ 'fields': ('topic', 'start_date', 'end_date'),
+ }),
+ )
diff --git a/django_topics/migrations/0001_initial.py b/django_topics/migrations/0001_initial.py
index 86d8cb24f2..4cf92fcec3 100644
--- a/django_topics/migrations/0001_initial.py
+++ b/django_topics/migrations/0001_initial.py
@@ -1,8 +1,9 @@
# -*- coding: utf-8 -*-
-# Generated by Django 1.11.29 on 2024-11-13 12:02
+# Generated by Django 1.11.29 on 2024-11-18 12:53
from __future__ import unicode_literals
from django.db import migrations, models
+import django.db.models.deletion
class Migration(migrations.Migration):
@@ -22,6 +23,19 @@ class Migration(migrations.Migration):
('he_title', models.CharField(blank=True, default='', max_length=255)),
],
),
+ migrations.CreateModel(
+ name='TopicOfTheDay',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('start_date', models.DateField()),
+ ('end_date', models.DateField(blank=True, null=True)),
+ ('topic', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='topic_of_the_day', to='django_topics.Topic')),
+ ],
+ options={
+ 'verbose_name': 'Topic of the Day',
+ 'verbose_name_plural': 'Topics of the Day',
+ },
+ ),
migrations.CreateModel(
name='TopicPool',
fields=[
@@ -32,6 +46,10 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='topic',
name='pools',
- field=models.ManyToManyField(related_name='topics', to='topics.TopicPool'),
+ field=models.ManyToManyField(blank=True, related_name='topics', to='django_topics.TopicPool'),
+ ),
+ migrations.AlterUniqueTogether(
+ name='topicoftheday',
+ unique_together=set([('topic', 'start_date', 'end_date')]),
),
]
diff --git a/django_topics/migrations/0002_auto_20241113_0809.py b/django_topics/migrations/0002_auto_20241113_0809.py
deleted file mode 100644
index 4fff2f2c79..0000000000
--- a/django_topics/migrations/0002_auto_20241113_0809.py
+++ /dev/null
@@ -1,20 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.11.29 on 2024-11-13 12:09
-from __future__ import unicode_literals
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('topics', '0001_initial'),
- ]
-
- operations = [
- migrations.AlterField(
- model_name='topic',
- name='pools',
- field=models.ManyToManyField(blank=True, related_name='topics', to='topics.TopicPool'),
- ),
- ]
diff --git a/django_topics/models/__init__.py b/django_topics/models/__init__.py
index 4c01d93533..87c91b0756 100644
--- a/django_topics/models/__init__.py
+++ b/django_topics/models/__init__.py
@@ -1,2 +1,3 @@
from .topic import Topic
from .pool import TopicPool, PoolType
+from .topic_of_the_day import TopicOfTheDay
diff --git a/django_topics/models/topic.py b/django_topics/models/topic.py
index 43e9db67ec..6f06c6b7e1 100644
--- a/django_topics/models/topic.py
+++ b/django_topics/models/topic.py
@@ -29,4 +29,4 @@ class Topic(models.Model):
objects = TopicManager()
def __str__(self):
- return f"Topic('{self.slug}')"
+ return self.slug
diff --git a/django_topics/models/topic_of_the_day.py b/django_topics/models/topic_of_the_day.py
new file mode 100644
index 0000000000..9c837aa3ca
--- /dev/null
+++ b/django_topics/models/topic_of_the_day.py
@@ -0,0 +1,38 @@
+from django.db import models
+from django_topics.models import Topic
+from django.core.exceptions import ValidationError
+
+
+class TopicOfTheDay(models.Model):
+ topic = models.ForeignKey(
+ Topic,
+ on_delete=models.CASCADE,
+ related_name='topic_of_the_day'
+ )
+ start_date = models.DateField()
+ end_date = models.DateField(blank=True, null=True)
+
+ class Meta:
+ unique_together = ('topic', 'start_date', 'end_date')
+ verbose_name = "Topic of the Day"
+ verbose_name_plural = "Topics of the Day"
+
+ def clean(self):
+ if not self.end_date:
+ # end_date is optional. When not passed, default it to use start_date
+ self.end_date = self.start_date
+
+ if self.start_date > self.end_date:
+ raise ValidationError("Start date cannot be after end date.")
+
+ def overlaps_with(self, other_start_date, other_end_date):
+ """
+ Check if this date range overlaps with another date range.
+ """
+ return (
+ (self.start_date <= other_end_date) and
+ (self.end_date >= other_start_date)
+ )
+
+ def __str__(self):
+ return f"{self.topic.slug} ({self.start_date} to {self.end_date})"
From 02858a68c8764c860d7d51f7b58490e42aed1225 Mon Sep 17 00:00:00 2001
From: nsantacruz
Date: Tue, 19 Nov 2024 10:26:43 +0200
Subject: [PATCH 061/125] feat(topics): add SeasonalTopic model with admin view
---
django_topics/admin.py | 54 ++++++++++++++++++++++++--
django_topics/models/__init__.py | 1 +
django_topics/models/seasonal_topic.py | 51 ++++++++++++++++++++++++
3 files changed, 103 insertions(+), 3 deletions(-)
create mode 100644 django_topics/models/seasonal_topic.py
diff --git a/django_topics/admin.py b/django_topics/admin.py
index 94d8ff8d29..0980b8f0c2 100644
--- a/django_topics/admin.py
+++ b/django_topics/admin.py
@@ -1,5 +1,5 @@
from django.contrib import admin, messages
-from django_topics.models import Topic, TopicPool, TopicOfTheDay
+from django_topics.models import Topic, TopicPool, TopicOfTheDay, SeasonalTopic
from django_topics.models.pool import PoolType
@@ -98,15 +98,63 @@ def topic_names(self, obj):
@admin.register(TopicOfTheDay)
class TopicOfTheDayAdmin(admin.ModelAdmin):
- list_display = ('topic', 'start_date', 'end_date')
+ list_display = ('topic', 'start_date')
list_filter = ('start_date',)
raw_id_fields = ('topic',)
search_fields = ('topic__slug', 'topic__en_title', 'topic__he_title')
date_hierarchy = 'start_date'
fieldsets = (
(None, {
- 'fields': ('topic', 'start_date', 'end_date'),
+ 'fields': ('topic', 'start_date'),
}),
)
+@admin.register(SeasonalTopic)
+class SeasonalTopicAdmin(admin.ModelAdmin):
+ list_display = (
+ 'topic',
+ 'secondary_topic',
+ 'start_date',
+ 'display_start_date_israel',
+ 'display_end_date_israel',
+ 'display_start_date_diaspora',
+ 'display_end_date_diaspora'
+ )
+ raw_id_fields = ('topic', 'secondary_topic')
+ list_filter = (
+ 'start_date',
+ 'display_start_date_israel',
+ 'display_start_date_diaspora'
+ )
+ search_fields = ('topic__slug', 'topic__en_title', 'topic__he_title', 'secondary_topic__slug')
+ autocomplete_fields = ('topic', 'secondary_topic')
+ date_hierarchy = 'start_date'
+ fieldsets = (
+ (None, {
+ 'fields': (
+ 'topic',
+ 'secondary_topic',
+ 'start_date'
+ )
+ }),
+ ('Israel Display Dates', {
+ 'fields': (
+ 'display_start_date_israel',
+ 'display_end_date_israel'
+ )
+ }),
+ ('Diaspora Display Dates', {
+ 'fields': (
+ 'display_start_date_diaspora',
+ 'display_end_date_diaspora'
+ )
+ }),
+ )
+
+ def save_model(self, request, obj, form, change):
+ """
+ Overriding the save_model to ensure the model's clean method is executed.
+ """
+ obj.clean()
+ super().save_model(request, obj, form, change)
diff --git a/django_topics/models/__init__.py b/django_topics/models/__init__.py
index 87c91b0756..7f02438730 100644
--- a/django_topics/models/__init__.py
+++ b/django_topics/models/__init__.py
@@ -1,3 +1,4 @@
from .topic import Topic
from .pool import TopicPool, PoolType
from .topic_of_the_day import TopicOfTheDay
+from .seasonal_topic import SeasonalTopic
diff --git a/django_topics/models/seasonal_topic.py b/django_topics/models/seasonal_topic.py
new file mode 100644
index 0000000000..dcfcab02b1
--- /dev/null
+++ b/django_topics/models/seasonal_topic.py
@@ -0,0 +1,51 @@
+from django.db import models
+from django_topics.models import Topic
+from django.core.exceptions import ValidationError
+
+
+class SeasonalTopic(models.Model):
+ topic = models.ForeignKey(
+ Topic,
+ on_delete=models.CASCADE,
+ related_name='seasonal_topic'
+ )
+ secondary_topic = models.ForeignKey(
+ Topic,
+ on_delete=models.CASCADE,
+ related_name='seasonal_secondary_topic',
+ blank=True,
+ null=True,
+ ) # e.g. for topic Teshuva, secondary_topic would be Yom Kippur
+ start_date = models.DateField()
+ display_start_date_israel = models.DateField(blank=True, null=True)
+ display_end_date_israel = models.DateField(blank=True, null=True)
+ display_start_date_diaspora = models.DateField(blank=True, null=True)
+ display_end_date_diaspora = models.DateField(blank=True, null=True)
+
+ class Meta:
+ unique_together = ('topic', 'start_date')
+ verbose_name = "Seasonal Topic"
+ verbose_name_plural = "Seasonal Topics"
+
+ def populate_field_based_on_field(self, field, reference_field):
+ if not getattr(self, field, None) and getattr(self, reference_field, None):
+ setattr(self, field, getattr(self, reference_field))
+
+ def validate_start_end_dates(self, start_date_field, end_date_field):
+ if not getattr(self, start_date_field, None) and getattr(self, end_date_field):
+ raise ValidationError(f"End date field '{end_date_field}' defined without start date.")
+ if getattr(self, start_date_field) > getattr(self, end_date_field):
+ raise ValidationError(f"Start date field '{start_date_field}' cannot be after end date.")
+
+ def clean(self):
+ self.populate_field_based_on_field('display_end_date_israel', 'display_start_date_israel')
+ self.populate_field_based_on_field('display_end_date_diaspora', 'display_start_date_diaspora')
+ self.populate_field_based_on_field('display_start_date_diaspora', 'display_start_date_israel')
+ self.populate_field_based_on_field('display_end_date_diaspora', 'display_end_date_israel')
+ if not getattr(self, 'display_start_date_israel') and getattr(self, 'display_start_date_diaspora'):
+ raise ValidationError("If diaspora date is defined, Israel date must also be defined.")
+ self.validate_start_end_dates('display_start_date_israel', 'display_end_date_israel')
+ self.validate_start_end_dates('display_start_date_diaspora', 'display_end_date_diaspora')
+
+ def __str__(self):
+ return f"{self.topic.slug} ({self.start_date})"
From 928d9ae94a2bc3bf9cbc38646b21e7baf90902cf Mon Sep 17 00:00:00 2001
From: nsantacruz
Date: Tue, 19 Nov 2024 10:26:56 +0200
Subject: [PATCH 062/125] feat(topics): remove end_date from TopicOfTheDay
---
django_topics/models/topic_of_the_day.py | 22 ++--------------------
1 file changed, 2 insertions(+), 20 deletions(-)
diff --git a/django_topics/models/topic_of_the_day.py b/django_topics/models/topic_of_the_day.py
index 9c837aa3ca..26002568d1 100644
--- a/django_topics/models/topic_of_the_day.py
+++ b/django_topics/models/topic_of_the_day.py
@@ -10,29 +10,11 @@ class TopicOfTheDay(models.Model):
related_name='topic_of_the_day'
)
start_date = models.DateField()
- end_date = models.DateField(blank=True, null=True)
class Meta:
- unique_together = ('topic', 'start_date', 'end_date')
+ unique_together = ('topic', 'start_date')
verbose_name = "Topic of the Day"
verbose_name_plural = "Topics of the Day"
- def clean(self):
- if not self.end_date:
- # end_date is optional. When not passed, default it to use start_date
- self.end_date = self.start_date
-
- if self.start_date > self.end_date:
- raise ValidationError("Start date cannot be after end date.")
-
- def overlaps_with(self, other_start_date, other_end_date):
- """
- Check if this date range overlaps with another date range.
- """
- return (
- (self.start_date <= other_end_date) and
- (self.end_date >= other_start_date)
- )
-
def __str__(self):
- return f"{self.topic.slug} ({self.start_date} to {self.end_date})"
+ return f"{self.topic.slug} ({self.start_date})"
From 5ee2b61f4f970a1a001fed3a37341015fdf217f2 Mon Sep 17 00:00:00 2001
From: nsantacruz
Date: Tue, 19 Nov 2024 10:27:14 +0200
Subject: [PATCH 063/125] chore(topics): add migration for all django topics
models
---
django_topics/migrations/0001_initial.py | 55 ------------------------
1 file changed, 55 deletions(-)
delete mode 100644 django_topics/migrations/0001_initial.py
diff --git a/django_topics/migrations/0001_initial.py b/django_topics/migrations/0001_initial.py
deleted file mode 100644
index 4cf92fcec3..0000000000
--- a/django_topics/migrations/0001_initial.py
+++ /dev/null
@@ -1,55 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.11.29 on 2024-11-18 12:53
-from __future__ import unicode_literals
-
-from django.db import migrations, models
-import django.db.models.deletion
-
-
-class Migration(migrations.Migration):
-
- initial = True
-
- dependencies = [
- ]
-
- operations = [
- migrations.CreateModel(
- name='Topic',
- fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('slug', models.CharField(max_length=255, unique=True)),
- ('en_title', models.CharField(blank=True, default='', max_length=255)),
- ('he_title', models.CharField(blank=True, default='', max_length=255)),
- ],
- ),
- migrations.CreateModel(
- name='TopicOfTheDay',
- fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('start_date', models.DateField()),
- ('end_date', models.DateField(blank=True, null=True)),
- ('topic', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='topic_of_the_day', to='django_topics.Topic')),
- ],
- options={
- 'verbose_name': 'Topic of the Day',
- 'verbose_name_plural': 'Topics of the Day',
- },
- ),
- migrations.CreateModel(
- name='TopicPool',
- fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('name', models.CharField(max_length=255, unique=True)),
- ],
- ),
- migrations.AddField(
- model_name='topic',
- name='pools',
- field=models.ManyToManyField(blank=True, related_name='topics', to='django_topics.TopicPool'),
- ),
- migrations.AlterUniqueTogether(
- name='topicoftheday',
- unique_together=set([('topic', 'start_date', 'end_date')]),
- ),
- ]
From 1a5f13fd8d132372d7a35c3393c467b192bb7551 Mon Sep 17 00:00:00 2001
From: nsantacruz
Date: Tue, 19 Nov 2024 11:07:22 +0200
Subject: [PATCH 064/125] chore(topics): add migration for all django topics
models
---
django_topics/migrations/0001_initial.py | 83 ++++++++++++++++++++++++
1 file changed, 83 insertions(+)
create mode 100644 django_topics/migrations/0001_initial.py
diff --git a/django_topics/migrations/0001_initial.py b/django_topics/migrations/0001_initial.py
new file mode 100644
index 0000000000..c73e0efeeb
--- /dev/null
+++ b/django_topics/migrations/0001_initial.py
@@ -0,0 +1,83 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.29 on 2024-11-19 08:18
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='SeasonalTopic',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('start_date', models.DateField()),
+ ('display_start_date_israel', models.DateField(blank=True, null=True)),
+ ('display_end_date_israel', models.DateField(blank=True, null=True)),
+ ('display_start_date_diaspora', models.DateField(blank=True, null=True)),
+ ('display_end_date_diaspora', models.DateField(blank=True, null=True)),
+ ],
+ options={
+ 'verbose_name': 'Seasonal Topic',
+ 'verbose_name_plural': 'Seasonal Topics',
+ },
+ ),
+ migrations.CreateModel(
+ name='Topic',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('slug', models.CharField(max_length=255, unique=True)),
+ ('en_title', models.CharField(blank=True, default='', max_length=255)),
+ ('he_title', models.CharField(blank=True, default='', max_length=255)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='TopicOfTheDay',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('start_date', models.DateField()),
+ ('topic', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='topic_of_the_day', to='django_topics.Topic')),
+ ],
+ options={
+ 'verbose_name': 'Topic of the Day',
+ 'verbose_name_plural': 'Topics of the Day',
+ },
+ ),
+ migrations.CreateModel(
+ name='TopicPool',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(max_length=255, unique=True)),
+ ],
+ ),
+ migrations.AddField(
+ model_name='topic',
+ name='pools',
+ field=models.ManyToManyField(blank=True, related_name='topics', to='django_topics.TopicPool'),
+ ),
+ migrations.AddField(
+ model_name='seasonaltopic',
+ name='secondary_topic',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='seasonal_secondary_topic', to='django_topics.Topic'),
+ ),
+ migrations.AddField(
+ model_name='seasonaltopic',
+ name='topic',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='seasonal_topic', to='django_topics.Topic'),
+ ),
+ migrations.AlterUniqueTogether(
+ name='topicoftheday',
+ unique_together=set([('topic', 'start_date')]),
+ ),
+ migrations.AlterUniqueTogether(
+ name='seasonaltopic',
+ unique_together=set([('topic', 'start_date')]),
+ ),
+ ]
From 8a60423f34ac65825dcfc2a251fa4e019285b83f Mon Sep 17 00:00:00 2001
From: nsantacruz
Date: Tue, 19 Nov 2024 13:46:25 +0200
Subject: [PATCH 065/125] fix(search): remove hard-coded DJANGO_SETTINGS env
var
---
sefaria/search.py | 2 --
1 file changed, 2 deletions(-)
diff --git a/sefaria/search.py b/sefaria/search.py
index 198ad66e4c..ce826e2915 100644
--- a/sefaria/search.py
+++ b/sefaria/search.py
@@ -10,8 +10,6 @@
import bleach
import pymongo
-# To allow these files to be run directly from command line (w/o Django shell)
-os.environ['DJANGO_SETTINGS_MODULE'] = "settings"
import structlog
import logging
From 27df1a8f66edb0715bbdd358b63ca93f777fd35e Mon Sep 17 00:00:00 2001
From: nsantacruz
Date: Tue, 19 Nov 2024 13:47:58 +0200
Subject: [PATCH 066/125] fix(topics): move library setup to startup script.
This prevents issues with django migration since this setup script was running too early for django.
---
reader/apps.py | 11 +++++++++++
reader/startup.py | 44 ++++++++++++++++++++++++++++++++++++++++++++
reader/views.py | 33 ---------------------------------
sefaria/settings.py | 2 +-
4 files changed, 56 insertions(+), 34 deletions(-)
create mode 100644 reader/apps.py
create mode 100644 reader/startup.py
diff --git a/reader/apps.py b/reader/apps.py
new file mode 100644
index 0000000000..832d345023
--- /dev/null
+++ b/reader/apps.py
@@ -0,0 +1,11 @@
+from django.apps import AppConfig
+import os
+
+
+class ReaderAppConfig(AppConfig):
+ name = 'reader'
+
+ def ready(self):
+ from .startup import init_library_cache
+ if not os.environ.get('RUN_MAIN', None):
+ init_library_cache()
diff --git a/reader/startup.py b/reader/startup.py
new file mode 100644
index 0000000000..b75d65039f
--- /dev/null
+++ b/reader/startup.py
@@ -0,0 +1,44 @@
+
+
+def init_library_cache():
+ import structlog
+ logger = structlog.get_logger(__name__)
+
+ from sefaria.model.text import library
+ from sefaria.system.multiserver.coordinator import server_coordinator
+ from django.conf import settings
+ logger.info("Initializing library objects.")
+ logger.info("Initializing TOC Tree")
+ library.get_toc_tree()
+
+ logger.info("Initializing Shared Cache")
+ library.init_shared_cache()
+
+ if not settings.DISABLE_AUTOCOMPLETER:
+ logger.info("Initializing Full Auto Completer")
+ library.build_full_auto_completer()
+
+ print("Initializing Ref Auto Completer")
+ logger.info("Initializing Ref Auto Completer")
+ library.build_ref_auto_completer()
+
+ print("Initializing Lexicon Auto Completers")
+ logger.info("Initializing Lexicon Auto Completers")
+ library.build_lexicon_auto_completers()
+
+ print("Initializing Cross Lexicon Auto Completer")
+ logger.info("Initializing Cross Lexicon Auto Completer")
+ library.build_cross_lexicon_auto_completer()
+
+ print('Initializing Topic Auto Completer')
+ logger.info("Initializing Topic Auto Completer")
+ library.build_topic_auto_completer()
+
+ if settings.ENABLE_LINKER:
+ print('Initializing Linker')
+ logger.info("Initializing Linker")
+ library.build_linker('he')
+
+ if server_coordinator:
+ server_coordinator.connect()
+ print("DONE")
diff --git a/reader/views.py b/reader/views.py
index abcdb36277..853cf8bfe4 100644
--- a/reader/views.py
+++ b/reader/views.py
@@ -90,39 +90,6 @@
import structlog
logger = structlog.get_logger(__name__)
-# # #
-# Initialized cache library objects that depend on sefaria.model being completely loaded.
-logger.info("Initializing library objects.")
-logger.info("Initializing TOC Tree")
-library.get_toc_tree()
-
-logger.info("Initializing Shared Cache")
-library.init_shared_cache()
-
-if not DISABLE_AUTOCOMPLETER:
- logger.info("Initializing Full Auto Completer")
- library.build_full_auto_completer()
-
- logger.info("Initializing Ref Auto Completer")
- library.build_ref_auto_completer()
-
- logger.info("Initializing Lexicon Auto Completers")
- library.build_lexicon_auto_completers()
-
- logger.info("Initializing Cross Lexicon Auto Completer")
- library.build_cross_lexicon_auto_completer()
-
- logger.info("Initializing Topic Auto Completer")
- library.build_topic_auto_completer()
-
-if ENABLE_LINKER:
- logger.info("Initializing Linker")
- library.build_linker('he')
-
-if server_coordinator:
- server_coordinator.connect()
-# # #
-
def render_template(request, template_name='base.html', app_props=None, template_context=None, content_type=None, status=None, using=None):
"""
diff --git a/sefaria/settings.py b/sefaria/settings.py
index 4a2c939b45..03a78b3c37 100644
--- a/sefaria/settings.py
+++ b/sefaria/settings.py
@@ -141,7 +141,7 @@
'django.contrib.staticfiles',
'django.contrib.humanize',
'emailusernames',
- 'reader',
+ 'reader.apps.ReaderAppConfig',
'sourcesheets',
'sefaria.gauth',
'django_topics',
From 25eec7129915c276a6da0b907c9f9f904fb2ad06 Mon Sep 17 00:00:00 2001
From: nsantacruz
Date: Tue, 19 Nov 2024 14:28:31 +0200
Subject: [PATCH 067/125] chore(topics): move os import first
---
reader/apps.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/reader/apps.py b/reader/apps.py
index 832d345023..1c8c50442b 100644
--- a/reader/apps.py
+++ b/reader/apps.py
@@ -1,5 +1,5 @@
-from django.apps import AppConfig
import os
+from django.apps import AppConfig
class ReaderAppConfig(AppConfig):
From 3589b5a3b6faa60ab5e93317684d9ad3add92560 Mon Sep 17 00:00:00 2001
From: nsantacruz
Date: Tue, 19 Nov 2024 15:01:40 +0200
Subject: [PATCH 068/125] chore(topics): remove extra print statements
---
reader/startup.py | 6 ------
1 file changed, 6 deletions(-)
diff --git a/reader/startup.py b/reader/startup.py
index b75d65039f..bbdf519c1d 100644
--- a/reader/startup.py
+++ b/reader/startup.py
@@ -18,27 +18,21 @@ def init_library_cache():
logger.info("Initializing Full Auto Completer")
library.build_full_auto_completer()
- print("Initializing Ref Auto Completer")
logger.info("Initializing Ref Auto Completer")
library.build_ref_auto_completer()
- print("Initializing Lexicon Auto Completers")
logger.info("Initializing Lexicon Auto Completers")
library.build_lexicon_auto_completers()
- print("Initializing Cross Lexicon Auto Completer")
logger.info("Initializing Cross Lexicon Auto Completer")
library.build_cross_lexicon_auto_completer()
- print('Initializing Topic Auto Completer')
logger.info("Initializing Topic Auto Completer")
library.build_topic_auto_completer()
if settings.ENABLE_LINKER:
- print('Initializing Linker')
logger.info("Initializing Linker")
library.build_linker('he')
if server_coordinator:
server_coordinator.connect()
- print("DONE")
From 3863b72cbb56502430298ccf5b96dab1f47d5d81 Mon Sep 17 00:00:00 2001
From: nsantacruz
Date: Tue, 19 Nov 2024 15:18:36 +0200
Subject: [PATCH 069/125] feat(topics): add helpful text for admins
---
django_topics/admin.py | 11 +++++++++--
django_topics/models/seasonal_topic.py | 5 +++--
2 files changed, 12 insertions(+), 4 deletions(-)
diff --git a/django_topics/admin.py b/django_topics/admin.py
index 0980b8f0c2..4087c6d6c5 100644
--- a/django_topics/admin.py
+++ b/django_topics/admin.py
@@ -142,13 +142,20 @@ class SeasonalTopicAdmin(admin.ModelAdmin):
'fields': (
'display_start_date_israel',
'display_end_date_israel'
- )
+ ),
+ 'description': 'Dates to be displayed to the user of when this topic is "happening". '
+ 'E.g. for a holiday, when the holiday occurs. '
+ 'When the dates are the same for both Israel and Diaspora, only fill out Israeli dates. '
+ 'Similarly, when the start and end dates are the same, only fill out start date.'
}),
('Diaspora Display Dates', {
'fields': (
'display_start_date_diaspora',
'display_end_date_diaspora'
- )
+ ),
+ 'description': 'When the dates are the same for both Israel and Diaspora, only fill out Israeli dates. '
+ 'Similarly, when the start and end dates are the same, only fill out start date.'
+
}),
)
diff --git a/django_topics/models/seasonal_topic.py b/django_topics/models/seasonal_topic.py
index dcfcab02b1..e7adf405df 100644
--- a/django_topics/models/seasonal_topic.py
+++ b/django_topics/models/seasonal_topic.py
@@ -15,8 +15,9 @@ class SeasonalTopic(models.Model):
related_name='seasonal_secondary_topic',
blank=True,
null=True,
- ) # e.g. for topic Teshuva, secondary_topic would be Yom Kippur
- start_date = models.DateField()
+ help_text="Secondary topic which will be displayed alongside `topic`. E.g. `topic` is 'Teshuva' then secondary topic could be 'Yom Kippur'."
+ )
+ start_date = models.DateField(help_text="Start date of when this will appear. End date is implied by when the next Seasonal Topic is displayed.")
display_start_date_israel = models.DateField(blank=True, null=True)
display_end_date_israel = models.DateField(blank=True, null=True)
display_start_date_diaspora = models.DateField(blank=True, null=True)
From add1abb57536995a95f1e135abad1175a6faaa69 Mon Sep 17 00:00:00 2001
From: nsantacruz
Date: Wed, 20 Nov 2024 08:58:27 +0200
Subject: [PATCH 070/125] fix(topics): only run init_library_cache() when
runserver is run
---
reader/apps.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/reader/apps.py b/reader/apps.py
index 1c8c50442b..d31ba3f13b 100644
--- a/reader/apps.py
+++ b/reader/apps.py
@@ -1,4 +1,5 @@
import os
+import sys
from django.apps import AppConfig
@@ -7,5 +8,5 @@ class ReaderAppConfig(AppConfig):
def ready(self):
from .startup import init_library_cache
- if not os.environ.get('RUN_MAIN', None):
+ if not os.environ.get('RUN_MAIN', None) and len(sys.argv) > 1 and sys.argv[1] == 'runserver':
init_library_cache()
From eef1df256edd07a63c1acec4f2c977330b653da1 Mon Sep 17 00:00:00 2001
From: nsantacruz
Date: Wed, 20 Nov 2024 09:47:14 +0200
Subject: [PATCH 071/125] chore(topics): add logs to understand why web pod is
failing
---
reader/apps.py | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/reader/apps.py b/reader/apps.py
index d31ba3f13b..b53b9df90f 100644
--- a/reader/apps.py
+++ b/reader/apps.py
@@ -2,11 +2,16 @@
import sys
from django.apps import AppConfig
+import structlog
+logger = structlog.get_logger(__name__)
+
class ReaderAppConfig(AppConfig):
name = 'reader'
def ready(self):
from .startup import init_library_cache
+ logger.info(f'Starting reader app: {os.environ.get("RUN_MAIN")} -- {", ".join(sys.argv)}')
+
if not os.environ.get('RUN_MAIN', None) and len(sys.argv) > 1 and sys.argv[1] == 'runserver':
init_library_cache()
From 6e0cdeeb54271b7cf6a31369c52f766f8dd5af77 Mon Sep 17 00:00:00 2001
From: nsantacruz
Date: Wed, 20 Nov 2024 12:54:47 +0200
Subject: [PATCH 072/125] fix(topics): remove if statements
---
reader/apps.py | 10 +---------
1 file changed, 1 insertion(+), 9 deletions(-)
diff --git a/reader/apps.py b/reader/apps.py
index b53b9df90f..cb60ade521 100644
--- a/reader/apps.py
+++ b/reader/apps.py
@@ -1,17 +1,9 @@
-import os
-import sys
from django.apps import AppConfig
-import structlog
-logger = structlog.get_logger(__name__)
-
class ReaderAppConfig(AppConfig):
name = 'reader'
def ready(self):
from .startup import init_library_cache
- logger.info(f'Starting reader app: {os.environ.get("RUN_MAIN")} -- {", ".join(sys.argv)}')
-
- if not os.environ.get('RUN_MAIN', None) and len(sys.argv) > 1 and sys.argv[1] == 'runserver':
- init_library_cache()
+ init_library_cache()
From 5b7eff66de1372883bea449df01c97e3170ce0e8 Mon Sep 17 00:00:00 2001
From: nsantacruz
Date: Wed, 20 Nov 2024 12:55:33 +0200
Subject: [PATCH 073/125] chore(topics): add log when starting reader
---
reader/apps.py | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/reader/apps.py b/reader/apps.py
index cb60ade521..f7664240cf 100644
--- a/reader/apps.py
+++ b/reader/apps.py
@@ -1,9 +1,13 @@
from django.apps import AppConfig
+import structlog
+logger = structlog.get_logger(__name__)
+
class ReaderAppConfig(AppConfig):
name = 'reader'
def ready(self):
from .startup import init_library_cache
+ logger.info('Starting reader')
init_library_cache()
From 5a9202cc82d1a8bd9f86499960f67cc373c40630 Mon Sep 17 00:00:00 2001
From: nsantacruz
Date: Wed, 20 Nov 2024 14:13:16 +0200
Subject: [PATCH 074/125] fix(topics): run library initialization to runserver
command
---
reader/apps.py | 13 -------------
reader/management/commands/__init__.py | 0
reader/management/commands/runserver.py | 10 ++++++++++
sefaria/settings.py | 2 +-
4 files changed, 11 insertions(+), 14 deletions(-)
delete mode 100644 reader/apps.py
create mode 100644 reader/management/commands/__init__.py
create mode 100644 reader/management/commands/runserver.py
diff --git a/reader/apps.py b/reader/apps.py
deleted file mode 100644
index f7664240cf..0000000000
--- a/reader/apps.py
+++ /dev/null
@@ -1,13 +0,0 @@
-from django.apps import AppConfig
-
-import structlog
-logger = structlog.get_logger(__name__)
-
-
-class ReaderAppConfig(AppConfig):
- name = 'reader'
-
- def ready(self):
- from .startup import init_library_cache
- logger.info('Starting reader')
- init_library_cache()
diff --git a/reader/management/commands/__init__.py b/reader/management/commands/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/reader/management/commands/runserver.py b/reader/management/commands/runserver.py
new file mode 100644
index 0000000000..adb5fea782
--- /dev/null
+++ b/reader/management/commands/runserver.py
@@ -0,0 +1,10 @@
+from django.contrib.staticfiles.management.commands.runserver import Command as RunserverCommand
+from reader.startup import init_library_cache
+
+
+class Command(RunserverCommand):
+
+ def get_handler(self, *args, **options):
+ handler = super(Command, self).get_handler(*args, **options)
+ init_library_cache()
+ return handler
diff --git a/sefaria/settings.py b/sefaria/settings.py
index 03a78b3c37..5a164d80b7 100644
--- a/sefaria/settings.py
+++ b/sefaria/settings.py
@@ -138,10 +138,10 @@
'django.contrib.sessions',
'django.contrib.sites',
'django.contrib.messages',
+ 'reader',
'django.contrib.staticfiles',
'django.contrib.humanize',
'emailusernames',
- 'reader.apps.ReaderAppConfig',
'sourcesheets',
'sefaria.gauth',
'django_topics',
From 967d1ebb844bece236f7c9ad6bad4b4f49ad2763 Mon Sep 17 00:00:00 2001
From: nsantacruz
Date: Wed, 20 Nov 2024 14:38:46 +0200
Subject: [PATCH 075/125] chore(topics): add logs
---
reader/management/commands/runserver.py | 3 +++
1 file changed, 3 insertions(+)
diff --git a/reader/management/commands/runserver.py b/reader/management/commands/runserver.py
index adb5fea782..c606e6739a 100644
--- a/reader/management/commands/runserver.py
+++ b/reader/management/commands/runserver.py
@@ -1,10 +1,13 @@
from django.contrib.staticfiles.management.commands.runserver import Command as RunserverCommand
from reader.startup import init_library_cache
+import structlog
+logger = structlog.get_logger(__name__)
class Command(RunserverCommand):
def get_handler(self, *args, **options):
handler = super(Command, self).get_handler(*args, **options)
+ logger.info("Starting reader application")
init_library_cache()
return handler
From 8f417b4190297e1dac97c456df557987e9fbf6b9 Mon Sep 17 00:00:00 2001
From: nsantacruz
Date: Wed, 20 Nov 2024 15:04:55 +0200
Subject: [PATCH 076/125] fix(topics): use middleware for startup logic so it
runs both locally and through gunicorn
---
reader/management/commands/__init__.py | 0
reader/management/commands/runserver.py | 13 -------------
sefaria/settings.py | 1 +
sefaria/system/middleware.py | 22 ++++++++++++++++++++++
4 files changed, 23 insertions(+), 13 deletions(-)
delete mode 100644 reader/management/commands/__init__.py
delete mode 100644 reader/management/commands/runserver.py
diff --git a/reader/management/commands/__init__.py b/reader/management/commands/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/reader/management/commands/runserver.py b/reader/management/commands/runserver.py
deleted file mode 100644
index c606e6739a..0000000000
--- a/reader/management/commands/runserver.py
+++ /dev/null
@@ -1,13 +0,0 @@
-from django.contrib.staticfiles.management.commands.runserver import Command as RunserverCommand
-from reader.startup import init_library_cache
-import structlog
-logger = structlog.get_logger(__name__)
-
-
-class Command(RunserverCommand):
-
- def get_handler(self, *args, **options):
- handler = super(Command, self).get_handler(*args, **options)
- logger.info("Starting reader application")
- init_library_cache()
- return handler
diff --git a/sefaria/settings.py b/sefaria/settings.py
index 5a164d80b7..add33f3f7e 100644
--- a/sefaria/settings.py
+++ b/sefaria/settings.py
@@ -119,6 +119,7 @@
'sefaria.system.middleware.ProfileMiddleware',
'sefaria.system.middleware.CORSDebugMiddleware',
'sefaria.system.middleware.SharedCacheMiddleware',
+ 'sefaria.system.middleware.StartupMiddleware',
'sefaria.system.multiserver.coordinator.MultiServerEventListenerMiddleware',
'django_structlog.middlewares.RequestMiddleware',
#'easy_timezones.middleware.EasyTimezoneMiddleware',
diff --git a/sefaria/system/middleware.py b/sefaria/system/middleware.py
index 0e67309ded..d3cdaef04f 100644
--- a/sefaria/system/middleware.py
+++ b/sefaria/system/middleware.py
@@ -16,6 +16,28 @@
from sefaria.system.cache import get_shared_cache_elem, set_shared_cache_elem
from django.utils.deprecation import MiddlewareMixin
+import structlog
+logger = structlog.get_logger(__name__)
+
+
+class StartupMiddleware:
+ initialized = False
+
+ def __init__(self, get_response):
+ self.get_response = get_response
+
+ def __call__(self, request):
+ if not self.initialized:
+ self.initialized = True
+ self.on_startup()
+ return self.get_response(request)
+
+ def on_startup(self):
+ from reader.startup import init_library_cache
+ logger.info("Server has started handling requests!")
+ print("Server has started handling requests!")
+ init_library_cache()
+
class SharedCacheMiddleware(MiddlewareMixin):
def process_request(self, request):
From c8f95754a8b719850efc33a15cb3f064af755fa7 Mon Sep 17 00:00:00 2001
From: nsantacruz
Date: Wed, 20 Nov 2024 20:55:08 +0200
Subject: [PATCH 077/125] fix(topics): catch data error in topics migration
---
scripts/migrations/migrate_good_to_promote_to_topic_pools.py | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/scripts/migrations/migrate_good_to_promote_to_topic_pools.py b/scripts/migrations/migrate_good_to_promote_to_topic_pools.py
index 0dcba95c52..b42a680bb6 100644
--- a/scripts/migrations/migrate_good_to_promote_to_topic_pools.py
+++ b/scripts/migrations/migrate_good_to_promote_to_topic_pools.py
@@ -1,5 +1,5 @@
import django
-from django.db import IntegrityError
+from django.db import IntegrityError, DataError
django.setup()
from sefaria.model import TopicSet, RefTopicLinkSet
@@ -60,6 +60,8 @@ def add_topics():
Topic.objects.create(slug=topic.slug, en_title=topic.get_primary_title('en'), he_title=topic.get_primary_title('he'))
except IntegrityError:
print('Duplicate topic', topic.slug)
+ except DataError:
+ print('Data error with topic', topic.slug)
def add_pools():
From e3a41d1ef9da06dd1b16064581f2573565f622db Mon Sep 17 00:00:00 2001
From: nsantacruz
Date: Wed, 20 Nov 2024 20:55:49 +0200
Subject: [PATCH 078/125] chore(topics): remove useless topic pool admin
---
django_topics/admin.py | 14 --------------
1 file changed, 14 deletions(-)
diff --git a/django_topics/admin.py b/django_topics/admin.py
index 4087c6d6c5..52ea412e2f 100644
--- a/django_topics/admin.py
+++ b/django_topics/admin.py
@@ -82,20 +82,6 @@ def is_in_pool_torah_tab(self, obj):
is_in_pool_torah_tab.short_description = "TorahTab?"
-@admin.register(TopicPool)
-class TopicPoolAdmin(admin.ModelAdmin):
- list_display = ('name', 'topic_names')
- filter_horizontal = ('topics',)
- readonly_fields = ('name',)
-
- def topic_names(self, obj):
- topic_slugs = obj.topics.all().values_list('slug', flat=True)
- str_rep = ', '.join(topic_slugs[:30])
- if len(topic_slugs) > 30:
- str_rep = str_rep + '...'
- return str_rep
-
-
@admin.register(TopicOfTheDay)
class TopicOfTheDayAdmin(admin.ModelAdmin):
list_display = ('topic', 'start_date')
From b2435c8ec8c564f1b6b1c21f0bb70947b52c7245 Mon Sep 17 00:00:00 2001
From: nsantacruz
Date: Wed, 20 Nov 2024 21:03:15 +0200
Subject: [PATCH 079/125] feat(topics): improve admin fields for topic and
secondary_topic
---
django_topics/admin.py | 15 +++++++++++++++
1 file changed, 15 insertions(+)
diff --git a/django_topics/admin.py b/django_topics/admin.py
index 52ea412e2f..0e3fe1d4d0 100644
--- a/django_topics/admin.py
+++ b/django_topics/admin.py
@@ -95,6 +95,12 @@ class TopicOfTheDayAdmin(admin.ModelAdmin):
}),
)
+ def formfield_for_foreignkey(self, db_field, request, **kwargs):
+ if db_field.name == "topic":
+ kwargs["label"] = "Topic ID num (not slug)"
+ kwargs["help_text"] = "Use the magnifying glass button to select a topic."
+ return super().formfield_for_foreignkey(db_field, request, **kwargs)
+
@admin.register(SeasonalTopic)
class SeasonalTopicAdmin(admin.ModelAdmin):
@@ -145,6 +151,15 @@ class SeasonalTopicAdmin(admin.ModelAdmin):
}),
)
+ def formfield_for_foreignkey(self, db_field, request, **kwargs):
+ if db_field.name == "topic":
+ kwargs["label"] = "Topic ID num (not slug)"
+ kwargs["help_text"] = "Use the magnifying glass button to select a topic."
+ if db_field.name == "secondary_topic":
+ kwargs["label"] = "Secondary Topic ID num (not slug)"
+ kwargs["help_text"] = kwargs["help_text"] + " Use the magnifying glass button to select a topic."
+ return super().formfield_for_foreignkey(db_field, request, **kwargs)
+
def save_model(self, request, obj, form, change):
"""
Overriding the save_model to ensure the model's clean method is executed.
From f682f5c33a0c8a56caa5ff21a7b1eadcc1846454 Mon Sep 17 00:00:00 2001
From: nsantacruz
Date: Wed, 20 Nov 2024 21:08:01 +0200
Subject: [PATCH 080/125] feat(topics): improve admin fields for topic and
secondary_topic
---
django_topics/admin.py | 1 -
1 file changed, 1 deletion(-)
diff --git a/django_topics/admin.py b/django_topics/admin.py
index 0e3fe1d4d0..d7d17c2584 100644
--- a/django_topics/admin.py
+++ b/django_topics/admin.py
@@ -157,7 +157,6 @@ def formfield_for_foreignkey(self, db_field, request, **kwargs):
kwargs["help_text"] = "Use the magnifying glass button to select a topic."
if db_field.name == "secondary_topic":
kwargs["label"] = "Secondary Topic ID num (not slug)"
- kwargs["help_text"] = kwargs["help_text"] + " Use the magnifying glass button to select a topic."
return super().formfield_for_foreignkey(db_field, request, **kwargs)
def save_model(self, request, obj, form, change):
From 373679ba58629ef14f75008f7de961c4b44659ec Mon Sep 17 00:00:00 2001
From: nsantacruz
Date: Wed, 20 Nov 2024 21:08:20 +0200
Subject: [PATCH 081/125] fix(topics): dont validate start and end dates if
both are None
---
django_topics/models/seasonal_topic.py | 3 +++
1 file changed, 3 insertions(+)
diff --git a/django_topics/models/seasonal_topic.py b/django_topics/models/seasonal_topic.py
index e7adf405df..eaacd2ddcd 100644
--- a/django_topics/models/seasonal_topic.py
+++ b/django_topics/models/seasonal_topic.py
@@ -33,6 +33,9 @@ def populate_field_based_on_field(self, field, reference_field):
setattr(self, field, getattr(self, reference_field))
def validate_start_end_dates(self, start_date_field, end_date_field):
+ if not getattr(self, start_date_field, None) and not getattr(self, end_date_field, None):
+ # no data
+ return
if not getattr(self, start_date_field, None) and getattr(self, end_date_field):
raise ValidationError(f"End date field '{end_date_field}' defined without start date.")
if getattr(self, start_date_field) > getattr(self, end_date_field):
From 5f61449db7900cb2e456f9793d464147c1f07bba Mon Sep 17 00:00:00 2001
From: nsantacruz
Date: Thu, 21 Nov 2024 12:16:39 +0200
Subject: [PATCH 082/125] fix(topics): modify admin labels
---
django_topics/apps.py | 6 ++++++
django_topics/models/seasonal_topic.py | 4 ++--
django_topics/models/topic.py | 4 ++++
django_topics/models/topic_of_the_day.py | 4 ++--
sefaria/settings.py | 2 +-
5 files changed, 15 insertions(+), 5 deletions(-)
create mode 100644 django_topics/apps.py
diff --git a/django_topics/apps.py b/django_topics/apps.py
new file mode 100644
index 0000000000..7b405a4a58
--- /dev/null
+++ b/django_topics/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class DjangoTopicsAppConfig(AppConfig):
+ name = "django_topics"
+ verbose_name = "Topics Management"
diff --git a/django_topics/models/seasonal_topic.py b/django_topics/models/seasonal_topic.py
index eaacd2ddcd..da945be27a 100644
--- a/django_topics/models/seasonal_topic.py
+++ b/django_topics/models/seasonal_topic.py
@@ -25,8 +25,8 @@ class SeasonalTopic(models.Model):
class Meta:
unique_together = ('topic', 'start_date')
- verbose_name = "Seasonal Topic"
- verbose_name_plural = "Seasonal Topics"
+ verbose_name = "Landing Page - Calendar"
+ verbose_name_plural = "Landing Page - Calendar"
def populate_field_based_on_field(self, field, reference_field):
if not getattr(self, field, None) and getattr(self, reference_field, None):
diff --git a/django_topics/models/topic.py b/django_topics/models/topic.py
index 6f06c6b7e1..a2baf0879e 100644
--- a/django_topics/models/topic.py
+++ b/django_topics/models/topic.py
@@ -28,5 +28,9 @@ class Topic(models.Model):
pools = models.ManyToManyField(TopicPool, related_name="topics", blank=True)
objects = TopicManager()
+ class Meta:
+ verbose_name = "Topic Pool Management"
+ verbose_name_plural = "Topic Pool Management"
+
def __str__(self):
return self.slug
diff --git a/django_topics/models/topic_of_the_day.py b/django_topics/models/topic_of_the_day.py
index 26002568d1..93aee212dc 100644
--- a/django_topics/models/topic_of_the_day.py
+++ b/django_topics/models/topic_of_the_day.py
@@ -13,8 +13,8 @@ class TopicOfTheDay(models.Model):
class Meta:
unique_together = ('topic', 'start_date')
- verbose_name = "Topic of the Day"
- verbose_name_plural = "Topics of the Day"
+ verbose_name = "Landing Page - Topic of the Day"
+ verbose_name_plural = "Landing Page - Topic of the Day"
def __str__(self):
return f"{self.topic.slug} ({self.start_date})"
diff --git a/sefaria/settings.py b/sefaria/settings.py
index add33f3f7e..ddcd08ab9b 100644
--- a/sefaria/settings.py
+++ b/sefaria/settings.py
@@ -145,7 +145,7 @@
'emailusernames',
'sourcesheets',
'sefaria.gauth',
- 'django_topics',
+ 'django_topics.apps.DjangoTopicsAppConfig',
'captcha',
'django.contrib.admin',
'anymail',
From 10e7ed84ecd2d1bd730aa379e10a123fe3b5b6fe Mon Sep 17 00:00:00 2001
From: nsantacruz
Date: Thu, 21 Nov 2024 12:19:13 +0200
Subject: [PATCH 083/125] chore(topics): add migration
---
.../migrations/0002_auto_20241121_0617.py | 38 +++++++++++++++++++
1 file changed, 38 insertions(+)
create mode 100644 django_topics/migrations/0002_auto_20241121_0617.py
diff --git a/django_topics/migrations/0002_auto_20241121_0617.py b/django_topics/migrations/0002_auto_20241121_0617.py
new file mode 100644
index 0000000000..ee2228f89e
--- /dev/null
+++ b/django_topics/migrations/0002_auto_20241121_0617.py
@@ -0,0 +1,38 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.29 on 2024-11-21 10:17
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('django_topics', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name='seasonaltopic',
+ options={'verbose_name': 'Landing Page - Calendar', 'verbose_name_plural': 'Landing Page - Calendar'},
+ ),
+ migrations.AlterModelOptions(
+ name='topic',
+ options={'verbose_name': 'Topic Pool Management', 'verbose_name_plural': 'Topic Pool Management'},
+ ),
+ migrations.AlterModelOptions(
+ name='topicoftheday',
+ options={'verbose_name': 'Landing Page - Topic of the Day', 'verbose_name_plural': 'Landing Page - Topic of the Day'},
+ ),
+ migrations.AlterField(
+ model_name='seasonaltopic',
+ name='secondary_topic',
+ field=models.ForeignKey(blank=True, help_text="Secondary topic which will be displayed alongside `topic`. E.g. `topic` is 'Teshuva' then secondary topic could be 'Yom Kippur'.", null=True, on_delete=django.db.models.deletion.CASCADE, related_name='seasonal_secondary_topic', to='django_topics.Topic'),
+ ),
+ migrations.AlterField(
+ model_name='seasonaltopic',
+ name='start_date',
+ field=models.DateField(help_text='Start date of when this will appear. End date is implied by when the next Seasonal Topic is displayed.'),
+ ),
+ ]
From 49b13f89f316360c8956c4137d39932fb1f29642 Mon Sep 17 00:00:00 2001
From: nsantacruz
Date: Thu, 21 Nov 2024 12:19:27 +0200
Subject: [PATCH 084/125] chore(topics): change where date is in list view
---
django_topics/admin.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/django_topics/admin.py b/django_topics/admin.py
index d7d17c2584..d4c82573b9 100644
--- a/django_topics/admin.py
+++ b/django_topics/admin.py
@@ -84,7 +84,7 @@ def is_in_pool_torah_tab(self, obj):
@admin.register(TopicOfTheDay)
class TopicOfTheDayAdmin(admin.ModelAdmin):
- list_display = ('topic', 'start_date')
+ list_display = ('start_date', 'topic')
list_filter = ('start_date',)
raw_id_fields = ('topic',)
search_fields = ('topic__slug', 'topic__en_title', 'topic__he_title')
@@ -105,9 +105,9 @@ def formfield_for_foreignkey(self, db_field, request, **kwargs):
@admin.register(SeasonalTopic)
class SeasonalTopicAdmin(admin.ModelAdmin):
list_display = (
+ 'start_date',
'topic',
'secondary_topic',
- 'start_date',
'display_start_date_israel',
'display_end_date_israel',
'display_start_date_diaspora',
From 432a464178afb6b4a984db1755cd31d41498bba5 Mon Sep 17 00:00:00 2001
From: nsantacruz
Date: Thu, 21 Nov 2024 12:22:32 +0200
Subject: [PATCH 085/125] chore(topics): change default sort of start_date
---
django_topics/admin.py | 2 ++
1 file changed, 2 insertions(+)
diff --git a/django_topics/admin.py b/django_topics/admin.py
index d4c82573b9..e9454e2a2f 100644
--- a/django_topics/admin.py
+++ b/django_topics/admin.py
@@ -89,6 +89,7 @@ class TopicOfTheDayAdmin(admin.ModelAdmin):
raw_id_fields = ('topic',)
search_fields = ('topic__slug', 'topic__en_title', 'topic__he_title')
date_hierarchy = 'start_date'
+ ordering = ['-start_date']
fieldsets = (
(None, {
'fields': ('topic', 'start_date'),
@@ -119,6 +120,7 @@ class SeasonalTopicAdmin(admin.ModelAdmin):
'display_start_date_israel',
'display_start_date_diaspora'
)
+ ordering = ['-start_date']
search_fields = ('topic__slug', 'topic__en_title', 'topic__he_title', 'secondary_topic__slug')
autocomplete_fields = ('topic', 'secondary_topic')
date_hierarchy = 'start_date'
From 17f5bd9663f2d80c2e1216c6a0504df0fb26b52f Mon Sep 17 00:00:00 2001
From: nsantacruz
Date: Thu, 21 Nov 2024 12:24:17 +0200
Subject: [PATCH 086/125] chore(topics): change labels of general and torah tab
pool
---
django_topics/admin.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/django_topics/admin.py b/django_topics/admin.py
index e9454e2a2f..9854e3c3f3 100644
--- a/django_topics/admin.py
+++ b/django_topics/admin.py
@@ -74,12 +74,12 @@ def get_queryset(self, request):
def is_in_pool_general(self, obj):
return obj.pools.filter(name=PoolType.GENERAL.value).exists()
is_in_pool_general.boolean = True
- is_in_pool_general.short_description = "General?"
+ is_in_pool_general.short_description = "General Pool"
def is_in_pool_torah_tab(self, obj):
return obj.pools.filter(name=PoolType.TORAH_TAB.value).exists()
is_in_pool_torah_tab.boolean = True
- is_in_pool_torah_tab.short_description = "TorahTab?"
+ is_in_pool_torah_tab.short_description = "TorahTab Pool"
@admin.register(TopicOfTheDay)
From e68b12129c6e576475bd02e68ac9893a34d37777 Mon Sep 17 00:00:00 2001
From: nsantacruz
Date: Thu, 21 Nov 2024 14:02:02 +0200
Subject: [PATCH 087/125] refactor(topics): use topic slug as django topic
primary key
---
.../migrations/0003_auto_20241121_0757.py | 24 +++++++++++++++++++
django_topics/models/topic.py | 2 +-
2 files changed, 25 insertions(+), 1 deletion(-)
create mode 100644 django_topics/migrations/0003_auto_20241121_0757.py
diff --git a/django_topics/migrations/0003_auto_20241121_0757.py b/django_topics/migrations/0003_auto_20241121_0757.py
new file mode 100644
index 0000000000..a6765ce614
--- /dev/null
+++ b/django_topics/migrations/0003_auto_20241121_0757.py
@@ -0,0 +1,24 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.29 on 2024-11-21 11:57
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('django_topics', '0002_auto_20241121_0617'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='topic',
+ name='id',
+ ),
+ migrations.AlterField(
+ model_name='topic',
+ name='slug',
+ field=models.CharField(max_length=255, primary_key=True, serialize=False),
+ ),
+ ]
diff --git a/django_topics/models/topic.py b/django_topics/models/topic.py
index a2baf0879e..4f5f09c83f 100644
--- a/django_topics/models/topic.py
+++ b/django_topics/models/topic.py
@@ -22,7 +22,7 @@ def get_topic_slugs_by_pool(self, pool: str) -> list[str]:
class Topic(models.Model):
- slug = models.CharField(max_length=255, unique=True)
+ slug = models.CharField(max_length=255, primary_key=True)
en_title = models.CharField(max_length=255, blank=True, default="")
he_title = models.CharField(max_length=255, blank=True, default="")
pools = models.ManyToManyField(TopicPool, related_name="topics", blank=True)
From 820927c9840d7876cb79843c1534640577f75051 Mon Sep 17 00:00:00 2001
From: nsantacruz
Date: Thu, 21 Nov 2024 14:52:37 +0200
Subject: [PATCH 088/125] chore(topics): temp disable library startup
---
sefaria/system/middleware.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/sefaria/system/middleware.py b/sefaria/system/middleware.py
index d3cdaef04f..500bd6f9fe 100644
--- a/sefaria/system/middleware.py
+++ b/sefaria/system/middleware.py
@@ -29,7 +29,7 @@ def __init__(self, get_response):
def __call__(self, request):
if not self.initialized:
self.initialized = True
- self.on_startup()
+ # self.on_startup()
return self.get_response(request)
def on_startup(self):
From 633d85040a5d55e3bc6d2bbea998c66f5f64d1ca Mon Sep 17 00:00:00 2001
From: nsantacruz
Date: Thu, 21 Nov 2024 14:55:45 +0200
Subject: [PATCH 089/125] chore(topics): temp disable library startup
---
reader/startup.py | 2 +-
sefaria/system/middleware.py | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/reader/startup.py b/reader/startup.py
index bbdf519c1d..0569faefdd 100644
--- a/reader/startup.py
+++ b/reader/startup.py
@@ -12,7 +12,7 @@ def init_library_cache():
library.get_toc_tree()
logger.info("Initializing Shared Cache")
- library.init_shared_cache()
+ # library.init_shared_cache()
if not settings.DISABLE_AUTOCOMPLETER:
logger.info("Initializing Full Auto Completer")
diff --git a/sefaria/system/middleware.py b/sefaria/system/middleware.py
index 500bd6f9fe..d3cdaef04f 100644
--- a/sefaria/system/middleware.py
+++ b/sefaria/system/middleware.py
@@ -29,7 +29,7 @@ def __init__(self, get_response):
def __call__(self, request):
if not self.initialized:
self.initialized = True
- # self.on_startup()
+ self.on_startup()
return self.get_response(request)
def on_startup(self):
From 5aaee2bad52a3787b4091c78554ca01935933cc2 Mon Sep 17 00:00:00 2001
From: nsantacruz
Date: Thu, 21 Nov 2024 15:15:17 +0200
Subject: [PATCH 090/125] chore(topics): temp disable library startup
---
sefaria/model/topic.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/sefaria/model/topic.py b/sefaria/model/topic.py
index 3881ca1b9e..b6e46e7607 100644
--- a/sefaria/model/topic.py
+++ b/sefaria/model/topic.py
@@ -186,7 +186,7 @@ def load(self, query, proj=None):
def _set_derived_attributes(self):
self.set_titles(getattr(self, "titles", None))
- self.pools = list(DjangoTopic.objects.get_pools_by_topic_slug(getattr(self, "slug", None)))
+ self.pools = [] # list(DjangoTopic.objects.get_pools_by_topic_slug(getattr(self, "slug", None)))
if self.__class__ != Topic and not getattr(self, "subclass", False):
# in a subclass. set appropriate "subclass" attribute
setattr(self, "subclass", self.reverse_subclass_map[self.__class__.__name__])
From 52f3a72ae2c1be42302014ed6ef234e9b4fcb533 Mon Sep 17 00:00:00 2001
From: nsantacruz
Date: Thu, 21 Nov 2024 15:40:46 +0200
Subject: [PATCH 091/125] chore(topics): reenable startup
---
reader/startup.py | 2 +-
sefaria/model/topic.py | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/reader/startup.py b/reader/startup.py
index 0569faefdd..bbdf519c1d 100644
--- a/reader/startup.py
+++ b/reader/startup.py
@@ -12,7 +12,7 @@ def init_library_cache():
library.get_toc_tree()
logger.info("Initializing Shared Cache")
- # library.init_shared_cache()
+ library.init_shared_cache()
if not settings.DISABLE_AUTOCOMPLETER:
logger.info("Initializing Full Auto Completer")
diff --git a/sefaria/model/topic.py b/sefaria/model/topic.py
index b6e46e7607..3881ca1b9e 100644
--- a/sefaria/model/topic.py
+++ b/sefaria/model/topic.py
@@ -186,7 +186,7 @@ def load(self, query, proj=None):
def _set_derived_attributes(self):
self.set_titles(getattr(self, "titles", None))
- self.pools = [] # list(DjangoTopic.objects.get_pools_by_topic_slug(getattr(self, "slug", None)))
+ self.pools = list(DjangoTopic.objects.get_pools_by_topic_slug(getattr(self, "slug", None)))
if self.__class__ != Topic and not getattr(self, "subclass", False):
# in a subclass. set appropriate "subclass" attribute
setattr(self, "subclass", self.reverse_subclass_map[self.__class__.__name__])
From 1a7981121a69eb69c3b2739916da25ad6e092bbb Mon Sep 17 00:00:00 2001
From: nsantacruz
Date: Thu, 21 Nov 2024 22:22:29 +0200
Subject: [PATCH 092/125] fix(topics): ensure slug is str before passing to
django model
---
sefaria/model/topic.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/sefaria/model/topic.py b/sefaria/model/topic.py
index 3881ca1b9e..59cfc6198a 100644
--- a/sefaria/model/topic.py
+++ b/sefaria/model/topic.py
@@ -186,7 +186,8 @@ def load(self, query, proj=None):
def _set_derived_attributes(self):
self.set_titles(getattr(self, "titles", None))
- self.pools = list(DjangoTopic.objects.get_pools_by_topic_slug(getattr(self, "slug", None)))
+ slug = getattr(self, "slug", None)
+ self.pools = list(DjangoTopic.objects.get_pools_by_topic_slug(slug)) if slug is not None else []
if self.__class__ != Topic and not getattr(self, "subclass", False):
# in a subclass. set appropriate "subclass" attribute
setattr(self, "subclass", self.reverse_subclass_map[self.__class__.__name__])
From e4332a48166885714e1eeb1614e36aa2a36a6c40 Mon Sep 17 00:00:00 2001
From: nsantacruz
Date: Fri, 22 Nov 2024 13:22:22 +0200
Subject: [PATCH 093/125] fix(topics): move library init to wsgi level so it
runs properly on gunicorn
---
sefaria/system/middleware.py | 19 -------------------
sefaria/wsgi.py | 3 +++
2 files changed, 3 insertions(+), 19 deletions(-)
diff --git a/sefaria/system/middleware.py b/sefaria/system/middleware.py
index d3cdaef04f..64937c2f19 100644
--- a/sefaria/system/middleware.py
+++ b/sefaria/system/middleware.py
@@ -20,25 +20,6 @@
logger = structlog.get_logger(__name__)
-class StartupMiddleware:
- initialized = False
-
- def __init__(self, get_response):
- self.get_response = get_response
-
- def __call__(self, request):
- if not self.initialized:
- self.initialized = True
- self.on_startup()
- return self.get_response(request)
-
- def on_startup(self):
- from reader.startup import init_library_cache
- logger.info("Server has started handling requests!")
- print("Server has started handling requests!")
- init_library_cache()
-
-
class SharedCacheMiddleware(MiddlewareMixin):
def process_request(self, request):
last_cached = get_shared_cache_elem("last_cached")
diff --git a/sefaria/wsgi.py b/sefaria/wsgi.py
index 502d6bb1cc..6c0d92d0e4 100644
--- a/sefaria/wsgi.py
+++ b/sefaria/wsgi.py
@@ -23,6 +23,9 @@
from django.core.wsgi import get_wsgi_application
application = get_wsgi_application()
+
+from reader.startup import init_library_cache
+init_library_cache()
# Apply WSGI middleware here.
# from helloworld.wsgi import HelloWorldApplication
# application = HelloWorldApplication(application)
From 7b5362b35abf633c48f4d62393dbcc48a6a1c8ff Mon Sep 17 00:00:00 2001
From: nsantacruz
Date: Sun, 24 Nov 2024 09:49:36 +0200
Subject: [PATCH 094/125] fix(topics): remove startup middleware from settings
---
sefaria/settings.py | 1 -
1 file changed, 1 deletion(-)
diff --git a/sefaria/settings.py b/sefaria/settings.py
index ddcd08ab9b..939d46c405 100644
--- a/sefaria/settings.py
+++ b/sefaria/settings.py
@@ -119,7 +119,6 @@
'sefaria.system.middleware.ProfileMiddleware',
'sefaria.system.middleware.CORSDebugMiddleware',
'sefaria.system.middleware.SharedCacheMiddleware',
- 'sefaria.system.middleware.StartupMiddleware',
'sefaria.system.multiserver.coordinator.MultiServerEventListenerMiddleware',
'django_structlog.middlewares.RequestMiddleware',
#'easy_timezones.middleware.EasyTimezoneMiddleware',
From fc1130bfd110ec7fb21c9f760ac59e46257410a9 Mon Sep 17 00:00:00 2001
From: Skyler Cohen
Date: Mon, 25 Nov 2024 18:22:22 -0500
Subject: [PATCH 095/125] static(ways-to-give): Update Sefaria's 990 Form
---
static/files/Sefaria_2023_990.pdf | Bin 0 -> 480188 bytes
static/js/StaticPages.jsx | 2 +-
templates/static/he/ways-to-give.html | 2 +-
3 files changed, 2 insertions(+), 2 deletions(-)
create mode 100644 static/files/Sefaria_2023_990.pdf
diff --git a/static/files/Sefaria_2023_990.pdf b/static/files/Sefaria_2023_990.pdf
new file mode 100644
index 0000000000000000000000000000000000000000..4fc2140ca1cc87fea036a99f13832ba8adbfa27c
GIT binary patch
literal 480188
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