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
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-- Add Video content type
ALTER TYPE "ContentType"
ADD VALUE 'VIDEO';
Original file line number Diff line number Diff line change
@@ -1,26 +1,22 @@
-- Add Video content type
ALTER TYPE "ContentType"
ADD VALUE 'VIDEO';

-- Add Learning Unit Content table
CREATE TABLE "learning_unit_content" (
CREATE TABLE "learning_unit_contents" (
"id" UUID NOT NULL DEFAULT uuid_generate_v7 (),
"unit_id" UUID NOT NULL,
"learning_unit_id" UUID NOT NULL,
"type" "ContentType" NOT NULL,
"url" TEXT,
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "learning_unit_content_pkey" PRIMARY KEY ("id")
CONSTRAINT "learning_unit_contents_pkey" PRIMARY KEY ("id")
);

ALTER TABLE "learning_unit_content"
ADD CONSTRAINT "learning_unit_content_unit_id_fkey" FOREIGN KEY ("unit_id") REFERENCES "learning_units" ("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "learning_unit_contents"
ADD CONSTRAINT "learning_unit_contents_learning_unit_id_fkey" FOREIGN KEY ("learning_unit_id") REFERENCES "learning_units" ("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- Backfill podcast content
INSERT INTO
learning_unit_content (
learning_unit_contents (
id,
unit_id,
learning_unit_id,
type,
url,
created_at,
Expand All @@ -43,21 +39,21 @@ CREATE TABLE "learning_journey_checkpoints" (
"updated_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"last_checkpoint" DECIMAL(65, 30) NOT NULL,
"learning_journey_id" UUID NOT NULL,
"content_item_id" UUID NOT NULL,
CONSTRAINT "learning_journey_checkpoints_pkey" PRIMARY KEY ("learning_journey_id", "content_item_id")
"learning_unit_content_id" UUID NOT NULL,
CONSTRAINT "learning_journey_checkpoints_pkey" PRIMARY KEY ("learning_journey_id", "learning_unit_content_id")
);

ALTER TABLE "learning_journey_checkpoints"
ADD CONSTRAINT "learning_journey_checkpoints_learning_journey_id_fkey" FOREIGN KEY ("learning_journey_id") REFERENCES "learning_journeys" ("id") ON DELETE CASCADE ON UPDATE CASCADE;

ALTER TABLE "learning_journey_checkpoints"
ADD CONSTRAINT "learning_journey_checkpoints_content_item_id_fkey" FOREIGN KEY ("content_item_id") REFERENCES "learning_unit_content" ("id") ON DELETE CASCADE ON UPDATE CASCADE;
ADD CONSTRAINT "learning_journey_checkpoints_learning_unit_content_id_fkey" FOREIGN KEY ("learning_unit_content_id") REFERENCES "learning_unit_contents" ("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- Backfill podcast endpoints
INSERT INTO
"learning_journey_checkpoints" (
"learning_journey_id",
"content_item_id",
"learning_unit_content_id",
"last_checkpoint",
"created_at",
"updated_at"
Expand All @@ -70,22 +66,27 @@ SELECT
NOW()
FROM
"learning_journeys" lj
JOIN "learning_unit_content" luc ON luc."unit_id" = lj."learning_unit_id"
JOIN "learning_unit_contents" luc ON luc."learning_unit_id" = lj."learning_unit_id"
AND luc."type" = 'PODCAST'
WHERE
lj."last_checkpoint" > 0
AND NOT EXISTS (
SELECT
1
FROM
"learning_unit_content" vid
"learning_unit_contents" vid
WHERE
vid."unit_id" = lj."learning_unit_id"
vid."learning_unit_id" = lj."learning_unit_id"
AND vid."type" = 'VIDEO'
)
ON CONFLICT ("learning_journey_id", "content_item_id") DO NOTHING;
ON CONFLICT ("learning_journey_id", "learning_unit_content_id") DO NOTHING;

-- Drop legacy columns now migrated to separate tables
ALTER TABLE "learning_units" DROP COLUMN "content_type";
ALTER TABLE "learning_units" DROP COLUMN "content_url";
ALTER TABLE "learning_journeys" DROP COLUMN "last_checkpoint";
ALTER TABLE "learning_units"
DROP COLUMN "content_type";

ALTER TABLE "learning_units"
DROP COLUMN "content_url";

ALTER TABLE "learning_journeys"
DROP COLUMN "last_checkpoint";
34 changes: 19 additions & 15 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -82,23 +82,27 @@ model LearningUnit {
learningJourneys LearningJourney[]
questionAnswers QuestionAnswer[]
sources LearningUnitSources[]
contentItems LearningUnitContent[]
contents LearningUnitContent[]

@@map("learning_units")
}

model LearningUnitContent {
id String @id @default(dbgenerated("uuid_generate_v7()")) @db.Uuid
unitId String @map("unit_id") @db.Uuid
type ContentType @map("type")
url String? @map("url")
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(3)

unit LearningUnit @relation(fields: [unitId], references: [id], onDelete: Cascade)
checkpoints LearningJourneyCheckpoint[]
id String @id @default(dbgenerated("uuid_generate_v7()")) @db.Uuid
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(3)

// Domain-Specific Fields.
type ContentType @map("type")
url String? @map("url")

// Relations.
learningUnitId String @map("learning_unit_id") @db.Uuid

learningUnit LearningUnit @relation(fields: [learningUnitId], references: [id], onDelete: Cascade)
checkpoints LearningJourneyCheckpoint[]

@@map("learning_unit_content")
@@map("learning_unit_contents")
}

model LearningUnitCollection {
Expand Down Expand Up @@ -193,12 +197,12 @@ model LearningJourneyCheckpoint {

// Relations.
learningJourneyId String @map("learning_journey_id") @db.Uuid
contentItemId String @map("content_item_id") @db.Uuid
learningUnitContentId String @map("learning_unit_content_id") @db.Uuid

learningJourney LearningJourney @relation(fields: [learningJourneyId], references: [id], onDelete: Cascade)
contentItem LearningUnitContent @relation(fields: [contentItemId], references: [id], onDelete: Cascade)
learningJourney LearningJourney @relation(fields: [learningJourneyId], references: [id], onDelete: Cascade)
learningUnitContent LearningUnitContent @relation(fields: [learningUnitContentId], references: [id], onDelete: Cascade)

@@id([learningJourneyId, contentItemId])
@@id([learningJourneyId, learningUnitContentId])
@@map("learning_journey_checkpoints")
}

Expand Down
24 changes: 12 additions & 12 deletions prisma/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,7 @@ const learningUnits: Prisma.LearningUnitCreateInput[] = [
questionAnswers: {
create: questionAnswers,
},
contentItems: {
contents: {
create: [{ type: ContentType.PODCAST, url: 'http://localhost:5173' }],
},
},
Expand Down Expand Up @@ -391,7 +391,7 @@ const learningUnits: Prisma.LearningUnitCreateInput[] = [
questionAnswers: {
create: questionAnswers,
},
contentItems: {
contents: {
create: [{ type: ContentType.PODCAST, url: 'http://localhost:5173' }],
},
},
Expand Down Expand Up @@ -436,7 +436,7 @@ const learningUnits: Prisma.LearningUnitCreateInput[] = [
questionAnswers: {
create: questionAnswers,
},
contentItems: {
contents: {
create: [{ type: ContentType.QUIZ }],
},
},
Expand Down Expand Up @@ -477,7 +477,7 @@ const learningUnits: Prisma.LearningUnitCreateInput[] = [
},
],
},
contentItems: {
contents: {
create: [
{ type: ContentType.VIDEO, url: 'https://vimeo.com/1172996293' },
{ type: ContentType.PODCAST, url: 'http://localhost:5173' },
Expand Down Expand Up @@ -524,7 +524,7 @@ const learningUnits: Prisma.LearningUnitCreateInput[] = [
questionAnswers: {
create: questionAnswers,
},
contentItems: {
contents: {
create: [
{ type: ContentType.VIDEO, url: 'http://localhost:5173' },
{ type: ContentType.PODCAST, url: 'http://localhost:5173' },
Expand Down Expand Up @@ -563,7 +563,7 @@ const learningUnits: Prisma.LearningUnitCreateInput[] = [
questionAnswers: {
create: questionAnswers,
},
contentItems: {
contents: {
create: [{ type: ContentType.PODCAST, url: 'http://localhost:5173' }],
},
},
Expand Down Expand Up @@ -598,7 +598,7 @@ const learningUnits: Prisma.LearningUnitCreateInput[] = [
questionAnswers: {
create: questionAnswers,
},
contentItems: {
contents: {
create: [{ type: ContentType.PODCAST, url: 'http://localhost:5173' }],
},
},
Expand Down Expand Up @@ -633,7 +633,7 @@ const learningUnits: Prisma.LearningUnitCreateInput[] = [
questionAnswers: {
create: questionAnswers,
},
contentItems: {
contents: {
create: [{ type: ContentType.PODCAST, url: 'http://localhost:5173' }],
},
},
Expand Down Expand Up @@ -668,7 +668,7 @@ const learningUnits: Prisma.LearningUnitCreateInput[] = [
questionAnswers: {
create: questionAnswers,
},
contentItems: {
contents: {
create: [{ type: ContentType.PODCAST, url: 'http://localhost:5173' }],
},
},
Expand Down Expand Up @@ -703,7 +703,7 @@ const learningUnits: Prisma.LearningUnitCreateInput[] = [
questionAnswers: {
create: questionAnswers,
},
contentItems: {
contents: {
create: [{ type: ContentType.PODCAST, url: 'http://localhost:5173' }],
},
},
Expand Down Expand Up @@ -738,7 +738,7 @@ const learningUnits: Prisma.LearningUnitCreateInput[] = [
questionAnswers: {
create: questionAnswers,
},
contentItems: {
contents: {
create: [{ type: ContentType.PODCAST, url: 'http://localhost:5173' }],
},
},
Expand Down Expand Up @@ -771,7 +771,7 @@ const learningUnits: Prisma.LearningUnitCreateInput[] = [
},
],
},
contentItems: {
contents: {
create: [
{ type: ContentType.VIDEO, url: 'https://vimeo.com/1172996293' },
{ type: ContentType.PODCAST, url: 'http://localhost:5173' },
Expand Down
24 changes: 11 additions & 13 deletions src/lib/components/LearningUnitForm/LearningUnitForm.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script module lang="ts">
export interface UnitState {
title: string;
contentItems: { type: 'VIDEO' | 'PODCAST' | 'QUIZ'; url: string | undefined }[];
contents: { type: 'VIDEO' | 'PODCAST' | 'QUIZ'; url: string | undefined }[];
collectionId: string;
summary: string;
objectives: string;
Expand Down Expand Up @@ -45,11 +45,11 @@
let { unit = $bindable(), data, form, onsubmit }: Props = $props();

const addContentItem = () => {
unit = { ...unit, contentItems: [...unit.contentItems, { type: 'PODCAST', url: '' }] };
unit = { ...unit, contents: [...unit.contents, { type: 'PODCAST', url: '' }] };
};

const removeContentItem = (index: number) => {
unit = { ...unit, contentItems: unit.contentItems.filter((_, i) => i !== index) };
unit = { ...unit, contents: unit.contents.filter((_, i) => i !== index) };
};

const minDueDate = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString().split('T')[0];
Expand Down Expand Up @@ -108,27 +108,25 @@
<div class="flex flex-col gap-3">
<span class="text-sm font-medium">Content</span>

{#if form?.errors?.contentItems?.message}
<p class="text-sm text-red-600">{form.errors.contentItems.message}</p>
{#if form?.errors?.contents?.message}
<p class="text-sm text-red-600">{form.errors.contents.message}</p>
{/if}

{#each unit.contentItems as item, i (i)}
{#each unit.contents as item, i (i)}
<div class="flex items-start gap-2">
<Select
id="contentItems-{i}-type"
id="contents-{i}-type"
bind:value={item.type}
onchange={() => {
if (item.type === 'QUIZ') {
unit = {
...unit,
contentItems: unit.contentItems.map((c, j) =>
j === i ? { ...c, url: undefined } : c,
),
contents: unit.contents.map((c, j) => (j === i ? { ...c, url: undefined } : c)),
};
} else {
unit = {
...unit,
contentItems: unit.contentItems.map((c, j) =>
contents: unit.contents.map((c, j) =>
j === i ? { ...c, url: c.url ?? '' } : c,
),
};
Expand All @@ -141,7 +139,7 @@
</Select>
{#if item.type !== 'QUIZ'}
<TextInput
id="contentItems-{i}-url"
id="contents-{i}-url"
type="url"
bind:value={item.url}
placeholder={item.type === 'VIDEO' ? 'https://...' : 'Podcast URL'}
Expand All @@ -156,7 +154,7 @@
<Button type="button" variant="secondary" onclick={addContentItem}>Add content item</Button>
</div>

<input type="hidden" name="contentItems" value={JSON.stringify(unit.contentItems)} />
<input type="hidden" name="contents" value={JSON.stringify(unit.contents)} />

<FormField
label="Collection"
Expand Down
12 changes: 6 additions & 6 deletions src/lib/components/LearningUnitForm/LearningUnitForm.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { LearningUnitForm } from './index.js';

const BASE_UNIT = {
title: '',
contentItems: [],
contents: [],
collectionId: '',
summary: '',
objectives: '',
Expand Down Expand Up @@ -38,7 +38,7 @@ describe('LearningUnitForm - content items', () => {
const user = userEvent.setup();
render(LearningUnitForm, {
props: {
unit: { ...BASE_UNIT, contentItems: [] },
unit: { ...BASE_UNIT, contents: [] },
data: BASE_DATA,
form: null,
onsubmit: vi.fn(),
Expand All @@ -53,7 +53,7 @@ describe('LearningUnitForm - content items', () => {
test('shows URL input for PODCAST type', async () => {
render(LearningUnitForm, {
props: {
unit: { ...BASE_UNIT, contentItems: [{ type: 'PODCAST', url: '' }] },
unit: { ...BASE_UNIT, contents: [{ type: 'PODCAST', url: '' }] },
data: BASE_DATA,
form: null,
onsubmit: vi.fn(),
Expand All @@ -65,7 +65,7 @@ describe('LearningUnitForm - content items', () => {
test('shows URL input for VIDEO type', () => {
render(LearningUnitForm, {
props: {
unit: { ...BASE_UNIT, contentItems: [{ type: 'VIDEO', url: '' }] },
unit: { ...BASE_UNIT, contents: [{ type: 'VIDEO', url: '' }] },
data: BASE_DATA,
form: null,
onsubmit: vi.fn(),
Expand All @@ -77,7 +77,7 @@ describe('LearningUnitForm - content items', () => {
test('does not show URL input for QUIZ type', () => {
render(LearningUnitForm, {
props: {
unit: { ...BASE_UNIT, contentItems: [{ type: 'QUIZ', url: undefined }] },
unit: { ...BASE_UNIT, contents: [{ type: 'QUIZ', url: undefined }] },
data: BASE_DATA,
form: null,
onsubmit: vi.fn(),
Expand All @@ -90,7 +90,7 @@ describe('LearningUnitForm - content items', () => {
const user = userEvent.setup();
render(LearningUnitForm, {
props: {
unit: { ...BASE_UNIT, contentItems: [{ type: 'PODCAST', url: 'https://example.com' }] },
unit: { ...BASE_UNIT, contents: [{ type: 'PODCAST', url: 'https://example.com' }] },
data: BASE_DATA,
form: null,
onsubmit: vi.fn(),
Expand Down
Loading
Loading