@studious-lms/server 1.21.0 → 1.23.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.
Files changed (120) hide show
  1. package/dist/models/agenda.d.ts +6 -0
  2. package/dist/models/agenda.d.ts.map +1 -1
  3. package/dist/models/announcement.d.ts +2 -0
  4. package/dist/models/announcement.d.ts.map +1 -1
  5. package/dist/models/assignment.d.ts +27 -0
  6. package/dist/models/assignment.d.ts.map +1 -1
  7. package/dist/models/auth.d.ts +4 -0
  8. package/dist/models/auth.d.ts.map +1 -1
  9. package/dist/models/class.d.ts +14 -0
  10. package/dist/models/class.d.ts.map +1 -1
  11. package/dist/models/event.d.ts +11 -0
  12. package/dist/models/event.d.ts.map +1 -1
  13. package/dist/models/file.d.ts +19 -0
  14. package/dist/models/file.d.ts.map +1 -1
  15. package/dist/models/folder.d.ts +4 -0
  16. package/dist/models/folder.d.ts.map +1 -1
  17. package/dist/models/gradebook.d.ts +180 -0
  18. package/dist/models/gradebook.d.ts.map +1 -0
  19. package/dist/models/gradebook.js +88 -0
  20. package/dist/models/gradebook.js.map +1 -0
  21. package/dist/models/labChat.d.ts +4 -0
  22. package/dist/models/labChat.d.ts.map +1 -1
  23. package/dist/models/newtonChat.d.ts +2 -0
  24. package/dist/models/newtonChat.d.ts.map +1 -1
  25. package/dist/models/school.d.ts +177 -0
  26. package/dist/models/school.d.ts.map +1 -1
  27. package/dist/models/school.js +75 -2
  28. package/dist/models/school.js.map +1 -1
  29. package/dist/models/section.d.ts +3 -0
  30. package/dist/models/section.d.ts.map +1 -1
  31. package/dist/models/worksheet.d.ts +6 -0
  32. package/dist/models/worksheet.d.ts.map +1 -1
  33. package/dist/routers/_app.d.ts +1475 -73
  34. package/dist/routers/_app.d.ts.map +1 -1
  35. package/dist/routers/_app.js +4 -2
  36. package/dist/routers/_app.js.map +1 -1
  37. package/dist/routers/admin.d.ts +164 -0
  38. package/dist/routers/admin.d.ts.map +1 -1
  39. package/dist/routers/admin.js +23 -4
  40. package/dist/routers/admin.js.map +1 -1
  41. package/dist/routers/agenda.d.ts +6 -0
  42. package/dist/routers/agenda.d.ts.map +1 -1
  43. package/dist/routers/assignment.d.ts +30 -3
  44. package/dist/routers/assignment.d.ts.map +1 -1
  45. package/dist/routers/class.d.ts +17 -0
  46. package/dist/routers/class.d.ts.map +1 -1
  47. package/dist/routers/event.d.ts +4 -0
  48. package/dist/routers/event.d.ts.map +1 -1
  49. package/dist/routers/file.d.ts +66 -0
  50. package/dist/routers/file.d.ts.map +1 -1
  51. package/dist/routers/file.js +111 -2
  52. package/dist/routers/file.js.map +1 -1
  53. package/dist/routers/gradebook.d.ts +299 -0
  54. package/dist/routers/gradebook.d.ts.map +1 -0
  55. package/dist/routers/gradebook.js +107 -0
  56. package/dist/routers/gradebook.js.map +1 -0
  57. package/dist/routers/school.d.ts +118 -0
  58. package/dist/routers/school.d.ts.map +1 -1
  59. package/dist/routers/school.js +32 -3
  60. package/dist/routers/school.js.map +1 -1
  61. package/dist/routers/section.d.ts +1 -1
  62. package/dist/routers/worksheet.d.ts +3 -1
  63. package/dist/routers/worksheet.d.ts.map +1 -1
  64. package/dist/services/adminInstitution.d.ts +51 -0
  65. package/dist/services/adminInstitution.d.ts.map +1 -1
  66. package/dist/services/adminInstitution.js +52 -2
  67. package/dist/services/adminInstitution.js.map +1 -1
  68. package/dist/services/adminMetrics.d.ts +17 -0
  69. package/dist/services/adminMetrics.d.ts.map +1 -0
  70. package/dist/services/adminMetrics.js +37 -0
  71. package/dist/services/adminMetrics.js.map +1 -0
  72. package/dist/services/adminRunJob.d.ts +9 -0
  73. package/dist/services/adminRunJob.d.ts.map +1 -1
  74. package/dist/services/adminRunJob.js +18 -2
  75. package/dist/services/adminRunJob.js.map +1 -1
  76. package/dist/services/adminUser.d.ts +67 -0
  77. package/dist/services/adminUser.d.ts.map +1 -0
  78. package/dist/services/adminUser.js +87 -0
  79. package/dist/services/adminUser.js.map +1 -0
  80. package/dist/services/agenda.d.ts +6 -0
  81. package/dist/services/agenda.d.ts.map +1 -1
  82. package/dist/services/assignment.d.ts +27 -0
  83. package/dist/services/assignment.d.ts.map +1 -1
  84. package/dist/services/class.d.ts +17 -0
  85. package/dist/services/class.d.ts.map +1 -1
  86. package/dist/services/event.d.ts +4 -0
  87. package/dist/services/event.d.ts.map +1 -1
  88. package/dist/services/gradeCalculation.d.ts +70 -0
  89. package/dist/services/gradeCalculation.d.ts.map +1 -0
  90. package/dist/services/gradeCalculation.js +148 -0
  91. package/dist/services/gradeCalculation.js.map +1 -0
  92. package/dist/services/gradebook.d.ts +197 -0
  93. package/dist/services/gradebook.d.ts.map +1 -0
  94. package/dist/services/gradebook.js +263 -0
  95. package/dist/services/gradebook.js.map +1 -0
  96. package/dist/services/school.d.ts +98 -0
  97. package/dist/services/school.d.ts.map +1 -1
  98. package/dist/services/school.js +116 -4
  99. package/dist/services/school.js.map +1 -1
  100. package/dist/services/worksheet.d.ts +2 -0
  101. package/dist/services/worksheet.d.ts.map +1 -1
  102. package/package.json +1 -1
  103. package/prisma/migrations/20260708011629_add_gradebook_categories_columns_policies/migration.sql +78 -0
  104. package/prisma/migrations/20260723142314_make_file_annotation_submission_optional/migration.sql +2 -0
  105. package/prisma/schema.prisma +62 -2
  106. package/src/models/gradebook.ts +91 -0
  107. package/src/models/school.ts +89 -0
  108. package/src/routers/_app.ts +2 -0
  109. package/src/routers/admin.ts +28 -0
  110. package/src/routers/file.ts +128 -0
  111. package/src/routers/gradebook.ts +173 -0
  112. package/src/routers/school.ts +45 -0
  113. package/src/services/adminInstitution.ts +52 -0
  114. package/src/services/adminMetrics.ts +46 -0
  115. package/src/services/adminRunJob.ts +24 -0
  116. package/src/services/adminUser.ts +93 -0
  117. package/src/services/gradeCalculation.ts +253 -0
  118. package/src/services/gradebook.ts +383 -0
  119. package/src/services/school.ts +178 -1
  120. package/tests/unit/services/gradeCalculation.test.ts +206 -0
