From de6f4721978926aa5a139b3890173a122207ba23 Mon Sep 17 00:00:00 2001
From: saengel
Date: Wed, 10 Jul 2024 16:02:19 +0300
Subject: [PATCH 001/151] fix(api): refine v1 texts error
---
sefaria/model/text.py | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/sefaria/model/text.py b/sefaria/model/text.py
index 18a1e1a28f..5d99eae01d 100644
--- a/sefaria/model/text.py
+++ b/sefaria/model/text.py
@@ -3990,8 +3990,10 @@ def padded_ref(self):
except AttributeError: # This is a schema node, try to get a default child
if self.has_default_child():
return self.default_child_ref().padded_ref()
+ elif self.is_book_level():
+ raise InputError("Please pass a more specific ref for this book, and try again. The ref you passed is a 'complex' book-level ref. We only support book-level refs in cases of texts with a 'simple' structure. ")
else:
- raise InputError("Can not pad a schema node ref")
+ raise InputError("Cannot pad a schema node ref.")
d = self._core_dict()
if self.is_talmud():
From c9d05f18517966244506219f81b53e2690f196af Mon Sep 17 00:00:00 2001
From: saengel
Date: Sun, 14 Jul 2024 13:55:32 +0300
Subject: [PATCH 002/151] fix(api): First pass at removing 500 from v3
---
api/views.py | 16 +++++++++++++---
1 file changed, 13 insertions(+), 3 deletions(-)
diff --git a/api/views.py b/api/views.py
index bd1525390a..f5aaa088c6 100644
--- a/api/views.py
+++ b/api/views.py
@@ -1,10 +1,12 @@
from sefaria.model import *
from sefaria.model.text_reuqest_adapter import TextRequestAdapter
from sefaria.client.util import jsonResponse
+from sefaria.system.exceptions import InputError
from django.views import View
from .api_warnings import *
+
class Text(View):
RETURN_FORMATS = ['default', 'wrap_all_entities', 'text_only', 'strip_only_footnotes']
@@ -53,6 +55,14 @@ def get(self, request, *args, **kwargs):
if return_format not in self.RETURN_FORMATS:
return jsonResponse({'error': f'return_format should be one of those formats: {self.RETURN_FORMATS}.'}, status=400)
text_manager = TextRequestAdapter(self.oref, versions_params, fill_in_missing_segments, return_format)
- data = text_manager.get_versions_for_query()
- data = self._handle_warnings(data)
- return jsonResponse(data)
+
+ try:
+ # For a SchemaNode, data is an error which handle_warnings doesn't handle (or even get triggered?)
+ # How to trigger a 400 appropriate error with an appropriate message?
+ data = text_manager.get_versions_for_query()
+ data = self._handle_warnings(data)
+ return jsonResponse(data)
+ except InputError as e:
+ raise InputError(e)
+
+
From 33d8b0d1a4a605b606c7ff6461759b3f6d7a3497 Mon Sep 17 00:00:00 2001
From: saengel
Date: Mon, 15 Jul 2024 10:35:14 +0300
Subject: [PATCH 003/151] feat(api): scaffolding out 400 error instead of 500
---
api/views.py | 17 +++++++++++------
1 file changed, 11 insertions(+), 6 deletions(-)
diff --git a/api/views.py b/api/views.py
index f5aaa088c6..14a4c2d3f5 100644
--- a/api/views.py
+++ b/api/views.py
@@ -56,13 +56,18 @@ def get(self, request, *args, **kwargs):
return jsonResponse({'error': f'return_format should be one of those formats: {self.RETURN_FORMATS}.'}, status=400)
text_manager = TextRequestAdapter(self.oref, versions_params, fill_in_missing_segments, return_format)
+
try:
- # For a SchemaNode, data is an error which handle_warnings doesn't handle (or even get triggered?)
- # How to trigger a 400 appropriate error with an appropriate message?
+ # Todo - Maybe this error should be inside the get_versions_for_query() fxn?
data = text_manager.get_versions_for_query()
- data = self._handle_warnings(data)
- return jsonResponse(data)
- except InputError as e:
- raise InputError(e)
+ except Exception as e:
+ # Todo - which 400 code exactly to pass? Will have to check the message of the error to
+ # make sure you're sending the right response to the user.
+ return jsonResponse({'error': "Please pass a more specific Ref"}, status=400)
+
+ data = self._handle_warnings(data)
+ return jsonResponse(data)
+
+
From fcfbe65101d6dfa9d8abbd8beb275bb13d4a4dcb Mon Sep 17 00:00:00 2001
From: saengel
Date: Mon, 15 Jul 2024 10:35:55 +0300
Subject: [PATCH 004/151] fix(api): update error message to include a link
---
api/views.py | 2 +-
sefaria/model/text.py | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/api/views.py b/api/views.py
index 14a4c2d3f5..a73f33583d 100644
--- a/api/views.py
+++ b/api/views.py
@@ -62,7 +62,7 @@ def get(self, request, *args, **kwargs):
data = text_manager.get_versions_for_query()
except Exception as e:
# Todo - which 400 code exactly to pass? Will have to check the message of the error to
- # make sure you're sending the right response to the user.
+ # make sure you're sending the right response to the user.
return jsonResponse({'error': "Please pass a more specific Ref"}, status=400)
data = self._handle_warnings(data)
diff --git a/sefaria/model/text.py b/sefaria/model/text.py
index 5d99eae01d..7a82d742c7 100644
--- a/sefaria/model/text.py
+++ b/sefaria/model/text.py
@@ -3991,7 +3991,7 @@ def padded_ref(self):
if self.has_default_child():
return self.default_child_ref().padded_ref()
elif self.is_book_level():
- raise InputError("Please pass a more specific ref for this book, and try again. The ref you passed is a 'complex' book-level ref. We only support book-level refs in cases of texts with a 'simple' structure. ")
+ raise InputError("Please pass a more specific ref for this book, and try again. The ref you passed is a 'complex' book-level ref. We only support book-level refs in cases of texts with a 'simple' structure. To learn more about the structure of a text on Sefaria, see: https://developers.sefaria.org/docs/the-schema-of-a-simple-text")
else:
raise InputError("Cannot pad a schema node ref.")
From f76702deee05ed294047046c8e6da7f685be11e8 Mon Sep 17 00:00:00 2001
From: saengel
Date: Wed, 17 Jul 2024 13:56:28 +0300
Subject: [PATCH 005/151] feat(api): add message check
---
api/views.py | 10 ++++------
1 file changed, 4 insertions(+), 6 deletions(-)
diff --git a/api/views.py b/api/views.py
index a73f33583d..c4f7101388 100644
--- a/api/views.py
+++ b/api/views.py
@@ -58,13 +58,11 @@ def get(self, request, *args, **kwargs):
try:
- # Todo - Maybe this error should be inside the get_versions_for_query() fxn?
data = text_manager.get_versions_for_query()
- except Exception as e:
- # Todo - which 400 code exactly to pass? Will have to check the message of the error to
- # make sure you're sending the right response to the user.
- return jsonResponse({'error': "Please pass a more specific Ref"}, status=400)
-
+ except InputError as e:
+ # Todo - which 400 code exactly to pass?
+ if str(e) == "Can not get TextRange at this level, please provide a more precise reference":
+ return jsonResponse({'error': "Please pass a more specific ref for this book, and try again. The ref you passed is a \'complex\' book-level ref. We only support book-level refs in cases of texts with a 'simple' structure. To learn more about the structure of a text on Sefaria, see: https://developers.sefaria.org/docs/the-schema-of-a-simple-text"}, status=400)
data = self._handle_warnings(data)
return jsonResponse(data)
From f87734dba5e992b7d6ca90c9e1b8b9b555c839b9 Mon Sep 17 00:00:00 2001
From: saengel
Date: Wed, 17 Jul 2024 20:50:23 +0300
Subject: [PATCH 006/151] chore(api): remove todos
---
api/views.py | 1 -
1 file changed, 1 deletion(-)
diff --git a/api/views.py b/api/views.py
index c4f7101388..a8d60b4662 100644
--- a/api/views.py
+++ b/api/views.py
@@ -60,7 +60,6 @@ def get(self, request, *args, **kwargs):
try:
data = text_manager.get_versions_for_query()
except InputError as e:
- # Todo - which 400 code exactly to pass?
if str(e) == "Can not get TextRange at this level, please provide a more precise reference":
return jsonResponse({'error': "Please pass a more specific ref for this book, and try again. The ref you passed is a \'complex\' book-level ref. We only support book-level refs in cases of texts with a 'simple' structure. To learn more about the structure of a text on Sefaria, see: https://developers.sefaria.org/docs/the-schema-of-a-simple-text"}, status=400)
data = self._handle_warnings(data)
From 6536f7459f32ca4759094c5a85903ebc4ed86242 Mon Sep 17 00:00:00 2001
From: saengel
Date: Fri, 19 Jul 2024 10:05:35 +0300
Subject: [PATCH 007/151] chore(api): ensure model level error message is
informative, and appropriate for the model
---
sefaria/model/text.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/sefaria/model/text.py b/sefaria/model/text.py
index 7a82d742c7..f9bea61dae 100644
--- a/sefaria/model/text.py
+++ b/sefaria/model/text.py
@@ -3991,7 +3991,7 @@ def padded_ref(self):
if self.has_default_child():
return self.default_child_ref().padded_ref()
elif self.is_book_level():
- raise InputError("Please pass a more specific ref for this book, and try again. The ref you passed is a 'complex' book-level ref. We only support book-level refs in cases of texts with a 'simple' structure. To learn more about the structure of a text on Sefaria, see: https://developers.sefaria.org/docs/the-schema-of-a-simple-text")
+ raise InputError("Can not get TextRange at this level, please provide a more precise reference")
else:
raise InputError("Cannot pad a schema node ref.")
From 390204e1cb03733c04f6b6254879ce2382998318 Mon Sep 17 00:00:00 2001
From: saengel
Date: Fri, 19 Jul 2024 10:05:59 +0300
Subject: [PATCH 008/151] feat(api): add a v1 view error message including a
link
---
sefaria/system/decorators.py | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/sefaria/system/decorators.py b/sefaria/system/decorators.py
index 94db751aea..73c1c74660 100644
--- a/sefaria/system/decorators.py
+++ b/sefaria/system/decorators.py
@@ -42,7 +42,10 @@ def wrapper(*args, **kwargs):
except exps.InputError as e:
logger.warning("An exception occurred processing request for '{}' while running {}. Caught as JSON".format(args[0].path, func.__name__), exc_info=True)
request = args[0]
- return jsonResponse({"error": str(e)}, callback=request.GET.get("callback", None))
+ if str(e) == "Can not get TextRange at this level, please provide a more precise reference":
+ return jsonResponse({'error': "Please pass a more specific ref for this book, and try again. The ref you passed is a \'complex\' book-level ref. We only support book-level refs in cases of texts with a 'simple' structure. To learn more about the structure of a text on Sefaria, see: https://developers.sefaria.org/docs/the-schema-of-a-simple-text"}, status=400)
+ else:
+ return jsonResponse({"error": str(e)}, callback=request.GET.get("callback", None))
return result
return wrapper
From 2509488bf619aea2eac63ed2b067d0c38946bde4 Mon Sep 17 00:00:00 2001
From: saengel
Date: Thu, 1 Aug 2024 13:11:00 -0400
Subject: [PATCH 009/151] fix(api): Move errors to view level
---
reader/views.py | 8 ++++++--
sefaria/system/decorators.py | 5 +----
2 files changed, 7 insertions(+), 6 deletions(-)
diff --git a/reader/views.py b/reader/views.py
index ef8682dd38..f1df2c94ee 100644
--- a/reader/views.py
+++ b/reader/views.py
@@ -1447,8 +1447,12 @@ def _get_text(oref, versionEn=versionEn, versionHe=versionHe, commentary=comment
return text
if not multiple or abs(multiple) == 1:
- text = _get_text(oref, versionEn=versionEn, versionHe=versionHe, commentary=commentary, context=context, pad=pad,
- alts=alts, wrapLinks=wrapLinks, layer_name=layer_name)
+ try:
+ text = _get_text(oref, versionEn=versionEn, versionHe=versionHe, commentary=commentary, context=context, pad=pad,
+ alts=alts, wrapLinks=wrapLinks, layer_name=layer_name)
+ except Exception as e:
+ if str(e) == "Can not get TextRange at this level, please provide a more precise reference":
+ return jsonResponse({'error': "Please pass a more specific ref for this book, and try again. The ref you passed is a \'complex\' book-level ref. We only support book-level refs in cases of texts with a 'simple' structure. To learn more about the structure of a text on Sefaria, see: https://developers.sefaria.org/docs/the-schema-of-a-simple-text"}, status=400)
return jsonResponse(text, cb)
else:
# Return list of many sections
diff --git a/sefaria/system/decorators.py b/sefaria/system/decorators.py
index 73c1c74660..94db751aea 100644
--- a/sefaria/system/decorators.py
+++ b/sefaria/system/decorators.py
@@ -42,10 +42,7 @@ def wrapper(*args, **kwargs):
except exps.InputError as e:
logger.warning("An exception occurred processing request for '{}' while running {}. Caught as JSON".format(args[0].path, func.__name__), exc_info=True)
request = args[0]
- if str(e) == "Can not get TextRange at this level, please provide a more precise reference":
- return jsonResponse({'error': "Please pass a more specific ref for this book, and try again. The ref you passed is a \'complex\' book-level ref. We only support book-level refs in cases of texts with a 'simple' structure. To learn more about the structure of a text on Sefaria, see: https://developers.sefaria.org/docs/the-schema-of-a-simple-text"}, status=400)
- else:
- return jsonResponse({"error": str(e)}, callback=request.GET.get("callback", None))
+ return jsonResponse({"error": str(e)}, callback=request.GET.get("callback", None))
return result
return wrapper
From ef5752b838f0ffecc82fc73a2d71c4ae4dba01e0 Mon Sep 17 00:00:00 2001
From: stevekaplan123
Date: Mon, 14 Oct 2024 10:01:42 +0300
Subject: [PATCH 010/151] chore: only show add to sheet if logged in
---
static/js/ConnectionsPanel.jsx | 2 +-
static/js/TextList.jsx | 2 +-
static/js/TranslationsBox.jsx | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/static/js/ConnectionsPanel.jsx b/static/js/ConnectionsPanel.jsx
index c040af9889..133e46e1e7 100644
--- a/static/js/ConnectionsPanel.jsx
+++ b/static/js/ConnectionsPanel.jsx
@@ -756,7 +756,7 @@ const ToolsList = ({ setConnectionsMode, toggleSignUpModal, openComparePanel, co
// A list of Resources in addition to connection
return (
-
!Sefaria._uid ? toggleSignUpModal(SignUpModalKind.AddToSheet) : setConnectionsMode("Add To Sheet", { "addSource": "mainPanel" })} />
+ {Sefaria._uid && setConnectionsMode("Add To Sheet", { "addSource": "mainPanel" })} />}
setConnectionsMode("Lexicon")} />
{openComparePanel ? : null}
!Sefaria._uid ? toggleSignUpModal(SignUpModalKind.Notes) : setConnectionsMode("Notes")} />
diff --git a/static/js/TextList.jsx b/static/js/TextList.jsx
index 41c6efe5b8..faf4d09e6d 100644
--- a/static/js/TextList.jsx
+++ b/static/js/TextList.jsx
@@ -235,7 +235,7 @@ class TextList extends Component {
/>
-
+ {Sefaria._uid && }
{Sefaria.is_moderator ?
: null
}
diff --git a/static/js/TranslationsBox.jsx b/static/js/TranslationsBox.jsx
index 8a1d7e9723..a8430fdc78 100644
--- a/static/js/TranslationsBox.jsx
+++ b/static/js/TranslationsBox.jsx
@@ -190,7 +190,7 @@ class VersionsTextList extends Component {
/>
-
+ {Sefaria._uid && }
);
}
From 353e652e87a5101e122fd9329d394e2d16983dda Mon Sep 17 00:00:00 2001
From: saengel
Date: Sun, 10 Nov 2024 13:28:37 +0200
Subject: [PATCH 011/151] chore(api): Correct param type to enable accurate
results
---
sefaria/views.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/sefaria/views.py b/sefaria/views.py
index 80f3939e0d..8309b8d0ef 100644
--- a/sefaria/views.py
+++ b/sefaria/views.py
@@ -470,7 +470,7 @@ def bulktext_api(request, refs):
g = lambda x: request.GET.get(x, None)
min_char = int(g("minChar")) if g("minChar") else None
max_char = int(g("maxChar")) if g("maxChar") else None
- res = bundle_many_texts(refs, g("useTextFamily"), g("asSizedString"), min_char, max_char, g("transLangPref"), g("ven"), g("vhe"))
+ res = bundle_many_texts(refs, int(g("useTextFamily")), g("asSizedString"), min_char, max_char, g("transLangPref"), g("ven"), g("vhe"))
resp = jsonResponse(res, cb)
return resp
From 25fc731124cdde62b0ab1d670bbb877fe6940797 Mon Sep 17 00:00:00 2001
From: saengel
Date: Mon, 11 Nov 2024 15:14:53 +0200
Subject: [PATCH 012/151] chore(api errors): Add exception as an official
exception vs string comp
---
reader/views.py | 6 +++---
sefaria/system/exceptions.py | 6 ++++++
2 files changed, 9 insertions(+), 3 deletions(-)
diff --git a/reader/views.py b/reader/views.py
index f1df2c94ee..527a185afb 100644
--- a/reader/views.py
+++ b/reader/views.py
@@ -53,7 +53,7 @@
from sefaria.site.site_settings import SITE_SETTINGS
from sefaria.system.multiserver.coordinator import server_coordinator
from sefaria.system.decorators import catch_error_as_json, sanitize_get_params, json_response_decorator
-from sefaria.system.exceptions import InputError, PartialRefInputError, BookNameError, NoVersionFoundError, DictionaryEntryNotFoundError
+from sefaria.system.exceptions import InputError, PartialRefInputError, BookNameError, NoVersionFoundError, DictionaryEntryNotFoundError, ComplexBookLevelRefError
from sefaria.system.cache import django_cache
from sefaria.system.database import db
from sefaria.helper.search import get_query_obj
@@ -1451,8 +1451,8 @@ def _get_text(oref, versionEn=versionEn, versionHe=versionHe, commentary=comment
text = _get_text(oref, versionEn=versionEn, versionHe=versionHe, commentary=commentary, context=context, pad=pad,
alts=alts, wrapLinks=wrapLinks, layer_name=layer_name)
except Exception as e:
- if str(e) == "Can not get TextRange at this level, please provide a more precise reference":
- return jsonResponse({'error': "Please pass a more specific ref for this book, and try again. The ref you passed is a \'complex\' book-level ref. We only support book-level refs in cases of texts with a 'simple' structure. To learn more about the structure of a text on Sefaria, see: https://developers.sefaria.org/docs/the-schema-of-a-simple-text"}, status=400)
+ if isinstance(e, ComplexBookLevelRefError):
+ return jsonResponse({'error': e.message}, status=400)
return jsonResponse(text, cb)
else:
# Return list of many sections
diff --git a/sefaria/system/exceptions.py b/sefaria/system/exceptions.py
index 72d3493f78..d0029d2105 100644
--- a/sefaria/system/exceptions.py
+++ b/sefaria/system/exceptions.py
@@ -99,3 +99,9 @@ def __init__(self, method):
self.method = method
self.message = f"'{method}' is not a valid HTTP API method."
super().__init__(self.message)
+
+class ComplexBookLevelRefError(Exception):
+ def __init__(self, book_ref):
+ self.book_ref = book_ref
+ self.message = f"Please pass a more specific ref for this book, and try again. You passed in a \'complex\' book-level ref. We only support book-level refs in cases of texts with a 'simple' structure. To learn more about the structure of a text on Sefaria, see: https://developers.sefaria.org/docs/the-schema-of-a-simple-text"
+ super().__init__(self.message)
From 6bea38ee9b70c7f6905b831c805d8f79b57ebb40 Mon Sep 17 00:00:00 2001
From: nsantacruz
Date: Mon, 11 Nov 2024 22:11:30 +0200
Subject: [PATCH 013/151] 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 014/151] 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 015/151] 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 016/151] 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 017/151] 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 018/151] 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 599180dbdd94ce6a9b6d013b82aac6d91641767b Mon Sep 17 00:00:00 2001
From: saengel
Date: Wed, 13 Nov 2024 12:43:33 +0200
Subject: [PATCH 019/151] chore(api errors): Raise exception, find the right
place in model for exception
---
api/views.py | 14 +++++++++-----
reader/views.py | 2 +-
sefaria/model/text.py | 8 ++++----
sefaria/system/exceptions.py | 8 ++++++--
4 files changed, 20 insertions(+), 12 deletions(-)
diff --git a/api/views.py b/api/views.py
index a8d60b4662..b91fa760c4 100644
--- a/api/views.py
+++ b/api/views.py
@@ -1,7 +1,7 @@
from sefaria.model import *
from sefaria.model.text_reuqest_adapter import TextRequestAdapter
from sefaria.client.util import jsonResponse
-from sefaria.system.exceptions import InputError
+from sefaria.system.exceptions import InputError, ComplexBookLevelRefError
from django.views import View
from .api_warnings import *
@@ -59,12 +59,16 @@ def get(self, request, *args, **kwargs):
try:
data = text_manager.get_versions_for_query()
- except InputError as e:
- if str(e) == "Can not get TextRange at this level, please provide a more precise reference":
- return jsonResponse({'error': "Please pass a more specific ref for this book, and try again. The ref you passed is a \'complex\' book-level ref. We only support book-level refs in cases of texts with a 'simple' structure. To learn more about the structure of a text on Sefaria, see: https://developers.sefaria.org/docs/the-schema-of-a-simple-text"}, status=400)
- data = self._handle_warnings(data)
+ data = self._handle_warnings(data)
+
+ except Exception as e:
+ if isinstance(e, InputError):
+ return jsonResponse({'error': e.message})
+
return jsonResponse(data)
+
+
diff --git a/reader/views.py b/reader/views.py
index 527a185afb..f3fae99373 100644
--- a/reader/views.py
+++ b/reader/views.py
@@ -1450,7 +1450,7 @@ def _get_text(oref, versionEn=versionEn, versionHe=versionHe, commentary=comment
try:
text = _get_text(oref, versionEn=versionEn, versionHe=versionHe, commentary=commentary, context=context, pad=pad,
alts=alts, wrapLinks=wrapLinks, layer_name=layer_name)
- except Exception as e:
+ except InputError as e:
if isinstance(e, ComplexBookLevelRefError):
return jsonResponse({'error': e.message}, status=400)
return jsonResponse(text, cb)
diff --git a/sefaria/model/text.py b/sefaria/model/text.py
index f9bea61dae..d9c78cc652 100644
--- a/sefaria/model/text.py
+++ b/sefaria/model/text.py
@@ -25,7 +25,7 @@
import sefaria.system.cache as scache
from sefaria.system.cache import in_memory_cache
from sefaria.system.exceptions import InputError, BookNameError, PartialRefInputError, IndexSchemaError, \
- NoVersionFoundError, DictionaryEntryNotFoundError, MissingKeyError
+ NoVersionFoundError, DictionaryEntryNotFoundError, MissingKeyError, ComplexBookLevelRefError
from sefaria.utils.hebrew import has_hebrew, is_all_hebrew, hebrew_term
from sefaria.utils.util import list_depth, truncate_string
from sefaria.datatype.jagged_array import JaggedTextArray, JaggedArray
@@ -1693,7 +1693,7 @@ def __init__(self, oref, lang, vtitle, merge_versions=False, versions=None):
elif oref.has_default_child(): #use default child:
self.oref = oref.default_child_ref()
else:
- raise InputError("Can not get TextRange at this level, please provide a more precise reference")
+ raise ComplexBookLevelRefError(book_ref=oref.normal())
self.lang = lang
self.vtitle = vtitle
self.merge_versions = merge_versions
@@ -2424,7 +2424,7 @@ def __init__(self, oref, context=1, commentary=True, version=None, lang=None,
self._alts = []
if not isinstance(oref.index_node, JaggedArrayNode) and not oref.index_node.is_virtual:
- raise InputError("Can not get TextFamily at this level, please provide a more precise reference")
+ raise InputError("Unable to find text for that ref")
for i in range(0, context):
oref = oref.context_ref()
@@ -3991,7 +3991,7 @@ def padded_ref(self):
if self.has_default_child():
return self.default_child_ref().padded_ref()
elif self.is_book_level():
- raise InputError("Can not get TextRange at this level, please provide a more precise reference")
+ raise ComplexBookLevelRefError(book_ref=self.normal())
else:
raise InputError("Cannot pad a schema node ref.")
diff --git a/sefaria/system/exceptions.py b/sefaria/system/exceptions.py
index d0029d2105..8ef21b8f59 100644
--- a/sefaria/system/exceptions.py
+++ b/sefaria/system/exceptions.py
@@ -100,8 +100,12 @@ def __init__(self, method):
self.message = f"'{method}' is not a valid HTTP API method."
super().__init__(self.message)
-class ComplexBookLevelRefError(Exception):
+class ComplexBookLevelRefError(InputError):
def __init__(self, book_ref):
self.book_ref = book_ref
- self.message = f"Please pass a more specific ref for this book, and try again. You passed in a \'complex\' book-level ref. We only support book-level refs in cases of texts with a 'simple' structure. To learn more about the structure of a text on Sefaria, see: https://developers.sefaria.org/docs/the-schema-of-a-simple-text"
+ self.message = (f"You passed '{book_ref}', please pass a more specific ref for this book, and try again. "
+ f"'{book_ref}' is a \'complex\' book-level ref. We only support book-level "
+ f"refs in cases of texts with a 'simple' structure. To learn more about the "
+ f"structure of a text on Sefaria, "
+ f"see: https://developers.sefaria.org/docs/the-schema-of-a-simple-text")
super().__init__(self.message)
From 1827d9db338d221cfe56ee073452ddd06ed43190 Mon Sep 17 00:00:00 2001
From: nsantacruz
Date: Wed, 13 Nov 2024 14:04:15 +0200
Subject: [PATCH 020/151] 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 021/151] 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 022/151] 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 023/151] 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 024/151] 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 025/151] 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 026/151] 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 027/151] 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 028/151] 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 029/151] 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 030/151] 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 031/151] 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 032/151] 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 033/151] 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 034/151] 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 035/151] 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 036/151] 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 037/151] 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 038/151] 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 039/151] 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 040/151] 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 041/151] 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 042/151] 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 043/151] 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 044/151] 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 045/151] 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 046/151] 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 047/151] 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 048/151] 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 049/151] 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 050/151] 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 051/151] 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 052/151] 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 053/151] 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 054/151] 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 055/151] 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 55051fcb2626a837e0255231b58d0c79cd22a43c Mon Sep 17 00:00:00 2001
From: saengel
Date: Wed, 20 Nov 2024 10:44:36 +0200
Subject: [PATCH 056/151] feat(integrating notes): first pass
---
sefaria/urls.py | 1 +
static/js/ReaderApp.jsx | 7 +++++++
static/js/ReaderPanel.jsx | 2 +-
static/js/UserHistoryPanel.jsx | 25 +++++++++++++++++++------
4 files changed, 28 insertions(+), 7 deletions(-)
diff --git a/sefaria/urls.py b/sefaria/urls.py
index eb5acc6dd2..91ad4c959a 100644
--- a/sefaria/urls.py
+++ b/sefaria/urls.py
@@ -30,6 +30,7 @@
url(r'^$', reader_views.home, name="home"),
url(r'^texts/?$', reader_views.texts_list, name="table_of_contents"),
url(r'^texts/saved/?$', reader_views.saved),
+ url(r'^texts/notes/?$', reader_views.notes),
url(r'^texts/history/?$', reader_views.user_history),
url(r'^texts/recent/?$', reader_views.old_recent_redirect),
url(r'^texts/(?P.+)?$', reader_views.texts_category_list),
diff --git a/static/js/ReaderApp.jsx b/static/js/ReaderApp.jsx
index edc2438b13..0e922f7c89 100644
--- a/static/js/ReaderApp.jsx
+++ b/static/js/ReaderApp.jsx
@@ -1114,6 +1114,10 @@ toggleSignUpModal(modalContentKind = SignUpModalKind.Default) {
} else if (path === "/texts/saved") {
this.showSaved();
+ }
+ else if (path === "/texts/notes") {
+ this.showNotes();
+
} else if (path.match(/\/texts\/.+/)) {
this.showLibrary(path.slice(7).split("/"));
@@ -1740,6 +1744,9 @@ toggleSignUpModal(modalContentKind = SignUpModalKind.Default) {
showSaved() {
this.setSinglePanelState({menuOpen: "saved"});
}
+ showNotes() {
+ this.setSinglePanelState({menuOpen: "notes"});
+ }
showHistory() {
this.setSinglePanelState({menuOpen: "history"});
}
diff --git a/static/js/ReaderPanel.jsx b/static/js/ReaderPanel.jsx
index bdfc71c30c..a6e36dd274 100644
--- a/static/js/ReaderPanel.jsx
+++ b/static/js/ReaderPanel.jsx
@@ -1038,7 +1038,7 @@ class ReaderPanel extends Component {
interfaceLang={this.props.interfaceLang} />
);
- } else if (this.state.menuOpen === "saved" || this.state.menuOpen === "history") {
+ } else if (this.state.menuOpen === "saved" || this.state.menuOpen === "history" || this.state.menuOpen === "notes") {
menu = (
{
const store = menuOpen === "saved" ? Sefaria.saved : Sefaria.userHistory;
+ const notes = Sefaria.allPrivateNotes();
const contentRef = useRef();
const title = (
@@ -34,6 +35,10 @@ const UserHistoryPanel = ({menuOpen, toggleLanguage, openDisplaySettings, openNa
History
+ {/**Add URL */}
+ {/**Change icon */}
+ Notes
+
);
@@ -55,12 +60,20 @@ const UserHistoryPanel = ({menuOpen, toggleLanguage, openDisplaySettings, openNa
{Sefaria.interfaceLang !== "hebrew" && Sefaria._siteSettings.TORAH_SPECIFIC ?
: null}
-
+ { menuOpen === "notes" ?
+ (notes.length ?
+ notes.map(function(item, i) {
+ return
+ }.bind(this))
+ : )
+ :
+
+ }
From 6e0cdeeb54271b7cf6a31369c52f766f8dd5af77 Mon Sep 17 00:00:00 2001
From: nsantacruz
Date: Wed, 20 Nov 2024 12:54:47 +0200
Subject: [PATCH 057/151] 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 058/151] 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 059/151] 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 060/151] 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 02431af4a720a8868d8f357a9923c645a7053adb Mon Sep 17 00:00:00 2001
From: saengel
Date: Wed, 20 Nov 2024 14:53:05 +0200
Subject: [PATCH 061/151] chore(api): improved generalized error catching on v3
---
api/views.py | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/api/views.py b/api/views.py
index b91fa760c4..85ab3e9cfc 100644
--- a/api/views.py
+++ b/api/views.py
@@ -62,8 +62,7 @@ def get(self, request, *args, **kwargs):
data = self._handle_warnings(data)
except Exception as e:
- if isinstance(e, InputError):
- return jsonResponse({'error': e.message})
+ return jsonResponse({'error': str(e)})
return jsonResponse(data)
From 9e703ae2f90def2d396323e0d9348e43a1b12a64 Mon Sep 17 00:00:00 2001
From: saengel
Date: Wed, 20 Nov 2024 14:56:56 +0200
Subject: [PATCH 062/151] chore(api): improved generalized error catching on v1
---
reader/views.py | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/reader/views.py b/reader/views.py
index f3fae99373..86ebb8452f 100644
--- a/reader/views.py
+++ b/reader/views.py
@@ -1450,9 +1450,8 @@ def _get_text(oref, versionEn=versionEn, versionHe=versionHe, commentary=comment
try:
text = _get_text(oref, versionEn=versionEn, versionHe=versionHe, commentary=commentary, context=context, pad=pad,
alts=alts, wrapLinks=wrapLinks, layer_name=layer_name)
- except InputError as e:
- if isinstance(e, ComplexBookLevelRefError):
- return jsonResponse({'error': e.message}, status=400)
+ except Exception as e:
+ return jsonResponse({'error': str(e)}, status=400)
return jsonResponse(text, cb)
else:
# Return list of many sections
From 8f417b4190297e1dac97c456df557987e9fbf6b9 Mon Sep 17 00:00:00 2001
From: nsantacruz
Date: Wed, 20 Nov 2024 15:04:55 +0200
Subject: [PATCH 063/151] 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 064/151] 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 065/151] 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 066/151] 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 067/151] 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 068/151] 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 069/151] 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 070/151] 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 071/151] 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 072/151] 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 073/151] 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 074/151] 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 075/151] 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 076/151] 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 077/151] 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 7c348a3bb40a0941438c02a574435b5907add19c Mon Sep 17 00:00:00 2001
From: saengel
Date: Thu, 21 Nov 2024 15:39:10 +0200
Subject: [PATCH 078/151] chore(notes integration): continued wiring, console
---
reader/views.py | 5 +++++
static/js/NoteListing.jsx | 2 ++
static/js/UserHistoryPanel.jsx | 28 +++++++++++++++++++++++++---
3 files changed, 32 insertions(+), 3 deletions(-)
diff --git a/reader/views.py b/reader/views.py
index 947651463b..9e2fd84f66 100644
--- a/reader/views.py
+++ b/reader/views.py
@@ -1069,6 +1069,11 @@ def user_history(request):
desc = _("See your user history on Sefaria")
return menu_page(request, props, page="history", title=title, desc=desc)
+def notes(request):
+ title = _("My Notes")
+ desc = _("See your notes on Sefaria")
+ props = {"saved": {"loaded": True, "items": []}}
+ return menu_page(request, None, page="notes", title=title, desc=desc)
@login_required
def user_stats(request):
diff --git a/static/js/NoteListing.jsx b/static/js/NoteListing.jsx
index 188ce844af..7943a7a954 100644
--- a/static/js/NoteListing.jsx
+++ b/static/js/NoteListing.jsx
@@ -37,6 +37,8 @@ class NoteListing extends Component {
var data = this.props.data;
var url = "/" + Sefaria.normRef(data.ref) + "?with=Notes";
+ console.log(data.ref, data.text);
+
return (
diff --git a/static/js/UserHistoryPanel.jsx b/static/js/UserHistoryPanel.jsx
index d2301a8768..340f97efc0 100644
--- a/static/js/UserHistoryPanel.jsx
+++ b/static/js/UserHistoryPanel.jsx
@@ -22,9 +22,30 @@ import {
const UserHistoryPanel = ({menuOpen, toggleLanguage, openDisplaySettings, openNav, compare, toggleSignUpModal}) => {
const store = menuOpen === "saved" ? Sefaria.saved : Sefaria.userHistory;
- const notes = Sefaria.allPrivateNotes();
+
+ let flattenedNotes = [];
+ let notes = null;
+
+// Call the allPrivateNotes function
+ Sefaria.allPrivateNotes((data) => {
+ if (Array.isArray(data)) {
+ // Map the data to extract 'ref' and 'text' pairs
+ flattenedNotes = data.map(note => ({
+ ref: note.ref,
+ text: note.text
+ }));
+
+ console.log("Flattened notes:", flattenedNotes); // Log the result
+ notes = flattenedNotes;
+ } else {
+ console.error("Unexpected data format:", data);
+ }
+});
+
const contentRef = useRef();
+ console.log(notes);
+
const title = (
@@ -35,7 +56,7 @@ const UserHistoryPanel = ({menuOpen, toggleLanguage, openDisplaySettings, openNa
History
- {/**Add URL */}
+
{/**Change icon */}
Notes
@@ -61,8 +82,9 @@ const UserHistoryPanel = ({menuOpen, toggleLanguage, openDisplaySettings, openNa
: null}
{ menuOpen === "notes" ?
- (notes.length ?
+ (notes && notes.length ?
notes.map(function(item, i) {
+ {console.log(`item ${item}, i: ${i}`);}
return
}.bind(this))
:
)
From 52f3a72ae2c1be42302014ed6ef234e9b4fcb533 Mon Sep 17 00:00:00 2001
From: nsantacruz
Date: Thu, 21 Nov 2024 15:40:46 +0200
Subject: [PATCH 079/151] 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 5b6a297425fa9efec01af67c66505d4f92c0ccf6 Mon Sep 17 00:00:00 2001
From: saengel
Date: Thu, 21 Nov 2024 16:03:33 +0200
Subject: [PATCH 080/151] chore(modules): Clean up naming for modules
---
static/js/AboutBox.jsx | 6 +-
static/js/BookPage.jsx | 6 +-
static/js/CalendarsPage.jsx | 4 +-
static/js/CollectionPage.jsx | 2 +-
static/js/CommunityPage.jsx | 6 +-
static/js/NavSidebar.jsx | 222 ++++++++++++++--------------
static/js/NotificationsPanel.jsx | 2 +-
static/js/PublicCollectionsPage.jsx | 6 +-
static/js/TextCategoryPage.jsx | 2 +-
static/js/TextsPage.jsx | 6 +-
static/js/TopicPage.jsx | 8 +-
static/js/TopicPageAll.jsx | 2 +-
static/js/TopicsPage.jsx | 6 +-
static/js/TranslationsPage.jsx | 4 +-
static/js/UserHistoryPanel.jsx | 2 +-
15 files changed, 142 insertions(+), 142 deletions(-)
diff --git a/static/js/AboutBox.jsx b/static/js/AboutBox.jsx
index 791b77e42c..1ede90b49b 100644
--- a/static/js/AboutBox.jsx
+++ b/static/js/AboutBox.jsx
@@ -5,7 +5,7 @@ import VersionBlock, {VersionsBlocksList} from './VersionBlock/VersionBlock';
import Component from 'react-class';
import {InterfaceText} from "./Misc";
import {ContentText} from "./ContentText";
-import { Modules } from './NavSidebar';
+import { SidebarModules } from './NavSidebar';
class AboutBox extends Component {
@@ -232,8 +232,8 @@ class AboutBox extends Component {
({versionSectionEn}{versionSectionHe}{alternateSectionHe}
) :
({versionSectionHe}{versionSectionEn}{alternateSectionHe}
)
}
-
- { !isDictionary ? : null}
+
+ { !isDictionary ? : null}
);
}
diff --git a/static/js/BookPage.jsx b/static/js/BookPage.jsx
index 4c87581a41..60060856e8 100644
--- a/static/js/BookPage.jsx
+++ b/static/js/BookPage.jsx
@@ -19,7 +19,7 @@ import React, { useState, useRef } from 'react';
import ReactDOM from 'react-dom';
import $ from './sefaria/sefariaJquery';
import Sefaria from './sefaria/sefaria';
-import { NavSidebar, Modules } from './NavSidebar';
+import { NavSidebar, SidebarModules } from './NavSidebar';
import DictionarySearch from './DictionarySearch';
import VersionBlock from './VersionBlock/VersionBlock';
import ExtendedNotes from './ExtendedNotes';
@@ -272,7 +272,7 @@ class BookPage extends Component {
{this.props.multiPanel ? null :
-
+
}
{this.isBookToc() && ! this.props.compare ?
- : null}
+ : null}
{this.isBookToc() && ! this.props.compare ?
: null}
diff --git a/static/js/CalendarsPage.jsx b/static/js/CalendarsPage.jsx
index c6d96a8f5f..53f1da95c8 100644
--- a/static/js/CalendarsPage.jsx
+++ b/static/js/CalendarsPage.jsx
@@ -28,7 +28,7 @@ const CalendarsPage = ({multiPanel, initialWidth}) => {
const weeklyListings = makeListings(weeklyCalendars);
const about = multiPanel ? null :
-
+
const sidebarModules = [
multiPanel ? {type: "AboutLearningSchedules"} : {type: null},
@@ -56,7 +56,7 @@ const CalendarsPage = ({multiPanel, initialWidth}) => {
-
+
diff --git a/static/js/CollectionPage.jsx b/static/js/CollectionPage.jsx
index 49adfa7131..5fb3bfe07a 100644
--- a/static/js/CollectionPage.jsx
+++ b/static/js/CollectionPage.jsx
@@ -294,7 +294,7 @@ class CollectionPage extends Component {
{content}
-
+
diff --git a/static/js/CommunityPage.jsx b/static/js/CommunityPage.jsx
index 8510b13093..f199ec2c3f 100644
--- a/static/js/CommunityPage.jsx
+++ b/static/js/CommunityPage.jsx
@@ -4,7 +4,7 @@ import $ from './sefaria/sefariaJquery';
import Sefaria from './sefaria/sefaria';
import PropTypes from 'prop-types';
import classNames from 'classnames';
-import { NavSidebar, Modules } from './NavSidebar';
+import { NavSidebar, SidebarModules } from './NavSidebar';
import Footer from'./Footer';
import {
InterfaceText,
@@ -62,7 +62,7 @@ const CommunityPage = ({multiPanel, toggleSignUpModal, initialWidth}) => {
-
+
@@ -103,7 +103,7 @@ const RecentlyPublished = ({multiPanel, toggleSignUpModal}) => {
recentSheets.map(s => );
const joinTheConversation = (
-
+
);
if (recentSheets) {
diff --git a/static/js/NavSidebar.jsx b/static/js/NavSidebar.jsx
index 06726385e6..38d0355549 100644
--- a/static/js/NavSidebar.jsx
+++ b/static/js/NavSidebar.jsx
@@ -7,10 +7,10 @@ import {InterfaceText, ProfileListing, Dropdown} from './Misc';
import { Promotions } from './Promotions'
import {SignUpModalKind} from "./sefaria/signupModalContent";
-const NavSidebar = ({modules}) => {
+const NavSidebar = ({sidebarModules}) => {
return
- {modules.map((m, i) =>
-
+
@@ -19,7 +19,7 @@ const NavSidebar = ({modules}) => {
};
-const Modules = ({type, props}) => {
+const SidebarModules = ({type, props}) => {
// Choose the appropriate module component to render by `type`
const moduleTypes = {
"AboutSefaria": AboutSefaria,
@@ -59,18 +59,18 @@ const Modules = ({type, props}) => {
"StudyCompanion": StudyCompanion,
};
if (!type) { return null; }
- const ModuleType = moduleTypes[type];
- return
+ const SidebarModuleType = moduleTypes[type];
+ return
};
-const Module = ({children, blue, wide}) => {
+const SidebarModule = ({children, blue, wide}) => {
const classes = classNames({navSidebarModule: 1, "sans-serif": 1, blue, wide});
return {children}
};
-const ModuleTitle = ({children, en, he, h1}) => {
+const SidebarModuleTitle = ({children, en, he, h1}) => {
const content = children ?
{children}
: ;
@@ -82,10 +82,10 @@ const ModuleTitle = ({children, en, he, h1}) => {
const TitledText = ({enTitle, heTitle, enText, heText}) => {
- return
-
+ return
+
-
+
};
const RecentlyViewedItem = ({oref}) => {
@@ -136,27 +136,27 @@ const RecentlyViewed = ({toggleSignUpModal, mobile}) => {
}
const allHistoryPhrase = mobile ? "All History" : "All history ";
const recentlyViewedList = ;
- return
+ return
{mobile && recentlyViewedList}
- ;
+ ;
}
const Promo = () =>
-
+
-
+
;
const StudyCompanion = () => (
-
- Study Companion
+
+ Study Companion
Get the Weekly Parashah Study Companion in your inbox.
(
Sign Up
-
+
)
const AboutSefaria = ({hideTitle}) => (
-
+
{!hideTitle ?
- A Living Library of Torah : null}
+ A Living Library of Torah : null}
Sefaria is home to 3,000 years of Jewish texts. We are a nonprofit organization offering free access to texts, translations,
@@ -209,7 +209,7 @@ const AboutSefaria = ({hideTitle}) => (
}
-
+
);
@@ -230,9 +230,9 @@ const AboutTranslatedText = ({translationsSlug}) => {
"yi": {title: "א לעבעדיקע ביבליאטעק פון תורה", body: "אין ספֿריאַ איז אַ היים פֿון 3,000 יאָר ייִדישע טעקסטן. מיר זענען אַ נאַן-נוץ אָרגאַניזאַציע וואָס אָפפערס פריי אַקסעס צו טעקסטן, איבערזעצונגען און קאָמענטאַרן אַזוי אַז אַלעמען קענען אָנטייל נעמען אין די אָנגאָינג פּראָצעס פון לערנען, ינטערפּריטיישאַן און שאפן תורה."}
}
return (
-
- {translationLookup[translationsSlug] ?
- translationLookup[translationsSlug]["title"] : "A Living Library of Torah"}
+
+ {translationLookup[translationsSlug] ?
+ translationLookup[translationsSlug]["title"] : "A Living Library of Torah"}
{ translationLookup[translationsSlug] ?
translationLookup[translationsSlug]["body"] :
@@ -247,13 +247,13 @@ const AboutTranslatedText = ({translationsSlug}) => {
}
-
+
);
}
const Resources = () => (
-
+
Resources
@@ -264,42 +264,42 @@ const Resources = () => (
-
+
);
const TheJewishLibrary = ({hideTitle}) => (
-
+
{!hideTitle ?
- The Jewish Library : null}
+ The Jewish Library : null}
The tradition of Torah texts is a vast, interconnected network that forms a conversation across space and time. The five books of the Torah form its foundation, and each generation of later texts functions as a commentary on those that came before it.
-
+
);
const SupportSefaria = ({blue}) => (
-
- Support Sefaria
+
+ Support Sefaria
Sefaria is an open source, nonprofit project. Support us by making a tax-deductible donation.
Make a Donation
-
+
);
const SponsorADay = () => (
-
- Sponsor A Day of Learning
+
+ Sponsor A Day of Learning
With your help, we can add more texts and translations to the library, develop new tools for learning, and keep Sefaria accessible for Torah study anytime, anywhere.
Sponsor A Day
-
+
);
@@ -314,10 +314,10 @@ const AboutTextCategory = ({cats}) => {
}
return (
-
+
-
+
);
};
@@ -343,9 +343,9 @@ const AboutText = ({index, hideTitle}) => {
if (!authors.length && !composed && !description) { return null; }
return (
-
+
{hideTitle ? null :
- About This Text}
+ About This Text}
{ composed || authors.length ?
@@ -371,7 +371,7 @@ const AboutText = ({index, hideTitle}) => {
{description ?
: null}
-
+
);
};
@@ -431,8 +431,8 @@ const DafLink = () => {
}
const Translations = () => {
- return (
- Translations
+ return (
+ Translations
Access key works from the library in several languages.
@@ -442,14 +442,14 @@ const Translations = () => {
- )
+ )
}
const LearningSchedules = () => {
return (
-
- Learning Schedules
+
+ Learning Schedules
Weekly Torah Portion:
@@ -474,15 +474,15 @@ const LearningSchedules = () => {
לוחות לימוד נוספים ›
-
+
);
};
const WeeklyTorahPortion = () => {
return (
-
- Weekly Torah Portion
+
+ Weekly Torah Portion
@@ -501,22 +501,22 @@ const WeeklyTorahPortion = () => {
פרשות השבוע ›
-
+
);
};
const DafYomi = () => {
return (
-
- Daily Learning
+
+ Daily Learning
Daf Yomi
-
+
);
};
@@ -551,8 +551,8 @@ const Visualizations = ({categories}) => {
if (links.length == 0) { return null; }
return (
-
- Visualizations
+
+ Visualizations
Explore interconnections among texts with our interactive visualizations.
{links.map((link, i) =>
@@ -568,15 +568,15 @@ const Visualizations = ({categories}) => {
תרשימים גרפיים נוספים ›
-
+
);
};
const AboutTopics = ({hideTitle}) => (
-
+
{hideTitle ? null :
- About Topics }
+ About Topics }
דפי הנושא מציגים מקורות נבחרים מארון הספרים היהודי עבור אלפי נושאים. ניתן לדפדף לפי קטגוריה או לחפש לפי נושא ספציפי, ובסרגל הצד מוצגים הנושאים הפופולריים ביותר ואלה הקשורים אליהם. הקליקו ושוטטו בין הנושאים השונים כדי ללמוד עוד.
@@ -585,19 +585,19 @@ const AboutTopics = ({hideTitle}) => (
Topics Pages present a curated selection of various genres of sources on thousands of chosen subjects. You can browse by category, search for something specific, or view the most popular topics — and related topics — on the sidebar. Explore and click through to learn more.
-
+
);
const TrendingTopics = () => (
-
- Trending Topics
+
+ Trending Topics
{Sefaria.trendingTopics.map((topic, i) =>
)}
-
+
);
@@ -610,8 +610,8 @@ const RelatedTopics = ({title}) => {
Sefaria.getIndexDetails(title).then(data => setTopics(data.relatedTopics));
},[title]);
return (topics.length ?
-
- Related Topics
+
+ Related Topics
{shownTopics.map((topic, i) =>
@@ -621,7 +621,7 @@ const RelatedTopics = ({title}) => {
{setShowMore(true);}}>
More
: null}
- : null
+ : null
);
};
@@ -630,9 +630,9 @@ const JoinTheConversation = ({wide}) => {
if (!Sefaria.multiPanel) { return null; } // Don't advertise create sheets on mobile (yet)
return (
-
+
- Join the Conversation
+ Join the Conversation
Combine sources from our library with your own comments, questions, images, and videos.
@@ -641,16 +641,16 @@ const JoinTheConversation = ({wide}) => {
Make a Sheet
-
+
);
};
const JoinTheCommunity = ({wide}) => {
return (
-
+
- Join the Conversation
+ Join the Conversation
People around the world use Sefaria to create and share Torah resources. You're invited to add your voice.
@@ -659,14 +659,14 @@ const JoinTheCommunity = ({wide}) => {
Explore the Community
-
+
);
};
const GetTheApp = () => (
-
- Get the Mobile App
+
+ Get the Mobile App
Access the Jewish library anywhere and anytime with the Sefaria mobile app.
(
platform='android'
altText={Sefaria._("Sefaria app on Android")}
/>
-
+
);
@@ -687,8 +687,8 @@ const StayConnected = () => { // TODO: remove? looks like we are not using this
const fbURL = Sefaria.interfaceLang == "hebrew" ? "https://www.facebook.com/sefaria.org.il" : "https://www.facebook.com/sefaria.org";
return (
-
- Stay Connected
+
+ Stay Connected
Get updates on new texts, learning resources, features, and more.
@@ -703,14 +703,14 @@ const StayConnected = () => { // TODO: remove? looks like we are not using this
-
+
);
};
const AboutLearningSchedules = () => (
-
- Learning Schedules
+
+ Learning Schedules
Since biblical times, the Torah has been divided into sections which are read each week on a set yearly calendar.
@@ -721,14 +721,14 @@ const AboutLearningSchedules = () => (
בעקבות המנהג הזה התפתחו לאורך השנים סדרי לימוד תקופתיים רבים נוספים, ובעזרתם יכולות קהילות וקבוצות של לומדים ללמוד יחד טקסטים שלמים.
-
+
);
const AboutCollections = ({hideTitle}) => (
-
+
{hideTitle ? null :
- About Collections}
+ About Collections}
Collections are user generated bundles of sheets which can be used privately, shared with friends, or made public on Sefaria.
אסופות הן מקבצים של דפי מקורות שנוצרו על ידי משתמשי האתר. הן ניתנות לשימוש פרטי, לצורך שיתוף עם אחרים או לשימוש ציבורי באתר ספריא.
@@ -740,13 +740,13 @@ const AboutCollections = ({hideTitle}) => (
Create a Collection
}
-
+
);
const ExploreCollections = () => (
-
- Collections
+
+ Collections
Organizations, communities and individuals around the world curate and share collections of sheets for you to explore.
-
+
);
const WhoToFollow = ({toggleSignUpModal}) => (
-
- Who to Follow
+
+ Who to Follow
{Sefaria.followRecommendations.map(user =>
)}
-
+
);
const Image = ({url}) => (
-
+
-
+
);
const Wrapper = ({title, content}) => (
-
- {title ? {title} : null}
+
+ {title ? {title} : null}
{content}
-
+
);
@@ -846,8 +846,8 @@ const DownloadVersions = ({sref}) => {
}, [sref]);
return(
-
- Download Text
+
+ Download Text
-
+
);
};
const PortalAbout = ({title, description, image_uri, image_caption}) => {
return(
-
-
+
+
-
+
)
};
const PortalMobile = ({title, description, android_link, ios_link}) => {
return(
-
+
-
+
)
};
const PortalOrganization = ({title, description}) => {
return(
-
-
+
+
{description && }
-
+
)
};
const PortalNewsletter = ({title, description}) => {
- let titleElement =
;
+ let titleElement =
;
return(
-
+
{titleElement}
{
emailPlaceholder={{en: "Email Address", he: "כתובת מייל"}}
subscribe={Sefaria.subscribeSefariaAndSteinsaltzNewsletter}
/>
-
+
)
};
export {
NavSidebar,
- Modules,
+ SidebarModules,
RecentlyViewed
};
diff --git a/static/js/NotificationsPanel.jsx b/static/js/NotificationsPanel.jsx
index 27abe785d4..567ba1e7a4 100644
--- a/static/js/NotificationsPanel.jsx
+++ b/static/js/NotificationsPanel.jsx
@@ -90,7 +90,7 @@ class NotificationsPanel extends Component {
notifications :
}
-
+
diff --git a/static/js/PublicCollectionsPage.jsx b/static/js/PublicCollectionsPage.jsx
index 5d71fd7b16..cb91d48041 100644
--- a/static/js/PublicCollectionsPage.jsx
+++ b/static/js/PublicCollectionsPage.jsx
@@ -4,7 +4,7 @@ import classNames from 'classnames';
import Footer from './Footer';
import Sefaria from './sefaria/sefaria';
import Component from 'react-class';
-import { NavSidebar, Modules } from './NavSidebar';
+import { NavSidebar, SidebarModules } from './NavSidebar';
import {
InterfaceText,
LoadingMessage,
@@ -70,7 +70,7 @@ const PublicCollectionsPage = ({multiPanel, initialWidth}) => {
{multiPanel ? null :
- }
+ }
{ !!collectionsList ?
@@ -89,7 +89,7 @@ const PublicCollectionsPage = ({multiPanel, initialWidth}) => {
: }
-
+
diff --git a/static/js/TextCategoryPage.jsx b/static/js/TextCategoryPage.jsx
index c8bcb4b9bb..a8e48de4e2 100644
--- a/static/js/TextCategoryPage.jsx
+++ b/static/js/TextCategoryPage.jsx
@@ -101,7 +101,7 @@ const TextCategoryPage = ({category, categories, setCategories, toggleLanguage,
initialWidth={initialWidth}
nestLevel={nestLevel} />
- {!compare ? : null}
+ {!compare ? : null}
{footer}
diff --git a/static/js/TextsPage.jsx b/static/js/TextsPage.jsx
index 53ef228dfb..8d0ce960cc 100644
--- a/static/js/TextsPage.jsx
+++ b/static/js/TextsPage.jsx
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import classNames from 'classnames';
import Sefaria from './sefaria/sefaria';
import $ from './sefaria/sefariaJquery';
-import { NavSidebar, Modules, RecentlyViewed } from './NavSidebar';
+import { NavSidebar, SidebarModules, RecentlyViewed } from './NavSidebar';
import TextCategoryPage from './TextCategoryPage';
import Footer from './Footer';
import ComparePanelHeader from './ComparePanelHeader';
@@ -79,7 +79,7 @@ const TextsPage = ({categories, settings, setCategories, onCompareBack, openSear
const about = compare || multiPanel ? null :
- ;
+ ;
const dedication = Sefaria._siteSettings.TORAH_SPECIFIC && !compare ? : null;
@@ -112,7 +112,7 @@ const TextsPage = ({categories, settings, setCategories, onCompareBack, openSear
{ !multiPanel && }
{ categoryListings }
- {!compare ? : null}
+ {!compare ? : null}
{footer}
diff --git a/static/js/TopicPage.jsx b/static/js/TopicPage.jsx
index 60f7207b1e..198b974f22 100644
--- a/static/js/TopicPage.jsx
+++ b/static/js/TopicPage.jsx
@@ -285,7 +285,7 @@ const TopicCategory = ({topic, topicTitle, setTopic, setNavTopic, compare, initi
-
+
@@ -533,16 +533,16 @@ const PortalNavSideBar = ({portal, entriesToDisplayList}) => {
"organization": "PortalOrganization",
"newsletter": "PortalNewsletter"
}
- const modules = [];
+ const sidebarModules = [];
for (let key of entriesToDisplayList) {
if (!portal[key]) { continue; }
- modules.push({
+ sidebarModules.push({
type: portalModuleTypeMap[key],
props: portal[key],
});
}
return(
-
+
)
};
diff --git a/static/js/TopicPageAll.jsx b/static/js/TopicPageAll.jsx
index d732d7c0cd..9a45177c09 100644
--- a/static/js/TopicPageAll.jsx
+++ b/static/js/TopicPageAll.jsx
@@ -119,7 +119,7 @@ class TopicPageAll extends Component {
}
-
+
diff --git a/static/js/TopicsPage.jsx b/static/js/TopicsPage.jsx
index b79ab2c52c..c1b2b4fdfd 100644
--- a/static/js/TopicsPage.jsx
+++ b/static/js/TopicsPage.jsx
@@ -7,7 +7,7 @@ import PropTypes from 'prop-types';
import classNames from 'classnames';
import Sefaria from './sefaria/sefaria';
import $ from './sefaria/sefariaJquery';
-import { NavSidebar, Modules } from './NavSidebar';
+import { NavSidebar, SidebarModules } from './NavSidebar';
import Footer from './Footer';
import {CategoryHeader} from "./Misc";
import Component from 'react-class';
@@ -45,7 +45,7 @@ const TopicsPage = ({setNavTopic, multiPanel, initialWidth}) => {
);
const about = multiPanel ? null :
- ;
+ ;
const sidebarModules = [
multiPanel ? {type: "AboutTopics"} : {type: null},
@@ -69,7 +69,7 @@ const TopicsPage = ({setNavTopic, multiPanel, initialWidth}) => {
{ about }
{ categoryListings }
-
+
diff --git a/static/js/TranslationsPage.jsx b/static/js/TranslationsPage.jsx
index 0b74aa7ea5..7afe428cd9 100644
--- a/static/js/TranslationsPage.jsx
+++ b/static/js/TranslationsPage.jsx
@@ -2,7 +2,7 @@ import { useState, useEffect } from "react";
import Sefaria from "./sefaria/sefaria";
import classNames from 'classnames';
import {InterfaceText, TabView} from './Misc';
-import { NavSidebar, Modules } from './NavSidebar';
+import { NavSidebar, SidebarModules } from './NavSidebar';
import Footer from './Footer';
@@ -89,7 +89,7 @@ const TranslationsPage = ({translationsSlug}) => {
>
}
-
+
diff --git a/static/js/UserHistoryPanel.jsx b/static/js/UserHistoryPanel.jsx
index c6aa7c30c8..1e6931c346 100644
--- a/static/js/UserHistoryPanel.jsx
+++ b/static/js/UserHistoryPanel.jsx
@@ -64,7 +64,7 @@ const UserHistoryPanel = ({menuOpen, toggleLanguage, openDisplaySettings, openNa
toggleSignUpModal={toggleSignUpModal}
key={menuOpen}/>
-
+
{footer}
From 1a7981121a69eb69c3b2739916da25ad6e092bbb Mon Sep 17 00:00:00 2001
From: nsantacruz
Date: Thu, 21 Nov 2024 22:22:29 +0200
Subject: [PATCH 081/151] 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 082/151] 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 083/151] 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 084/151] 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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