@studious-lms/server 1.25.0 → 1.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/models/marketplace.d.ts +35 -0
- package/dist/models/marketplace.d.ts.map +1 -1
- package/dist/models/marketplace.js +9 -2
- package/dist/models/marketplace.js.map +1 -1
- package/dist/models/module.d.ts +228 -0
- package/dist/models/module.d.ts.map +1 -0
- package/dist/models/module.js +69 -0
- package/dist/models/module.js.map +1 -0
- package/dist/pipelines/gradeWorksheet.d.ts.map +1 -1
- package/dist/pipelines/gradeWorksheet.js +4 -2
- package/dist/pipelines/gradeWorksheet.js.map +1 -1
- package/dist/routers/_app.d.ts +760 -0
- package/dist/routers/_app.d.ts.map +1 -1
- package/dist/routers/_app.js +4 -2
- package/dist/routers/_app.js.map +1 -1
- package/dist/routers/marketplace.d.ts +41 -0
- package/dist/routers/marketplace.d.ts.map +1 -1
- package/dist/routers/marketplace.js +13 -3
- package/dist/routers/marketplace.js.map +1 -1
- package/dist/routers/module.d.ts +345 -0
- package/dist/routers/module.d.ts.map +1 -0
- package/dist/routers/module.js +82 -0
- package/dist/routers/module.js.map +1 -0
- package/dist/services/assignment.d.ts.map +1 -1
- package/dist/services/assignment.js +8 -3
- package/dist/services/assignment.js.map +1 -1
- package/dist/services/marketplace.d.ts +36 -0
- package/dist/services/marketplace.d.ts.map +1 -1
- package/dist/services/marketplace.js +30 -3
- package/dist/services/marketplace.js.map +1 -1
- package/dist/services/module.d.ts +288 -0
- package/dist/services/module.d.ts.map +1 -0
- package/dist/services/module.js +518 -0
- package/dist/services/module.js.map +1 -0
- package/dist/services/school.d.ts.map +1 -1
- package/dist/services/school.js +17 -5
- package/dist/services/school.js.map +1 -1
- package/package.json +1 -1
- package/prisma/migrations/20260708013000_add_modules/migration.sql +100 -0
- package/prisma/schema.prisma +93 -0
- package/src/models/marketplace.ts +16 -0
- package/src/models/module.ts +77 -0
- package/src/pipelines/gradeWorksheet.ts +6 -0
- package/src/routers/_app.ts +2 -0
- package/src/routers/marketplace.ts +16 -0
- package/src/routers/module.ts +118 -0
- package/src/services/assignment.ts +12 -1
- package/src/services/marketplace.ts +39 -0
- package/src/services/module.ts +683 -0
- package/src/services/school.ts +15 -3
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module model – Canvas-style class modules, items and student progress.
|
|
3
|
+
*/
|
|
4
|
+
import { prisma } from "../lib/prisma.js";
|
|
5
|
+
import { classTeacherOrSchoolAdminWhere } from "./access.js";
|
|
6
|
+
|
|
7
|
+
/** @returns Class if user is a teacher or school admin. */
|
|
8
|
+
export function findClassWithTeacher(classId: string, teacherId: string) {
|
|
9
|
+
return prisma.class.findFirst({
|
|
10
|
+
where: {
|
|
11
|
+
id: classId,
|
|
12
|
+
...classTeacherOrSchoolAdminWhere(teacherId),
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const moduleInclude = {
|
|
18
|
+
items: {
|
|
19
|
+
orderBy: { position: "asc" as const },
|
|
20
|
+
},
|
|
21
|
+
prerequisites: {
|
|
22
|
+
select: { prerequisiteId: true },
|
|
23
|
+
},
|
|
24
|
+
} as const;
|
|
25
|
+
|
|
26
|
+
/** @returns All modules of a class ordered by position. */
|
|
27
|
+
export function findModulesByClassId(classId: string, publishedOnly = false) {
|
|
28
|
+
return prisma.module.findMany({
|
|
29
|
+
where: { classId, ...(publishedOnly && { published: true }) },
|
|
30
|
+
orderBy: { position: "asc" },
|
|
31
|
+
include: moduleInclude,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** @returns Module by ID scoped to a class. */
|
|
36
|
+
export function findModuleByClassAndId(id: string, classId: string) {
|
|
37
|
+
return prisma.module.findFirst({
|
|
38
|
+
where: { id, classId },
|
|
39
|
+
include: moduleInclude,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** @returns Module item with its module, scoped to a class. */
|
|
44
|
+
export function findModuleItemInClass(itemId: string, classId: string) {
|
|
45
|
+
return prisma.courseModuleItem.findFirst({
|
|
46
|
+
where: { id: itemId, module: { classId } },
|
|
47
|
+
include: { module: true },
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** @returns Progress rows of a student for all items of a class. */
|
|
52
|
+
export function findStudentProgressForClass(classId: string, studentId: string) {
|
|
53
|
+
return prisma.moduleItemProgress.findMany({
|
|
54
|
+
where: { studentId, moduleItem: { module: { classId } } },
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Upsert a progress row for a student and item. */
|
|
59
|
+
export function upsertItemProgress(
|
|
60
|
+
moduleItemId: string,
|
|
61
|
+
studentId: string,
|
|
62
|
+
status: "INCOMPLETE" | "COMPLETE"
|
|
63
|
+
) {
|
|
64
|
+
return prisma.moduleItemProgress.upsert({
|
|
65
|
+
where: { moduleItemId_studentId: { moduleItemId, studentId } },
|
|
66
|
+
create: {
|
|
67
|
+
moduleItemId,
|
|
68
|
+
studentId,
|
|
69
|
+
status,
|
|
70
|
+
...(status === "COMPLETE" && { completedAt: new Date() }),
|
|
71
|
+
},
|
|
72
|
+
update:
|
|
73
|
+
status === "COMPLETE"
|
|
74
|
+
? { status, completedAt: new Date() }
|
|
75
|
+
: {},
|
|
76
|
+
});
|
|
77
|
+
}
|
|
@@ -9,6 +9,7 @@ import z from "zod";
|
|
|
9
9
|
import { inference } from "../utils/inference.js";
|
|
10
10
|
import { getAIUserId } from "../utils/aiUser.js";
|
|
11
11
|
import { pusher, worksheetChannel } from "../lib/pusher.js";
|
|
12
|
+
import { syncWorksheetModuleProgress } from "../services/module.js";
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
const removeAllPreviousAIComments = async (worksheetQuestionProgressId: string) => {
|
|
@@ -369,6 +370,11 @@ export const gradeWorksheetPipeline = async (
|
|
|
369
370
|
}
|
|
370
371
|
|
|
371
372
|
};
|
|
373
|
+
|
|
374
|
+
await syncWorksheetModuleProgress(
|
|
375
|
+
worksheetResponse.studentId,
|
|
376
|
+
worksheetResponse.worksheetId,
|
|
377
|
+
);
|
|
372
378
|
};
|
|
373
379
|
|
|
374
380
|
export const cancelGradePipeline = async (worksheetResponseId: string, worksheetQuestionProgressId: string) => {
|
package/src/routers/_app.ts
CHANGED
|
@@ -29,6 +29,7 @@ import { permissionsRouter } from "./permissions.js";
|
|
|
29
29
|
import { discussionRouter } from "./discussion.js";
|
|
30
30
|
import { adminRouter } from "./admin.js";
|
|
31
31
|
import { embeddingRouter } from "./embedding.js";
|
|
32
|
+
import { moduleRouter } from "./module.js";
|
|
32
33
|
import { gradebookRouter } from "./gradebook.js";
|
|
33
34
|
import { feesRouter } from "./fees.js";
|
|
34
35
|
import { marketplaceRouter } from "./marketplace.js";
|
|
@@ -65,6 +66,7 @@ export const appRouter = createTRPCRouter({
|
|
|
65
66
|
discussion: discussionRouter,
|
|
66
67
|
admin: adminRouter,
|
|
67
68
|
embedding: embeddingRouter,
|
|
69
|
+
module: moduleRouter,
|
|
68
70
|
gradebook: gradebookRouter,
|
|
69
71
|
fees: feesRouter,
|
|
70
72
|
marketplace: marketplaceRouter,
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
publishWorksheetListing,
|
|
16
16
|
reportMarketplaceListing,
|
|
17
17
|
unpublishMarketplaceListing,
|
|
18
|
+
updateMarketplaceListingDetails,
|
|
18
19
|
} from "../services/marketplace.js";
|
|
19
20
|
|
|
20
21
|
const listingTypeSchema = z.enum(["MARKSCHEME", "WORKSHEET", "FILE"]);
|
|
@@ -109,6 +110,21 @@ export const marketplaceRouter = createTRPCRouter({
|
|
|
109
110
|
importMarketplaceListing(ctx.user!.id, input)
|
|
110
111
|
),
|
|
111
112
|
|
|
113
|
+
/** Edit listing metadata (author or school admin). */
|
|
114
|
+
update: protectedProcedure
|
|
115
|
+
.input(
|
|
116
|
+
z.object({
|
|
117
|
+
listingId: z.string(),
|
|
118
|
+
title: z.string().trim().min(1).max(160).optional(),
|
|
119
|
+
description: z.string().trim().max(2000).nullable().optional(),
|
|
120
|
+
visibility: visibilitySchema.optional(),
|
|
121
|
+
tags: tagsSchema,
|
|
122
|
+
})
|
|
123
|
+
)
|
|
124
|
+
.mutation(({ ctx, input }) =>
|
|
125
|
+
updateMarketplaceListingDetails(ctx.user!.id, input)
|
|
126
|
+
),
|
|
127
|
+
|
|
112
128
|
report: protectedProcedure
|
|
113
129
|
.input(z.object({ listingId: z.string() }))
|
|
114
130
|
.mutation(({ ctx, input }) =>
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { CourseModuleItemType as ModuleItemType, ModuleItemRequirementType } from "@prisma/client";
|
|
3
|
+
import {
|
|
4
|
+
createTRPCRouter,
|
|
5
|
+
protectedClassMemberProcedure,
|
|
6
|
+
protectedTeacherProcedure,
|
|
7
|
+
} from "../trpc.js";
|
|
8
|
+
import {
|
|
9
|
+
listModulesRecord,
|
|
10
|
+
createModuleRecord,
|
|
11
|
+
updateModuleRecord,
|
|
12
|
+
deleteModuleRecord,
|
|
13
|
+
reorderModulesRecord,
|
|
14
|
+
createModuleItemRecord,
|
|
15
|
+
updateModuleItemRecord,
|
|
16
|
+
deleteModuleItemRecord,
|
|
17
|
+
reorderModuleItemsRecord,
|
|
18
|
+
getStudentModulesState,
|
|
19
|
+
markItemViewedRecord,
|
|
20
|
+
} from "../services/module.js";
|
|
21
|
+
|
|
22
|
+
const itemTypeSchema = z.nativeEnum(ModuleItemType);
|
|
23
|
+
const requirementTypeSchema = z.nativeEnum(ModuleItemRequirementType);
|
|
24
|
+
|
|
25
|
+
export const moduleRouter = createTRPCRouter({
|
|
26
|
+
/** Teacher view: every module (published or not) with its items. */
|
|
27
|
+
list: protectedTeacherProcedure
|
|
28
|
+
.input(z.object({ classId: z.string() }))
|
|
29
|
+
.query(({ ctx, input }) => listModulesRecord(ctx.user!.id, input.classId)),
|
|
30
|
+
|
|
31
|
+
/** Student view: published modules with lock state and progress. */
|
|
32
|
+
getStudentState: protectedClassMemberProcedure
|
|
33
|
+
.input(z.object({ classId: z.string() }))
|
|
34
|
+
.query(({ ctx, input }) =>
|
|
35
|
+
getStudentModulesState(input.classId, ctx.user!.id)
|
|
36
|
+
),
|
|
37
|
+
|
|
38
|
+
create: protectedTeacherProcedure
|
|
39
|
+
.input(
|
|
40
|
+
z.object({
|
|
41
|
+
classId: z.string(),
|
|
42
|
+
title: z.string().min(1),
|
|
43
|
+
unlockAt: z.coerce.date().nullish(),
|
|
44
|
+
requireSequential: z.boolean().optional(),
|
|
45
|
+
})
|
|
46
|
+
)
|
|
47
|
+
.mutation(({ ctx, input }) => createModuleRecord(ctx.user!.id, input)),
|
|
48
|
+
|
|
49
|
+
update: protectedTeacherProcedure
|
|
50
|
+
.input(
|
|
51
|
+
z.object({
|
|
52
|
+
classId: z.string(),
|
|
53
|
+
id: z.string(),
|
|
54
|
+
title: z.string().min(1).optional(),
|
|
55
|
+
unlockAt: z.coerce.date().nullable().optional(),
|
|
56
|
+
requireSequential: z.boolean().optional(),
|
|
57
|
+
published: z.boolean().optional(),
|
|
58
|
+
prerequisiteIds: z.array(z.string()).optional(),
|
|
59
|
+
})
|
|
60
|
+
)
|
|
61
|
+
.mutation(({ ctx, input }) => updateModuleRecord(ctx.user!.id, input)),
|
|
62
|
+
|
|
63
|
+
delete: protectedTeacherProcedure
|
|
64
|
+
.input(z.object({ classId: z.string(), id: z.string() }))
|
|
65
|
+
.mutation(({ ctx, input }) => deleteModuleRecord(ctx.user!.id, input)),
|
|
66
|
+
|
|
67
|
+
reorder: protectedTeacherProcedure
|
|
68
|
+
.input(z.object({ classId: z.string(), orderedIds: z.array(z.string()) }))
|
|
69
|
+
.mutation(({ ctx, input }) => reorderModulesRecord(ctx.user!.id, input)),
|
|
70
|
+
|
|
71
|
+
createItem: protectedTeacherProcedure
|
|
72
|
+
.input(
|
|
73
|
+
z.object({
|
|
74
|
+
classId: z.string(),
|
|
75
|
+
moduleId: z.string(),
|
|
76
|
+
itemType: itemTypeSchema,
|
|
77
|
+
itemId: z.string().optional(),
|
|
78
|
+
externalUrl: z.string().url().optional(),
|
|
79
|
+
title: z.string().optional(),
|
|
80
|
+
indent: z.number().int().min(0).max(4).optional(),
|
|
81
|
+
requirementType: requirementTypeSchema.optional(),
|
|
82
|
+
requiredScore: z.number().min(0).optional(),
|
|
83
|
+
})
|
|
84
|
+
)
|
|
85
|
+
.mutation(({ ctx, input }) => createModuleItemRecord(ctx.user!.id, input)),
|
|
86
|
+
|
|
87
|
+
updateItem: protectedTeacherProcedure
|
|
88
|
+
.input(
|
|
89
|
+
z.object({
|
|
90
|
+
classId: z.string(),
|
|
91
|
+
id: z.string(),
|
|
92
|
+
title: z.string().nullable().optional(),
|
|
93
|
+
externalUrl: z.string().url().optional(),
|
|
94
|
+
indent: z.number().int().min(0).max(4).optional(),
|
|
95
|
+
requirementType: requirementTypeSchema.nullable().optional(),
|
|
96
|
+
requiredScore: z.number().min(0).nullable().optional(),
|
|
97
|
+
})
|
|
98
|
+
)
|
|
99
|
+
.mutation(({ ctx, input }) => updateModuleItemRecord(ctx.user!.id, input)),
|
|
100
|
+
|
|
101
|
+
deleteItem: protectedTeacherProcedure
|
|
102
|
+
.input(z.object({ classId: z.string(), id: z.string() }))
|
|
103
|
+
.mutation(({ ctx, input }) => deleteModuleItemRecord(ctx.user!.id, input)),
|
|
104
|
+
|
|
105
|
+
reorderItems: protectedTeacherProcedure
|
|
106
|
+
.input(
|
|
107
|
+
z.object({
|
|
108
|
+
classId: z.string(),
|
|
109
|
+
items: z.array(z.object({ id: z.string(), moduleId: z.string() })),
|
|
110
|
+
})
|
|
111
|
+
)
|
|
112
|
+
.mutation(({ ctx, input }) => reorderModuleItemsRecord(ctx.user!.id, input)),
|
|
113
|
+
|
|
114
|
+
/** Record that a student viewed an item (completes VIEW requirements). */
|
|
115
|
+
markItemViewed: protectedClassMemberProcedure
|
|
116
|
+
.input(z.object({ classId: z.string(), itemId: z.string() }))
|
|
117
|
+
.mutation(({ ctx, input }) => markItemViewedRecord(ctx.user!.id, input)),
|
|
118
|
+
});
|
|
@@ -28,6 +28,7 @@ import { enqueueRunJob } from "../workers/queues.js";
|
|
|
28
28
|
import { logger } from "../utils/logger.js";
|
|
29
29
|
import { invalidateStudentProgressInferenceCache } from "./studentProgressInferenceCache.js";
|
|
30
30
|
import { enqueueEmbedding } from "./embedding.js";
|
|
31
|
+
import { syncAssignmentModuleProgress } from "./module.js";
|
|
31
32
|
import { resolveAssignmentBehavior } from "../lib/assignmentTypeDefaults.js";
|
|
32
33
|
|
|
33
34
|
export async function assignmentExists(id: string) {
|
|
@@ -803,7 +804,7 @@ export async function updateSubmissionRecord(
|
|
|
803
804
|
}
|
|
804
805
|
}
|
|
805
806
|
}
|
|
806
|
-
|
|
807
|
+
const updatedSubmission = await prisma.submission.update({
|
|
807
808
|
where: { id: submission.id },
|
|
808
809
|
data: {
|
|
809
810
|
submitted: !submission.submitted,
|
|
@@ -821,6 +822,8 @@ export async function updateSubmissionRecord(
|
|
|
821
822
|
},
|
|
822
823
|
},
|
|
823
824
|
});
|
|
825
|
+
await syncAssignmentModuleProgress(userId, submission.assignmentId);
|
|
826
|
+
return updatedSubmission;
|
|
824
827
|
}
|
|
825
828
|
|
|
826
829
|
const isLateUpdate = submission.assignment.dueDate < new Date();
|
|
@@ -1002,6 +1005,10 @@ export async function updateSubmissionAsTeacherRecord(
|
|
|
1002
1005
|
data: { returned: !submission.returned },
|
|
1003
1006
|
include: submissionTeacherInclude,
|
|
1004
1007
|
});
|
|
1008
|
+
await syncAssignmentModuleProgress(
|
|
1009
|
+
submission.studentId,
|
|
1010
|
+
submission.assignmentId
|
|
1011
|
+
);
|
|
1005
1012
|
|
|
1006
1013
|
if (updated.returned) {
|
|
1007
1014
|
sendNotification({
|
|
@@ -1072,6 +1079,10 @@ export async function updateSubmissionAsTeacherRecord(
|
|
|
1072
1079
|
submission.assignment.classId,
|
|
1073
1080
|
submission.studentId,
|
|
1074
1081
|
);
|
|
1082
|
+
await syncAssignmentModuleProgress(
|
|
1083
|
+
submission.studentId,
|
|
1084
|
+
submission.assignmentId
|
|
1085
|
+
);
|
|
1075
1086
|
}
|
|
1076
1087
|
|
|
1077
1088
|
// Index submission for RAG search after teacher grades/feedback
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
incrementMarketplaceReportCount,
|
|
22
22
|
searchMarketplaceListings,
|
|
23
23
|
setMarketplaceListingHidden,
|
|
24
|
+
updateMarketplaceListing,
|
|
24
25
|
} from "../models/marketplace.js";
|
|
25
26
|
import {
|
|
26
27
|
findActiveSchoolAdminMembership,
|
|
@@ -659,6 +660,44 @@ export async function reportMarketplaceListing(
|
|
|
659
660
|
return incrementMarketplaceReportCount(listingId);
|
|
660
661
|
}
|
|
661
662
|
|
|
663
|
+
export async function updateMarketplaceListingDetails(
|
|
664
|
+
userId: string,
|
|
665
|
+
input: {
|
|
666
|
+
listingId: string;
|
|
667
|
+
title?: string;
|
|
668
|
+
description?: string | null;
|
|
669
|
+
visibility?: "SCHOOL" | "PUBLIC";
|
|
670
|
+
tags?: string[];
|
|
671
|
+
}
|
|
672
|
+
) {
|
|
673
|
+
const listing = await findMarketplaceListingById(input.listingId);
|
|
674
|
+
if (!listing) {
|
|
675
|
+
throw new TRPCError({ code: "NOT_FOUND", message: "Listing not found" });
|
|
676
|
+
}
|
|
677
|
+
const isAuthor = listing.authorId === userId;
|
|
678
|
+
const isSchoolAdmin =
|
|
679
|
+
listing.schoolId != null &&
|
|
680
|
+
Boolean(await findActiveSchoolAdminMembership(listing.schoolId, userId));
|
|
681
|
+
if (!isAuthor && !isSchoolAdmin) {
|
|
682
|
+
throw new TRPCError({
|
|
683
|
+
code: "FORBIDDEN",
|
|
684
|
+
message: "Only the author or a school admin can edit this listing",
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
if (input.visibility === "SCHOOL" && listing.schoolId == null) {
|
|
688
|
+
throw new TRPCError({
|
|
689
|
+
code: "BAD_REQUEST",
|
|
690
|
+
message: "Listing has no school; it cannot use school visibility",
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
return updateMarketplaceListing(input.listingId, {
|
|
694
|
+
title: input.title,
|
|
695
|
+
description: input.description,
|
|
696
|
+
visibility: input.visibility,
|
|
697
|
+
tags: input.tags ? normalizeTags(input.tags) : undefined,
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
|
|
662
701
|
export async function hideMarketplaceListing(
|
|
663
702
|
userId: string,
|
|
664
703
|
listingId: string,
|