@@ -0,0 +1,206 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import {
3
+ computeFinalGrade,
4
+ resolveLetterGrade,
5
+ roundPercentage,
6
+ type GradeCalculationPolicies,
7
+ type GradeCategoryInput,
8
+ type ScoreInput,
9
+ } from "../../../src/services/gradeCalculation.js";
10
+
11
+ const noPolicies: GradeCalculationPolicies = {
12
+ treatMissingAsZero: false,
13
+ roundingMode: "NONE",
14
+ };
15
+
16
+ const categories: GradeCategoryInput[] = [
17
+ { id: "hw", name: "Homework", weight: 40, dropLowest: 0 },
18
+ { id: "exam", name: "Exams", weight: 60, dropLowest: 0 },
19
+ ];
20
+
21
+ describe("computeFinalGrade", () => {
22
+ it("returns null percentage with no scores", () => {
23
+ const result = computeFinalGrade(noPolicies, categories, []);
24
+ expect(result.percentage).toBeNull();
25
+ expect(result.categories).toHaveLength(2);
26
+ expect(result.categories[0].average).toBeNull();
27
+ });
28
+
29
+ it("computes weighted average of category averages", () => {
30
+ const scores: ScoreInput[] = [
31
+ { categoryId: "hw", pointsEarned: 8, maxPoints: 10 }, // 80%
32
+ { categoryId: "exam", pointsEarned: 50, maxPoints: 100 }, // 50%
33
+ ];
34
+ const result = computeFinalGrade(noPolicies, categories, scores);
35
+ // 80 * 0.4 + 50 * 0.6 = 62
36
+ expect(result.percentage).toBeCloseTo(62);
37
+ });
38
+
39
+ it("puts uncategorized scores into a default category with weight 1", () => {
40
+ const scores: ScoreInput[] = [
41
+ { categoryId: "hw", pointsEarned: 10, maxPoints: 10 }, // 100%
42
+ { categoryId: null, pointsEarned: 5, maxPoints: 10 }, // 50%
43
+ ];
44
+ const result = computeFinalGrade(noPolicies, categories, scores);
45
+ // (100 * 40 + 50 * 1) / 41
46
+ expect(result.percentage).toBeCloseTo((100 * 40 + 50) / 41);
47
+ const fallback = result.categories.find((c) => c.categoryId === null);
48
+ expect(fallback?.average).toBeCloseTo(50);
49
+ expect(fallback?.weight).toBe(1);
50
+ });
51
+
52
+ it("treats unknown category ids as uncategorized", () => {
53
+ const scores: ScoreInput[] = [
54
+ { categoryId: "deleted", pointsEarned: 5, maxPoints: 10 },
55
+ ];
56
+ const result = computeFinalGrade(noPolicies, categories, scores);
57
+ expect(
58
+ result.categories.find((c) => c.categoryId === null)?.average
59
+ ).toBeCloseTo(50);
60
+ });
61
+
62
+ it("excludes missing scores by default", () => {
63
+ const scores: ScoreInput[] = [
64
+ { categoryId: "hw", pointsEarned: 8, maxPoints: 10 },
65
+ { categoryId: "hw", pointsEarned: null, maxPoints: 10 },
66
+ ];
67
+ const result = computeFinalGrade(noPolicies, categories, scores);
68
+ expect(
69
+ result.categories.find((c) => c.categoryId === "hw")?.average
70
+ ).toBeCloseTo(80);
71
+ });
72
+
73
+ it("counts missing scores as zero when treatMissingAsZero is set", () => {
74
+ const scores: ScoreInput[] = [
75
+ { categoryId: "hw", pointsEarned: 8, maxPoints: 10 },
76
+ { categoryId: "hw", pointsEarned: null, maxPoints: 10 },
77
+ ];
78
+ const result = computeFinalGrade(
79
+ { ...noPolicies, treatMissingAsZero: true },
80
+ categories,
81
+ scores
82
+ );
83
+ expect(
84
+ result.categories.find((c) => c.categoryId === "hw")?.average
85
+ ).toBeCloseTo(40);
86
+ });
87
+
88
+ it("respects per-score weights within a category", () => {
89
+ const scores: ScoreInput[] = [
90
+ { categoryId: "hw", pointsEarned: 10, maxPoints: 10, weight: 3 }, // 100%
91
+ { categoryId: "hw", pointsEarned: 0, maxPoints: 10, weight: 1 }, // 0%
92
+ ];
93
+ const result = computeFinalGrade(noPolicies, categories, scores);
94
+ expect(
95
+ result.categories.find((c) => c.categoryId === "hw")?.average
96
+ ).toBeCloseTo(75);
97
+ });
98
+
99
+ it("drops the lowest N scores in a category", () => {
100
+ const dropCategories: GradeCategoryInput[] = [
101
+ { id: "hw", name: "Homework", weight: 1, dropLowest: 1 },
102
+ ];
103
+ const scores: ScoreInput[] = [
104
+ { categoryId: "hw", pointsEarned: 10, maxPoints: 10 }, // 100%
105
+ { categoryId: "hw", pointsEarned: 2, maxPoints: 10 }, // 20% (dropped)
106
+ { categoryId: "hw", pointsEarned: 6, maxPoints: 10 }, // 60%
107
+ ];
108
+ const result = computeFinalGrade(noPolicies, dropCategories, scores);
109
+ expect(result.percentage).toBeCloseTo(80);
110
+ expect(result.categories[0].droppedScores).toBe(1);
111
+ expect(result.categories[0].countedScores).toBe(2);
112
+ });
113
+
114
+ it("never drops every score in a category", () => {
115
+ const dropCategories: GradeCategoryInput[] = [
116
+ { id: "hw", name: "Homework", weight: 1, dropLowest: 5 },
117
+ ];
118
+ const scores: ScoreInput[] = [
119
+ { categoryId: "hw", pointsEarned: 4, maxPoints: 10 },
120
+ { categoryId: "hw", pointsEarned: 8, maxPoints: 10 },
121
+ ];
122
+ const result = computeFinalGrade(noPolicies, dropCategories, scores);
123
+ expect(result.percentage).toBeCloseTo(80);
124
+ expect(result.categories[0].droppedScores).toBe(1);
125
+ });
126
+
127
+ it("ignores scores with non-positive maxPoints", () => {
128
+ const scores: ScoreInput[] = [
129
+ { categoryId: "hw", pointsEarned: 5, maxPoints: 0 },
130
+ { categoryId: "hw", pointsEarned: 9, maxPoints: 10 },
131
+ ];
132
+ const result = computeFinalGrade(noPolicies, categories, scores);
133
+ expect(
134
+ result.categories.find((c) => c.categoryId === "hw")?.average
135
+ ).toBeCloseTo(90);
136
+ });
137
+
138
+ it("excludes empty categories from the weighted average", () => {
139
+ const scores: ScoreInput[] = [
140
+ { categoryId: "hw", pointsEarned: 9, maxPoints: 10 },
141
+ ];
142
+ const result = computeFinalGrade(noPolicies, categories, scores);
143
+ expect(result.percentage).toBeCloseTo(90);
144
+ });
145
+
146
+ it("applies the rounding mode to the final percentage", () => {
147
+ const scores: ScoreInput[] = [
148
+ { categoryId: "hw", pointsEarned: 5, maxPoints: 6 }, // 83.33%
149
+ ];
150
+ expect(
151
+ computeFinalGrade(
152
+ { ...noPolicies, roundingMode: "FLOOR" },
153
+ categories,
154
+ scores
155
+ ).percentage
156
+ ).toBe(83);
157
+ expect(
158
+ computeFinalGrade(
159
+ { ...noPolicies, roundingMode: "CEILING" },
160
+ categories,
161
+ scores
162
+ ).percentage
163
+ ).toBe(84);
164
+ expect(
165
+ computeFinalGrade(
166
+ { ...noPolicies, roundingMode: "HALF_UP" },
167
+ categories,
168
+ scores
169
+ ).percentage
170
+ ).toBe(83);
171
+ });
172
+ });
173
+
174
+ describe("roundPercentage", () => {
175
+ it("rounds according to mode", () => {
176
+ expect(roundPercentage(89.5, "HALF_UP")).toBe(90);
177
+ expect(roundPercentage(89.9, "FLOOR")).toBe(89);
178
+ expect(roundPercentage(89.1, "CEILING")).toBe(90);
179
+ expect(roundPercentage(89.123, "NONE")).toBeCloseTo(89.123);
180
+ });
181
+ });
182
+
183
+ describe("resolveLetterGrade", () => {
184
+ const structured = JSON.stringify({
185
+ boundaries: [
186
+ { grade: "A", minPercentage: 90, maxPercentage: 100 },
187
+ { grade: "B", minPercentage: 80, maxPercentage: 89 },
188
+ { grade: "C", minPercentage: 70, maxPercentage: 79 },
189
+ ],
190
+ });
191
+
192
+ it("maps percentages to letters", () => {
193
+ expect(resolveLetterGrade(structured, 95)).toBe("A");
194
+ expect(resolveLetterGrade(structured, 85)).toBe("B");
195
+ expect(resolveLetterGrade(structured, 70)).toBe("C");
196
+ });
197
+
198
+ it("returns null below all boundaries", () => {
199
+ expect(resolveLetterGrade(structured, 10)).toBeNull();
200
+ });
201
+
202
+ it("returns null for malformed payloads", () => {
203
+ expect(resolveLetterGrade("not json", 90)).toBeNull();
204
+ expect(resolveLetterGrade("{}", 90)).toBeNull();
205
+ });
206
+ });