From 34149af696b8de27083fea72f876e1d069c398df Mon Sep 17 00:00:00 2001 From: Matt Bertrand Date: Mon, 8 Jun 2026 14:34:30 -0400 Subject: [PATCH 1/6] Remove video shorts app (Phase 2) (#3385) --- main/settings.py | 1 - video_shorts/__init__.py | 0 video_shorts/migrations/0001_initial.py | 33 ------------- .../migrations/0002_multi_thumbnails.py | 48 ------------------- .../migrations/0003_delete_videoshort.py | 15 ------ video_shorts/migrations/__init__.py | 0 video_shorts/models.py | 1 - video_shorts/tasks.py | 13 ----- 8 files changed, 111 deletions(-) delete mode 100644 video_shorts/__init__.py delete mode 100644 video_shorts/migrations/0001_initial.py delete mode 100644 video_shorts/migrations/0002_multi_thumbnails.py delete mode 100644 video_shorts/migrations/0003_delete_videoshort.py delete mode 100644 video_shorts/migrations/__init__.py delete mode 100644 video_shorts/models.py delete mode 100644 video_shorts/tasks.py diff --git a/main/settings.py b/main/settings.py index 07b3d6df78..2204833316 100644 --- a/main/settings.py +++ b/main/settings.py @@ -137,7 +137,6 @@ "testimonials", "data_fixtures", "vector_search", - "video_shorts", "ol_hubspot", "mitol.scim.apps.ScimApp", "health_check", diff --git a/video_shorts/__init__.py b/video_shorts/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/video_shorts/migrations/0001_initial.py b/video_shorts/migrations/0001_initial.py deleted file mode 100644 index ce3c7ce28e..0000000000 --- a/video_shorts/migrations/0001_initial.py +++ /dev/null @@ -1,33 +0,0 @@ -# Generated by Django 4.2.25 on 2025-10-22 18:36 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - initial = True - - dependencies = [] - - operations = [ - migrations.CreateModel( - name="VideoShort", - fields=[ - ("created_on", models.DateTimeField(auto_now_add=True, db_index=True)), - ("updated_on", models.DateTimeField(auto_now=True)), - ( - "youtube_id", - models.CharField(max_length=20, primary_key=True, serialize=False), - ), - ("title", models.CharField(max_length=255)), - ("description", models.TextField(blank=True)), - ("published_at", models.DateTimeField()), - ("thumbnail_height", models.IntegerField()), - ("thumbnail_width", models.IntegerField()), - ("thumbnail_url", models.CharField()), - ("video_url", models.CharField()), - ], - options={ - "abstract": False, - }, - ), - ] diff --git a/video_shorts/migrations/0002_multi_thumbnails.py b/video_shorts/migrations/0002_multi_thumbnails.py deleted file mode 100644 index 6eb8929423..0000000000 --- a/video_shorts/migrations/0002_multi_thumbnails.py +++ /dev/null @@ -1,48 +0,0 @@ -# Generated by Django 4.2.26 on 2026-01-07 18:37 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("video_shorts", "0001_initial"), - ] - - operations = [ - migrations.RenameField( - model_name="videoshort", - old_name="youtube_id", - new_name="video_id", - ), - migrations.RemoveField( - model_name="videoshort", - name="description", - ), - migrations.RemoveField( - model_name="videoshort", - name="thumbnail_height", - ), - migrations.RemoveField( - model_name="videoshort", - name="thumbnail_url", - ), - migrations.RemoveField( - model_name="videoshort", - name="thumbnail_width", - ), - migrations.AddField( - model_name="videoshort", - name="thumbnail_small_url", - field=models.CharField(blank=True), - ), - migrations.AddField( - model_name="videoshort", - name="thumbnail_large_url", - field=models.CharField(blank=True), - ), - migrations.AlterField( - model_name="videoshort", - name="video_url", - field=models.CharField(blank=True), - ), - ] diff --git a/video_shorts/migrations/0003_delete_videoshort.py b/video_shorts/migrations/0003_delete_videoshort.py deleted file mode 100644 index 0a0fe59990..0000000000 --- a/video_shorts/migrations/0003_delete_videoshort.py +++ /dev/null @@ -1,15 +0,0 @@ -# Generated by Django 4.2.28 on 2026-05-28 - -from django.db import migrations - - -class Migration(migrations.Migration): - dependencies = [ - ("video_shorts", "0002_multi_thumbnails"), - ] - - operations = [ - migrations.DeleteModel( - name="VideoShort", - ), - ] diff --git a/video_shorts/migrations/__init__.py b/video_shorts/migrations/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/video_shorts/models.py b/video_shorts/models.py deleted file mode 100644 index a560d79857..0000000000 --- a/video_shorts/models.py +++ /dev/null @@ -1 +0,0 @@ -"""Migration-only app stub for the retired video_shorts app.""" diff --git a/video_shorts/tasks.py b/video_shorts/tasks.py deleted file mode 100644 index f276698534..0000000000 --- a/video_shorts/tasks.py +++ /dev/null @@ -1,13 +0,0 @@ -"""Task shims for the retired video_shorts app.""" - -import logging - -from main.celery import app - -log = logging.getLogger(__name__) - - -@app.task -def delete_video_short_from_s3(video_url: str) -> None: - """No-op compatibility shim for queued tasks from the retired app.""" - log.info("Skipping video short S3 deletion for retired app: %s", video_url) From 0f2fc1456ae7ae25f8cb22073eea8a1f3dd705c9 Mon Sep 17 00:00:00 2001 From: Ahtesham Quraish Date: Tue, 9 Jun 2026 14:28:04 +0500 Subject: [PATCH 2/6] fix: add link to the creation page of article and news (#3430) * fix: add link to the creation page of article and news --------- Co-authored-by: Ahtesham Quraish --- .../app-pages/Articles/ArticleListingPage.tsx | 40 +++++++++++-------- .../main/src/app-pages/News/NewsBanner.tsx | 36 ++++++++++++++--- .../app-pages/News/NewsListingPage.test.tsx | 29 ++++++++++++++ .../page-components/Header/Header.test.tsx | 27 +++++++++++++ .../src/page-components/Header/UserMenu.tsx | 13 ++++++ 5 files changed, 123 insertions(+), 22 deletions(-) diff --git a/frontends/main/src/app-pages/Articles/ArticleListingPage.tsx b/frontends/main/src/app-pages/Articles/ArticleListingPage.tsx index df312e0f7b..ddcc08ccde 100644 --- a/frontends/main/src/app-pages/Articles/ArticleListingPage.tsx +++ b/frontends/main/src/app-pages/Articles/ArticleListingPage.tsx @@ -22,7 +22,9 @@ import type { WebsiteContent } from "api/v1" import { LocalDate } from "ol-utilities" import { useWebsiteContentList } from "api/hooks/website_content" import { extractArticleContent } from "@/common/websiteContentUtils" -import { articleView } from "@/common/urls" +import { articleView, websiteContentCreateView } from "@/common/urls" +import { Permission, useUserHasPermission } from "api/hooks/user" +import { ButtonLink } from "@mitodl/smoot-design" const PAGE_SIZE = 10 const MAX_PAGE = 50 @@ -223,6 +225,10 @@ const BannerGridContainer = styled.div` max-width: 842px; margin: 0 auto; padding: 64px 0; + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; ${theme.breakpoints.down("sm")} { max-width: 100%; @@ -299,6 +305,10 @@ const BannerSection = styled.div` background: ${theme.custom.colors.white}; border-bottom: 1px solid ${theme.custom.colors.lightGray2}; ` +const NewArticleLink = styled(ButtonLink)` + display: flex; + justify-content: end; +` const EmptyState = styled.div` display: flex; @@ -319,7 +329,6 @@ const EmptyState = styled.div` const BannerTitle = styled(Typography)` color: ${theme.custom.colors.black}; - margin-top: 8px; ${theme.breakpoints.down("md")} { ${{ ...theme.typography.h2 }} } @@ -329,16 +338,6 @@ const BannerTitle = styled(Typography)` } ` as typeof Typography -const BannerDescription = styled(Typography)` - color: ${theme.custom.colors.black}; - margin-top: 16px; - font-size: 20px; - line-height: 32px; - ${theme.breakpoints.down("sm")} { - ${{ ...theme.typography.body2 }} - } -` - const BreadcrumbBar = styled.div(({ theme }) => ({ width: "100%", padding: "18px 0 2px 0", @@ -418,6 +417,8 @@ const ArticleListingPage: React.FC = () => { const parsedPage = Number.parseInt(searchParams.get("page") ?? "1", 10) const page = Number.isFinite(parsedPage) && parsedPage > 0 ? parsedPage : 1 + const isArticleEditor = useUserHasPermission(Permission.ArticleEditor) + const { data: articles, isLoading } = useWebsiteContentList({ limit: PAGE_SIZE, offset: (page - 1) * PAGE_SIZE, @@ -454,11 +455,16 @@ const ArticleListingPage: React.FC = () => { Articles - - Dive into articles covering emerging technologies, global - challenges, and creative thinking. Stay connected with the latest - insights and discoveries from MIT. - + {isArticleEditor && ( + + + New Article + + + )} diff --git a/frontends/main/src/app-pages/News/NewsBanner.tsx b/frontends/main/src/app-pages/News/NewsBanner.tsx index cf3acc519f..422e912143 100644 --- a/frontends/main/src/app-pages/News/NewsBanner.tsx +++ b/frontends/main/src/app-pages/News/NewsBanner.tsx @@ -7,6 +7,9 @@ import { Breadcrumbs, BannerBackground, } from "ol-components" +import { ButtonLink } from "@mitodl/smoot-design" +import { Permission, useUserHasPermission } from "api/hooks/user" +import { websiteContentCreateView } from "@/common/urls" export const DEFAULT_BACKGROUND_IMAGE_URL = "/images/backgrounds/backgroung_steps.jpg" @@ -30,7 +33,17 @@ const BannerSection = styled(BannerBackground)` z-index: 2; } ` - +const NewArticleLink = styled(ButtonLink)` + display: flex; + justify-content: end; +` +const InfoContainer = styled.div` + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + gap: 16px; +` const BannerTitle = styled(Typography)` color: ${theme.custom.colors.white}; margin-top: 8px; @@ -65,6 +78,7 @@ const NewsBanner: React.FC = ({ backgroundUrl = DEFAULT_BACKGROUND_IMAGE_URL, className, }) => { + const isArticleEditor = useUserHasPermission(Permission.ArticleEditor) return ( = ({ ancestors={[{ href: "/", label: "Home" }]} current={currentBreadcrumb} /> - - {title} - - {description} + +
+ + {title} + + {description} +
+ {isArticleEditor && ( + + Add news + + )} +
) diff --git a/frontends/main/src/app-pages/News/NewsListingPage.test.tsx b/frontends/main/src/app-pages/News/NewsListingPage.test.tsx index ef1140b62e..400b7ff894 100644 --- a/frontends/main/src/app-pages/News/NewsListingPage.test.tsx +++ b/frontends/main/src/app-pages/News/NewsListingPage.test.tsx @@ -23,6 +23,7 @@ describe("NewsListingPage", () => { beforeEach(() => { mockedUseFeatureFlagEnabled.mockReturnValue(true) mockedUseFeatureFlagsLoaded.mockReturnValue(true) + setMockResponse.get(urls.userMe.get(), { is_authenticated: false }) }) afterEach(() => { @@ -82,6 +83,34 @@ describe("NewsListingPage", () => { }) }) + test("shows 'Add news' button in banner when user is an article editor", async () => { + setMockResponse.get(urls.userMe.get(), { + is_authenticated: true, + is_article_editor: true, + }) + setupAPI(21) + renderWithProviders() + + await waitFor(() => { + expect( + screen.getByRole("link", { name: /add news/i }), + ).toBeInTheDocument() + }) + }) + + test("hides 'Add news' button in banner when user is not an article editor", async () => { + setupAPI(21) + renderWithProviders() + + await waitFor(() => { + expect(screen.queryByRole("progressbar")).not.toBeInTheDocument() + }) + + expect( + screen.queryByRole("link", { name: /add news/i }), + ).not.toBeInTheDocument() + }) + test("displays News images when available", async () => { const news = setupAPI(21) renderWithProviders() diff --git a/frontends/main/src/page-components/Header/Header.test.tsx b/frontends/main/src/page-components/Header/Header.test.tsx index 36c404e59e..6633465e77 100644 --- a/frontends/main/src/page-components/Header/Header.test.tsx +++ b/frontends/main/src/page-components/Header/Header.test.tsx @@ -148,4 +148,31 @@ describe("UserMenu", () => { }) expect(link).toBe(null) }) + + test("Article editors see 'Article' and 'News' links in the user menu", async () => { + setMockResponse.get(urls.userMe.get(), { + is_authenticated: true, + is_article_editor: true, + }) + renderWithProviders(
) + const menu = await findUserMenu() + + const articleLink = within(menu).getByRole("menuitem", { name: "Article" }) + expect(articleLink).toHaveAttribute("href", "/website_content/article/new") + + const newsLink = within(menu).getByRole("menuitem", { name: "News" }) + expect(newsLink).toHaveAttribute("href", "/website_content/news/new") + }) + + test("Users WITHOUT ArticleEditor permission do not see 'Article' or 'News' links", async () => { + setMockResponse.get(urls.userMe.get(), { + is_authenticated: true, + is_article_editor: false, + }) + renderWithProviders(
) + const menu = await findUserMenu() + + expect(within(menu).queryByRole("menuitem", { name: "Article" })).toBe(null) + expect(within(menu).queryByRole("menuitem", { name: "News" })).toBe(null) + }) }) diff --git a/frontends/main/src/page-components/Header/UserMenu.tsx b/frontends/main/src/page-components/Header/UserMenu.tsx index 0c25aa1113..fa0a558695 100644 --- a/frontends/main/src/page-components/Header/UserMenu.tsx +++ b/frontends/main/src/page-components/Header/UserMenu.tsx @@ -13,6 +13,7 @@ import { } from "@remixicon/react" import { useUserMe, User } from "api/hooks/user" import MITLogoLink from "@/components/MITLogoLink/MITLogoLink" +import { websiteContentCreateView } from "@/common/urls" const FlexContainer = styled.div({ display: "flex", @@ -158,6 +159,18 @@ const UserMenu: React.FC = ({ variant }) => { allow: !!user?.is_learning_path_editor, href: urls.LEARNINGPATH_LISTING, }, + { + label: "Article", + key: "articles", + allow: !!user?.is_article_editor, + href: websiteContentCreateView("article"), + }, + { + label: "News", + key: "news", + allow: !!user?.is_article_editor, + href: websiteContentCreateView("news"), + }, { label: "Log Out", key: "logout", From ef5ee3efab90a4e9979db701ff80229b73100c14 Mon Sep 17 00:00:00 2001 From: Shankar Ambady Date: Tue, 9 Jun 2026 14:04:08 -0400 Subject: [PATCH 3/6] remove duplicate resource task (#3436) --- learning_resources/tasks.py | 33 +------------------------------- learning_resources/tasks_test.py | 33 -------------------------------- main/settings_celery.py | 4 ---- 3 files changed, 1 insertion(+), 69 deletions(-) diff --git a/learning_resources/tasks.py b/learning_resources/tasks.py index 8d42d0088b..9944b60c8a 100644 --- a/learning_resources/tasks.py +++ b/learning_resources/tasks.py @@ -11,7 +11,7 @@ from celery.exceptions import Ignore from django.conf import settings from django.db import OperationalError -from django.db.models import Count, Q +from django.db.models import Q from django.utils import timezone from learning_resources.constants import LearningResourceType @@ -51,7 +51,6 @@ ) from learning_resources_search.constants import ( CONTENT_FILE_TYPE, - COURSE_TYPE, SEARCH_CONN_EXCEPTIONS, ) from learning_resources_search.exceptions import RetryError @@ -65,36 +64,6 @@ CLEANUP_RETRY_EXCEPTIONS = (*SEARCH_CONN_EXCEPTIONS, OperationalError) -@app.task(bind=True) -def remove_duplicate_resources(self): - """Remove duplicate unpublished resources""" - from vector_search.tasks import generate_embeddings - - duplicates = ( - LearningResource.objects.values("readable_id") - .annotate(count_id=Count("id")) - .filter(count_id__gt=1) - ) - embed_tasks = [] - for duplicate in duplicates: - unpublished_resources = LearningResource.objects.filter( - readable_id=duplicate["readable_id"], - published=False, - ).values_list("id", flat=True) - published_resources = list( - LearningResource.objects.filter( - readable_id=duplicate["readable_id"], - published=True, - ).values_list("id", flat=True) - ) - # keep the most recently created resource, delete the rest - LearningResource.objects.filter(id__in=unpublished_resources).delete() - embed_tasks.append( - generate_embeddings.si(published_resources, COURSE_TYPE, overwrite=True) - ) - self.replace(celery.chain(*embed_tasks)) - - @app.task def update_next_start_date_and_prices(): """Update expired next start dates and prices""" diff --git a/learning_resources/tasks_test.py b/learning_resources/tasks_test.py index 9191a70aae..0797411892 100644 --- a/learning_resources/tasks_test.py +++ b/learning_resources/tasks_test.py @@ -17,7 +17,6 @@ from learning_resources.factories import ( ContentFileFactory, LearningResourceFactory, - LearningResourcePlatformFactory, LearningResourceRunFactory, ) from learning_resources.models import ContentFile, LearningResource @@ -27,7 +26,6 @@ get_youtube_data, get_youtube_transcripts, marketing_page_for_resources, - remove_duplicate_resources, scrape_marketing_pages, sync_canvas_courses, update_next_start_date_and_prices, @@ -839,37 +837,6 @@ def test_sync_canvas_courses(settings, mocker, django_assert_num_queries, canvas assert mock_ingest_course.call_count == 2 -def test_remove_duplicate_resources(mocker, mocked_celery): - """ - Test that remove_duplicate_resources removes duplicate unpublished resources - while keeping the most recently created resource. - """ - duplicate_id = "duplicate_id" - - for platform_type in [PlatformType.edx, PlatformType.xpro, PlatformType.youtube]: - LearningResourceFactory.create( - readable_id=duplicate_id, - published=False, - platform=LearningResourcePlatformFactory.create(code=platform_type.name), - ) - - published_reasource = LearningResourceFactory.create( - readable_id=duplicate_id, - published=True, - platform=LearningResourcePlatformFactory.create( - code=platform_type.mitxonline.name - ), - ) - generate_embeddings_mock = mocker.patch( - "vector_search.tasks.generate_embeddings", autospec=True - ) - assert LearningResource.objects.filter(readable_id=duplicate_id).count() == 4 - with pytest.raises(mocked_celery.replace_exception_class): - remove_duplicate_resources() - assert generate_embeddings_mock.mock_calls[0].args[0] == [published_reasource.id] - assert LearningResource.objects.filter(readable_id=duplicate_id).count() == 1 - - @pytest.mark.parametrize( ("etl_source", "archive_path", "overwrite"), [ diff --git a/main/settings_celery.py b/main/settings_celery.py index 292f4a177d..870d54e55b 100644 --- a/main/settings_celery.py +++ b/main/settings_celery.py @@ -169,10 +169,6 @@ "SCRAPE_MARKETING_PAGES_SCHEDULE_SECONDS", 60 * 60 * 12 ), # default is every 12 hours }, - "remove-duplicate-courses-every-6-hours": { - "task": "learning_resources.tasks.remove_duplicate_resources", - "schedule": crontab(minute=0, hour=9), # 5:00am EST - }, "daily_embed_new_learning_resources": { "task": "vector_search.tasks.embed_new_learning_resources", "schedule": get_int( From b83bb638d074a92b219eaf62618a34d767666f23 Mon Sep 17 00:00:00 2001 From: Anastasia Beglova Date: Tue, 9 Jun 2026 15:04:19 -0400 Subject: [PATCH 4/6] add scm as synonym to search (#3449) --- learning_resources_search/constants.py | 1 + 1 file changed, 1 insertion(+) diff --git a/learning_resources_search/constants.py b/learning_resources_search/constants.py index e902d9da6c..2ee3d09440 100644 --- a/learning_resources_search/constants.py +++ b/learning_resources_search/constants.py @@ -513,4 +513,5 @@ class FilterConfig: "natural language processing, nlp", "large language model, llm", "micromasters, micro masters", + "scm, supply chain management", ] From 7355edf10a4f4a4f96033770d6b8c5ad4d6c5b62 Mon Sep 17 00:00:00 2001 From: Chris Chudzicki Date: Tue, 9 Jun 2026 15:07:49 -0400 Subject: [PATCH 5/6] Fix error pages after swap to runtime env vars (#3448) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(nextjs): make NEXT_PUBLIC env available on error pages notFound() and thrown render errors are served as a client-rendered HTML shell, not server-rendered through the root layout — so the inline PublicEnvScript