Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -152,3 +152,6 @@ load_testing/data/

# Hacksnack game assets (copied by postinstall)
frontends/main/public/games/hacksnack

# drf-lint cross-file index cache
.drf_lint_cache.json
12 changes: 12 additions & 0 deletions RELEASE.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
Release Notes
=============

Version 0.80.2
--------------

- Only offer a run's courseware once it has started (#3925)
- chore: refresh drf-lint baseline for ORM003-ORM006 (#3928)
- fix(sentry): set max_request_body_size to small and scrub Postgres DETAIL rows (#3915)
- Sanitize and render rich-text descriptions from OVS (#3879)
- Build internal resource links from learn_url (#3885)
- Update dependency sharp to v0.35.4 [SECURITY] (#3917)
- Skip staff-only OLX content when ingesting edX course archives (#3909)
- Update certificate description in Product Page CertificateTrackCard (#3924)

Version 0.80.1
--------------

Expand Down
43 changes: 42 additions & 1 deletion drf_lint_baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,46 @@
"channels/serializers.py:132:24:ORM002",
"channels/serializers.py:134:24:ORM002",
"channels/serializers.py:136:24:ORM002",
"profiles/serializers.py:205:16:ORM002"
"channels/serializers.py:97:32:ORM003",
"learning_resources/serializers.py:103:4:ORM005",
"learning_resources/serializers.py:1281:44:ORM005",
"learning_resources/serializers.py:1315:61:ORM003",
"learning_resources/serializers.py:1320:59:ORM003",
"learning_resources/serializers.py:1328:12:ORM004",
"learning_resources/serializers.py:1344:21:ORM004",
"learning_resources/serializers.py:1357:11:ORM006",
"learning_resources/serializers.py:1358:60:ORM006",
"learning_resources/serializers.py:1362:24:ORM004",
"learning_resources/serializers.py:1525:11:ORM006",
"learning_resources/serializers.py:1526:19:ORM006",
"learning_resources/serializers.py:1527:11:ORM006",
"learning_resources/serializers.py:1528:19:ORM006",
"learning_resources/serializers.py:1529:15:ORM006",
"learning_resources/serializers.py:153:4:ORM005",
"learning_resources/serializers.py:163:4:ORM005",
"learning_resources/serializers.py:1822:4:ORM005",
"learning_resources/serializers.py:1830:20:ORM004",
"learning_resources/serializers.py:213:4:ORM005",
"learning_resources/serializers.py:428:4:ORM005",
"learning_resources/serializers.py:429:4:ORM005",
"learning_resources/serializers.py:487:49:ORM006",
"learning_resources/serializers.py:502:12:ORM006",
"learning_resources/serializers.py:541:4:ORM005",
"learning_resources/serializers.py:590:4:ORM005",
"learning_resources/serializers.py:605:17:ORM006",
"learning_resources/serializers.py:610:17:ORM006",
"learning_resources/serializers.py:620:17:ORM006",
"learning_resources/serializers.py:877:18:ORM004",
"learning_resources/serializers.py:926:16:ORM004",
"learning_resources/serializers.py:943:20:ORM004",
"learning_resources/serializers.py:965:20:ORM004",
"learning_resources_search/serializers.py:661:4:ORM005",
"learning_resources_search/serializers.py:663:4:ORM005",
"profiles/serializers.py:108:39:ORM006",
"profiles/serializers.py:108:60:ORM006",
"profiles/serializers.py:113:19:ORM006",
"profiles/serializers.py:205:16:ORM002",
"profiles/serializers.py:353:45:ORM004",
"profiles/serializers.py:359:45:ORM004",
"profiles/serializers.py:502:8:ORM006"
]
4 changes: 4 additions & 0 deletions frontends/api/src/generated/v0/api.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions frontends/api/src/generated/v1/api.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -655,6 +655,7 @@ const podcastEpisode: LearningResourceFactory<PodcastEpisodeResource> = (
id: parentPodcastId,
title: faker.lorem.words(3),
readable_id: faker.string.uuid(),
learn_url: `${TEST_APP_ORIGIN}/podcast/${parentPodcastId}/podcast`,
},
],
duration: faker.helpers.arrayElement(["PT1H13M44S", "PT2H30M", "PT1M"]),
Expand Down
2 changes: 1 addition & 1 deletion frontends/main/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@
"react-hotkeys-hook": "^5.2.1",
"react-markdown": "^10.0.0",
"react-slick": "^0.31.0",
"sharp": "0.35.0",
"sharp": "0.35.4",
"slick-carousel": "^1.8.1",
"tiny-invariant": "^1.3.3",
"video.js": "^8.23.7",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,55 @@ describe.each([
},
)

test.each([
{ runDates: currentRunDates, expectLink: true, case: "started" },
{ runDates: futureRunDates, expectLink: false, case: "not started" },
])(
"Title links to courseware only once the run has started ($case)",
async ({ runDates, expectLink }) => {
setupUserApis()
const coursewareUrl = faker.internet.url()
const enrollment = mitxonline.factories.enrollment.courseEnrollment({
grades: [],
certificate: null,
run: { ...runDates, courseware_url: coursewareUrl },
})
renderWithProviders(<EnrolledCourseCard enrollment={enrollment} />)
const card = getCard()
const title = enrollment.run.course.title

if (expectLink) {
expect(within(card).getByRole("link", { name: title })).toHaveAttribute(
"href",
coursewareUrl,
)
} else {
// The heading still names the course, it just isn't a way in.
await waitFor(() => {
expect(
within(card).queryByRole("link", { name: title }),
).not.toBeInTheDocument()
})
expect(within(card).getByText(title)).toBeInTheDocument()
}
},
)

test("Title links to courseware for staff before the run starts", async () => {
setupUserApis({ is_staff: true })
const coursewareUrl = faker.internet.url()
const enrollment = mitxonline.factories.enrollment.courseEnrollment({
grades: [],
certificate: null,
run: { ...futureRunDates, courseware_url: coursewareUrl },
})
renderWithProviders(<EnrolledCourseCard enrollment={enrollment} />)
const link = await within(getCard()).findByRole("link", {
name: enrollment.run.course.title,
})
expect(link).toHaveAttribute("href", coursewareUrl)
})

test("Courseware button is a navigable link for staff even when course has not started", async () => {
setupUserApis({ is_staff: true })
const coursewareUrl = faker.internet.url()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
getDashboardEnrollmentStatus,
pickCertificateEnrollment,
} from "./model/dashboardViewModel"
import { getCourseDateText } from "./courseDateUtils"
import { canOpenCourseware, getCourseDateText } from "./courseDateUtils"
import { isVerifiedEnrollmentMode } from "@/common/mitxonline"
import { RiArrowUpCircleLine, RiAwardLine, RiMore2Line } from "@remixicon/react"
import { useReplaceBasketItem } from "@/common/mitxonline/useReplaceBasketItem"
Expand Down Expand Up @@ -287,7 +287,7 @@ export const EnrolledCourseCard = ({
const enrollmentMode = enrollment?.enrollment_mode
const offerUpgrade = !enrollment?.b2b_contract_id
const startDate = run?.start_date
const hasStarted = startDate ? isInPast(startDate) : true
const coursewareOpen = canOpenCourseware(startDate, { isStaff })
const endDate = run?.end_date
const hasEnded = endDate ? isInPast(endDate) : false
const hasCourseDateText = getCourseDateText(startDate, endDate) !== null
Expand Down Expand Up @@ -335,7 +335,7 @@ export const EnrolledCourseCard = ({
productId={run?.upgrade_product_id}
isVerifiedProgramEnrollment={isVerifiedProgramEnrollment}
readableId={run?.courseware_id}
coursewareUrl={coursewareUrl ?? undefined}
coursewareUrl={coursewareOpen ? (coursewareUrl ?? undefined) : undefined}
programReadableIds={ancestorContext?.parentProgramReadableIds}
programCoursewareId={
ancestorContext?.programEnrollment?.program.readable_id
Expand Down Expand Up @@ -364,7 +364,7 @@ export const EnrolledCourseCard = ({
) : null
const titleSection = (
<Stack gap="6px">
{coursewareUrl ? (
{coursewareUrl && coursewareOpen ? (
<TitleHeading as={headingLevel}>
<EnrolledTitleLink
size="medium"
Expand All @@ -381,13 +381,8 @@ export const EnrolledCourseCard = ({
{endDateAndUpgradeSection}
</Stack>
)
// Determine if button should be disabled
// Staff can access courseware even before the course has started
const courseHasEnded = run?.end_date ? isInPast(run.end_date) : false
const isDisabled = Boolean(
!coursewareUrl || // Enrolled but no action available
(!!startDate && !hasStarted && !isStaff), // Enrolled but course hasn't started yet
)
const isDisabled = Boolean(!coursewareUrl || !coursewareOpen)
const isCompleted =
enrollmentStatus === EnrollmentStatus.Completed || courseHasEnded
const buttonText = isCompleted ? "View" : "Continue"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
screen,
setMockResponse,
user,
waitFor,
within,
} from "@/test-utils"
import * as mitxonline from "api/mitxonline-test-utils"
Expand All @@ -29,6 +30,12 @@ beforeEach(() => {
// Each row resolves its own Receipt item from the order history; default to
// none, tests override.
setupOrderHistory()
// Rows read is_staff to decide whether pre-start courseware is reachable.
// The factory randomises it, so pin it off; the staff test overrides.
setMockResponse.get(
mitxonline.urls.userMe.get(),
mitxonline.factories.user.user({ is_staff: false }),
)
setPerRunMenus(true)
})

Expand Down Expand Up @@ -358,6 +365,53 @@ describe("SiblingRunsToggle + SiblingRunsPanel", () => {
).not.toBeInTheDocument()
})

// A run that hasn't started cannot be completed, so drop the factory's
// default certificate; completion outranks the dates for the row label.
const makeUpcomingEnrollment = () =>
mitxonline.factories.enrollment.courseEnrollment({
certificate: null,
grades: [],
run: {
start_date: moment().add(30, "days").toISOString(),
end_date: moment().add(90, "days").toISOString(),
courseware_url: faker.internet.url(),
},
})

test.each([
{ isStaff: false, expectLink: false },
{ isStaff: true, expectLink: true },
])(
"upcoming sibling run offers 'View content' only to staff (isStaff=$isStaff)",
async ({ isStaff, expectLink }) => {
setMockResponse.get(
mitxonline.urls.userMe.get(),
mitxonline.factories.user.user({ is_staff: isStaff }),
)
renderWithProviders(
<SiblingRunsAccordionHarness
enrollment={makeEnrollment()}
siblingEnrollments={[makeUpcomingEnrollment()]}
/>,
)
await expandAccordion()
expect(await screen.findByText(/^Upcoming:/)).toBeInTheDocument()

const link = screen.queryByRole("link", {
name: /View content for Upcoming/,
})
if (expectLink) {
await waitFor(() => {
expect(
screen.getByRole("link", { name: /View content for Upcoming/ }),
).toBeInTheDocument()
})
} else {
expect(link).not.toBeInTheDocument()
}
},
)

test("upcoming sibling run label starts with 'Upcoming:'", async () => {
// A run that hasn't started cannot be completed, so drop the factory's
// default certificate; completion outranks the dates.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ import {
RiSubtractLine,
RiTimeLine,
} from "@remixicon/react"
import { formatRunIdentifier, getRunTimeState } from "./courseDateUtils"
import {
canOpenCourseware,
formatRunIdentifier,
getRunTimeState,
} from "./courseDateUtils"
import type { RunTimeState } from "./courseDateUtils"
import { ActionButton, VisuallyHidden } from "@mitodl/smoot-design"
import { EnrollmentStatusIcon } from "./EnrollmentStatus"
Expand All @@ -22,6 +26,8 @@ import { useOrderIdForRun } from "@/common/mitxonline/useOrderIdForResource"
import { getRunMenuItems } from "./runMenuItems"
import { useFeatureFlagEnabled } from "posthog-js/react"
import { FeatureFlags } from "@/common/feature_flags"
import { useQuery } from "@tanstack/react-query"
import { mitxUserQueries } from "api/mitxonline-hooks/user"

const UpcomingRunIcon = styled(RiTimeLine)(({ theme }) => ({
width: "16px",
Expand Down Expand Up @@ -194,7 +200,11 @@ const RunListRow: React.FC<RunListRowProps> = ({
enrollment,
isFirst,
}) => {
const mitxOnlineUser = useQuery(mitxUserQueries.me())
const coursewareUrl = enrollment.run?.courseware_url
const coursewareOpen = canOpenCourseware(enrollment.run?.start_date, {
isStaff: mitxOnlineUser.data?.is_staff,
})
/**
* Resolved per row so each run's Receipt item reflects that run's own order.
* Every row shares the one `orders/history` query, so N rows still cost a
Expand Down Expand Up @@ -241,7 +251,7 @@ const RunListRow: React.FC<RunListRowProps> = ({
</Stack>
</Stack>
<Stack direction="row" gap="4px" alignItems="center" flexShrink={0}>
{coursewareUrl && (
{coursewareUrl && coursewareOpen && (
<>
<ViewContentLink
href={coursewareUrl}
Expand Down
Loading
Loading