patch curriculum..

This commit is contained in:
2026-08-07 10:27:19 +09:00
parent f8ccd8b0a3
commit 196ba51ec6
9 changed files with 353 additions and 199 deletions
@@ -0,0 +1,106 @@
WITH guide_week AS (
SELECT w."id"
FROM "course_weeks" w
JOIN "courses" c ON c."id" = w."course_id"
WHERE c."slug" = 'autonomy-studio-lms-guide'
AND w."week" = 1
), modules AS (
SELECT *
FROM (
VALUES
(
1,
'Simulator',
'NAV2-Gazebo',
'[lms-nav2-gazebo-simulator]nav2-simulator_v1.0.0_ws',
'http://192.168.0.242:3000/templates/lms-nav2-gazebo-simulator.git',
'future-nav2-gazebo:humble',
'["gui:=false"]'::JSONB,
'[{"url":"https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-simulator/1.0.0/prebuilt/latest/lms-nav2-gazebo-simulator-build-install.tar.zst","type":"build-install","fileName":"lms-nav2-gazebo-simulator-build-install.tar.zst","required":false,"extractPath":"."}]'::JSONB
),
(
2,
'Vehicle',
'NAV2-TurtleBot3Burger',
'[lms-nav2-gazebo-vehicle]nav2-vehicle_v1.0.0_ws',
'http://192.168.0.242:3000/templates/lms-nav2-gazebo-vehicle.git',
'future-nav2-gazebo:humble',
'["sim_mode:=true","platform:=turtlebot3","level:=2d"]'::JSONB,
'[{"url":"https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-vehicle/1.0.0/prebuilt/latest/lms-nav2-gazebo-vehicle-build-install.tar.zst","type":"build-install","fileName":"lms-nav2-gazebo-vehicle-build-install.tar.zst","required":false,"extractPath":"."}]'::JSONB
),
(
3,
'Localization',
'NAV2-SLAM',
'[lms-nav2-gazebo-slam]nav2-slam_v1.0.0_ws',
'http://192.168.0.242:3000/templates/lms-nav2-gazebo-slam.git',
'future-nav2-gazebo:humble',
'[]'::JSONB,
'[{"url":"https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-slam/1.0.0/prebuilt/latest/lms-nav2-gazebo-slam-build-install.tar.zst","type":"build-install","fileName":"lms-nav2-gazebo-slam-build-install.tar.zst","required":false,"extractPath":"."}]'::JSONB
)
) AS value("sort_order", "category", "name", "folder_name", "template_repository_url", "docker_image", "default_parameters", "prebuilt_artifacts")
)
INSERT INTO "course_week_modules" (
"week_id",
"sort_order",
"category",
"name",
"folder_name",
"template_repository_url",
"source_type",
"prebuilt_artifacts",
"default_parameters",
"docker_image",
"updated_at"
)
SELECT
guide_week."id",
modules."sort_order",
modules."category",
modules."name",
modules."folder_name",
modules."template_repository_url",
'GIT',
modules."prebuilt_artifacts",
modules."default_parameters",
modules."docker_image",
CURRENT_TIMESTAMP
FROM guide_week
CROSS JOIN modules
ON CONFLICT ("week_id", "folder_name") DO UPDATE
SET "category" = EXCLUDED."category",
"name" = EXCLUDED."name",
"template_repository_url" = EXCLUDED."template_repository_url",
"source_type" = EXCLUDED."source_type",
"prebuilt_artifacts" = EXCLUDED."prebuilt_artifacts",
"default_parameters" = EXCLUDED."default_parameters",
"docker_image" = EXCLUDED."docker_image",
"updated_at" = CURRENT_TIMESTAMP;
DELETE FROM "course_week_maps" maps
USING "courses" course
WHERE maps."course_id" = course."id"
AND course."slug" = 'autonomy-studio-lms-guide'
AND maps."week" = 1
AND maps."map_key" <> 'nav2-turtlebot3-world';
INSERT INTO "course_week_maps" (
"id",
"course_id",
"week",
"map_key",
"sort_order",
"updated_at"
)
SELECT
gen_random_uuid(),
course."id",
1,
'nav2-turtlebot3-world',
0,
CURRENT_TIMESTAMP
FROM "courses" course
WHERE course."slug" = 'autonomy-studio-lms-guide'
ON CONFLICT ("course_id", "week", "map_key") DO UPDATE
SET "sort_order" = EXCLUDED."sort_order",
"updated_at" = CURRENT_TIMESTAMP;
@@ -0,0 +1,2 @@
DROP TABLE IF EXISTS "course_week_visualization_platforms";
DROP TABLE IF EXISTS "visualization_platforms";
@@ -0,0 +1,53 @@
UPDATE "course_week_modules" modules
SET "sort_order" = 4,
"updated_at" = CURRENT_TIMESTAMP
FROM "course_weeks" week, "courses" course
WHERE modules."week_id" = week."id"
AND week."course_id" = course."id"
AND course."slug" = 'autonomy-studio-lms-guide'
AND week."week" = 1
AND modules."folder_name" = '[lms-nav2-gazebo-slam]nav2-slam_v1.0.0_ws';
WITH guide_week AS (
SELECT w."id"
FROM "course_weeks" w
JOIN "courses" c ON c."id" = w."course_id"
WHERE c."slug" = 'autonomy-studio-lms-guide'
AND w."week" = 1
)
INSERT INTO "course_week_modules" (
"week_id",
"sort_order",
"category",
"name",
"folder_name",
"template_repository_url",
"source_type",
"prebuilt_artifacts",
"default_parameters",
"docker_image",
"updated_at"
)
SELECT
guide_week."id",
3,
'Perception',
'NAV2-Perception',
'[lms-nav2-gazebo-perception]nav2-perception_v1.0.0_ws',
'http://192.168.0.242:3000/templates/lms-nav2-gazebo-perception.git',
'GIT',
'[{"url":"https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-perception/1.0.0/prebuilt/latest/lms-nav2-gazebo-perception-build-install.tar.zst","type":"build-install","fileName":"lms-nav2-gazebo-perception-build-install.tar.zst","required":false,"extractPath":"."}]'::JSONB,
'["sim_mode:=true"]'::JSONB,
'future-nav2-gazebo:humble',
CURRENT_TIMESTAMP
FROM guide_week
ON CONFLICT ("week_id", "folder_name") DO UPDATE
SET "sort_order" = EXCLUDED."sort_order",
"category" = EXCLUDED."category",
"name" = EXCLUDED."name",
"template_repository_url" = EXCLUDED."template_repository_url",
"source_type" = EXCLUDED."source_type",
"prebuilt_artifacts" = EXCLUDED."prebuilt_artifacts",
"default_parameters" = EXCLUDED."default_parameters",
"docker_image" = EXCLUDED."docker_image",
"updated_at" = CURRENT_TIMESTAMP;
-34
View File
@@ -162,7 +162,6 @@ model Course {
instructorCurriculumSettings InstructorCurriculumSetting[] instructorCurriculumSettings InstructorCurriculumSetting[]
instructorCurriculumWeekOverrides InstructorCurriculumWeekOverride[] instructorCurriculumWeekOverrides InstructorCurriculumWeekOverride[]
environmentProfiles CourseEnvironmentProfile[] environmentProfiles CourseEnvironmentProfile[]
visualizationSelections CourseWeekVisualizationPlatform[]
maps CourseMap[] maps CourseMap[]
weekMaps CourseWeekMap[] weekMaps CourseWeekMap[]
savedMaps StudentSavedMap[] savedMaps StudentSavedMap[]
@@ -195,39 +194,6 @@ model CourseEnvironmentProfile {
@@map("course_environment_profiles") @@map("course_environment_profiles")
} }
model VisualizationPlatform {
key String @id @map("platform_key")
type String @unique
displayName String @map("display_name")
description String?
defaultConfig Json @map("default_config")
sortOrder Int @default(0) @map("sort_order")
enabled Boolean @default(true)
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
courseWeekSelections CourseWeekVisualizationPlatform[]
@@index([enabled, sortOrder])
@@map("visualization_platforms")
}
model CourseWeekVisualizationPlatform {
id String @id @default(uuid()) @db.Uuid
courseId String @map("course_id") @db.Uuid
week Int
platformKey String @map("platform_key")
enabled Boolean @default(true)
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
course Course @relation(fields: [courseId], references: [id], onDelete: Cascade)
platform VisualizationPlatform @relation(fields: [platformKey], references: [key], onDelete: Cascade)
@@unique([courseId, week, platformKey])
@@index([courseId, week])
@@index([platformKey])
@@map("course_week_visualization_platforms")
}
model CourseMap { model CourseMap {
id String @id @default(uuid()) @db.Uuid id String @id @default(uuid()) @db.Uuid
courseId String @map("course_id") @db.Uuid courseId String @map("course_id") @db.Uuid
+10
View File
@@ -46,6 +46,16 @@ export class CurriculumController {
return this.curriculumService.listCourses(this.getBearerToken(request)); return this.curriculumService.listCourses(this.getBearerToken(request));
} }
@Get('course-catalog')
searchCourseCatalog(@Req() request: Request, @Query('query') query?: string) {
return this.curriculumService.searchCourseCatalog(this.getBearerToken(request), query);
}
@Post('course-catalog/:courseId/enroll')
enrollCourse(@Req() request: Request, @Param('courseId') courseId: string) {
return this.curriculumService.enrollCourse(this.getBearerToken(request), courseId);
}
@Get('references/:referenceId/download') @Get('references/:referenceId/download')
async downloadReference( async downloadReference(
@Req() request: Request, @Req() request: Request,
+137 -36
View File
@@ -2,7 +2,7 @@ import { BadRequestException, ConflictException, Injectable, NotFoundException,
import { Prisma } from '@prisma/client'; import { Prisma } from '@prisma/client';
import { join } from 'node:path'; import { join } from 'node:path';
import { AuthService } from '../auth/auth.service'; import { AuthResponseUser, AuthService } from '../auth/auth.service';
import { PlatformControlService } from '../platform-control/platform-control.service'; import { PlatformControlService } from '../platform-control/platform-control.service';
import { PrismaService } from '../prisma/prisma.service'; import { PrismaService } from '../prisma/prisma.service';
import { R2PublicFileService } from '../storage/r2-public-file.service'; import { R2PublicFileService } from '../storage/r2-public-file.service';
@@ -30,20 +30,21 @@ export class CurriculumService {
const lmsUser = authUser?.email const lmsUser = authUser?.email
? await this.prisma.user.findUnique({ where: { email: authUser.email } }) ? await this.prisma.user.findUnique({ where: { email: authUser.email } })
: null; : null;
const guideCourse = await this.findPublicGuideCourse(lmsUser?.id);
const guideCourseItem = guideCourse ? this.serializeCourseListItem(guideCourse, true) : null;
if (!lmsUser) { if (!lmsUser) {
const guideCourse = await this.findPublicGuideCourse(); return { courses: [], guideCourse: guideCourseItem };
return {
courses: guideCourse ? [this.serializeCourseListItem(guideCourse, true)] : [],
};
} }
const enrollments = await this.prisma.studentEnrollment.findMany({ const enrollments = await this.prisma.studentEnrollment.findMany({
where: { where: {
userId: lmsUser.id, userId: lmsUser.id,
status: 'ACTIVE', status: 'ACTIVE',
course: { isActive: true }, course: {
isActive: true,
slug: { not: PUBLIC_GUIDE_COURSE_SLUG },
},
}, },
include: { include: {
course: { course: {
@@ -66,19 +67,128 @@ export class CurriculumService {
orderBy: { updatedAt: 'desc' }, orderBy: { updatedAt: 'desc' },
}); });
if (enrollments.length) { return {
return { courses: enrollments.map((enrollment) => this.serializeCourseListItem(enrollment.course)),
courses: enrollments.map((enrollment) => this.serializeCourseListItem(enrollment.course)), guideCourse: guideCourseItem,
}; };
} }
const guideCourse = await this.findPublicGuideCourse(lmsUser.id); async searchCourseCatalog(accessToken: string | undefined, query?: string) {
const { user: authUser } = await this.authService.me(accessToken);
if (!authUser?.email) throw new UnauthorizedException('Invalid user');
const keyword = query?.trim() ?? '';
const courses = await this.prisma.course.findMany({
where: {
isActive: true,
slug: { not: PUBLIC_GUIDE_COURSE_SLUG },
...(keyword
? {
OR: [
{ title: { contains: keyword, mode: 'insensitive' } },
{ teacher: { name: { contains: keyword, mode: 'insensitive' } } },
{ teacher: { email: { contains: keyword, mode: 'insensitive' } } },
],
}
: {}),
},
select: {
id: true,
title: true,
subtitle: true,
organizationName: true,
teacher: {
select: {
name: true,
email: true,
primaryOrganizationName: true,
},
},
},
orderBy: [{ title: 'asc' }],
take: 20,
});
const lmsUser = await this.prisma.user.findUnique({
where: { email: authUser.email },
select: { id: true },
});
const enrolledIds = lmsUser
? new Set((await this.prisma.studentEnrollment.findMany({
where: {
userId: lmsUser.id,
courseId: { in: courses.map((course) => course.id) },
status: 'ACTIVE',
},
select: { courseId: true },
})).map((enrollment) => enrollment.courseId))
: new Set<string>();
return { return {
courses: guideCourse ? [this.serializeCourseListItem(guideCourse, true)] : [], courses: courses.map((course) => ({
...course,
enrolled: enrolledIds.has(course.id),
})),
}; };
} }
async enrollCourse(accessToken: string | undefined, courseId: string) {
const { user: authUser } = await this.authService.me(accessToken);
if (!authUser?.email) throw new UnauthorizedException('Invalid user');
const normalizedCourseId = courseId.trim();
if (!normalizedCourseId) throw new BadRequestException('courseId is required');
const course = await this.prisma.course.findFirst({
where: {
id: normalizedCourseId,
isActive: true,
slug: { not: PUBLIC_GUIDE_COURSE_SLUG },
},
include: {
teacher: {
select: {
primaryOrganizationId: true,
},
},
},
});
if (!course) throw new NotFoundException('수강 신청할 강좌를 찾을 수 없습니다.');
const lmsUser = await this.prisma.user.upsert({
where: { email: authUser.email },
update: this.toUserSyncData(authUser),
create: {
email: authUser.email,
...this.toUserSyncData(authUser),
},
});
await this.prisma.studentEnrollment.upsert({
where: {
userId_courseId: {
userId: lmsUser.id,
courseId: course.id,
},
},
update: {
organizationId: course.teacher.primaryOrganizationId,
organizationName: course.organizationName,
status: 'ACTIVE',
removedAt: null,
},
create: {
userId: lmsUser.id,
courseId: course.id,
organizationId: course.teacher.primaryOrganizationId,
organizationName: course.organizationName,
status: 'ACTIVE',
},
});
return { enrolled: true, courseId: course.id };
}
async getActiveCurriculum(accessToken?: string, courseId?: string | null) { async getActiveCurriculum(accessToken?: string, courseId?: string | null) {
const authUser = accessToken const authUser = accessToken
? await this.authService.me(accessToken).then((response) => response.user).catch(() => null) ? await this.authService.me(accessToken).then((response) => response.user).catch(() => null)
@@ -197,15 +307,6 @@ export class CurriculumService {
customModulesByWeek.set(module.week, items); customModulesByWeek.set(module.week, items);
}); });
const visualizationSelections = await this.prisma.courseWeekVisualizationPlatform.findMany({
where: {
courseId: course.id,
enabled: true,
},
include: {
platform: true,
},
});
const mapKeysByWeek = new Map<number, string[]>(); const mapKeysByWeek = new Map<number, string[]>();
course.weekMaps.forEach((selection) => { course.weekMaps.forEach((selection) => {
const items = mapKeysByWeek.get(selection.week) ?? []; const items = mapKeysByWeek.get(selection.week) ?? [];
@@ -232,19 +333,6 @@ export class CurriculumService {
}); });
visualizationProfilesByWeek.set(profile.week, items); visualizationProfilesByWeek.set(profile.week, items);
}); });
visualizationSelections.forEach((selection) => {
const items = visualizationProfilesByWeek.get(selection.week) ?? [];
if (items.some((profile) => profile.platformKey === selection.platformKey)) return;
items.push({
id: selection.id,
week: selection.week,
platformKey: selection.platformKey,
profileType: selection.platform.type,
name: selection.platform.displayName,
config: selection.platform.defaultConfig,
});
visualizationProfilesByWeek.set(selection.week, items);
});
const progress = course.studentProgress[0]; const progress = course.studentProgress[0];
const enrollment = lmsUser const enrollment = lmsUser
? await this.prisma.studentEnrollment.findFirst({ ? await this.prisma.studentEnrollment.findFirst({
@@ -861,6 +949,19 @@ export class CurriculumService {
}; };
} }
private toUserSyncData(user: AuthResponseUser) {
return {
authUserId: user.id,
name: user.name,
authRole: user.role,
primaryOrganizationId: user.primaryAffiliation?.organizationId ?? null,
primaryOrganizationName: user.primaryAffiliation?.organizationName ?? null,
primaryOrganizationType: user.primaryAffiliation?.organizationType ?? null,
department: user.primaryAffiliation?.department ?? null,
position: user.primaryAffiliation?.position ?? null,
};
}
private serializeLearningStats( private serializeLearningStats(
progress: { progress: {
completedWeeks: number; completedWeeks: number;
@@ -106,11 +106,6 @@ export class UpdateInstructorCurriculumDto {
}>; }>;
}>; }>;
@IsOptional()
@IsArray()
@IsString({ each: true })
visualizationPlatformKeys?: string[];
@IsOptional() @IsOptional()
@IsArray() @IsArray()
@IsString({ each: true }) @IsString({ each: true })
+2 -105
View File
@@ -1406,26 +1406,7 @@ export class InstructorService {
courseId: course.id, courseId: course.id,
}, },
}); });
const visualizationPlatforms = await this.prisma.visualizationPlatform.findMany({
where: { enabled: true },
orderBy: { sortOrder: 'asc' },
});
const tagOptions = await this.findCurriculumTagOptions(); const tagOptions = await this.findCurriculumTagOptions();
const visualizationSelections = await this.prisma.courseWeekVisualizationPlatform.findMany({
where: {
courseId: course.id,
enabled: true,
},
include: {
platform: true,
},
});
const visualizationSelectionsByWeek = new Map<number, typeof visualizationSelections>();
visualizationSelections.forEach((selection) => {
const items = visualizationSelectionsByWeek.get(selection.week) ?? [];
items.push(selection);
visualizationSelectionsByWeek.set(selection.week, items);
});
const courseMaps = await this.prisma.courseMap.findMany({ const courseMaps = await this.prisma.courseMap.findMany({
where: { courseId: course.id }, where: { courseId: course.id },
orderBy: { createdAt: 'asc' }, orderBy: { createdAt: 'asc' },
@@ -1505,13 +1486,6 @@ export class InstructorService {
name: profile.name, name: profile.name,
config: profile.config, config: profile.config,
})), })),
visualizationPlatforms: (visualizationSelectionsByWeek.get(week.week) ?? []).map((selection) => ({
key: selection.platform.key,
type: selection.platform.type,
displayName: selection.platform.displayName,
description: selection.platform.description,
config: selection.platform.defaultConfig,
})),
mapKeys: mapKeysByWeek.get(week.week) ?? [], mapKeys: mapKeysByWeek.get(week.week) ?? [],
}; };
}); });
@@ -1546,13 +1520,6 @@ export class InstructorService {
}, },
isTeamProject: override.isTeamProject ?? false, isTeamProject: override.isTeamProject ?? false,
modules: [], modules: [],
visualizationPlatforms: (visualizationSelectionsByWeek.get(override.week) ?? []).map((selection) => ({
key: selection.platform.key,
type: selection.platform.type,
displayName: selection.platform.displayName,
description: selection.platform.description,
config: selection.platform.defaultConfig,
})),
mapKeys: mapKeysByWeek.get(override.week) ?? [], mapKeys: mapKeysByWeek.get(override.week) ?? [],
}; };
}); });
@@ -1567,13 +1534,6 @@ export class InstructorService {
}, },
weeks: [...baseWeeks, ...addedWeeks].sort((left, right) => left.week - right.week), weeks: [...baseWeeks, ...addedWeeks].sort((left, right) => left.week - right.week),
tagOptions, tagOptions,
visualizationPlatforms: visualizationPlatforms.map((platform) => ({
key: platform.key,
type: platform.type,
displayName: platform.displayName,
description: platform.description,
config: platform.defaultConfig,
})),
mapOptions: [ mapOptions: [
...NAV2_SYSTEM_MAPS, ...NAV2_SYSTEM_MAPS,
...courseMaps.map((map) => ({ ...courseMaps.map((map) => ({
@@ -1710,15 +1670,6 @@ export class InstructorService {
}); });
return items; return items;
}, []); }, []);
const selections = await this.prisma.courseWeekVisualizationPlatform.findMany({
where: {
courseId: course.id,
week: input.activeWeek,
enabled: true,
},
include: { platform: true },
});
const selectionsByPlatformKey = new Map(selections.map((selection) => [selection.platformKey, selection]));
const profiles = await this.prisma.courseEnvironmentProfile.findMany({ const profiles = await this.prisma.courseEnvironmentProfile.findMany({
where: { courseId: course.id, week: input.activeWeek }, where: { courseId: course.id, week: input.activeWeek },
}); });
@@ -1732,33 +1683,22 @@ export class InstructorService {
&& (profile.config as Prisma.JsonObject).target === 'remote-jetson') && (profile.config as Prisma.JsonObject).target === 'remote-jetson')
); );
const wantsPlatform = logicalEnvironment === 'platform'; const wantsPlatform = logicalEnvironment === 'platform';
const selection = isLogicalEnvironment
? selections.find((candidate) => wantsPlatform === isPlatformProfile({
profileType: candidate.platform.type,
config: candidate.platform.defaultConfig,
}))
: selectionsByPlatformKey.get(item.platformKey);
const existingProfile = isLogicalEnvironment const existingProfile = isLogicalEnvironment
? profiles.find((profile) => wantsPlatform === isPlatformProfile(profile)) ? profiles.find((profile) => wantsPlatform === isPlatformProfile(profile))
: profilesByPlatformKey.get(item.platformKey); : profilesByPlatformKey.get(item.platformKey);
if (!isLogicalEnvironment && !selection && !existingProfile) { if (!isLogicalEnvironment && !existingProfile) {
throw new BadRequestException('선택한 환경 프로필을 찾을 수 없습니다.'); throw new BadRequestException('선택한 환경 프로필을 찾을 수 없습니다.');
} }
const resolvedPlatformKey = existingProfile?.platformKey const resolvedPlatformKey = existingProfile?.platformKey
?? selection?.platformKey
?? (wantsPlatform ? 'futuredrive-platform' : 'futuredrive-local-pc'); ?? (wantsPlatform ? 'futuredrive-platform' : 'futuredrive-local-pc');
const profileType = existingProfile?.profileType const profileType = existingProfile?.profileType
?? selection?.platform.type
?? (wantsPlatform ? 'PLATFORM' : 'LOCAL'); ?? (wantsPlatform ? 'PLATFORM' : 'LOCAL');
const profileName = existingProfile?.name const profileName = existingProfile?.name
?? selection?.platform.displayName
?? (wantsPlatform ? '플랫폼' : '로컬'); ?? (wantsPlatform ? '플랫폼' : '로컬');
const existingConfig = existingProfile?.config && typeof existingProfile.config === 'object' && !Array.isArray(existingProfile.config) const existingConfig = existingProfile?.config && typeof existingProfile.config === 'object' && !Array.isArray(existingProfile.config)
? existingProfile.config as Prisma.JsonObject ? existingProfile.config as Prisma.JsonObject
: {}; : {};
const defaultConfig = selection?.platform.defaultConfig && typeof selection.platform.defaultConfig === 'object' && !Array.isArray(selection.platform.defaultConfig) const defaultConfig = wantsPlatform ? { target: 'remote-jetson' } : { target: 'local' };
? selection.platform.defaultConfig as Prisma.JsonObject
: wantsPlatform ? { target: 'remote-jetson' } : { target: 'local' };
const config = { const config = {
...defaultConfig, ...defaultConfig,
...existingConfig, ...existingConfig,
@@ -1855,49 +1795,6 @@ export class InstructorService {
}); });
} }
if (input.visualizationPlatformKeys !== undefined) {
const selectedKeys = Array.from(new Set(input.visualizationPlatformKeys.map((key) => key.trim().toUpperCase()).filter(Boolean)));
const validPlatforms = selectedKeys.length
? await this.prisma.visualizationPlatform.findMany({
where: {
key: { in: selectedKeys },
enabled: true,
},
select: { key: true },
})
: [];
const validKeySet = new Set(validPlatforms.map((platform) => platform.key));
await this.prisma.courseWeekVisualizationPlatform.deleteMany({
where: {
courseId: course.id,
week: input.activeWeek,
platformKey: { notIn: selectedKeys },
},
});
await Promise.all(selectedKeys.filter((key) => validKeySet.has(key)).map((key) => (
this.prisma.courseWeekVisualizationPlatform.upsert({
where: {
courseId_week_platformKey: {
courseId: course.id,
week: input.activeWeek,
platformKey: key,
},
},
create: {
courseId: course.id,
week: input.activeWeek,
platformKey: key,
enabled: true,
},
update: {
enabled: true,
},
})
)));
}
if (input.mapKeys !== undefined) { if (input.mapKeys !== undefined) {
const selectedKeys = Array.from(new Set(input.mapKeys.map((key) => key.trim()).filter(Boolean))); const selectedKeys = Array.from(new Set(input.mapKeys.map((key) => key.trim()).filter(Boolean)));
const customMaps = selectedKeys.length const customMaps = selectedKeys.length
+43 -19
View File
@@ -12,6 +12,8 @@ import { AuthService } from '../auth/auth.service';
import { PrismaService } from '../prisma/prisma.service'; import { PrismaService } from '../prisma/prisma.service';
import { R2PublicFileService } from '../storage/r2-public-file.service'; import { R2PublicFileService } from '../storage/r2-public-file.service';
const PUBLIC_GUIDE_COURSE_SLUG = 'autonomy-studio-lms-guide';
@Injectable() @Injectable()
export class StudioService { export class StudioService {
private readonly giteaBaseUrl: string; private readonly giteaBaseUrl: string;
@@ -53,26 +55,7 @@ export class StudioService {
const folderName = input.folderName?.trim(); const folderName = input.folderName?.trim();
if (!moduleName && !folderName) throw new BadRequestException('moduleName or folderName is required'); if (!moduleName && !folderName) throw new BadRequestException('moduleName or folderName is required');
const lmsUser = await this.prisma.user.findUnique({
where: { email: authUser.email },
});
if (!lmsUser) throw new NotFoundException('LMS user not found');
const courseId = input.courseId?.trim(); const courseId = input.courseId?.trim();
let enrollmentOrganizationName: string | undefined;
if (courseId) {
const enrollment = await this.prisma.studentEnrollment.findFirst({
where: {
userId: lmsUser.id,
courseId,
status: 'ACTIVE',
},
select: { organizationName: true },
});
if (!enrollment) throw new NotFoundException('Student enrollment not found');
enrollmentOrganizationName = enrollment.organizationName;
}
const courseWeek = await this.prisma.courseWeek.findFirst({ const courseWeek = await this.prisma.courseWeek.findFirst({
where: courseId where: courseId
? { ? {
@@ -87,6 +70,11 @@ export class StudioService {
isActive: true, isActive: true,
}, },
}, },
include: {
course: {
select: { slug: true },
},
},
}); });
if (!courseWeek) throw new NotFoundException('Course week not found'); if (!courseWeek) throw new NotFoundException('Course week not found');
@@ -108,6 +96,42 @@ export class StudioService {
throw new BadRequestException('templateRepositoryUrl is required'); throw new BadRequestException('templateRepositoryUrl is required');
} }
const isPublicGuide = courseWeek.course.slug === PUBLIC_GUIDE_COURSE_SLUG;
if (isPublicGuide) {
return {
id: `guide-${module.id}`,
owner: null,
repoName: null,
repositoryUrl: null,
repositoryUrlWithToken: null,
templateRepositoryUrl,
templateRepositoryUrlWithToken: this.withToken(templateRepositoryUrl),
sourceType: module.sourceType ?? 'GIT',
prebuiltArtifacts: module.prebuiltArtifacts ?? [],
workspaceScope: 'PERSONAL',
groupId: null,
};
}
const lmsUser = await this.prisma.user.findUnique({
where: { email: authUser.email },
});
if (!lmsUser) throw new NotFoundException('LMS user not found');
let enrollmentOrganizationName: string | undefined;
if (courseId) {
const enrollment = await this.prisma.studentEnrollment.findFirst({
where: {
userId: lmsUser.id,
courseId,
status: 'ACTIVE',
},
select: { organizationName: true },
});
if (!enrollment) throw new NotFoundException('Student enrollment not found');
enrollmentOrganizationName = enrollment.organizationName;
}
const organizationName = enrollmentOrganizationName ?? lmsUser.primaryOrganizationName ?? undefined; const organizationName = enrollmentOrganizationName ?? lmsUser.primaryOrganizationName ?? undefined;
const teamProject = await this.isTeamProjectWeek(courseWeek.courseId, courseWeek.week, organizationName); const teamProject = await this.isTeamProjectWeek(courseWeek.courseId, courseWeek.week, organizationName);
const studentGroup = teamProject const studentGroup = teamProject