@studious-lms/server 1.7.0 → 1.9.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/middleware/security.d.ts.map +1 -1
- package/dist/middleware/security.js +3 -3
- package/dist/middleware/security.js.map +1 -1
- package/dist/models/access.d.ts +21 -66
- package/dist/models/access.d.ts.map +1 -1
- package/dist/models/access.js +10 -26
- package/dist/models/access.js.map +1 -1
- package/dist/models/agenda.d.ts +4 -2
- package/dist/models/agenda.d.ts.map +1 -1
- package/dist/models/announcement.d.ts +4 -3
- package/dist/models/announcement.d.ts.map +1 -1
- package/dist/models/announcement.js +2 -3
- package/dist/models/announcement.js.map +1 -1
- package/dist/models/assignment.d.ts +40 -33
- package/dist/models/assignment.d.ts.map +1 -1
- package/dist/models/auth.d.ts +2 -4
- package/dist/models/auth.d.ts.map +1 -1
- package/dist/models/auth.js +2 -3
- package/dist/models/auth.js.map +1 -1
- package/dist/models/class.d.ts +34 -6
- package/dist/models/class.d.ts.map +1 -1
- package/dist/models/class.js +15 -2
- package/dist/models/class.js.map +1 -1
- package/dist/models/cloudStorage.d.ts +153 -0
- package/dist/models/cloudStorage.d.ts.map +1 -0
- package/dist/models/cloudStorage.js +295 -0
- package/dist/models/cloudStorage.js.map +1 -0
- package/dist/models/comment.d.ts +0 -24
- package/dist/models/comment.d.ts.map +1 -1
- package/dist/models/comment.js +2 -6
- package/dist/models/comment.js.map +1 -1
- package/dist/models/conversation.d.ts +22 -22
- package/dist/models/department.d.ts +154 -0
- package/dist/models/department.d.ts.map +1 -0
- package/dist/models/department.js +135 -0
- package/dist/models/department.js.map +1 -0
- package/dist/models/event.d.ts +10 -4
- package/dist/models/event.d.ts.map +1 -1
- package/dist/models/event.js +2 -5
- package/dist/models/event.js.map +1 -1
- package/dist/models/file.d.ts +52 -36
- package/dist/models/file.d.ts.map +1 -1
- package/dist/models/file.js +4 -42
- package/dist/models/file.js.map +1 -1
- package/dist/models/folder.d.ts +47 -0
- package/dist/models/folder.d.ts.map +1 -1
- package/dist/models/labChat.d.ts +11 -9
- package/dist/models/labChat.d.ts.map +1 -1
- package/dist/models/message.d.ts +46 -12
- package/dist/models/message.d.ts.map +1 -1
- package/dist/models/message.js +19 -10
- package/dist/models/message.js.map +1 -1
- package/dist/models/newtonChat.d.ts +2 -1
- package/dist/models/newtonChat.d.ts.map +1 -1
- package/dist/models/school.d.ts +45 -12
- package/dist/models/school.d.ts.map +1 -1
- package/dist/models/school.js +19 -2
- package/dist/models/school.js.map +1 -1
- package/dist/models/section.d.ts +2 -1
- package/dist/models/section.d.ts.map +1 -1
- package/dist/models/tag.d.ts +117 -0
- package/dist/models/tag.d.ts.map +1 -0
- package/dist/models/tag.js +113 -0
- package/dist/models/tag.js.map +1 -0
- package/dist/models/worksheet.d.ts +3 -0
- package/dist/models/worksheet.d.ts.map +1 -1
- package/dist/pipelines/aiLabChatContract.d.ts +8 -8
- package/dist/routers/_app.d.ts +6704 -5222
- package/dist/routers/_app.d.ts.map +1 -1
- package/dist/routers/_app.js +8 -2
- package/dist/routers/_app.js.map +1 -1
- package/dist/routers/agenda.d.ts +4 -2
- package/dist/routers/agenda.d.ts.map +1 -1
- package/dist/routers/announcement.d.ts.map +1 -1
- package/dist/routers/announcement.js +3 -5
- package/dist/routers/announcement.js.map +1 -1
- package/dist/routers/assignment.d.ts +49 -40
- package/dist/routers/assignment.d.ts.map +1 -1
- package/dist/routers/assignment.js +2 -3
- package/dist/routers/assignment.js.map +1 -1
- package/dist/routers/class.d.ts +92 -19
- package/dist/routers/class.d.ts.map +1 -1
- package/dist/routers/class.js +5 -2
- package/dist/routers/class.js.map +1 -1
- package/dist/routers/conversation.d.ts +5 -5
- package/dist/routers/department.d.ts +293 -0
- package/dist/routers/department.d.ts.map +1 -0
- package/dist/routers/department.js +93 -0
- package/dist/routers/department.js.map +1 -0
- package/dist/routers/event.d.ts +4 -3
- package/dist/routers/event.d.ts.map +1 -1
- package/dist/routers/file.d.ts +10 -8
- package/dist/routers/file.d.ts.map +1 -1
- package/dist/routers/file.js +26 -17
- package/dist/routers/file.js.map +1 -1
- package/dist/routers/folder.d.ts +163 -186
- package/dist/routers/folder.d.ts.map +1 -1
- package/dist/routers/folder.js +102 -88
- package/dist/routers/folder.js.map +1 -1
- package/dist/routers/labChat.d.ts +13 -4
- package/dist/routers/labChat.d.ts.map +1 -1
- package/dist/routers/message.d.ts +20 -2
- package/dist/routers/message.d.ts.map +1 -1
- package/dist/routers/newtonChat.d.ts +10 -1
- package/dist/routers/newtonChat.d.ts.map +1 -1
- package/dist/routers/permissions.d.ts +120 -0
- package/dist/routers/permissions.d.ts.map +1 -0
- package/dist/routers/permissions.js +50 -0
- package/dist/routers/permissions.js.map +1 -0
- package/dist/routers/school.d.ts +160 -17
- package/dist/routers/school.d.ts.map +1 -1
- package/dist/routers/school.js +46 -3
- package/dist/routers/school.js.map +1 -1
- package/dist/routers/tag.d.ts +119 -0
- package/dist/routers/tag.d.ts.map +1 -0
- package/dist/routers/tag.js +52 -0
- package/dist/routers/tag.js.map +1 -0
- package/dist/routers/worksheet.d.ts +1 -0
- package/dist/routers/worksheet.d.ts.map +1 -1
- package/dist/seedDatabase.d.ts +0 -1
- package/dist/seedDatabase.d.ts.map +1 -1
- package/dist/services/agenda.d.ts +4 -2
- package/dist/services/agenda.d.ts.map +1 -1
- package/dist/services/assignment.d.ts +49 -40
- package/dist/services/assignment.d.ts.map +1 -1
- package/dist/services/class.d.ts +92 -19
- package/dist/services/class.d.ts.map +1 -1
- package/dist/services/class.js +92 -5
- package/dist/services/class.js.map +1 -1
- package/dist/services/conversation.d.ts +5 -5
- package/dist/services/department.d.ts +200 -0
- package/dist/services/department.d.ts.map +1 -0
- package/dist/services/department.js +368 -0
- package/dist/services/department.js.map +1 -0
- package/dist/services/event.d.ts +4 -3
- package/dist/services/event.d.ts.map +1 -1
- package/dist/services/file.d.ts +105 -3
- package/dist/services/file.d.ts.map +1 -1
- package/dist/services/file.js +167 -41
- package/dist/services/file.js.map +1 -1
- package/dist/services/folder.d.ts +220 -149
- package/dist/services/folder.d.ts.map +1 -1
- package/dist/services/folder.js +326 -196
- package/dist/services/folder.js.map +1 -1
- package/dist/services/labChat.d.ts +13 -4
- package/dist/services/labChat.d.ts.map +1 -1
- package/dist/services/labChat.js +4 -3
- package/dist/services/labChat.js.map +1 -1
- package/dist/services/message.d.ts +20 -2
- package/dist/services/message.d.ts.map +1 -1
- package/dist/services/message.js +7 -23
- package/dist/services/message.js.map +1 -1
- package/dist/services/newtonChat.d.ts +9 -0
- package/dist/services/newtonChat.d.ts.map +1 -1
- package/dist/services/newtonChat.js +4 -3
- package/dist/services/newtonChat.js.map +1 -1
- package/dist/services/permissions.d.ts +188 -0
- package/dist/services/permissions.d.ts.map +1 -0
- package/dist/services/permissions.js +661 -0
- package/dist/services/permissions.js.map +1 -0
- package/dist/services/school.d.ts +142 -15
- package/dist/services/school.d.ts.map +1 -1
- package/dist/services/school.js +308 -54
- package/dist/services/school.js.map +1 -1
- package/dist/services/tag.d.ts +71 -0
- package/dist/services/tag.d.ts.map +1 -0
- package/dist/services/tag.js +178 -0
- package/dist/services/tag.js.map +1 -0
- package/dist/services/worksheet.d.ts +1 -0
- package/dist/services/worksheet.d.ts.map +1 -1
- package/dist/utils/aiUser.d.ts.map +1 -1
- package/dist/utils/aiUser.js +2 -3
- package/dist/utils/aiUser.js.map +1 -1
- package/package.json +1 -1
- package/prisma/migrations/20260509220000_institution_overhaul/migration.sql +184 -0
- package/prisma/migrations/20260510170000_add_folder_permissions/migration.sql +93 -0
- package/prisma/schema.prisma +193 -35
- package/src/middleware/security.ts +1 -1
- package/src/models/access.ts +8 -24
- package/src/models/announcement.ts +0 -1
- package/src/models/auth.ts +0 -1
- package/src/models/class.ts +13 -0
- package/src/models/cloudStorage.ts +327 -0
- package/src/models/comment.ts +0 -4
- package/src/models/department.ts +159 -0
- package/src/models/event.ts +0 -3
- package/src/models/file.ts +2 -40
- package/src/models/message.ts +19 -5
- package/src/models/school.ts +26 -0
- package/src/models/tag.ts +140 -0
- package/src/routers/_app.ts +6 -0
- package/src/routers/announcement.ts +1 -3
- package/src/routers/assignment.ts +0 -1
- package/src/routers/class.ts +3 -0
- package/src/routers/department.ts +174 -0
- package/src/routers/file.ts +29 -26
- package/src/routers/folder.ts +127 -118
- package/src/routers/permissions.ts +71 -0
- package/src/routers/school.ts +83 -0
- package/src/routers/tag.ts +78 -0
- package/src/services/class.ts +109 -4
- package/src/services/department.ts +478 -0
- package/src/services/file.ts +172 -43
- package/src/services/folder.ts +399 -237
- package/src/services/labChat.ts +2 -1
- package/src/services/message.ts +5 -21
- package/src/services/newtonChat.ts +2 -1
- package/src/services/permissions.ts +778 -0
- package/src/services/school.ts +369 -57
- package/src/services/tag.ts +229 -0
- package/src/utils/aiUser.ts +0 -1
- package/tests/unit/services/permissions.test.ts +463 -0
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Department service — admin CRUD, members, and template management for
|
|
3
|
+
* department-level rubrics and grading boundaries.
|
|
4
|
+
*/
|
|
5
|
+
import { TRPCError } from "@trpc/server";
|
|
6
|
+
import { prisma } from "../lib/prisma.js";
|
|
7
|
+
import {
|
|
8
|
+
addDepartmentMembers,
|
|
9
|
+
createDepartment,
|
|
10
|
+
deleteDepartment,
|
|
11
|
+
findDepartmentMembership,
|
|
12
|
+
getDepartment,
|
|
13
|
+
listDepartmentsForSchool,
|
|
14
|
+
listDepartmentsForUser,
|
|
15
|
+
removeDepartmentMember,
|
|
16
|
+
updateDepartment,
|
|
17
|
+
} from "../models/department.js";
|
|
18
|
+
import {
|
|
19
|
+
findActiveSchoolAdminMembership,
|
|
20
|
+
findSchoolMembership,
|
|
21
|
+
} from "../models/school.js";
|
|
22
|
+
|
|
23
|
+
async function requireAdmin(schoolId: string, userId: string) {
|
|
24
|
+
const membership = await findActiveSchoolAdminMembership(schoolId, userId);
|
|
25
|
+
if (!membership) {
|
|
26
|
+
throw new TRPCError({
|
|
27
|
+
code: "FORBIDDEN",
|
|
28
|
+
message: "Only school admins can manage departments",
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function requireMember(schoolId: string, userId: string) {
|
|
34
|
+
const membership = await findSchoolMembership(schoolId, userId);
|
|
35
|
+
if (!membership || membership.status !== "ACTIVE") {
|
|
36
|
+
throw new TRPCError({
|
|
37
|
+
code: "FORBIDDEN",
|
|
38
|
+
message: "You aren't a member of this institution",
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function listDepartments(
|
|
44
|
+
userId: string,
|
|
45
|
+
schoolId: string
|
|
46
|
+
) {
|
|
47
|
+
await requireMember(schoolId, userId);
|
|
48
|
+
return listDepartmentsForSchool(schoolId);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function listMyDepartments(
|
|
52
|
+
userId: string,
|
|
53
|
+
schoolId: string
|
|
54
|
+
) {
|
|
55
|
+
await requireMember(schoolId, userId);
|
|
56
|
+
return listDepartmentsForUser(userId, schoolId);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function getDepartmentDetails(
|
|
60
|
+
userId: string,
|
|
61
|
+
departmentId: string
|
|
62
|
+
) {
|
|
63
|
+
const department = await getDepartment(departmentId);
|
|
64
|
+
if (!department) {
|
|
65
|
+
throw new TRPCError({
|
|
66
|
+
code: "NOT_FOUND",
|
|
67
|
+
message: "Department not found",
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
await requireMember(department.schoolId, userId);
|
|
71
|
+
return department;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function createDepartmentRecord(
|
|
75
|
+
userId: string,
|
|
76
|
+
input: {
|
|
77
|
+
schoolId: string;
|
|
78
|
+
name: string;
|
|
79
|
+
description?: string;
|
|
80
|
+
color?: string;
|
|
81
|
+
}
|
|
82
|
+
) {
|
|
83
|
+
await requireAdmin(input.schoolId, userId);
|
|
84
|
+
return createDepartment(input);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function updateDepartmentRecord(
|
|
88
|
+
userId: string,
|
|
89
|
+
input: {
|
|
90
|
+
departmentId: string;
|
|
91
|
+
name?: string;
|
|
92
|
+
description?: string | null;
|
|
93
|
+
color?: string;
|
|
94
|
+
}
|
|
95
|
+
) {
|
|
96
|
+
const department = await getDepartment(input.departmentId);
|
|
97
|
+
if (!department) {
|
|
98
|
+
throw new TRPCError({
|
|
99
|
+
code: "NOT_FOUND",
|
|
100
|
+
message: "Department not found",
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
await requireAdmin(department.schoolId, userId);
|
|
104
|
+
return updateDepartment(input.departmentId, {
|
|
105
|
+
name: input.name,
|
|
106
|
+
description: input.description,
|
|
107
|
+
color: input.color,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function deleteDepartmentRecord(
|
|
112
|
+
userId: string,
|
|
113
|
+
departmentId: string
|
|
114
|
+
) {
|
|
115
|
+
const department = await getDepartment(departmentId);
|
|
116
|
+
if (!department) {
|
|
117
|
+
throw new TRPCError({
|
|
118
|
+
code: "NOT_FOUND",
|
|
119
|
+
message: "Department not found",
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
await requireAdmin(department.schoolId, userId);
|
|
123
|
+
await deleteDepartment(departmentId);
|
|
124
|
+
return { success: true };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export async function addMembersToDepartment(
|
|
128
|
+
userId: string,
|
|
129
|
+
input: { departmentId: string; userIds: string[] }
|
|
130
|
+
) {
|
|
131
|
+
const department = await getDepartment(input.departmentId);
|
|
132
|
+
if (!department) {
|
|
133
|
+
throw new TRPCError({
|
|
134
|
+
code: "NOT_FOUND",
|
|
135
|
+
message: "Department not found",
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
await requireAdmin(department.schoolId, userId);
|
|
139
|
+
|
|
140
|
+
// Members must already be in the institution.
|
|
141
|
+
const memberships = await prisma.schoolMembership.findMany({
|
|
142
|
+
where: {
|
|
143
|
+
schoolId: department.schoolId,
|
|
144
|
+
userId: { in: input.userIds },
|
|
145
|
+
status: "ACTIVE",
|
|
146
|
+
},
|
|
147
|
+
select: { userId: true },
|
|
148
|
+
});
|
|
149
|
+
const validIds = new Set(memberships.map((m) => m.userId));
|
|
150
|
+
const validUserIds = input.userIds.filter((id) => validIds.has(id));
|
|
151
|
+
|
|
152
|
+
if (validUserIds.length === 0) {
|
|
153
|
+
throw new TRPCError({
|
|
154
|
+
code: "BAD_REQUEST",
|
|
155
|
+
message: "None of the supplied users are institution members",
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
return addDepartmentMembers(input.departmentId, validUserIds);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export async function removeMemberFromDepartment(
|
|
162
|
+
userId: string,
|
|
163
|
+
input: { departmentId: string; memberId: string }
|
|
164
|
+
) {
|
|
165
|
+
const department = await getDepartment(input.departmentId);
|
|
166
|
+
if (!department) {
|
|
167
|
+
throw new TRPCError({
|
|
168
|
+
code: "NOT_FOUND",
|
|
169
|
+
message: "Department not found",
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
await requireAdmin(department.schoolId, userId);
|
|
173
|
+
return removeDepartmentMember(input.departmentId, input.memberId);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Department template authoring (rubrics + boundaries). */
|
|
177
|
+
export async function createRubricTemplate(
|
|
178
|
+
userId: string,
|
|
179
|
+
input: { departmentId: string; name: string; structured: string }
|
|
180
|
+
) {
|
|
181
|
+
const department = await getDepartment(input.departmentId);
|
|
182
|
+
if (!department) {
|
|
183
|
+
throw new TRPCError({
|
|
184
|
+
code: "NOT_FOUND",
|
|
185
|
+
message: "Department not found",
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
await requireAdmin(department.schoolId, userId);
|
|
189
|
+
return prisma.markScheme.create({
|
|
190
|
+
data: {
|
|
191
|
+
name: input.name,
|
|
192
|
+
structured: input.structured.replace(/\\n/g, "\n"),
|
|
193
|
+
department: { connect: { id: input.departmentId } },
|
|
194
|
+
},
|
|
195
|
+
select: { id: true, name: true, structured: true, departmentId: true },
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export async function updateRubricTemplate(
|
|
200
|
+
userId: string,
|
|
201
|
+
input: {
|
|
202
|
+
templateId: string;
|
|
203
|
+
name?: string;
|
|
204
|
+
structured?: string;
|
|
205
|
+
}
|
|
206
|
+
) {
|
|
207
|
+
const template = await prisma.markScheme.findUnique({
|
|
208
|
+
where: { id: input.templateId },
|
|
209
|
+
select: { departmentId: true, department: { select: { schoolId: true } } },
|
|
210
|
+
});
|
|
211
|
+
if (!template?.departmentId || !template.department) {
|
|
212
|
+
throw new TRPCError({
|
|
213
|
+
code: "NOT_FOUND",
|
|
214
|
+
message: "Template not found",
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
await requireAdmin(template.department.schoolId, userId);
|
|
218
|
+
return prisma.markScheme.update({
|
|
219
|
+
where: { id: input.templateId },
|
|
220
|
+
data: {
|
|
221
|
+
...(input.name !== undefined ? { name: input.name } : {}),
|
|
222
|
+
...(input.structured !== undefined
|
|
223
|
+
? { structured: input.structured.replace(/\\n/g, "\n") }
|
|
224
|
+
: {}),
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export async function deleteRubricTemplate(
|
|
230
|
+
userId: string,
|
|
231
|
+
templateId: string
|
|
232
|
+
) {
|
|
233
|
+
const template = await prisma.markScheme.findUnique({
|
|
234
|
+
where: { id: templateId },
|
|
235
|
+
select: { departmentId: true, department: { select: { schoolId: true } } },
|
|
236
|
+
});
|
|
237
|
+
if (!template?.departmentId || !template.department) {
|
|
238
|
+
throw new TRPCError({
|
|
239
|
+
code: "NOT_FOUND",
|
|
240
|
+
message: "Template not found",
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
await requireAdmin(template.department.schoolId, userId);
|
|
244
|
+
return prisma.markScheme.delete({ where: { id: templateId } });
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export async function createBoundaryTemplate(
|
|
248
|
+
userId: string,
|
|
249
|
+
input: { departmentId: string; name: string; structured: string }
|
|
250
|
+
) {
|
|
251
|
+
const department = await getDepartment(input.departmentId);
|
|
252
|
+
if (!department) {
|
|
253
|
+
throw new TRPCError({
|
|
254
|
+
code: "NOT_FOUND",
|
|
255
|
+
message: "Department not found",
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
await requireAdmin(department.schoolId, userId);
|
|
259
|
+
return prisma.gradingBoundary.create({
|
|
260
|
+
data: {
|
|
261
|
+
name: input.name,
|
|
262
|
+
structured: input.structured.replace(/\\n/g, "\n"),
|
|
263
|
+
department: { connect: { id: input.departmentId } },
|
|
264
|
+
},
|
|
265
|
+
select: { id: true, name: true, structured: true, departmentId: true },
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export async function updateBoundaryTemplate(
|
|
270
|
+
userId: string,
|
|
271
|
+
input: {
|
|
272
|
+
templateId: string;
|
|
273
|
+
name?: string;
|
|
274
|
+
structured?: string;
|
|
275
|
+
}
|
|
276
|
+
) {
|
|
277
|
+
const template = await prisma.gradingBoundary.findUnique({
|
|
278
|
+
where: { id: input.templateId },
|
|
279
|
+
select: { departmentId: true, department: { select: { schoolId: true } } },
|
|
280
|
+
});
|
|
281
|
+
if (!template?.departmentId || !template.department) {
|
|
282
|
+
throw new TRPCError({
|
|
283
|
+
code: "NOT_FOUND",
|
|
284
|
+
message: "Template not found",
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
await requireAdmin(template.department.schoolId, userId);
|
|
288
|
+
return prisma.gradingBoundary.update({
|
|
289
|
+
where: { id: input.templateId },
|
|
290
|
+
data: {
|
|
291
|
+
...(input.name !== undefined ? { name: input.name } : {}),
|
|
292
|
+
...(input.structured !== undefined
|
|
293
|
+
? { structured: input.structured.replace(/\\n/g, "\n") }
|
|
294
|
+
: {}),
|
|
295
|
+
},
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export async function deleteBoundaryTemplate(
|
|
300
|
+
userId: string,
|
|
301
|
+
templateId: string
|
|
302
|
+
) {
|
|
303
|
+
const template = await prisma.gradingBoundary.findUnique({
|
|
304
|
+
where: { id: templateId },
|
|
305
|
+
select: { departmentId: true, department: { select: { schoolId: true } } },
|
|
306
|
+
});
|
|
307
|
+
if (!template?.departmentId || !template.department) {
|
|
308
|
+
throw new TRPCError({
|
|
309
|
+
code: "NOT_FOUND",
|
|
310
|
+
message: "Template not found",
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
await requireAdmin(template.department.schoolId, userId);
|
|
314
|
+
return prisma.gradingBoundary.delete({ where: { id: templateId } });
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Apply a department template to a class by *copying* the structured payload
|
|
319
|
+
* into a class-scoped record. Teacher-only (the class teacher).
|
|
320
|
+
*/
|
|
321
|
+
export async function applyRubricTemplate(
|
|
322
|
+
userId: string,
|
|
323
|
+
input: { classId: string; templateId: string }
|
|
324
|
+
) {
|
|
325
|
+
const cls = await prisma.class.findFirst({
|
|
326
|
+
where: {
|
|
327
|
+
id: input.classId,
|
|
328
|
+
OR: [
|
|
329
|
+
{ teachers: { some: { id: userId } } },
|
|
330
|
+
{
|
|
331
|
+
school: {
|
|
332
|
+
memberships: {
|
|
333
|
+
some: { userId, role: "ADMIN", status: "ACTIVE" },
|
|
334
|
+
},
|
|
335
|
+
},
|
|
336
|
+
},
|
|
337
|
+
],
|
|
338
|
+
},
|
|
339
|
+
select: { id: true, departmentId: true },
|
|
340
|
+
});
|
|
341
|
+
if (!cls) {
|
|
342
|
+
throw new TRPCError({
|
|
343
|
+
code: "FORBIDDEN",
|
|
344
|
+
message: "You don't have permission to apply templates to this class",
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const template = await prisma.markScheme.findUnique({
|
|
349
|
+
where: { id: input.templateId },
|
|
350
|
+
select: { id: true, structured: true, departmentId: true, name: true },
|
|
351
|
+
});
|
|
352
|
+
if (!template?.departmentId) {
|
|
353
|
+
throw new TRPCError({
|
|
354
|
+
code: "NOT_FOUND",
|
|
355
|
+
message: "Template not found",
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
if (cls.departmentId && cls.departmentId !== template.departmentId) {
|
|
359
|
+
throw new TRPCError({
|
|
360
|
+
code: "FORBIDDEN",
|
|
361
|
+
message:
|
|
362
|
+
"This template belongs to a different department than the class",
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return prisma.markScheme.create({
|
|
367
|
+
data: {
|
|
368
|
+
class: { connect: { id: input.classId } },
|
|
369
|
+
structured: template.structured,
|
|
370
|
+
name: template.name,
|
|
371
|
+
},
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
export async function applyBoundaryTemplate(
|
|
376
|
+
userId: string,
|
|
377
|
+
input: { classId: string; templateId: string }
|
|
378
|
+
) {
|
|
379
|
+
const cls = await prisma.class.findFirst({
|
|
380
|
+
where: {
|
|
381
|
+
id: input.classId,
|
|
382
|
+
OR: [
|
|
383
|
+
{ teachers: { some: { id: userId } } },
|
|
384
|
+
{
|
|
385
|
+
school: {
|
|
386
|
+
memberships: {
|
|
387
|
+
some: { userId, role: "ADMIN", status: "ACTIVE" },
|
|
388
|
+
},
|
|
389
|
+
},
|
|
390
|
+
},
|
|
391
|
+
],
|
|
392
|
+
},
|
|
393
|
+
select: { id: true, departmentId: true },
|
|
394
|
+
});
|
|
395
|
+
if (!cls) {
|
|
396
|
+
throw new TRPCError({
|
|
397
|
+
code: "FORBIDDEN",
|
|
398
|
+
message: "You don't have permission to apply templates to this class",
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const template = await prisma.gradingBoundary.findUnique({
|
|
403
|
+
where: { id: input.templateId },
|
|
404
|
+
select: { id: true, structured: true, departmentId: true, name: true },
|
|
405
|
+
});
|
|
406
|
+
if (!template?.departmentId) {
|
|
407
|
+
throw new TRPCError({
|
|
408
|
+
code: "NOT_FOUND",
|
|
409
|
+
message: "Template not found",
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
if (cls.departmentId && cls.departmentId !== template.departmentId) {
|
|
413
|
+
throw new TRPCError({
|
|
414
|
+
code: "FORBIDDEN",
|
|
415
|
+
message:
|
|
416
|
+
"This template belongs to a different department than the class",
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return prisma.gradingBoundary.create({
|
|
421
|
+
data: {
|
|
422
|
+
class: { connect: { id: input.classId } },
|
|
423
|
+
structured: template.structured,
|
|
424
|
+
name: template.name,
|
|
425
|
+
},
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/** Helper used by the auto-tag flow on class creation. */
|
|
430
|
+
export async function setClassDepartment(
|
|
431
|
+
userId: string,
|
|
432
|
+
input: { classId: string; departmentId: string | null }
|
|
433
|
+
) {
|
|
434
|
+
const cls = await prisma.class.findFirst({
|
|
435
|
+
where: {
|
|
436
|
+
id: input.classId,
|
|
437
|
+
OR: [
|
|
438
|
+
{ teachers: { some: { id: userId } } },
|
|
439
|
+
{
|
|
440
|
+
school: {
|
|
441
|
+
memberships: {
|
|
442
|
+
some: { userId, role: "ADMIN", status: "ACTIVE" },
|
|
443
|
+
},
|
|
444
|
+
},
|
|
445
|
+
},
|
|
446
|
+
],
|
|
447
|
+
},
|
|
448
|
+
select: { id: true, schoolId: true },
|
|
449
|
+
});
|
|
450
|
+
if (!cls?.schoolId) {
|
|
451
|
+
throw new TRPCError({
|
|
452
|
+
code: "BAD_REQUEST",
|
|
453
|
+
message: "Class is not affiliated with an institution",
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (input.departmentId) {
|
|
458
|
+
const dept = await getDepartment(input.departmentId);
|
|
459
|
+
if (!dept || dept.schoolId !== cls.schoolId) {
|
|
460
|
+
throw new TRPCError({
|
|
461
|
+
code: "BAD_REQUEST",
|
|
462
|
+
message: "Department does not belong to this institution",
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
return prisma.class.update({
|
|
467
|
+
where: { id: input.classId },
|
|
468
|
+
data: { departmentId: input.departmentId },
|
|
469
|
+
select: { id: true, departmentId: true },
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
export async function checkDepartmentMember(
|
|
474
|
+
userId: string,
|
|
475
|
+
departmentId: string
|
|
476
|
+
) {
|
|
477
|
+
return Boolean(await findDepartmentMembership(departmentId, userId));
|
|
478
|
+
}
|