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
10 changes: 9 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,16 @@ EXPOSE 8061
ENV PORT=8061
CMD ["sh", "-c", "exec granian --interface asginl --reload --host 0.0.0.0 --port ${PORT:-8061} --workers ${GRANIAN_WORKERS:-3} --blocking-threads 1 main.asgi:application"]

# ─── Development target ───────────────────────────────────────────────────────
# ─── Development target (docker compose) ─────────────────────────────────────
FROM final AS development

RUN --mount=type=cache,target=/opt/uv-cache,uid=1000,gid=1000 \
uv sync --frozen --no-install-project

# ─── Local-dev target (ol-infrastructure local-dev k8s/Tilt stack) ───────────
# Runtime user owns /src (live-synced source); dev deps come from `development`.
FROM development AS local-dev

USER root
RUN chown -R mitodl:mitodl /src
USER mitodl
11 changes: 11 additions & 0 deletions RELEASE.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
Release Notes
=============

Version 0.73.2
--------------

- fix: handle no-max-learners contracts in B2B seat assignment UI (#3564)
- feat: local-dev Docker targets and granian kill timeout (#3579)
- fix: changing the positions of share feature on videos and podcasts episode (#3540)
- Bound program marketing-page embedding input to avoid token overflow (#3562)
- fix(vector-search): relax qdrant-client pin to allow 1.18.x patches (#3546)
- feat: Add "Send Test Email to Me" to the seat assignment confirmation modal (#3558)
- Upgrade django-health-check from pinned git commit to PyPI 3.24.0 (#3556)

Version 0.73.1 (Released July 07, 2026)
--------------

Expand Down
2 changes: 1 addition & 1 deletion frontends/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
"ol-test-utilities": "0.0.0"
},
"dependencies": {
"@mitodl/mitxonline-api-axios": "2026.6.30-1",
"@mitodl/mitxonline-api-axios": "2026.7.7",
"@tanstack/react-query": "^5.66.0",
"axios": "^1.12.2",
"tiny-invariant": "^1.3.3"
Expand Down
10 changes: 10 additions & 0 deletions frontends/api/src/mitxonline/hooks/organizations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
B2bApiB2bManagerOrganizationsContractsCodesReassignUpdateRequest,
B2bApiB2bManagerOrganizationsContractsCodesRemindCreateRequest,
B2bApiB2bManagerOrganizationsContractsCodesRevokeDestroyRequest,
B2bApiB2bManagerOrganizationsContractsCodesSendTestEmailCreateRequest,
} from "@mitodl/mitxonline-api-axios/v2"
import {
organizationQueries,
Expand Down Expand Up @@ -106,6 +107,14 @@ const useReassignCode = () => {
})
}

/** Send a test enrollment code to the given email address. */
const useSendTestEmail = () =>
useMutation({
mutationFn: (
opts: B2bApiB2bManagerOrganizationsContractsCodesSendTestEmailCreateRequest,
) => b2bApi.b2bManagerOrganizationsContractsCodesSendTestEmailCreate(opts),
})

export {
organizationQueries,
managerOrganizationQueries,
Expand All @@ -114,6 +123,7 @@ export {
useReassignCode,
useRemindCode,
useRevokeCode,
useSendTestEmail,
}
export type {
ManagerEnrollmentCode,
Expand Down
2 changes: 2 additions & 0 deletions frontends/api/src/mitxonline/test-utils/urls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@ const contracts = {
code: string,
) =>
`${getApiBaseUrl()}/api/v0/b2b/manager/organizations/${orgId}/contracts/${contractId}/codes/${code}/reassign/`,
managerContractSendTestEmail: (orgId: number, contractId: number) =>
`${getApiBaseUrl()}/api/v0/b2b/manager/organizations/${orgId}/contracts/${contractId}/codes/send_test_email/`,
}

const certificates = {
Expand Down
7 changes: 7 additions & 0 deletions frontends/main/Dockerfile.web
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,13 @@ ENV NEXT_BUILD_CI=1

RUN yarn build

# STAGE: local-dev
# Local-dev target (ol-infrastructure local-dev k8s/Tilt stack): runs `next dev`
FROM base AS local-dev

ENV NODE_ENV=development
CMD ["yarn", "dev"]

# STAGE: runner (default)
# Copies only the standalone server, static assets, and public directory from
# the build stage into a clean image. No workspace install is needed; the
Expand Down
2 changes: 1 addition & 1 deletion frontends/main/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"@mitodl/arithmix": "^0.2.2",
"@mitodl/course-search-utils": "^3.5.2",
"@mitodl/hacksnack": "^0.1.0",
"@mitodl/mitxonline-api-axios": "2026.6.30-1",
"@mitodl/mitxonline-api-axios": "2026.7.7",
"@mitodl/smoot-design": "^6.27.0",
"@mui/base": "5.0.0-beta.70",
"@mui/material": "^6.4.5",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { act } from "react"
import { renderWithTheme, screen, user } from "@/test-utils"
import { renderWithTheme, screen, user, waitFor } from "@/test-utils"
import { AssignSeatsConfirmModal } from "./AssignSeatsConfirmModal"

const baseProps = {
Expand Down Expand Up @@ -297,6 +297,222 @@ describe("AssignSeatsConfirmModal — over-capacity state (CSV only)", () => {
})
})

describe("AssignSeatsConfirmModal — unlimited contract (availableSeats null)", () => {
beforeEach(() => jest.clearAllMocks())

const unlimitedProps = { ...baseProps, availableSeats: null }

test("shows unlimited messaging in the seats stat instead of a number", () => {
renderWithTheme(
<AssignSeatsConfirmModal {...unlimitedProps} validCount={3} />,
)

// Confirm step still renders normally.
expect(
screen.getByRole("heading", { name: /ready to send invitations/i }),
).toBeInTheDocument()

// Stat value is a dash and the label / group name convey no seat limit.
expect(screen.getByText("—")).toBeInTheDocument()
expect(screen.getByText("No seat limit")).toBeInTheDocument()
expect(
screen.getByRole("group", { name: /no seat limit on this contract/i }),
).toBeInTheDocument()
})

test("does not display a numeric or negative 'seats remaining after sending'", () => {
// validCount far exceeds any real cap; with no cap there must be no negative.
renderWithTheme(
<AssignSeatsConfirmModal {...unlimitedProps} validCount={100} />,
)

expect(
screen.queryByText(/seats remaining after sending/i),
).not.toBeInTheDocument()
expect(
screen.queryByRole("group", { name: /seats remaining after sending/i }),
).not.toBeInTheDocument()
// A cap of null run through `availableSeats - validCount` would render -100.
expect(screen.queryByText("-100")).not.toBeInTheDocument()
})

test("never enters the over-capacity 'Not enough seats available' state", () => {
renderWithTheme(
<AssignSeatsConfirmModal {...unlimitedProps} validCount={9999} />,
)

expect(
screen.getByRole("heading", { name: /ready to send invitations/i }),
).toBeInTheDocument()
expect(
screen.queryByRole("heading", { name: /not enough seats available/i }),
).not.toBeInTheDocument()
})

test("dialog accessible description conveys no seat limit", () => {
renderWithTheme(
<AssignSeatsConfirmModal {...unlimitedProps} validCount={3} />,
)

expect(screen.getByRole("dialog")).toHaveAccessibleDescription(
/no seat limit on this contract/i,
)
})
})

describe("AssignSeatsConfirmModal — email preview (send test email)", () => {
const sendTestEmailProps = {
...baseProps,
userEmail: "manager@test.com",
onSendTestEmail: jest.fn(),
}

beforeEach(() => jest.clearAllMocks())

test("does not show Email Preview section when onSendTestEmail is not provided", () => {
renderWithTheme(<AssignSeatsConfirmModal {...baseProps} />)

expect(screen.queryByText("Email Preview")).not.toBeInTheDocument()
expect(
screen.queryByRole("button", { name: /send test email to me/i }),
).not.toBeInTheDocument()
})

test("shows Email Preview section with button when onSendTestEmail is provided", () => {
renderWithTheme(<AssignSeatsConfirmModal {...sendTestEmailProps} />)

expect(screen.getByText("Email Preview")).toBeInTheDocument()
expect(
screen.getByRole("button", { name: /send test email to me/i }),
).toBeInTheDocument()
})

test("button is disabled when userEmail is not provided", () => {
renderWithTheme(
<AssignSeatsConfirmModal {...baseProps} onSendTestEmail={jest.fn()} />,
)

expect(
screen.getByRole("button", { name: /send test email to me/i }),
).toBeDisabled()
})

test("shows 'Sending…' while test email is being sent", async () => {
const { promise, resolve } = Promise.withResolvers<void>()
renderWithTheme(
<AssignSeatsConfirmModal
{...sendTestEmailProps}
onSendTestEmail={() => promise}
/>,
)

await user.click(
screen.getByRole("button", { name: /send test email to me/i }),
)

expect(
screen.getByRole("button", { name: /sending…/i }),
).toBeInTheDocument()

await act(async () => resolve())
})

test("shows success alert after test email is sent successfully", async () => {
renderWithTheme(
<AssignSeatsConfirmModal
{...sendTestEmailProps}
onSendTestEmail={jest.fn().mockResolvedValue(undefined)}
/>,
)

await user.click(
screen.getByRole("button", { name: /send test email to me/i }),
)

expect(await screen.findByRole("alert")).toHaveTextContent(
/test email successfully sent to manager@test\.com/i,
)
})

test("shows error alert when test email fails", async () => {
renderWithTheme(
<AssignSeatsConfirmModal
{...sendTestEmailProps}
onSendTestEmail={jest
.fn()
.mockRejectedValue(new Error("Network error"))}
/>,
)

await user.click(
screen.getByRole("button", { name: /send test email to me/i }),
)

expect(await screen.findByRole("alert")).toHaveTextContent(
/something went wrong sending the test email/i,
)
})

test("Email Preview section is not shown in review step", () => {
renderWithTheme(
<AssignSeatsConfirmModal
{...sendTestEmailProps}
invalidEmails={["bad@"]}
/>,
)

expect(screen.queryByText("Email Preview")).not.toBeInTheDocument()
})

test("Email Preview section is not shown in over-capacity state", () => {
renderWithTheme(
<AssignSeatsConfirmModal
{...sendTestEmailProps}
validCount={15}
availableSeats={10}
/>,
)

expect(screen.queryByText("Email Preview")).not.toBeInTheDocument()
})

test("resets to idle state when modal is reopened", async () => {
const { rerender } = renderWithTheme(
<AssignSeatsConfirmModal
{...sendTestEmailProps}
onSendTestEmail={jest.fn().mockResolvedValue(undefined)}
/>,
)

await user.click(
screen.getByRole("button", { name: /send test email to me/i }),
)
await screen.findByRole("alert")

// Close then reopen
rerender(
<AssignSeatsConfirmModal
{...sendTestEmailProps}
open={false}
onSendTestEmail={jest.fn().mockResolvedValue(undefined)}
/>,
)
rerender(
<AssignSeatsConfirmModal
{...sendTestEmailProps}
onSendTestEmail={jest.fn().mockResolvedValue(undefined)}
/>,
)

await waitFor(() =>
expect(screen.queryByRole("alert")).not.toBeInTheDocument(),
)
expect(
screen.getByRole("button", { name: /send test email to me/i }),
).not.toBeDisabled()
})
})

describe("AssignSeatsConfirmModal — copy buttons", () => {
afterEach(() => {
Object.defineProperty(navigator, "clipboard", {
Expand Down
Loading
Loading