From a4ed82e74d74517db064c6bb395f73a4a756985e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 12 Nov 2025 10:22:49 -0500 Subject: [PATCH 01/17] fix(deps): update dependency django to v4.2.26 [security] (#2678) --- poetry.lock | 8 ++++---- pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/poetry.lock b/poetry.lock index 7490e12498..c16506d56e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1431,14 +1431,14 @@ static3 = "*" [[package]] name = "django" -version = "4.2.25" +version = "4.2.26" description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "django-4.2.25-py3-none-any.whl", hash = "sha256:9584cf26b174b35620e53c2558b09d7eb180a655a3470474f513ff9acb494f8c"}, - {file = "django-4.2.25.tar.gz", hash = "sha256:2391ab3d78191caaae2c963c19fd70b99e9751008da22a0adcc667c5a4f8d311"}, + {file = "django-4.2.26-py3-none-any.whl", hash = "sha256:c96e64fc3c359d051a6306871bd26243db1bd02317472a62ffdbe6c3cae14280"}, + {file = "django-4.2.26.tar.gz", hash = "sha256:9398e487bcb55e3f142cb56d19fbd9a83e15bb03a97edc31f408361ee76d9d7a"}, ] [package.dependencies] @@ -9255,4 +9255,4 @@ cffi = ["cffi (>=1.11)"] [metadata] lock-version = "2.1" python-versions = "~3.12" -content-hash = "2aa8bbab597a9404503908bf4680ef08bbe1254c277ba98e37ed13ba9c1526fb" +content-hash = "10d0ed2e309679dc79ce652d4c78a3035906ff572fa82763baec34039d97f03f" diff --git a/pyproject.toml b/pyproject.toml index 953a353ef1..62973be77d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ requires-poetry = ">2.1,<3" [tool.poetry.dependencies] python = "~3.12" -Django = "4.2.25" +Django = "4.2.26" attrs = "^25.0.0" base36 = "^0.1.1" beautifulsoup4 = "^4.8.2" From 0c13fefa62fd30e51087c857117d6169c747199d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 12 Nov 2025 10:55:15 -0500 Subject: [PATCH 02/17] chore(deps): update nginx docker tag to v1.29.3 (#2667) --- nginx/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nginx/Dockerfile b/nginx/Dockerfile index b944ee70be..7664c8cf69 100644 --- a/nginx/Dockerfile +++ b/nginx/Dockerfile @@ -3,7 +3,7 @@ # it's primary purpose is to emulate heroku-buildpack-nginx's # functionality that compiles config/nginx.conf.erb # See https://github.com/heroku/heroku-buildpack-nginx/blob/fefac6c569f28182b3459cb8e34b8ccafc403fde/bin/start-nginx -FROM nginx:1.29.2 +FROM nginx:1.29.3 # Logs are configured to a relatic path under /etc/nginx # but the container expects /var/log From 9f5720b9608eac22b26b445b0ebdfb8d00759289 Mon Sep 17 00:00:00 2001 From: Matt Bertrand Date: Wed, 12 Nov 2025 10:56:06 -0500 Subject: [PATCH 03/17] Avoid n+1 queries on video.playlists serializer field (#2662) --- learning_resources/filters_test.py | 9 ++++--- learning_resources/models.py | 18 +++++++++++++ learning_resources/serializers.py | 6 +---- learning_resources/serializers_test.py | 36 +++++++++++++++++++++++++- learning_resources/views_test.py | 4 +-- 5 files changed, 62 insertions(+), 11 deletions(-) diff --git a/learning_resources/filters_test.py b/learning_resources/filters_test.py index 9a33a3dc1b..6021ea6ac1 100644 --- a/learning_resources/filters_test.py +++ b/learning_resources/filters_test.py @@ -446,15 +446,18 @@ def test_learning_resource_filter_course_features(client): """Test that the resource_content_tag filter works""" resource_with_exams = LearningResourceFactory.create( - content_tags=LearningResourceContentTagFactory.create_batch(1, name="Exams") + content_tags=LearningResourceContentTagFactory.create_batch(1, name="Exams"), + resource_type=LearningResourceType.video.name, ) resource_with_notes = LearningResourceFactory.create( content_tags=LearningResourceContentTagFactory.create_batch( 1, name="Lecture Notes" - ) + ), + resource_type=LearningResourceType.video.name, ) LearningResourceFactory.create( - content_tags=LearningResourceContentTagFactory.create_batch(1, name="Other") + content_tags=LearningResourceContentTagFactory.create_batch(1, name="Other"), + resource_type=LearningResourceType.video.name, ) results = client.get(f"{RESOURCE_API_URL}?course_feature=exams").json()["results"] diff --git a/learning_resources/models.py b/learning_resources/models.py index f9c946d360..fc0661e68d 100644 --- a/learning_resources/models.py +++ b/learning_resources/models.py @@ -390,6 +390,13 @@ def for_serialization(self, *, user: Optional["User"] = None): ), to_attr="_podcasts", ), + Prefetch( + "parents", + queryset=LearningResourceRelationship.objects.filter( + relation_type=LearningResourceRelationTypes.PLAYLIST_VIDEOS.value, + ), + to_attr="_playlists", + ), Prefetch( "children", queryset=LearningResourceRelationship.objects.select_related( @@ -604,6 +611,17 @@ def podcasts(self) -> list["LearningResourceRelationship"]: ), ) + @cached_property + def playlists(self) -> list["LearningResourceRelationship"]: + """Return a list of playlists that the resource is in""" + return getattr( + self, + "_playlists", + self.parents.filter( + relation_type=LearningResourceRelationTypes.PLAYLIST_VIDEOS.value, + ), + ) + class Meta: unique_together = (("platform", "readable_id", "resource_type"),) diff --git a/learning_resources/serializers.py b/learning_resources/serializers.py index 9b13d0291b..0f56349eca 100644 --- a/learning_resources/serializers.py +++ b/learning_resources/serializers.py @@ -1097,11 +1097,7 @@ class VideoResourceSerializer(LearningResourceBaseSerializer): def get_playlists(self, instance) -> list[str]: """Get the playlist id(s) the video belongs to""" - return list( - instance.parents.filter( - relation_type=constants.LearningResourceRelationTypes.PLAYLIST_VIDEOS.value - ).values_list("parent__id", flat=True) - ) + return [playlist.parent_id for playlist in instance.playlists] class VideoPlaylistResourceSerializer(LearningResourceBaseSerializer): diff --git a/learning_resources/serializers_test.py b/learning_resources/serializers_test.py index c153f0ce11..06a065356b 100644 --- a/learning_resources/serializers_test.py +++ b/learning_resources/serializers_test.py @@ -33,7 +33,11 @@ LearningResourcePriceFactory, LearningResourceRunFactory, ) -from learning_resources.models import ContentFile, LearningResource +from learning_resources.models import ( + ContentFile, + LearningResource, + LearningResourceRelationship, +) from main.test_utils import assert_json_equal, drf_datetime from main.utils import frontend_absolute_url @@ -141,6 +145,36 @@ def test_serialize_podcast_episode_to_json(): ) +def test_serialize_video_resource_playlists_to_json(): + """ + Verify that a serialized video resource has the correct playlist data + """ + playlist = factories.VideoPlaylistFactory.create() + video = factories.VideoFactory.create() + LearningResourceRelationship.objects.get_or_create( + parent=playlist.learning_resource, + child=video.learning_resource, + relation_type=LearningResourceRelationTypes.PLAYLIST_VIDEOS.value, + ) + serializer = serializers.VideoResourceSerializer(instance=video.learning_resource) + assert serializer.data["playlists"] == [playlist.learning_resource.id] + + +def test_serialize_podcast_episode_playlists_to_json(): + """ + Verify that a serialized podcast episode resource has the correct podcast data + """ + podcast = factories.PodcastFactory.create() + podcast_episode = factories.PodcastEpisodeFactory.create() + LearningResourceRelationship.objects.get_or_create( + parent=podcast.learning_resource, + child=podcast_episode.learning_resource, + relation_type=LearningResourceRelationTypes.PODCAST_EPISODES.value, + ) + serializer = serializers.PodcastEpisodeSerializer(instance=podcast_episode) + assert serializer.data["podcasts"] == [podcast.learning_resource.id] + + @pytest.mark.parametrize("has_context", [True, False]) @pytest.mark.parametrize( ("params", "detail_key", "specific_serializer_cls", "detail_serializer_cls"), diff --git a/learning_resources/views_test.py b/learning_resources/views_test.py index ae5356958c..274d702eb6 100644 --- a/learning_resources/views_test.py +++ b/learning_resources/views_test.py @@ -194,7 +194,7 @@ def test_program_detail_endpoint(client, django_assert_num_queries, url): """Test program endpoint""" program = ProgramFactory.create() assert program.learning_resource.children.count() > 0 - with django_assert_num_queries(20): # should be same # regardless of child count + with django_assert_num_queries(21): # should be same # regardless of child count resp = client.get(reverse(url, args=[program.learning_resource.id])) assert resp.data.get("title") == program.learning_resource.title assert resp.data.get("resource_type") == LearningResourceType.program.name @@ -239,7 +239,7 @@ def test_no_excess_queries(rf, user, mocker, django_assert_num_queries, course_c request = rf.get("/") request.user = user - with django_assert_num_queries(22): + with django_assert_num_queries(23): view = CourseViewSet(request=request) results = view.get_queryset().all() assert len(results) == course_count From a43d1bf0dc4c9d633b54920ac08df41ea01d1a20 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 12 Nov 2025 11:26:18 -0500 Subject: [PATCH 04/17] chore(deps): update dependency ruff to v0.14.4 (#2666) --- poetry.lock | 42 +++++++++++++++++++++--------------------- pyproject.toml | 2 +- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/poetry.lock b/poetry.lock index c16506d56e..1a0975a980 100644 --- a/poetry.lock +++ b/poetry.lock @@ -7664,31 +7664,31 @@ files = [ [[package]] name = "ruff" -version = "0.14.2" +version = "0.14.4" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" groups = ["dev"] files = [ - {file = "ruff-0.14.2-py3-none-linux_armv6l.whl", hash = "sha256:7cbe4e593505bdec5884c2d0a4d791a90301bc23e49a6b1eb642dd85ef9c64f1"}, - {file = "ruff-0.14.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:8d54b561729cee92f8d89c316ad7a3f9705533f5903b042399b6ae0ddfc62e11"}, - {file = "ruff-0.14.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5c8753dfa44ebb2cde10ce5b4d2ef55a41fb9d9b16732a2c5df64620dbda44a3"}, - {file = "ruff-0.14.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d0bbeffb8d9f4fccf7b5198d566d0bad99a9cb622f1fc3467af96cb8773c9e3"}, - {file = "ruff-0.14.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7047f0c5a713a401e43a88d36843d9c83a19c584e63d664474675620aaa634a8"}, - {file = "ruff-0.14.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bf8d2f9aa1602599217d82e8e0af7fd33e5878c4d98f37906b7c93f46f9a839"}, - {file = "ruff-0.14.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:1c505b389e19c57a317cf4b42db824e2fca96ffb3d86766c1c9f8b96d32048a7"}, - {file = "ruff-0.14.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a307fc45ebd887b3f26b36d9326bb70bf69b01561950cdcc6c0bdf7bb8e0f7cc"}, - {file = "ruff-0.14.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:61ae91a32c853172f832c2f40bd05fd69f491db7289fb85a9b941ebdd549781a"}, - {file = "ruff-0.14.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1967e40286f63ee23c615e8e7e98098dedc7301568bd88991f6e544d8ae096"}, - {file = "ruff-0.14.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:2877f02119cdebf52a632d743a2e302dea422bfae152ebe2f193d3285a3a65df"}, - {file = "ruff-0.14.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e681c5bc777de5af898decdcb6ba3321d0d466f4cb43c3e7cc2c3b4e7b843a05"}, - {file = "ruff-0.14.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e21be42d72e224736f0c992cdb9959a2fa53c7e943b97ef5d081e13170e3ffc5"}, - {file = "ruff-0.14.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b8264016f6f209fac16262882dbebf3f8be1629777cf0f37e7aff071b3e9b92e"}, - {file = "ruff-0.14.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5ca36b4cb4db3067a3b24444463ceea5565ea78b95fe9a07ca7cb7fd16948770"}, - {file = "ruff-0.14.2-py3-none-win32.whl", hash = "sha256:41775927d287685e08f48d8eb3f765625ab0b7042cc9377e20e64f4eb0056ee9"}, - {file = "ruff-0.14.2-py3-none-win_amd64.whl", hash = "sha256:0df3424aa5c3c08b34ed8ce099df1021e3adaca6e90229273496b839e5a7e1af"}, - {file = "ruff-0.14.2-py3-none-win_arm64.whl", hash = "sha256:ea9d635e83ba21569fbacda7e78afbfeb94911c9434aff06192d9bc23fd5495a"}, - {file = "ruff-0.14.2.tar.gz", hash = "sha256:98da787668f239313d9c902ca7c523fe11b8ec3f39345553a51b25abc4629c96"}, + {file = "ruff-0.14.4-py3-none-linux_armv6l.whl", hash = "sha256:e6604613ffbcf2297cd5dcba0e0ac9bd0c11dc026442dfbb614504e87c349518"}, + {file = "ruff-0.14.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d99c0b52b6f0598acede45ee78288e5e9b4409d1ce7f661f0fa36d4cbeadf9a4"}, + {file = "ruff-0.14.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:9358d490ec030f1b51d048a7fd6ead418ed0826daf6149e95e30aa67c168af33"}, + {file = "ruff-0.14.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81b40d27924f1f02dfa827b9c0712a13c0e4b108421665322218fc38caf615c2"}, + {file = "ruff-0.14.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f5e649052a294fe00818650712083cddc6cc02744afaf37202c65df9ea52efa5"}, + {file = "ruff-0.14.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa082a8f878deeba955531f975881828fd6afd90dfa757c2b0808aadb437136e"}, + {file = "ruff-0.14.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:1043c6811c2419e39011890f14d0a30470f19d47d197c4858b2787dfa698f6c8"}, + {file = "ruff-0.14.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a9f3a936ac27fb7c2a93e4f4b943a662775879ac579a433291a6f69428722649"}, + {file = "ruff-0.14.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:95643ffd209ce78bc113266b88fba3d39e0461f0cbc8b55fb92505030fb4a850"}, + {file = "ruff-0.14.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:456daa2fa1021bc86ca857f43fe29d5d8b3f0e55e9f90c58c317c1dcc2afc7b5"}, + {file = "ruff-0.14.4-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:f911bba769e4a9f51af6e70037bb72b70b45a16db5ce73e1f72aefe6f6d62132"}, + {file = "ruff-0.14.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:76158a7369b3979fa878612c623a7e5430c18b2fd1c73b214945c2d06337db67"}, + {file = "ruff-0.14.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f3b8f3b442d2b14c246e7aeca2e75915159e06a3540e2f4bed9f50d062d24469"}, + {file = "ruff-0.14.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c62da9a06779deecf4d17ed04939ae8b31b517643b26370c3be1d26f3ef7dbde"}, + {file = "ruff-0.14.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5a443a83a1506c684e98acb8cb55abaf3ef725078be40237463dae4463366349"}, + {file = "ruff-0.14.4-py3-none-win32.whl", hash = "sha256:643b69cb63cd996f1fc7229da726d07ac307eae442dd8974dbc7cf22c1e18fff"}, + {file = "ruff-0.14.4-py3-none-win_amd64.whl", hash = "sha256:26673da283b96fe35fa0c939bf8411abec47111644aa9f7cfbd3c573fb125d2c"}, + {file = "ruff-0.14.4-py3-none-win_arm64.whl", hash = "sha256:dd09c292479596b0e6fec8cd95c65c3a6dc68e9ad17b8f2382130f87ff6a75bb"}, + {file = "ruff-0.14.4.tar.gz", hash = "sha256:f459a49fe1085a749f15414ca76f61595f1a2cc8778ed7c279b6ca2e1fd19df3"}, ] [[package]] @@ -9255,4 +9255,4 @@ cffi = ["cffi (>=1.11)"] [metadata] lock-version = "2.1" python-versions = "~3.12" -content-hash = "10d0ed2e309679dc79ce652d4c78a3035906ff572fa82763baec34039d97f03f" +content-hash = "42a914162ecf635b336582e61aceb5effbb6ce5eef71b8b66f9c52c4a40d1674" diff --git a/pyproject.toml b/pyproject.toml index 62973be77d..1f9ad4c1db 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -134,7 +134,7 @@ pytest-env = "^1.0.0" pytest-freezegun = "^0.4.2" pytest-mock = "^3.10.0" responses = "^0.25.0" -ruff = "0.14.2" +ruff = "0.14.4" safety = "^3.0.0" semantic-version = "^2.10.0" freezegun = "^1.4.0" From 0ae051371f0391bff27f7b7bcdc1d9f0e8b84de0 Mon Sep 17 00:00:00 2001 From: Shankar Ambady Date: Wed, 12 Nov 2025 16:04:38 -0500 Subject: [PATCH 05/17] limit offered by facet to specific offerors (#2692) * adding facet display field and migration * adding display facet to opensearch * make sure we only show offerrors with display_facet set to true * regenerate spec * check null * fix test * fixing test * lint fix * adding test for offered by filter * lint fix * lint fix * lint fix * fix flaky test * removed use of 'in' operator * remove display_facet from search index * get offeror directly from object in db * fix tests * fix typecheck --- frontends/api/src/generated/v0/api.ts | 18 ++++++ frontends/api/src/generated/v1/api.ts | 18 ++++++ .../app-pages/SearchPage/SearchPage.test.tsx | 55 +++++++++++++++++++ .../SearchDisplay/SearchDisplay.tsx | 46 +++++++++++++--- ...0_learningresourceofferor_display_facet.py | 17 ++++++ learning_resources/models.py | 2 + learning_resources/serializers.py | 2 +- learning_resources/serializers_test.py | 1 + openapi/specs/v0.yaml | 6 ++ openapi/specs/v1.yaml | 6 ++ vector_search/tasks.py | 26 +++++---- 11 files changed, 176 insertions(+), 21 deletions(-) create mode 100644 learning_resources/migrations/0100_learningresourceofferor_display_facet.py diff --git a/frontends/api/src/generated/v0/api.ts b/frontends/api/src/generated/v0/api.ts index 49f4ba2a50..fd4558e580 100644 --- a/frontends/api/src/generated/v0/api.ts +++ b/frontends/api/src/generated/v0/api.ts @@ -2714,6 +2714,12 @@ export interface LearningResourceOfferor { * @memberof LearningResourceOfferor */ channel_url: string | null + /** + * + * @type {boolean} + * @memberof LearningResourceOfferor + */ + display_facet?: boolean } /** * Serializer for LearningResourceOfferor with all details @@ -2793,6 +2799,12 @@ export interface LearningResourceOfferorDetail { * @memberof LearningResourceOfferorDetail */ value_prop?: string + /** + * + * @type {boolean} + * @memberof LearningResourceOfferorDetail + */ + display_facet?: boolean } /** * Serializer for LearningResourceOfferor with all details @@ -2866,6 +2878,12 @@ export interface LearningResourceOfferorDetailRequest { * @memberof LearningResourceOfferorDetailRequest */ value_prop?: string + /** + * + * @type {boolean} + * @memberof LearningResourceOfferorDetailRequest + */ + display_facet?: boolean } /** * Serializer for LearningResourcePlatform diff --git a/frontends/api/src/generated/v1/api.ts b/frontends/api/src/generated/v1/api.ts index a19401073e..c39d485372 100644 --- a/frontends/api/src/generated/v1/api.ts +++ b/frontends/api/src/generated/v1/api.ts @@ -2911,6 +2911,12 @@ export interface LearningResourceOfferor { * @memberof LearningResourceOfferor */ channel_url: string | null + /** + * + * @type {boolean} + * @memberof LearningResourceOfferor + */ + display_facet?: boolean } /** * Serializer for LearningResourceOfferor with all details @@ -2990,6 +2996,12 @@ export interface LearningResourceOfferorDetail { * @memberof LearningResourceOfferorDetail */ value_prop?: string + /** + * + * @type {boolean} + * @memberof LearningResourceOfferorDetail + */ + display_facet?: boolean } /** * Serializer for LearningResourceOfferor with basic details @@ -3009,6 +3021,12 @@ export interface LearningResourceOfferorRequest { * @memberof LearningResourceOfferorRequest */ name: string + /** + * + * @type {boolean} + * @memberof LearningResourceOfferorRequest + */ + display_facet?: boolean } /** * Serializer for LearningResourcePlatform diff --git a/frontends/main/src/app-pages/SearchPage/SearchPage.test.tsx b/frontends/main/src/app-pages/SearchPage/SearchPage.test.tsx index 92a365a7de..16099f0853 100644 --- a/frontends/main/src/app-pages/SearchPage/SearchPage.test.tsx +++ b/frontends/main/src/app-pages/SearchPage/SearchPage.test.tsx @@ -583,9 +583,17 @@ describe("Search Page Tabs", () => { test("Facet 'Offered By' uses API response for names", async () => { const offerors = factories.learningResources.offerors({ count: 3 }) + for (const offeror of offerors.results) { + offeror.display_facet = true + } + const resources = factories.learningResources.resources({ + count: 3, + }).results + setMockApiResponses({ offerors, search: { + results: resources, metadata: { aggregations: { offered_by: offerors.results.map((o, i) => ({ @@ -618,6 +626,53 @@ test("Facet 'Offered By' uses API response for names", async () => { expect(offeror2).toBeVisible() }) +test("Facet 'Offered By' only shows facets with 'display_facet' set to true", async () => { + const offerors = factories.learningResources.offerors({ count: 3 }) + + const resources = factories.learningResources.resources({ + count: 3, + }).results + + offerors.results[0].display_facet = true + offerors.results[1]!.display_facet = false + offerors.results[2]!.display_facet = false + + setMockApiResponses({ + search: { + results: resources, + metadata: { + aggregations: { + offered_by: offerors.results.map((o, i) => ({ + key: o.code, + doc_count: 10 + i, + })), + }, + suggestions: [], + }, + }, + offerors: offerors, + }) + renderWithProviders() + const showFacetButton = await screen.findByRole("button", { + name: /Offered By/i, + }) + + await user.click(showFacetButton) + + const offeror0 = await screen.findByRole("checkbox", { + name: `${offerors.results[0].name} 10`, + }) + const offeror1 = screen.queryByRole("checkbox", { + name: `${offerors.results[1].name} 11`, + }) + const offeror2 = screen.queryByRole("checkbox", { + name: `${offerors.results[2].name} 12`, + }) + expect(offeror0).toBeVisible() + expect(offeror1).not.toBeInTheDocument() + expect(offeror2).not.toBeInTheDocument() +}) + test("Set sort", async () => { setMockApiResponses({ search: { count: 137 } }) diff --git a/frontends/main/src/page-components/SearchDisplay/SearchDisplay.tsx b/frontends/main/src/page-components/SearchDisplay/SearchDisplay.tsx index 95018492b1..e8c6927dab 100644 --- a/frontends/main/src/page-components/SearchDisplay/SearchDisplay.tsx +++ b/frontends/main/src/page-components/SearchDisplay/SearchDisplay.tsx @@ -19,7 +19,7 @@ import { childCheckboxStyles, VisuallyHidden, } from "@mitodl/smoot-design" - +import { keyBy } from "lodash" import { RiCloseLine, RiArrowLeftLine, @@ -28,13 +28,16 @@ import { RiArrowUpSLine, RiArrowDownSLine, } from "@remixicon/react" - +import { + useOfferorsList, + learningResourceQueries, +} from "api/hooks/learningResources" import { LearningResourcesSearchApiLearningResourcesSearchRetrieveRequest as LRSearchRequest, ResourceCategoryEnum, SearchModeEnumDescriptions, } from "api" -import { useLearningResourcesSearch } from "api/hooks/learningResources" +import { keepPreviousData, useQuery } from "@tanstack/react-query" import { useAdminSearchParams } from "api/hooks/adminSearchParams" import { AvailableFacets, @@ -568,12 +571,39 @@ const SearchDisplay: React.FC = ({ facetNames, page, ]) + const offerorsQuery = useOfferorsList() + const offerors = useMemo(() => { + return keyBy(offerorsQuery.data?.results ?? [], (o) => o.code) + }, [offerorsQuery.data?.results]) + + const { data, isLoading, isFetching } = useQuery({ + ...learningResourceQueries.search(allParams as LRSearchRequest), + placeholderData: keepPreviousData, + select: (data) => { + // Handle missing data gracefully + if (!data.metadata.aggregations.offered_by || data.results.length === 0) { + return data + } - const { data, isLoading, isFetching } = useLearningResourcesSearch( - allParams as LRSearchRequest, - { keepPreviousData: true }, - ) - + // only show offerors with display_facet set + const displayOfferors = Object.values(offerors) + .filter((value) => value.code && value.display_facet) + .map((value) => value?.code) + + return { + ...data, + metadata: { + ...data.metadata, + aggregations: { + ...data.metadata.aggregations, + offered_by: data.metadata.aggregations.offered_by.filter( + (value) => value && displayOfferors.includes(value.key), + ), + }, + }, + } + }, + }) const { data: user } = useUserMe() const [mobileDrawerOpen, setMobileDrawerOpen] = React.useState(false) diff --git a/learning_resources/migrations/0100_learningresourceofferor_display_facet.py b/learning_resources/migrations/0100_learningresourceofferor_display_facet.py new file mode 100644 index 0000000000..13054b65cc --- /dev/null +++ b/learning_resources/migrations/0100_learningresourceofferor_display_facet.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.25 on 2025-11-06 19:55 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("learning_resources", "0099_alter_learningresource_resource_type"), + ] + + operations = [ + migrations.AddField( + model_name="learningresourceofferor", + name="display_facet", + field=models.BooleanField(default=True), + ), + ] diff --git a/learning_resources/models.py b/learning_resources/models.py index fc0661e68d..ef5391b82b 100644 --- a/learning_resources/models.py +++ b/learning_resources/models.py @@ -173,6 +173,8 @@ class LearningResourceOfferor(TimestampedModel): more_information = models.URLField(blank=True) # This field name means "value proposition" value_prop = models.TextField(blank=True) + # whether or not to show this offeror as a facet in the UI + display_facet = models.BooleanField(default=True) @cached_property def channel_url(self): diff --git a/learning_resources/serializers.py b/learning_resources/serializers.py index 0f56349eca..cd91b5e582 100644 --- a/learning_resources/serializers.py +++ b/learning_resources/serializers.py @@ -113,7 +113,7 @@ class LearningResourceOfferorSerializer(serializers.ModelSerializer): class Meta: model = models.LearningResourceOfferor - fields = ("code", "name", "channel_url") + fields = ("code", "name", "channel_url", "display_facet") class LearningResourceOfferorDetailSerializer(LearningResourceOfferorSerializer): diff --git a/learning_resources/serializers_test.py b/learning_resources/serializers_test.py index 06a065356b..8a5eb81a85 100644 --- a/learning_resources/serializers_test.py +++ b/learning_resources/serializers_test.py @@ -558,6 +558,7 @@ def test_content_file_serializer(settings, expected_types, has_channels): "offered_by": { "name": content_file.run.learning_resource.offered_by.name, "code": content_file.run.learning_resource.offered_by.code, + "display_facet": True, "channel_url": frontend_absolute_url( f"/c/unit/{Channel.objects.get(unit_detail__unit=content_file.run.learning_resource.offered_by).name}/" ) diff --git a/openapi/specs/v0.yaml b/openapi/specs/v0.yaml index df688e73ff..5c23c5dbb1 100644 --- a/openapi/specs/v0.yaml +++ b/openapi/specs/v0.yaml @@ -3379,6 +3379,8 @@ components: type: string readOnly: true nullable: true + display_facet: + type: boolean required: - channel_url - code @@ -3435,6 +3437,8 @@ components: maxLength: 200 value_prop: type: string + display_facet: + type: boolean required: - channel_url - code @@ -3495,6 +3499,8 @@ components: maxLength: 200 value_prop: type: string + display_facet: + type: boolean required: - code - name diff --git a/openapi/specs/v1.yaml b/openapi/specs/v1.yaml index 5d11aa2aab..cb26bd097b 100644 --- a/openapi/specs/v1.yaml +++ b/openapi/specs/v1.yaml @@ -11626,6 +11626,8 @@ components: type: string readOnly: true nullable: true + display_facet: + type: boolean required: - channel_url - code @@ -11682,6 +11684,8 @@ components: maxLength: 200 value_prop: type: string + display_facet: + type: boolean required: - channel_url - code @@ -11698,6 +11702,8 @@ components: type: string minLength: 1 maxLength: 256 + display_facet: + type: boolean required: - code - name diff --git a/vector_search/tasks.py b/vector_search/tasks.py index b9e724e695..36d475e8e8 100644 --- a/vector_search/tasks.py +++ b/vector_search/tasks.py @@ -381,7 +381,8 @@ def embeddings_healthcheck(): """ Check for missing embeddings and summaries in Qdrant and log warnings to Sentry """ - remaining_content_files = [] + + remaining_content_file_ids = [] remaining_resources = [] resource_point_ids = {} all_resources = LearningResource.objects.filter( @@ -405,10 +406,14 @@ def embeddings_healthcheck(): ) content_file_point_ids[point_id] = {"key": cf.key, "id": cf.id} for batch in chunks(content_file_point_ids.keys(), chunk_size=200): - remaining_content_files.extend( - filter_existing_qdrant_points_by_ids( - batch, collection_name=CONTENT_FILES_COLLECTION_NAME - ) + remaining_content_files = filter_existing_qdrant_points_by_ids( + batch, collection_name=CONTENT_FILES_COLLECTION_NAME + ) + remaining_content_file_ids.extend( + [ + content_file_point_ids.get(p, {}).get("id") + for p in remaining_content_files + ] ) for batch in chunks( @@ -422,16 +427,13 @@ def embeddings_healthcheck(): ) ) - remaining_content_file_ids = [ - content_file_point_ids.get(p, {}).get("id") for p in remaining_content_files - ] remaining_resource_ids = [ resource_point_ids.get(p, {}).get("id") for p in remaining_resources ] missing_summaries = _missing_summaries() log.info( "Embeddings healthcheck found %d missing content file embeddings", - len(remaining_content_files), + len(remaining_content_file_ids), ) log.info( "Embeddings healthcheck found %d missing resource embeddings", @@ -442,12 +444,12 @@ def embeddings_healthcheck(): len(missing_summaries), ) - if len(remaining_content_files) > 0: + if len(remaining_content_file_ids) > 0: _sentry_healthcheck_log( "embeddings", "missing_content_file_embeddings", { - "count": len(remaining_content_files), + "count": len(remaining_content_file_ids), "ids": remaining_content_file_ids, "run_ids": set( ContentFile.objects.filter( @@ -455,7 +457,7 @@ def embeddings_healthcheck(): ).values_list("run__run_id", flat=True)[:100] ), }, - f"Warning: {len(remaining_content_files)} missing content file " + f"Warning: {len(remaining_content_file_ids)} missing content file " "embeddings detected", ) From 2f2ff78339d1a479b0edf9636724b70569975960 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 12 Nov 2025 16:23:09 -0500 Subject: [PATCH 06/17] fix(deps): update dependency litellm to v1.79.3 (#2618) --- poetry.lock | 99 +++++++++++++++++++++++++++++++++++++++++++++++--- pyproject.toml | 2 +- 2 files changed, 95 insertions(+), 6 deletions(-) diff --git a/poetry.lock b/poetry.lock index 1a0975a980..6da2843fe2 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2025,6 +2025,94 @@ requests = ">=2.31,<3.0" tokenizers = ">=0.15,<1.0" tqdm = ">=4.66,<5.0" +[[package]] +name = "fastuuid" +version = "0.14.0" +description = "Python bindings to Rust's UUID library." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "fastuuid-0.14.0-cp310-cp310-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:6e6243d40f6c793c3e2ee14c13769e341b90be5ef0c23c82fa6515a96145181a"}, + {file = "fastuuid-0.14.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:13ec4f2c3b04271f62be2e1ce7e95ad2dd1cf97e94503a3760db739afbd48f00"}, + {file = "fastuuid-0.14.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b2fdd48b5e4236df145a149d7125badb28e0a383372add3fbaac9a6b7a394470"}, + {file = "fastuuid-0.14.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f74631b8322d2780ebcf2d2d75d58045c3e9378625ec51865fe0b5620800c39d"}, + {file = "fastuuid-0.14.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83cffc144dc93eb604b87b179837f2ce2af44871a7b323f2bfed40e8acb40ba8"}, + {file = "fastuuid-0.14.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a771f135ab4523eb786e95493803942a5d1fc1610915f131b363f55af53b219"}, + {file = "fastuuid-0.14.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4edc56b877d960b4eda2c4232f953a61490c3134da94f3c28af129fb9c62a4f6"}, + {file = "fastuuid-0.14.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:bcc96ee819c282e7c09b2eed2b9bd13084e3b749fdb2faf58c318d498df2efbe"}, + {file = "fastuuid-0.14.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7a3c0bca61eacc1843ea97b288d6789fbad7400d16db24e36a66c28c268cfe3d"}, + {file = "fastuuid-0.14.0-cp310-cp310-win32.whl", hash = "sha256:7f2f3efade4937fae4e77efae1af571902263de7b78a0aee1a1653795a093b2a"}, + {file = "fastuuid-0.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:ae64ba730d179f439b0736208b4c279b8bc9c089b102aec23f86512ea458c8a4"}, + {file = "fastuuid-0.14.0-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:73946cb950c8caf65127d4e9a325e2b6be0442a224fd51ba3b6ac44e1912ce34"}, + {file = "fastuuid-0.14.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:12ac85024637586a5b69645e7ed986f7535106ed3013640a393a03e461740cb7"}, + {file = "fastuuid-0.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:05a8dde1f395e0c9b4be515b7a521403d1e8349443e7641761af07c7ad1624b1"}, + {file = "fastuuid-0.14.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09378a05020e3e4883dfdab438926f31fea15fd17604908f3d39cbeb22a0b4dc"}, + {file = "fastuuid-0.14.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbb0c4b15d66b435d2538f3827f05e44e2baafcc003dd7d8472dc67807ab8fd8"}, + {file = "fastuuid-0.14.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cd5a7f648d4365b41dbf0e38fe8da4884e57bed4e77c83598e076ac0c93995e7"}, + {file = "fastuuid-0.14.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c0a94245afae4d7af8c43b3159d5e3934c53f47140be0be624b96acd672ceb73"}, + {file = "fastuuid-0.14.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:2b29e23c97e77c3a9514d70ce343571e469098ac7f5a269320a0f0b3e193ab36"}, + {file = "fastuuid-0.14.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1e690d48f923c253f28151b3a6b4e335f2b06bf669c68a02665bc150b7839e94"}, + {file = "fastuuid-0.14.0-cp311-cp311-win32.whl", hash = "sha256:a6f46790d59ab38c6aa0e35c681c0484b50dc0acf9e2679c005d61e019313c24"}, + {file = "fastuuid-0.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:e150eab56c95dc9e3fefc234a0eedb342fac433dacc273cd4d150a5b0871e1fa"}, + {file = "fastuuid-0.14.0-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:77e94728324b63660ebf8adb27055e92d2e4611645bf12ed9d88d30486471d0a"}, + {file = "fastuuid-0.14.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:caa1f14d2102cb8d353096bc6ef6c13b2c81f347e6ab9d6fbd48b9dea41c153d"}, + {file = "fastuuid-0.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d23ef06f9e67163be38cece704170486715b177f6baae338110983f99a72c070"}, + {file = "fastuuid-0.14.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c9ec605ace243b6dbe3bd27ebdd5d33b00d8d1d3f580b39fdd15cd96fd71796"}, + {file = "fastuuid-0.14.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:808527f2407f58a76c916d6aa15d58692a4a019fdf8d4c32ac7ff303b7d7af09"}, + {file = "fastuuid-0.14.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2fb3c0d7fef6674bbeacdd6dbd386924a7b60b26de849266d1ff6602937675c8"}, + {file = "fastuuid-0.14.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab3f5d36e4393e628a4df337c2c039069344db5f4b9d2a3c9cea48284f1dd741"}, + {file = "fastuuid-0.14.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:b9a0ca4f03b7e0b01425281ffd44e99d360e15c895f1907ca105854ed85e2057"}, + {file = "fastuuid-0.14.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3acdf655684cc09e60fb7e4cf524e8f42ea760031945aa8086c7eae2eeeabeb8"}, + {file = "fastuuid-0.14.0-cp312-cp312-win32.whl", hash = "sha256:9579618be6280700ae36ac42c3efd157049fe4dd40ca49b021280481c78c3176"}, + {file = "fastuuid-0.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:d9e4332dc4ba054434a9594cbfaf7823b57993d7d8e7267831c3e059857cf397"}, + {file = "fastuuid-0.14.0-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:77a09cb7427e7af74c594e409f7731a0cf887221de2f698e1ca0ebf0f3139021"}, + {file = "fastuuid-0.14.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:9bd57289daf7b153bfa3e8013446aa144ce5e8c825e9e366d455155ede5ea2dc"}, + {file = "fastuuid-0.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ac60fc860cdf3c3f327374db87ab8e064c86566ca8c49d2e30df15eda1b0c2d5"}, + {file = "fastuuid-0.14.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab32f74bd56565b186f036e33129da77db8be09178cd2f5206a5d4035fb2a23f"}, + {file = "fastuuid-0.14.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33e678459cf4addaedd9936bbb038e35b3f6b2061330fd8f2f6a1d80414c0f87"}, + {file = "fastuuid-0.14.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1e3cc56742f76cd25ecb98e4b82a25f978ccffba02e4bdce8aba857b6d85d87b"}, + {file = "fastuuid-0.14.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cb9a030f609194b679e1660f7e32733b7a0f332d519c5d5a6a0a580991290022"}, + {file = "fastuuid-0.14.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:09098762aad4f8da3a888eb9ae01c84430c907a297b97166b8abc07b640f2995"}, + {file = "fastuuid-0.14.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:1383fff584fa249b16329a059c68ad45d030d5a4b70fb7c73a08d98fd53bcdab"}, + {file = "fastuuid-0.14.0-cp313-cp313-win32.whl", hash = "sha256:a0809f8cc5731c066c909047f9a314d5f536c871a7a22e815cc4967c110ac9ad"}, + {file = "fastuuid-0.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:0df14e92e7ad3276327631c9e7cec09e32572ce82089c55cb1bb8df71cf394ed"}, + {file = "fastuuid-0.14.0-cp314-cp314-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:b852a870a61cfc26c884af205d502881a2e59cc07076b60ab4a951cc0c94d1ad"}, + {file = "fastuuid-0.14.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c7502d6f54cd08024c3ea9b3514e2d6f190feb2f46e6dbcd3747882264bb5f7b"}, + {file = "fastuuid-0.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1ca61b592120cf314cfd66e662a5b54a578c5a15b26305e1b8b618a6f22df714"}, + {file = "fastuuid-0.14.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa75b6657ec129d0abded3bec745e6f7ab642e6dba3a5272a68247e85f5f316f"}, + {file = "fastuuid-0.14.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8a0dfea3972200f72d4c7df02c8ac70bad1bb4c58d7e0ec1e6f341679073a7f"}, + {file = "fastuuid-0.14.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1bf539a7a95f35b419f9ad105d5a8a35036df35fdafae48fb2fd2e5f318f0d75"}, + {file = "fastuuid-0.14.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:9a133bf9cc78fdbd1179cb58a59ad0100aa32d8675508150f3658814aeefeaa4"}, + {file = "fastuuid-0.14.0-cp314-cp314-musllinux_1_1_i686.whl", hash = "sha256:f54d5b36c56a2d5e1a31e73b950b28a0d83eb0c37b91d10408875a5a29494bad"}, + {file = "fastuuid-0.14.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:ec27778c6ca3393ef662e2762dba8af13f4ec1aaa32d08d77f71f2a70ae9feb8"}, + {file = "fastuuid-0.14.0-cp314-cp314-win32.whl", hash = "sha256:e23fc6a83f112de4be0cc1990e5b127c27663ae43f866353166f87df58e73d06"}, + {file = "fastuuid-0.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:df61342889d0f5e7a32f7284e55ef95103f2110fee433c2ae7c2c0956d76ac8a"}, + {file = "fastuuid-0.14.0-cp38-cp38-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:47c821f2dfe95909ead0085d4cb18d5149bca704a2b03e03fb3f81a5202d8cea"}, + {file = "fastuuid-0.14.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:3964bab460c528692c70ab6b2e469dd7a7b152fbe8c18616c58d34c93a6cf8d4"}, + {file = "fastuuid-0.14.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c501561e025b7aea3508719c5801c360c711d5218fc4ad5d77bf1c37c1a75779"}, + {file = "fastuuid-0.14.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2dce5d0756f046fa792a40763f36accd7e466525c5710d2195a038f93ff96346"}, + {file = "fastuuid-0.14.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:193ca10ff553cf3cc461572da83b5780fc0e3eea28659c16f89ae5202f3958d4"}, + {file = "fastuuid-0.14.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0737606764b29785566f968bd8005eace73d3666bd0862f33a760796e26d1ede"}, + {file = "fastuuid-0.14.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e0976c0dff7e222513d206e06341503f07423aceb1db0b83ff6851c008ceee06"}, + {file = "fastuuid-0.14.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6fbc49a86173e7f074b1a9ec8cf12ca0d54d8070a85a06ebf0e76c309b84f0d0"}, + {file = "fastuuid-0.14.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:de01280eabcd82f7542828ecd67ebf1551d37203ecdfd7ab1f2e534edb78d505"}, + {file = "fastuuid-0.14.0-cp38-cp38-win32.whl", hash = "sha256:af5967c666b7d6a377098849b07f83462c4fedbafcf8eb8bc8ff05dcbe8aa209"}, + {file = "fastuuid-0.14.0-cp38-cp38-win_amd64.whl", hash = "sha256:c3091e63acf42f56a6f74dc65cfdb6f99bfc79b5913c8a9ac498eb7ca09770a8"}, + {file = "fastuuid-0.14.0-cp39-cp39-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:2ec3d94e13712a133137b2805073b65ecef4a47217d5bac15d8ac62376cefdb4"}, + {file = "fastuuid-0.14.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:139d7ff12bb400b4a0c76be64c28cbe2e2edf60b09826cbfd85f33ed3d0bbe8b"}, + {file = "fastuuid-0.14.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d55b7e96531216fc4f071909e33e35e5bfa47962ae67d9e84b00a04d6e8b7173"}, + {file = "fastuuid-0.14.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c0eb25f0fd935e376ac4334927a59e7c823b36062080e2e13acbaf2af15db836"}, + {file = "fastuuid-0.14.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:089c18018fdbdda88a6dafd7d139f8703a1e7c799618e33ea25eb52503d28a11"}, + {file = "fastuuid-0.14.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2fc37479517d4d70c08696960fad85494a8a7a0af4e93e9a00af04d74c59f9e3"}, + {file = "fastuuid-0.14.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:73657c9f778aba530bc96a943d30e1a7c80edb8278df77894fe9457540df4f85"}, + {file = "fastuuid-0.14.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d31f8c257046b5617fc6af9c69be066d2412bdef1edaa4bdf6a214cf57806105"}, + {file = "fastuuid-0.14.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5816d41f81782b209843e52fdef757a361b448d782452d96abedc53d545da722"}, + {file = "fastuuid-0.14.0-cp39-cp39-win32.whl", hash = "sha256:448aa6833f7a84bfe37dd47e33df83250f404d591eb83527fa2cac8d1e57d7f3"}, + {file = "fastuuid-0.14.0-cp39-cp39-win_amd64.whl", hash = "sha256:84b0779c5abbdec2a9511d5ffbfcd2e53079bf889824b32be170c0d8ef5fc74c"}, + {file = "fastuuid-0.14.0.tar.gz", hash = "sha256:178947fc2f995b38497a74172adee64fdeb8b7ec18f2a5934d037641ba265d26"}, +] + [[package]] name = "feedparser" version = "6.0.11" @@ -3659,19 +3747,20 @@ pytest = ["pytest (>=7.0.0)", "rich (>=13.9.4,<14.0.0)"] [[package]] name = "litellm" -version = "1.76.0" +version = "1.79.3" description = "Library to easily interface with LLM API providers" optional = false python-versions = "!=2.7.*,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,!=3.7.*,>=3.8" groups = ["main"] files = [ - {file = "litellm-1.76.0-py3-none-any.whl", hash = "sha256:357464242fc1eeda384810c9e334e48ad67a50ecd30cf61e86c15f89e2f2e0b4"}, - {file = "litellm-1.76.0.tar.gz", hash = "sha256:d26d12333135edd72af60e0e310284dac3b079f4d7c47c79dfbb2430b9b4b421"}, + {file = "litellm-1.79.3-py3-none-any.whl", hash = "sha256:16314049d109e5cadb2abdccaf2e07ea03d2caa3a9b3f54f34b5b825092b4eeb"}, + {file = "litellm-1.79.3.tar.gz", hash = "sha256:4da4716f8da3e1b77838262c36d3016146860933e0489171658a9d4a3fd59b1b"}, ] [package.dependencies] aiohttp = ">=3.10" click = "*" +fastuuid = ">=0.13.0" httpx = ">=0.23.0" importlib-metadata = ">=6.8.0" jinja2 = ">=3.1.2,<4.0.0" @@ -3686,7 +3775,7 @@ tokenizers = "*" caching = ["diskcache (>=5.6.1,<6.0.0)"] extra-proxy = ["azure-identity (>=1.15.0,<2.0.0)", "azure-keyvault-secrets (>=4.8.0,<5.0.0)", "google-cloud-iam (>=2.19.1,<3.0.0)", "google-cloud-kms (>=2.21.3,<3.0.0)", "prisma (==0.11.0)", "redisvl (>=0.4.1,<0.5.0) ; python_version >= \"3.9\" and python_version < \"3.14\"", "resend (>=0.8.0,<0.9.0)"] mlflow = ["mlflow (>3.1.4) ; python_version >= \"3.10\""] -proxy = ["PyJWT (>=2.8.0,<3.0.0)", "apscheduler (>=3.10.4,<4.0.0)", "azure-identity (>=1.15.0,<2.0.0)", "azure-storage-blob (>=12.25.1,<13.0.0)", "backoff", "boto3 (==1.36.0)", "cryptography (>=43.0.1,<44.0.0)", "fastapi (>=0.115.5,<0.116.0)", "fastapi-sso (>=0.16.0,<0.17.0)", "gunicorn (>=23.0.0,<24.0.0)", "litellm-enterprise (==0.1.19)", "litellm-proxy-extras (==0.2.18)", "mcp (>=1.10.0,<2.0.0) ; python_version >= \"3.10\"", "orjson (>=3.9.7,<4.0.0)", "polars (>=1.31.0,<2.0.0) ; python_version >= \"3.10\"", "pynacl (>=1.5.0,<2.0.0)", "python-multipart (>=0.0.18,<0.0.19)", "pyyaml (>=6.0.1,<7.0.0)", "rich (==13.7.1)", "rq", "uvicorn (>=0.29.0,<0.30.0)", "uvloop (>=0.21.0,<0.22.0) ; sys_platform != \"win32\"", "websockets (>=13.1.0,<14.0.0)"] +proxy = ["PyJWT (>=2.8.0,<3.0.0)", "apscheduler (>=3.10.4,<4.0.0)", "azure-identity (>=1.15.0,<2.0.0)", "azure-storage-blob (>=12.25.1,<13.0.0)", "backoff", "boto3 (==1.36.0)", "cryptography", "fastapi (>=0.120.1)", "fastapi-sso (>=0.16.0,<0.17.0)", "gunicorn (>=23.0.0,<24.0.0)", "litellm-enterprise (==0.1.20)", "litellm-proxy-extras (==0.4.3)", "mcp (>=1.10.0,<2.0.0) ; python_version >= \"3.10\"", "orjson (>=3.9.7,<4.0.0)", "polars (>=1.31.0,<2.0.0) ; python_version >= \"3.10\"", "pynacl (>=1.5.0,<2.0.0)", "python-multipart (>=0.0.18,<0.0.19)", "pyyaml (>=6.0.1,<7.0.0)", "rich (==13.7.1)", "rq", "soundfile (>=0.12.1,<0.13.0)", "uvicorn (>=0.29.0,<0.30.0)", "uvloop (>=0.21.0,<0.22.0) ; sys_platform != \"win32\"", "websockets (>=13.1.0,<14.0.0)"] semantic-router = ["semantic-router ; python_version >= \"3.9\""] utils = ["numpydoc"] @@ -9255,4 +9344,4 @@ cffi = ["cffi (>=1.11)"] [metadata] lock-version = "2.1" python-versions = "~3.12" -content-hash = "42a914162ecf635b336582e61aceb5effbb6ce5eef71b8b66f9c52c4a40d1674" +content-hash = "fcfdd6b556b0d7b9973ad98960e02869ba1616878a7e1799fa94a84763a61ca5" diff --git a/pyproject.toml b/pyproject.toml index 1f9ad4c1db..fefb17ee84 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,7 +59,7 @@ jedi = "^0.19.0" langchain = "^0.3.11" langchain-experimental = "^0.3.4" langchain-openai = "^0.3.2" -litellm = "1.76.0" +litellm = "1.79.3" llama-index = "^0.13.0" llama-index-llms-openai = "^0.5.0" lxml = "^6.0.0" From 4169b0cbf2e72d16be8e4fabe9be81599c9434b6 Mon Sep 17 00:00:00 2001 From: Ahtesham Quraish Date: Thu, 13 Nov 2025 20:15:44 +0500 Subject: [PATCH 07/17] feat: incorporating the tiptap in articles CRUD operations (#2693) * feat: adding tiptap integration with article crud operations --------- Co-authored-by: Ahtesham Quraish --- .../app-pages/Articles/ArticleDetailPage.tsx | 22 +-- .../Articles/ArticleEditPage.test.tsx | 30 +++- .../app-pages/Articles/ArticleEditPage.tsx | 67 ++++---- .../src/app-pages/Articles/ArticleNewPage.tsx | 62 +++---- .../TiptapEditor/EditorContainer.tsx | 154 ++++++++++++++++++ .../components/TiptapEditor/TiptapEditor.tsx | 104 +++--------- .../image-upload-node/image-upload-node.tsx | 24 ++- .../simple/simple-editor.scss | 8 +- .../TiptapEditor/lib/tiptap-utils.ts | 15 +- frontends/ol-components/src/index.ts | 2 + 10 files changed, 305 insertions(+), 183 deletions(-) create mode 100644 frontends/ol-components/src/components/TiptapEditor/EditorContainer.tsx diff --git a/frontends/main/src/app-pages/Articles/ArticleDetailPage.tsx b/frontends/main/src/app-pages/Articles/ArticleDetailPage.tsx index e8f73d53d8..950aeceba6 100644 --- a/frontends/main/src/app-pages/Articles/ArticleDetailPage.tsx +++ b/frontends/main/src/app-pages/Articles/ArticleDetailPage.tsx @@ -2,7 +2,13 @@ import React from "react" import { useArticleDetail } from "api/hooks/articles" -import { Container, LoadingSpinner, styled, Typography } from "ol-components" +import { + Container, + LoadingSpinner, + styled, + Typography, + TiptapEditorContainer, +} from "ol-components" import { ButtonLink } from "@mitodl/smoot-design" import { notFound } from "next/navigation" import { Permission } from "api/hooks/user" @@ -24,14 +30,6 @@ const WrapperContainer = styled.div({ paddingBottom: "10px", }) -const PreTag = styled.pre({ - background: "#f6f6f6", - padding: "16px", - borderRadius: "8px", - fontSize: "14px", - overflowX: "auto", -}) - export const ArticleDetailPage = ({ articleId }: { articleId: number }) => { const id = Number(articleId) const { data, isLoading } = useArticleDetail(id) @@ -58,7 +56,11 @@ export const ArticleDetailPage = ({ articleId }: { articleId: number }) => { - {JSON.stringify(data.content, null, 2)} + ) diff --git a/frontends/main/src/app-pages/Articles/ArticleEditPage.test.tsx b/frontends/main/src/app-pages/Articles/ArticleEditPage.test.tsx index 2f1bf9b013..e2d95495ab 100644 --- a/frontends/main/src/app-pages/Articles/ArticleEditPage.test.tsx +++ b/frontends/main/src/app-pages/Articles/ArticleEditPage.test.tsx @@ -24,7 +24,15 @@ describe("ArticleEditPage", () => { const article = factories.articles.article({ id: 42, title: "Existing Title", - content: { id: 1, content: "Existing content" }, + content: { + type: "doc", + content: [ + { + type: "paragraph", + content: [{ type: "text", text: "Existing Title" }], + }, + ], + }, }) setMockResponse.get(urls.articles.details(article.id), article) @@ -45,7 +53,15 @@ describe("ArticleEditPage", () => { const article = factories.articles.article({ id: 123, title: "Existing Title", - content: { id: 1, content: "Existing content" }, + content: { + type: "doc", + content: [ + { + type: "paragraph", + content: [{ type: "text", text: "Existing Title" }], + }, + ], + }, }) setMockResponse.get(urls.articles.details(article.id), article) @@ -77,7 +93,15 @@ describe("ArticleEditPage", () => { const article = factories.articles.article({ id: 7, title: "Old Title", - content: { id: 1, content: "Bad content" }, + content: { + type: "doc", + content: [ + { + type: "paragraph", + content: [{ type: "text", text: "Existing Title" }], + }, + ], + }, }) setMockResponse.get(urls.articles.details(article.id), article) diff --git a/frontends/main/src/app-pages/Articles/ArticleEditPage.tsx b/frontends/main/src/app-pages/Articles/ArticleEditPage.tsx index dc4c8a5a35..22cb88b48b 100644 --- a/frontends/main/src/app-pages/Articles/ArticleEditPage.tsx +++ b/frontends/main/src/app-pages/Articles/ArticleEditPage.tsx @@ -1,11 +1,19 @@ "use client" -import React, { useEffect, useState, ChangeEvent } from "react" +import React, { useEffect, useState } from "react" import { Permission } from "api/hooks/user" import { useRouter } from "next-nprogress-bar" import { useArticleDetail, useArticlePartialUpdate } from "api/hooks/articles" -import { Button, Input, Alert } from "@mitodl/smoot-design" +import { Button, Alert } from "@mitodl/smoot-design" import RestrictedRoute from "@/components/RestrictedRoute/RestrictedRoute" -import { Container, Typography, styled, LoadingSpinner } from "ol-components" +import { + Container, + Typography, + styled, + LoadingSpinner, + TiptapEditorContainer, + JSONContent, +} from "ol-components" + import { notFound } from "next/navigation" import { articlesView } from "@/common/urls" @@ -19,11 +27,6 @@ const ClientContainer = styled.div({ margin: "10px 0", }) -const TitleInput = styled(Input)({ - width: "100%", - margin: "10px 0", -}) - const ArticleEditPage = ({ articleId }: { articleId: string }) => { const router = useRouter() @@ -31,8 +34,10 @@ const ArticleEditPage = ({ articleId }: { articleId: string }) => { const { data: article, isLoading } = useArticleDetail(id) const [title, setTitle] = useState("") - const [text, setText] = useState("") - const [json, setJson] = useState({}) + const [json, setJson] = useState({ + type: "doc", + content: [{ type: "paragraph", content: [] }], + }) const [alertText, setAlertText] = useState("") const { mutate: updateArticle, isPending } = useArticlePartialUpdate() @@ -57,7 +62,6 @@ const ArticleEditPage = ({ articleId }: { articleId: string }) => { useEffect(() => { if (article && !title) { setTitle(article.title) - setText(article.content ? JSON.stringify(article.content, null, 2) : "") setJson(article.content) } // eslint-disable-next-line react-hooks/exhaustive-deps @@ -70,20 +74,13 @@ const ArticleEditPage = ({ articleId }: { articleId: string }) => { return notFound() } - const handleChange = (e: ChangeEvent) => { - const value = e.target.value - setText(value) - - try { - const parsed = JSON.parse(value) - setJson(parsed) - } catch { - setJson({}) - } + const handleChange = (json: object) => { + setJson(json) } + return ( - + Edit Article @@ -99,25 +96,17 @@ const ArticleEditPage = ({ articleId }: { articleId: string }) => { )} - { - console.log("Title input changed:", e.target.value) - setTitle(e.target.value) - setAlertText("") - }} - placeholder="Enter article title" - className="input-field" - /> - - -