@studious-lms/server 1.4.6 → 1.4.9
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/.github/workflows/npm.yml +28 -0
- package/dist/models/access.d.ts +112 -0
- package/dist/models/access.d.ts.map +1 -0
- package/dist/models/access.js +49 -0
- package/dist/models/access.js.map +1 -0
- package/dist/models/announcement.d.ts +17 -2
- package/dist/models/announcement.d.ts.map +1 -1
- package/dist/models/announcement.js +21 -4
- package/dist/models/announcement.js.map +1 -1
- package/dist/models/assignment.d.ts +69 -37
- package/dist/models/assignment.d.ts.map +1 -1
- package/dist/models/assignment.js +124 -15
- package/dist/models/assignment.js.map +1 -1
- package/dist/models/attendance.d.ts +1 -1
- package/dist/models/attendance.d.ts.map +1 -1
- package/dist/models/attendance.js +6 -5
- package/dist/models/attendance.js.map +1 -1
- package/dist/models/auth.d.ts +5 -5
- package/dist/models/auth.d.ts.map +1 -1
- package/dist/models/auth.js +7 -11
- package/dist/models/auth.js.map +1 -1
- package/dist/models/class.d.ts +26 -26
- package/dist/models/class.d.ts.map +1 -1
- package/dist/models/class.js +7 -11
- package/dist/models/class.js.map +1 -1
- package/dist/models/comment.d.ts +134 -70
- package/dist/models/comment.d.ts.map +1 -1
- package/dist/models/comment.js +30 -42
- package/dist/models/comment.js.map +1 -1
- package/dist/models/conversation.d.ts +7 -7
- package/dist/models/event.d.ts +52 -3
- package/dist/models/event.d.ts.map +1 -1
- package/dist/models/event.js +56 -6
- package/dist/models/event.js.map +1 -1
- package/dist/models/file.d.ts +178 -103
- package/dist/models/file.d.ts.map +1 -1
- package/dist/models/file.js +54 -14
- package/dist/models/file.js.map +1 -1
- package/dist/models/folder.d.ts +22 -22
- package/dist/models/folder.d.ts.map +1 -1
- package/dist/models/folder.js +5 -4
- package/dist/models/folder.js.map +1 -1
- package/dist/models/labChat.d.ts +20 -20
- package/dist/models/labChat.d.ts.map +1 -1
- package/dist/models/labChat.js +8 -10
- package/dist/models/labChat.js.map +1 -1
- package/dist/models/marketing.d.ts +1 -1
- package/dist/models/message.d.ts +6 -6
- package/dist/models/newtonChat.d.ts +5 -5
- package/dist/models/school.d.ts +361 -0
- package/dist/models/school.d.ts.map +1 -0
- package/dist/models/school.js +314 -0
- package/dist/models/school.js.map +1 -0
- package/dist/models/section.d.ts +1 -1
- package/dist/models/section.d.ts.map +1 -1
- package/dist/models/section.js +5 -4
- package/dist/models/section.js.map +1 -1
- package/dist/models/user.d.ts +4 -4
- package/dist/models/worksheet.d.ts +163 -141
- package/dist/models/worksheet.d.ts.map +1 -1
- package/dist/models/worksheet.js +42 -17
- package/dist/models/worksheet.js.map +1 -1
- package/dist/pipelines/gradeWorksheet.d.ts +2 -2
- package/dist/pipelines/pdfToWorksheet.d.ts +1 -1
- package/dist/routers/_app.d.ts +1271 -327
- 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/announcement.d.ts +5 -5
- package/dist/routers/announcement.d.ts.map +1 -1
- package/dist/routers/announcement.js +25 -5
- package/dist/routers/announcement.js.map +1 -1
- package/dist/routers/assignment.d.ts +79 -44
- package/dist/routers/assignment.d.ts.map +1 -1
- package/dist/routers/assignment.js +32 -12
- package/dist/routers/assignment.js.map +1 -1
- package/dist/routers/class.d.ts +40 -20
- package/dist/routers/class.d.ts.map +1 -1
- package/dist/routers/class.js +3 -2
- package/dist/routers/class.js.map +1 -1
- package/dist/routers/comment.d.ts +5 -5
- package/dist/routers/conversation.d.ts +6 -6
- package/dist/routers/event.d.ts +21 -2
- package/dist/routers/event.d.ts.map +1 -1
- package/dist/routers/file.d.ts +2 -2
- package/dist/routers/folder.d.ts +24 -24
- package/dist/routers/labChat.d.ts +7 -7
- package/dist/routers/school.d.ts +384 -0
- package/dist/routers/school.d.ts.map +1 -0
- package/dist/routers/school.js +79 -0
- package/dist/routers/school.js.map +1 -0
- package/dist/routers/search.d.ts +1 -1
- package/dist/routers/worksheet.d.ts +36 -20
- package/dist/routers/worksheet.d.ts.map +1 -1
- package/dist/routers/worksheet.js +10 -3
- package/dist/routers/worksheet.js.map +1 -1
- package/dist/services/announcement.d.ts.map +1 -1
- package/dist/services/announcement.js +10 -6
- package/dist/services/announcement.js.map +1 -1
- package/dist/services/assignment.d.ts +66 -36
- package/dist/services/assignment.d.ts.map +1 -1
- package/dist/services/assignment.js +37 -3
- package/dist/services/assignment.js.map +1 -1
- package/dist/services/class.d.ts +39 -19
- package/dist/services/class.d.ts.map +1 -1
- package/dist/services/class.js +52 -15
- package/dist/services/class.js.map +1 -1
- package/dist/services/comment.d.ts +5 -5
- package/dist/services/comment.d.ts.map +1 -1
- package/dist/services/comment.js +8 -3
- package/dist/services/comment.js.map +1 -1
- package/dist/services/conversation.d.ts +6 -6
- package/dist/services/event.d.ts +21 -2
- package/dist/services/event.d.ts.map +1 -1
- package/dist/services/event.js +11 -7
- package/dist/services/event.js.map +1 -1
- package/dist/services/file.d.ts +2 -2
- package/dist/services/file.d.ts.map +1 -1
- package/dist/services/file.js +17 -8
- package/dist/services/file.js.map +1 -1
- package/dist/services/folder.d.ts +24 -24
- package/dist/services/labChat.d.ts +7 -7
- package/dist/services/school.d.ts +299 -0
- package/dist/services/school.d.ts.map +1 -0
- package/dist/services/school.js +225 -0
- package/dist/services/school.js.map +1 -0
- package/dist/services/worksheet.d.ts +31 -20
- package/dist/services/worksheet.d.ts.map +1 -1
- package/dist/services/worksheet.js +37 -3
- package/dist/services/worksheet.js.map +1 -1
- package/package.json +1 -1
- package/prisma/migrations/20260504071500_school_memberships/migration.sql +81 -0
- package/prisma/schema.prisma +66 -2
- package/src/models/access.ts +48 -0
- package/src/models/announcement.ts +19 -2
- package/src/models/assignment.ts +149 -13
- package/src/models/attendance.ts +4 -3
- package/src/models/auth.ts +8 -9
- package/src/models/class.ts +8 -9
- package/src/models/comment.ts +29 -40
- package/src/models/event.ts +54 -4
- package/src/models/file.ts +53 -12
- package/src/models/folder.ts +3 -2
- package/src/models/labChat.ts +9 -8
- package/src/models/school.ts +361 -0
- package/src/models/section.ts +3 -2
- package/src/models/worksheet.ts +61 -12
- package/src/routers/_app.ts +2 -0
- package/src/routers/announcement.ts +30 -4
- package/src/routers/assignment.ts +46 -9
- package/src/routers/class.ts +1 -0
- package/src/routers/school.ts +122 -0
- package/src/routers/worksheet.ts +18 -0
- package/src/services/announcement.ts +12 -4
- package/src/services/assignment.ts +47 -1
- package/src/services/class.ts +61 -14
- package/src/services/comment.ts +10 -1
- package/src/services/event.ts +23 -6
- package/src/services/file.ts +20 -6
- package/src/services/school.ts +308 -0
- package/src/services/worksheet.ts +46 -0
- package/dist/lib/notificationHandler.d.ts +0 -25
- package/dist/lib/notificationHandler.d.ts.map +0 -1
- package/dist/lib/notificationHandler.js +0 -32
- package/dist/lib/notificationHandler.js.map +0 -1
- package/dist/server/pipelines/aiLabChat.d.ts +0 -21
- package/dist/server/pipelines/aiLabChat.d.ts.map +0 -1
- package/dist/server/pipelines/aiLabChat.js +0 -456
- package/dist/server/pipelines/aiLabChat.js.map +0 -1
- package/dist/server/pipelines/aiNewtonChat.d.ts +0 -30
- package/dist/server/pipelines/aiNewtonChat.d.ts.map +0 -1
- package/dist/server/pipelines/aiNewtonChat.js +0 -285
- package/dist/server/pipelines/aiNewtonChat.js.map +0 -1
- package/dist/server/pipelines/gradeWorksheet.d.ts +0 -30
- package/dist/server/pipelines/gradeWorksheet.d.ts.map +0 -1
- package/dist/server/pipelines/gradeWorksheet.js +0 -248
- package/dist/server/pipelines/gradeWorksheet.js.map +0 -1
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* School model - school membership, invites, and admin lookups.
|
|
3
|
+
*/
|
|
4
|
+
import { SchoolMembershipRole } from "@prisma/client";
|
|
5
|
+
import { prisma } from "../lib/prisma.js";
|
|
6
|
+
|
|
7
|
+
const memberSelect = {
|
|
8
|
+
id: true,
|
|
9
|
+
role: true,
|
|
10
|
+
status: true,
|
|
11
|
+
createdAt: true,
|
|
12
|
+
updatedAt: true,
|
|
13
|
+
user: {
|
|
14
|
+
select: {
|
|
15
|
+
id: true,
|
|
16
|
+
username: true,
|
|
17
|
+
email: true,
|
|
18
|
+
profile: {
|
|
19
|
+
select: {
|
|
20
|
+
displayName: true,
|
|
21
|
+
profilePicture: true,
|
|
22
|
+
profilePictureThumbnail: true,
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
} as const;
|
|
28
|
+
|
|
29
|
+
export function findSchoolMembership(schoolId: string, userId: string) {
|
|
30
|
+
return prisma.schoolMembership.findUnique({
|
|
31
|
+
where: { schoolId_userId: { schoolId, userId } },
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function findActiveSchoolMembership(schoolId: string, userId: string) {
|
|
36
|
+
return prisma.schoolMembership.findFirst({
|
|
37
|
+
where: { schoolId, userId, status: "ACTIVE" },
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function findActiveSchoolAdminMembership(schoolId: string, userId: string) {
|
|
42
|
+
return prisma.schoolMembership.findFirst({
|
|
43
|
+
where: { schoolId, userId, role: "ADMIN", status: "ACTIVE" },
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function listUserSchoolMemberships(userId: string) {
|
|
48
|
+
return prisma.schoolMembership.findMany({
|
|
49
|
+
where: { userId, status: "ACTIVE" },
|
|
50
|
+
include: {
|
|
51
|
+
school: {
|
|
52
|
+
select: {
|
|
53
|
+
id: true,
|
|
54
|
+
name: true,
|
|
55
|
+
subdomain: true,
|
|
56
|
+
logoId: true,
|
|
57
|
+
createdAt: true,
|
|
58
|
+
updatedAt: true,
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
orderBy: { createdAt: "asc" },
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function createSchoolWithAdmin(data: {
|
|
67
|
+
name: string;
|
|
68
|
+
subdomain?: string;
|
|
69
|
+
logoId?: string;
|
|
70
|
+
userId: string;
|
|
71
|
+
}) {
|
|
72
|
+
return prisma.school.create({
|
|
73
|
+
data: {
|
|
74
|
+
name: data.name,
|
|
75
|
+
subdomain: data.subdomain,
|
|
76
|
+
logoId: data.logoId,
|
|
77
|
+
memberships: {
|
|
78
|
+
create: {
|
|
79
|
+
userId: data.userId,
|
|
80
|
+
role: "ADMIN",
|
|
81
|
+
status: "ACTIVE",
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
include: {
|
|
86
|
+
memberships: {
|
|
87
|
+
where: { userId: data.userId },
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function getSchoolById(schoolId: string) {
|
|
94
|
+
return prisma.school.findUnique({
|
|
95
|
+
where: { id: schoolId },
|
|
96
|
+
select: {
|
|
97
|
+
id: true,
|
|
98
|
+
name: true,
|
|
99
|
+
subdomain: true,
|
|
100
|
+
logoId: true,
|
|
101
|
+
createdAt: true,
|
|
102
|
+
updatedAt: true,
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function getSchoolOverview(schoolId: string) {
|
|
108
|
+
return prisma.school.findUnique({
|
|
109
|
+
where: { id: schoolId },
|
|
110
|
+
select: {
|
|
111
|
+
id: true,
|
|
112
|
+
name: true,
|
|
113
|
+
subdomain: true,
|
|
114
|
+
_count: {
|
|
115
|
+
select: {
|
|
116
|
+
classes: true,
|
|
117
|
+
memberships: { where: { status: "ACTIVE" } },
|
|
118
|
+
invites: { where: { status: "PENDING" } },
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
memberships: {
|
|
122
|
+
where: { status: "ACTIVE" },
|
|
123
|
+
select: { role: true },
|
|
124
|
+
},
|
|
125
|
+
classes: {
|
|
126
|
+
orderBy: { name: "asc" },
|
|
127
|
+
take: 5,
|
|
128
|
+
select: {
|
|
129
|
+
id: true,
|
|
130
|
+
name: true,
|
|
131
|
+
subject: true,
|
|
132
|
+
section: true,
|
|
133
|
+
color: true,
|
|
134
|
+
_count: { select: { teachers: true, students: true } },
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
invites: {
|
|
138
|
+
where: { status: "PENDING" },
|
|
139
|
+
orderBy: { createdAt: "desc" },
|
|
140
|
+
take: 5,
|
|
141
|
+
select: {
|
|
142
|
+
id: true,
|
|
143
|
+
email: true,
|
|
144
|
+
role: true,
|
|
145
|
+
expiresAt: true,
|
|
146
|
+
createdAt: true,
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function listSchoolClasses(schoolId: string) {
|
|
154
|
+
return prisma.class.findMany({
|
|
155
|
+
where: { schoolId },
|
|
156
|
+
orderBy: [{ name: "asc" }, { section: "asc" }],
|
|
157
|
+
include: {
|
|
158
|
+
teachers: {
|
|
159
|
+
select: {
|
|
160
|
+
id: true,
|
|
161
|
+
username: true,
|
|
162
|
+
profile: {
|
|
163
|
+
select: {
|
|
164
|
+
displayName: true,
|
|
165
|
+
profilePicture: true,
|
|
166
|
+
profilePictureThumbnail: true,
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
students: { select: { id: true } },
|
|
172
|
+
_count: { select: { teachers: true, students: true, assignments: true } },
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function listSchoolMembers(schoolId: string) {
|
|
178
|
+
return prisma.schoolMembership.findMany({
|
|
179
|
+
where: { schoolId },
|
|
180
|
+
select: memberSelect,
|
|
181
|
+
orderBy: [{ role: "asc" }, { createdAt: "asc" }],
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function countActiveAdmins(schoolId: string) {
|
|
186
|
+
return prisma.schoolMembership.count({
|
|
187
|
+
where: { schoolId, role: "ADMIN", status: "ACTIVE" },
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function upsertSchoolMembership(data: {
|
|
192
|
+
schoolId: string;
|
|
193
|
+
userId: string;
|
|
194
|
+
role: SchoolMembershipRole;
|
|
195
|
+
}) {
|
|
196
|
+
return prisma.schoolMembership.upsert({
|
|
197
|
+
where: {
|
|
198
|
+
schoolId_userId: {
|
|
199
|
+
schoolId: data.schoolId,
|
|
200
|
+
userId: data.userId,
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
update: {
|
|
204
|
+
role: data.role,
|
|
205
|
+
status: "ACTIVE",
|
|
206
|
+
},
|
|
207
|
+
create: {
|
|
208
|
+
schoolId: data.schoolId,
|
|
209
|
+
userId: data.userId,
|
|
210
|
+
role: data.role,
|
|
211
|
+
status: "ACTIVE",
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function updateSchoolMembershipRole(
|
|
217
|
+
schoolId: string,
|
|
218
|
+
userId: string,
|
|
219
|
+
role: SchoolMembershipRole
|
|
220
|
+
) {
|
|
221
|
+
return prisma.schoolMembership.update({
|
|
222
|
+
where: { schoolId_userId: { schoolId, userId } },
|
|
223
|
+
data: { role },
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function removeSchoolMembershipPreservingContent(schoolId: string, userId: string) {
|
|
228
|
+
return prisma.$transaction(async (tx) => {
|
|
229
|
+
const classes = await tx.class.findMany({
|
|
230
|
+
where: { schoolId },
|
|
231
|
+
select: { id: true },
|
|
232
|
+
});
|
|
233
|
+
const classIds = classes.map((cls) => ({ id: cls.id }));
|
|
234
|
+
|
|
235
|
+
for (const classWhere of classIds) {
|
|
236
|
+
await tx.class.update({
|
|
237
|
+
where: classWhere,
|
|
238
|
+
data: {
|
|
239
|
+
teachers: { disconnect: { id: userId } },
|
|
240
|
+
students: { disconnect: { id: userId } },
|
|
241
|
+
},
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return tx.schoolMembership.delete({
|
|
246
|
+
where: { schoolId_userId: { schoolId, userId } },
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function findUserByEmailForSchool(email: string) {
|
|
252
|
+
return prisma.user.findFirst({
|
|
253
|
+
where: { email },
|
|
254
|
+
select: { id: true, email: true, username: true },
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export function findUserByIdForSchool(userId: string) {
|
|
259
|
+
return prisma.user.findUnique({
|
|
260
|
+
where: { id: userId },
|
|
261
|
+
select: { id: true, email: true, username: true },
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export function createSchoolInvite(data: {
|
|
266
|
+
schoolId: string;
|
|
267
|
+
invitedById: string;
|
|
268
|
+
email: string;
|
|
269
|
+
role: SchoolMembershipRole;
|
|
270
|
+
token: string;
|
|
271
|
+
expiresAt: Date;
|
|
272
|
+
}) {
|
|
273
|
+
return prisma.schoolInvite.create({
|
|
274
|
+
data,
|
|
275
|
+
include: {
|
|
276
|
+
school: { select: { id: true, name: true } },
|
|
277
|
+
},
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export function listSchoolInvites(schoolId: string) {
|
|
282
|
+
return prisma.schoolInvite.findMany({
|
|
283
|
+
where: { schoolId },
|
|
284
|
+
orderBy: { createdAt: "desc" },
|
|
285
|
+
select: {
|
|
286
|
+
id: true,
|
|
287
|
+
email: true,
|
|
288
|
+
role: true,
|
|
289
|
+
status: true,
|
|
290
|
+
expiresAt: true,
|
|
291
|
+
acceptedAt: true,
|
|
292
|
+
revokedAt: true,
|
|
293
|
+
createdAt: true,
|
|
294
|
+
invitedBy: {
|
|
295
|
+
select: {
|
|
296
|
+
id: true,
|
|
297
|
+
username: true,
|
|
298
|
+
profile: { select: { displayName: true } },
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export function revokeSchoolInvite(inviteId: string, schoolId: string) {
|
|
306
|
+
return prisma.schoolInvite.update({
|
|
307
|
+
where: { id: inviteId, schoolId },
|
|
308
|
+
data: {
|
|
309
|
+
status: "REVOKED",
|
|
310
|
+
revokedAt: new Date(),
|
|
311
|
+
},
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export function findPendingInviteByToken(token: string) {
|
|
316
|
+
return prisma.schoolInvite.findUnique({
|
|
317
|
+
where: { token },
|
|
318
|
+
include: { school: { select: { id: true, name: true } } },
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export function acceptSchoolInvite(inviteId: string, userId: string) {
|
|
323
|
+
return prisma.$transaction(async (tx) => {
|
|
324
|
+
const invite = await tx.schoolInvite.findUnique({
|
|
325
|
+
where: { id: inviteId },
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
if (!invite) {
|
|
329
|
+
throw new Error("Invite not found");
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const membership = await tx.schoolMembership.upsert({
|
|
333
|
+
where: {
|
|
334
|
+
schoolId_userId: {
|
|
335
|
+
schoolId: invite.schoolId,
|
|
336
|
+
userId,
|
|
337
|
+
},
|
|
338
|
+
},
|
|
339
|
+
update: {
|
|
340
|
+
role: invite.role,
|
|
341
|
+
status: "ACTIVE",
|
|
342
|
+
},
|
|
343
|
+
create: {
|
|
344
|
+
schoolId: invite.schoolId,
|
|
345
|
+
userId,
|
|
346
|
+
role: invite.role,
|
|
347
|
+
status: "ACTIVE",
|
|
348
|
+
},
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
await tx.schoolInvite.update({
|
|
352
|
+
where: { id: inviteId },
|
|
353
|
+
data: {
|
|
354
|
+
status: "ACCEPTED",
|
|
355
|
+
acceptedAt: new Date(),
|
|
356
|
+
},
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
return membership;
|
|
360
|
+
});
|
|
361
|
+
}
|
package/src/models/section.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Section model – class sections, ordering, assignment grouping.
|
|
3
3
|
*/
|
|
4
4
|
import { prisma } from "../lib/prisma.js";
|
|
5
|
+
import { classTeacherOrSchoolAdminWhere } from "./access.js";
|
|
5
6
|
|
|
6
7
|
/** @returns Section by ID. */
|
|
7
8
|
export function findSectionById(id: string) {
|
|
@@ -10,12 +11,12 @@ export function findSectionById(id: string) {
|
|
|
10
11
|
});
|
|
11
12
|
}
|
|
12
13
|
|
|
13
|
-
/** @returns Class if user is a teacher. */
|
|
14
|
+
/** @returns Class if user is a teacher or school admin. */
|
|
14
15
|
export function findClassWithTeacher(classId: string, teacherId: string) {
|
|
15
16
|
return prisma.class.findFirst({
|
|
16
17
|
where: {
|
|
17
18
|
id: classId,
|
|
18
|
-
|
|
19
|
+
...classTeacherOrSchoolAdminWhere(teacherId),
|
|
19
20
|
},
|
|
20
21
|
});
|
|
21
22
|
}
|
package/src/models/worksheet.ts
CHANGED
|
@@ -1,22 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Worksheet model – worksheets, questions, submissions, student progress, grading.
|
|
3
3
|
*/
|
|
4
|
+
import type { Prisma } from "@prisma/client";
|
|
4
5
|
import { prisma } from "../lib/prisma.js";
|
|
5
6
|
import type { WorksheetQuestionType, GenerationStatus } from "@prisma/client";
|
|
7
|
+
import {
|
|
8
|
+
classMemberOrSchoolAdminWhere,
|
|
9
|
+
classTeacherOrSchoolAdminWhere,
|
|
10
|
+
} from "./access.js";
|
|
6
11
|
|
|
7
12
|
const worksheetClassMemberWhere = (userId: string) => ({
|
|
8
|
-
class:
|
|
9
|
-
OR: [
|
|
10
|
-
{ teachers: { some: { id: userId } } },
|
|
11
|
-
{ students: { some: { id: userId } } },
|
|
12
|
-
],
|
|
13
|
-
},
|
|
13
|
+
class: classMemberOrSchoolAdminWhere(userId),
|
|
14
14
|
});
|
|
15
15
|
|
|
16
16
|
const worksheetTeacherWhere = (userId: string) => ({
|
|
17
|
-
class:
|
|
18
|
-
teachers: { some: { id: userId } },
|
|
19
|
-
},
|
|
17
|
+
class: classTeacherOrSchoolAdminWhere(userId),
|
|
20
18
|
});
|
|
21
19
|
|
|
22
20
|
/** @returns Worksheet with questions and class. */
|
|
@@ -118,6 +116,59 @@ export function deleteWorksheet(id: string) {
|
|
|
118
116
|
});
|
|
119
117
|
}
|
|
120
118
|
|
|
119
|
+
/**
|
|
120
|
+
* Clone a worksheet and its questions into another class with a new name.
|
|
121
|
+
* Does not copy student responses or assignment links.
|
|
122
|
+
*/
|
|
123
|
+
export async function duplicateWorksheetWithClient(
|
|
124
|
+
tx: Prisma.TransactionClient,
|
|
125
|
+
sourceWorksheetId: string,
|
|
126
|
+
targetClassId: string,
|
|
127
|
+
name: string
|
|
128
|
+
) {
|
|
129
|
+
const source = await tx.worksheet.findUnique({
|
|
130
|
+
where: { id: sourceWorksheetId },
|
|
131
|
+
include: {
|
|
132
|
+
questions: { orderBy: { order: "asc" } },
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
if (!source) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const created = await tx.worksheet.create({
|
|
141
|
+
data: { classId: targetClassId, name },
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
if (source.questions.length > 0) {
|
|
145
|
+
await tx.worksheetQuestion.createMany({
|
|
146
|
+
data: source.questions.map((q) => ({
|
|
147
|
+
worksheetId: created.id,
|
|
148
|
+
type: q.type,
|
|
149
|
+
question: q.question,
|
|
150
|
+
answer: q.answer,
|
|
151
|
+
points: q.points,
|
|
152
|
+
options: q.options ?? undefined,
|
|
153
|
+
markScheme: q.markScheme ?? undefined,
|
|
154
|
+
order: q.order,
|
|
155
|
+
})),
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return created;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export async function duplicateWorksheet(
|
|
163
|
+
sourceWorksheetId: string,
|
|
164
|
+
targetClassId: string,
|
|
165
|
+
name: string
|
|
166
|
+
) {
|
|
167
|
+
return prisma.$transaction((tx) =>
|
|
168
|
+
duplicateWorksheetWithClient(tx, sourceWorksheetId, targetClassId, name)
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
121
172
|
/** @returns Questions for a worksheet. */
|
|
122
173
|
export function findQuestionsByWorksheetId(worksheetId: string) {
|
|
123
174
|
return prisma.worksheetQuestion.findMany({
|
|
@@ -207,9 +258,7 @@ export function findSubmissionByIdForWorksheetAccess(
|
|
|
207
258
|
},
|
|
208
259
|
{
|
|
209
260
|
assignment: {
|
|
210
|
-
class:
|
|
211
|
-
teachers: { some: { id: userId } },
|
|
212
|
-
},
|
|
261
|
+
class: classTeacherOrSchoolAdminWhere(userId),
|
|
213
262
|
},
|
|
214
263
|
},
|
|
215
264
|
],
|
package/src/routers/_app.ts
CHANGED
|
@@ -22,6 +22,7 @@ import { worksheetRouter } from "./worksheet.js";
|
|
|
22
22
|
import { commentRouter } from "./comment.js";
|
|
23
23
|
import { studentProgressRouter } from "./studentProgress.js";
|
|
24
24
|
import { searchRouter } from "./search.js";
|
|
25
|
+
import { schoolRouter } from "./school.js";
|
|
25
26
|
|
|
26
27
|
export const appRouter = createTRPCRouter({
|
|
27
28
|
class: classRouter,
|
|
@@ -45,6 +46,7 @@ export const appRouter = createTRPCRouter({
|
|
|
45
46
|
comment: commentRouter,
|
|
46
47
|
studentProgress: studentProgressRouter,
|
|
47
48
|
search: searchRouter,
|
|
49
|
+
school: schoolRouter,
|
|
48
50
|
});
|
|
49
51
|
|
|
50
52
|
// Export type router type definition
|
|
@@ -339,6 +339,24 @@ export const announcementRouter = createTRPCRouter({
|
|
|
339
339
|
class: {
|
|
340
340
|
include: {
|
|
341
341
|
teachers: true,
|
|
342
|
+
school: {
|
|
343
|
+
include: {
|
|
344
|
+
users: {
|
|
345
|
+
where: {
|
|
346
|
+
role: "ADMIN",
|
|
347
|
+
},
|
|
348
|
+
},
|
|
349
|
+
memberships: {
|
|
350
|
+
where: {
|
|
351
|
+
role: "ADMIN",
|
|
352
|
+
status: "ACTIVE",
|
|
353
|
+
},
|
|
354
|
+
select: {
|
|
355
|
+
userId: true,
|
|
356
|
+
},
|
|
357
|
+
},
|
|
358
|
+
},
|
|
359
|
+
},
|
|
342
360
|
},
|
|
343
361
|
},
|
|
344
362
|
},
|
|
@@ -353,17 +371,25 @@ export const announcementRouter = createTRPCRouter({
|
|
|
353
371
|
});
|
|
354
372
|
}
|
|
355
373
|
|
|
356
|
-
// Only the author
|
|
374
|
+
// Only the author, a class teacher, or the class school admin can delete comments
|
|
357
375
|
const userId = ctx.user.id;
|
|
358
376
|
const isAuthor = comment.authorId === userId;
|
|
359
377
|
const isClassTeacher = comment.announcement!.class.teachers.some(
|
|
360
378
|
(teacher) => teacher.id === userId
|
|
361
379
|
);
|
|
380
|
+
const isSchoolAdmin = Boolean(
|
|
381
|
+
comment.announcement!.class.school?.memberships.some(
|
|
382
|
+
(admin) => admin.userId === userId
|
|
383
|
+
) ||
|
|
384
|
+
comment.announcement!.class.school?.users.some(
|
|
385
|
+
(admin) => admin.id === userId
|
|
386
|
+
)
|
|
387
|
+
);
|
|
362
388
|
|
|
363
|
-
if (!isAuthor && !isClassTeacher) {
|
|
389
|
+
if (!isAuthor && !isClassTeacher && !isSchoolAdmin) {
|
|
364
390
|
throw new TRPCError({
|
|
365
391
|
code: "FORBIDDEN",
|
|
366
|
-
message: "Only the comment author
|
|
392
|
+
message: "Only the comment author, class teachers, or school admins can delete comments",
|
|
367
393
|
});
|
|
368
394
|
}
|
|
369
395
|
|
|
@@ -696,4 +722,4 @@ export const announcementRouter = createTRPCRouter({
|
|
|
696
722
|
message: "Unexpected error",
|
|
697
723
|
});
|
|
698
724
|
}),
|
|
699
|
-
});
|
|
725
|
+
});
|
|
@@ -6,6 +6,10 @@ import { createDirectUploadFiles, type DirectUploadFile, confirmDirectUpload, up
|
|
|
6
6
|
import { deleteFile } from "../lib/googleCloudStorage.js";
|
|
7
7
|
import { logger } from "../utils/logger.js";
|
|
8
8
|
import { gradeWorksheetPipeline } from "../pipelines/gradeWorksheet.js";
|
|
9
|
+
import {
|
|
10
|
+
classMemberOrSchoolAdminWhere,
|
|
11
|
+
classTeacherOrSchoolAdminWhere,
|
|
12
|
+
} from "../models/access.js";
|
|
9
13
|
import {
|
|
10
14
|
assignmentExists,
|
|
11
15
|
getDueToday,
|
|
@@ -14,6 +18,7 @@ import {
|
|
|
14
18
|
getSubmissionById,
|
|
15
19
|
getSubmissions,
|
|
16
20
|
createAssignmentRecord,
|
|
21
|
+
duplicateAssignmentRecord,
|
|
17
22
|
updateAssignmentRecord,
|
|
18
23
|
deleteAssignmentRecord,
|
|
19
24
|
updateSubmissionRecord,
|
|
@@ -269,6 +274,24 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
269
274
|
create: protectedTeacherProcedure
|
|
270
275
|
.input(createAssignmentSchema)
|
|
271
276
|
.mutation(({ ctx, input }) => createAssignmentRecord(ctx.user!.id, input)),
|
|
277
|
+
|
|
278
|
+
duplicateAssignment: protectedTeacherProcedure
|
|
279
|
+
.input(
|
|
280
|
+
z.object({
|
|
281
|
+
classId: z.string(),
|
|
282
|
+
title: z.string(),
|
|
283
|
+
assignmentId: z.string(),
|
|
284
|
+
})
|
|
285
|
+
)
|
|
286
|
+
.mutation(({ ctx, input }) =>
|
|
287
|
+
duplicateAssignmentRecord(
|
|
288
|
+
input.assignmentId,
|
|
289
|
+
input.classId,
|
|
290
|
+
input.title,
|
|
291
|
+
ctx.user!.id
|
|
292
|
+
)
|
|
293
|
+
),
|
|
294
|
+
|
|
272
295
|
update: protectedTeacherProcedure
|
|
273
296
|
.input(updateAssignmentSchema)
|
|
274
297
|
.mutation(({ ctx, input }) => updateAssignmentRecord(ctx.user!.id, input)),
|
|
@@ -394,16 +417,11 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
394
417
|
assignmentId: input.assignmentId,
|
|
395
418
|
assignment: {
|
|
396
419
|
classId: input.classId,
|
|
397
|
-
class:
|
|
398
|
-
OR: [
|
|
399
|
-
{ teachers: { some: { id: ctx.user!.id } } },
|
|
400
|
-
{ students: { some: { id: ctx.user!.id } } },
|
|
401
|
-
],
|
|
402
|
-
},
|
|
420
|
+
class: classMemberOrSchoolAdminWhere(ctx.user!.id),
|
|
403
421
|
},
|
|
404
422
|
OR: [
|
|
405
423
|
{ studentId: ctx.user!.id },
|
|
406
|
-
{ assignment: { class:
|
|
424
|
+
{ assignment: { class: classTeacherOrSchoolAdminWhere(ctx.user!.id) } },
|
|
407
425
|
],
|
|
408
426
|
},
|
|
409
427
|
select: { id: true },
|
|
@@ -437,7 +455,7 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
437
455
|
assignment: { classId: input.classId },
|
|
438
456
|
OR: [
|
|
439
457
|
{ studentId: ctx.user!.id },
|
|
440
|
-
{ assignment: { class:
|
|
458
|
+
{ assignment: { class: classTeacherOrSchoolAdminWhere(ctx.user!.id) } },
|
|
441
459
|
],
|
|
442
460
|
},
|
|
443
461
|
select: { id: true },
|
|
@@ -630,6 +648,21 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
630
648
|
include: {
|
|
631
649
|
students: true,
|
|
632
650
|
teachers: true,
|
|
651
|
+
school: {
|
|
652
|
+
include: {
|
|
653
|
+
users: {
|
|
654
|
+
where: { role: "ADMIN" },
|
|
655
|
+
select: { id: true },
|
|
656
|
+
},
|
|
657
|
+
memberships: {
|
|
658
|
+
where: {
|
|
659
|
+
role: "ADMIN",
|
|
660
|
+
status: "ACTIVE",
|
|
661
|
+
},
|
|
662
|
+
select: { userId: true },
|
|
663
|
+
},
|
|
664
|
+
},
|
|
665
|
+
},
|
|
633
666
|
},
|
|
634
667
|
},
|
|
635
668
|
},
|
|
@@ -647,8 +680,12 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
647
680
|
// Check if user is the student who owns the submission or a teacher of the class
|
|
648
681
|
const isStudent = submission.studentId === ctx.user.id;
|
|
649
682
|
const isTeacher = submission.assignment.class.teachers.some(teacher => teacher.id === ctx.user?.id);
|
|
683
|
+
const isSchoolAdmin = Boolean(
|
|
684
|
+
submission.assignment.class.school?.memberships.some(admin => admin.userId === ctx.user?.id) ||
|
|
685
|
+
submission.assignment.class.school?.users.some(admin => admin.id === ctx.user?.id)
|
|
686
|
+
);
|
|
650
687
|
|
|
651
|
-
if (!isStudent && !isTeacher) {
|
|
688
|
+
if (!isStudent && !isTeacher && !isSchoolAdmin) {
|
|
652
689
|
throw new TRPCError({
|
|
653
690
|
code: "FORBIDDEN",
|
|
654
691
|
message: "You don't have permission to upload files to this submission",
|