| 1 | CREATE OR REPLACE FUNCTION fn_export_exam_results(
|
|---|
| 2 | p_exam_id integer,
|
|---|
| 3 | p_teacher_id integer
|
|---|
| 4 | )
|
|---|
| 5 | RETURNS integer
|
|---|
| 6 | LANGUAGE plpgsql AS $$
|
|---|
| 7 | DECLARE
|
|---|
| 8 | v_edition_id integer;
|
|---|
| 9 | v_finished_at timestamp;
|
|---|
| 10 | v_data jsonb;
|
|---|
| 11 | v_new_id integer;
|
|---|
| 12 | BEGIN
|
|---|
| 13 |
|
|---|
| 14 | SELECT
|
|---|
| 15 | course_edition_id,
|
|---|
| 16 | scheduled_at + (duration_minutes * INTERVAL '1 minute')
|
|---|
| 17 | INTO v_edition_id, v_finished_at
|
|---|
| 18 | FROM Exam
|
|---|
| 19 | WHERE id = p_exam_id;
|
|---|
| 20 |
|
|---|
| 21 | IF NOT FOUND THEN
|
|---|
| 22 | RAISE EXCEPTION 'Exam % does not exist.', p_exam_id;
|
|---|
| 23 | END IF;
|
|---|
| 24 |
|
|---|
| 25 |
|
|---|
| 26 | IF NOT EXISTS (
|
|---|
| 27 | SELECT 1 FROM Teaches
|
|---|
| 28 | WHERE teacher_id = p_teacher_id
|
|---|
| 29 | AND course_edition_id = v_edition_id
|
|---|
| 30 | ) THEN
|
|---|
| 31 | RAISE EXCEPTION
|
|---|
| 32 | 'Teacher % does not teach course edition %.',
|
|---|
| 33 | p_teacher_id, v_edition_id;
|
|---|
| 34 | END IF;
|
|---|
| 35 |
|
|---|
| 36 | IF v_finished_at > CURRENT_TIMESTAMP THEN
|
|---|
| 37 | RAISE EXCEPTION
|
|---|
| 38 | 'Exam % has not yet finished (ends at %).', p_exam_id, v_finished_at;
|
|---|
| 39 | END IF;
|
|---|
| 40 |
|
|---|
| 41 | IF EXISTS (
|
|---|
| 42 | SELECT 1 FROM Exam_Results
|
|---|
| 43 | WHERE exam_id = p_exam_id
|
|---|
| 44 | AND teaches_teacher_id = p_teacher_id
|
|---|
| 45 | AND teaches_course_edition_id = v_edition_id
|
|---|
| 46 | AND exported_at > CURRENT_TIMESTAMP - INTERVAL '60 seconds'
|
|---|
| 47 | ) THEN
|
|---|
| 48 | RAISE EXCEPTION
|
|---|
| 49 | 'An export for exam % by teacher % was already created within the last 60 seconds.',
|
|---|
| 50 | p_exam_id, p_teacher_id;
|
|---|
| 51 | END IF;
|
|---|
| 52 |
|
|---|
| 53 | SELECT jsonb_agg(
|
|---|
| 54 | jsonb_build_object(
|
|---|
| 55 | 'index', s.index,
|
|---|
| 56 | 'points', COALESCE(ea.total_points, 0)
|
|---|
| 57 | )
|
|---|
| 58 | ORDER BY s.index
|
|---|
| 59 | )
|
|---|
| 60 | INTO v_data
|
|---|
| 61 | FROM Course_Enrollment ce
|
|---|
| 62 | JOIN Semester_Enrollment se ON se.id = ce.semester_enrollment_id
|
|---|
| 63 | JOIN Program_Enrollment pe ON pe.id = se.program_enrollment_id
|
|---|
| 64 | JOIN Student s ON s.id = pe.student_id
|
|---|
| 65 | LEFT JOIN Exam_Attempt ea
|
|---|
| 66 | ON ea.exam_id = p_exam_id
|
|---|
| 67 | AND ea.course_enrollment_id = ce.id
|
|---|
| 68 | WHERE ce.course_edition_id = v_edition_id;
|
|---|
| 69 |
|
|---|
| 70 | IF v_data IS NULL OR jsonb_array_length(v_data) = 0 THEN
|
|---|
| 71 | RAISE EXCEPTION
|
|---|
| 72 | 'No enrolled students found for course edition %. Nothing to export.',
|
|---|
| 73 | v_edition_id;
|
|---|
| 74 | END IF;
|
|---|
| 75 |
|
|---|
| 76 |
|
|---|
| 77 | INSERT INTO Exam_Results (
|
|---|
| 78 | exported_at,
|
|---|
| 79 | teaches_teacher_id,
|
|---|
| 80 | teaches_course_edition_id,
|
|---|
| 81 | exam_id,
|
|---|
| 82 | data
|
|---|
| 83 | )
|
|---|
| 84 | VALUES (
|
|---|
| 85 | CURRENT_TIMESTAMP,
|
|---|
| 86 | p_teacher_id,
|
|---|
| 87 | v_edition_id,
|
|---|
| 88 | p_exam_id,
|
|---|
| 89 | v_data
|
|---|
| 90 | )
|
|---|
| 91 | RETURNING id INTO v_new_id;
|
|---|
| 92 |
|
|---|
| 93 | RETURN v_new_id;
|
|---|
| 94 | END;
|
|---|
| 95 | $$;
|
|---|
| 96 |
|
|---|
| 97 |
|
|---|
| 98 |
|
|---|
| 99 |
|
|---|
| 100 |
|
|---|
| 101 | CREATE OR REPLACE FUNCTION fn_export_aggregated_results(
|
|---|
| 102 | p_teacher_id integer,
|
|---|
| 103 | p_edition_id integer
|
|---|
| 104 | )
|
|---|
| 105 | RETURNS integer
|
|---|
| 106 | LANGUAGE plpgsql AS $$
|
|---|
| 107 | DECLARE
|
|---|
| 108 | v_data jsonb;
|
|---|
| 109 | v_new_id integer;
|
|---|
| 110 | v_sem_mid date;
|
|---|
| 111 | v_sem_start date;
|
|---|
| 112 | v_sem_end date;
|
|---|
| 113 | BEGIN
|
|---|
| 114 |
|
|---|
| 115 | IF NOT EXISTS (
|
|---|
| 116 | SELECT 1 FROM Teaches
|
|---|
| 117 | WHERE teacher_id = p_teacher_id AND course_edition_id = p_edition_id
|
|---|
| 118 | ) THEN
|
|---|
| 119 | RAISE EXCEPTION
|
|---|
| 120 | 'Teacher % does not teach course edition %.', p_teacher_id, p_edition_id;
|
|---|
| 121 | END IF;
|
|---|
| 122 |
|
|---|
| 123 |
|
|---|
| 124 |
|
|---|
| 125 | IF NOT EXISTS (
|
|---|
| 126 | SELECT 1
|
|---|
| 127 | FROM Exam e
|
|---|
| 128 | WHERE e.course_edition_id = p_edition_id
|
|---|
| 129 | AND e.type = 'partial_2'
|
|---|
| 130 | AND e.scheduled_at + (e.duration_minutes * INTERVAL '1 minute')
|
|---|
| 131 | <= CURRENT_TIMESTAMP
|
|---|
| 132 | ) THEN
|
|---|
| 133 | RAISE EXCEPTION
|
|---|
| 134 | 'Cannot export aggregated results for edition % — partial_2 has not yet finished.',
|
|---|
| 135 | p_edition_id;
|
|---|
| 136 | END IF;
|
|---|
| 137 |
|
|---|
| 138 |
|
|---|
| 139 | SELECT
|
|---|
| 140 | CASE WHEN ed.semester::int = 1
|
|---|
| 141 | THEN make_date(ed.academic_year, 10, 1)
|
|---|
| 142 | ELSE make_date(ed.academic_year + 1, 2, 15)
|
|---|
| 143 | END,
|
|---|
| 144 | CASE WHEN ed.semester::int = 1
|
|---|
| 145 | THEN make_date(ed.academic_year + 1, 2, 14)
|
|---|
| 146 | ELSE make_date(ed.academic_year + 1, 7, 15)
|
|---|
| 147 | END
|
|---|
| 148 | INTO v_sem_start, v_sem_end
|
|---|
| 149 | FROM Course_Edition ed
|
|---|
| 150 | WHERE ed.id = p_edition_id;
|
|---|
| 151 |
|
|---|
| 152 | v_sem_mid := v_sem_start + (v_sem_end - v_sem_start) / 2;
|
|---|
| 153 |
|
|---|
| 154 |
|
|---|
| 155 | WITH
|
|---|
| 156 |
|
|---|
| 157 |
|
|---|
| 158 | enrolled AS (
|
|---|
| 159 | SELECT DISTINCT
|
|---|
| 160 | s.id AS student_id,
|
|---|
| 161 | s.index,
|
|---|
| 162 | ce.id AS ce_id
|
|---|
| 163 | FROM Course_Enrollment ce
|
|---|
| 164 | JOIN Semester_Enrollment se ON se.id = ce.semester_enrollment_id
|
|---|
| 165 | JOIN Program_Enrollment pe ON pe.id = se.program_enrollment_id
|
|---|
| 166 | JOIN Student s ON s.id = pe.student_id
|
|---|
| 167 | WHERE ce.course_edition_id = p_edition_id
|
|---|
| 168 | ),
|
|---|
| 169 |
|
|---|
| 170 |
|
|---|
| 171 | exam_max AS (
|
|---|
| 172 | SELECT
|
|---|
| 173 | e.type,
|
|---|
| 174 | COALESCE(SUM(ep.points), 0) AS max_pts
|
|---|
| 175 | FROM Exam e
|
|---|
| 176 | JOIN Exam_Problem ep ON ep.exam_id = e.id
|
|---|
| 177 | WHERE e.course_edition_id = p_edition_id
|
|---|
| 178 | GROUP BY e.type
|
|---|
| 179 | ),
|
|---|
| 180 |
|
|---|
| 181 |
|
|---|
| 182 | p1 AS (
|
|---|
| 183 | SELECT ea.course_enrollment_id AS ce_id, ea.total_points
|
|---|
| 184 | FROM Exam_Attempt ea
|
|---|
| 185 | JOIN Exam e ON e.id = ea.exam_id
|
|---|
| 186 | WHERE e.course_edition_id = p_edition_id
|
|---|
| 187 | AND e.type = 'partial_1'
|
|---|
| 188 | ),
|
|---|
| 189 |
|
|---|
| 190 |
|
|---|
| 191 | p2 AS (
|
|---|
| 192 | SELECT ea.course_enrollment_id AS ce_id, ea.total_points
|
|---|
| 193 | FROM Exam_Attempt ea
|
|---|
| 194 | JOIN Exam e ON e.id = ea.exam_id
|
|---|
| 195 | WHERE e.course_edition_id = p_edition_id
|
|---|
| 196 | AND e.type = 'partial_2'
|
|---|
| 197 | ),
|
|---|
| 198 |
|
|---|
| 199 |
|
|---|
| 200 |
|
|---|
| 201 | sess AS (
|
|---|
| 202 | SELECT
|
|---|
| 203 | ea.course_enrollment_id AS ce_id,
|
|---|
| 204 | MAX(ea.total_points) AS total_points
|
|---|
| 205 | FROM Exam_Attempt ea
|
|---|
| 206 | JOIN Exam e ON e.id = ea.exam_id
|
|---|
| 207 | WHERE e.course_edition_id = p_edition_id
|
|---|
| 208 | AND e.type IN ('session_jan', 'session_jun', 'session_sep')
|
|---|
| 209 | AND e.scheduled_at <= CURRENT_TIMESTAMP
|
|---|
| 210 | GROUP BY ea.course_enrollment_id
|
|---|
| 211 | ),
|
|---|
| 212 |
|
|---|
| 213 |
|
|---|
| 214 | sess_max AS (
|
|---|
| 215 | SELECT MAX(max_pts) AS max_pts
|
|---|
| 216 | FROM exam_max
|
|---|
| 217 | WHERE type IN ('session_jan', 'session_jun', 'session_sep')
|
|---|
| 218 | ),
|
|---|
| 219 |
|
|---|
| 220 |
|
|---|
| 221 | lab AS (
|
|---|
| 222 | SELECT
|
|---|
| 223 | esub.course_enrollment_id AS ce_id,
|
|---|
| 224 | SUM(esub.grade) AS lab_pts
|
|---|
| 225 | FROM Exercise_Submission esub
|
|---|
| 226 | JOIN Exercise ex ON ex.id = esub.exercise_id
|
|---|
| 227 | WHERE ex.course_edition_id = p_edition_id
|
|---|
| 228 | AND ex.created_at < v_sem_mid
|
|---|
| 229 | AND esub.grade IS NOT NULL
|
|---|
| 230 | GROUP BY esub.course_enrollment_id
|
|---|
| 231 | ),
|
|---|
| 232 |
|
|---|
| 233 |
|
|---|
| 234 | lab_max AS (
|
|---|
| 235 | SELECT COALESCE(SUM(ex.max_grade), 0) AS max_pts
|
|---|
| 236 | FROM Exercise ex
|
|---|
| 237 | WHERE ex.course_edition_id = p_edition_id
|
|---|
| 238 | AND ex.created_at < v_sem_mid
|
|---|
| 239 | ),
|
|---|
| 240 |
|
|---|
| 241 |
|
|---|
| 242 | proj AS (
|
|---|
| 243 | SELECT
|
|---|
| 244 | esub.course_enrollment_id AS ce_id,
|
|---|
| 245 | SUM(esub.grade) AS proj_pts
|
|---|
| 246 | FROM Exercise_Submission esub
|
|---|
| 247 | JOIN Exercise ex ON ex.id = esub.exercise_id
|
|---|
| 248 | WHERE ex.course_edition_id = p_edition_id
|
|---|
| 249 | AND ex.created_at >= v_sem_mid
|
|---|
| 250 | AND esub.grade IS NOT NULL
|
|---|
| 251 | GROUP BY esub.course_enrollment_id
|
|---|
| 252 | ),
|
|---|
| 253 |
|
|---|
| 254 |
|
|---|
| 255 | proj_max AS (
|
|---|
| 256 | SELECT COALESCE(SUM(ex.max_grade), 0) AS max_pts
|
|---|
| 257 | FROM Exercise ex
|
|---|
| 258 | WHERE ex.course_edition_id = p_edition_id
|
|---|
| 259 | AND ex.created_at >= v_sem_mid
|
|---|
| 260 | ),
|
|---|
| 261 |
|
|---|
| 262 |
|
|---|
| 263 | total_max AS (
|
|---|
| 264 | SELECT
|
|---|
| 265 | COALESCE((SELECT max_pts FROM exam_max WHERE type = 'partial_1'), 0)
|
|---|
| 266 | + COALESCE((SELECT max_pts FROM exam_max WHERE type = 'partial_2'), 0)
|
|---|
| 267 | + COALESCE((SELECT max_pts FROM sess_max), 0)
|
|---|
| 268 | + COALESCE((SELECT max_pts FROM lab_max), 0)
|
|---|
| 269 | + COALESCE((SELECT max_pts FROM proj_max), 0)
|
|---|
| 270 | AS max_pts
|
|---|
| 271 | ),
|
|---|
| 272 |
|
|---|
| 273 |
|
|---|
| 274 | per_student AS (
|
|---|
| 275 | SELECT
|
|---|
| 276 | en.index,
|
|---|
| 277 | ROUND(COALESCE(p1.total_points, 0)::numeric, 2) AS partial1_pts,
|
|---|
| 278 | ROUND(COALESCE(p2.total_points, 0)::numeric, 2) AS partial2_pts,
|
|---|
| 279 | ROUND(COALESCE(sess.total_points, NULL)::numeric, 2) AS session_pts,
|
|---|
| 280 | ROUND(COALESCE(lab.lab_pts, 0)::numeric, 2) AS lab_pts,
|
|---|
| 281 | ROUND(COALESCE(proj.proj_pts, NULL)::numeric, 2) AS project_pts,
|
|---|
| 282 | ROUND((
|
|---|
| 283 | COALESCE(p1.total_points, 0)
|
|---|
| 284 | + COALESCE(p2.total_points, 0)
|
|---|
| 285 | + COALESCE(sess.total_points, 0)
|
|---|
| 286 | + COALESCE(lab.lab_pts, 0)
|
|---|
| 287 | + COALESCE(proj.proj_pts, 0)
|
|---|
| 288 | )::numeric, 2) AS total_pts,
|
|---|
| 289 | (SELECT max_pts FROM total_max) AS max_pts,
|
|---|
| 290 | ROUND(
|
|---|
| 291 | 100.0 * (
|
|---|
| 292 | COALESCE(p1.total_points, 0)
|
|---|
| 293 | + COALESCE(p2.total_points, 0)
|
|---|
| 294 | + COALESCE(sess.total_points, 0)
|
|---|
| 295 | + COALESCE(lab.lab_pts, 0)
|
|---|
| 296 | + COALESCE(proj.proj_pts, 0)
|
|---|
| 297 | )
|
|---|
| 298 | / NULLIF((SELECT max_pts FROM total_max), 0)
|
|---|
| 299 | , 1) AS percentage
|
|---|
| 300 | FROM enrolled en
|
|---|
| 301 | LEFT JOIN p1 ON p1.ce_id = en.ce_id
|
|---|
| 302 | LEFT JOIN p2 ON p2.ce_id = en.ce_id
|
|---|
| 303 | LEFT JOIN sess ON sess.ce_id = en.ce_id
|
|---|
| 304 | LEFT JOIN lab ON lab.ce_id = en.ce_id
|
|---|
| 305 | LEFT JOIN proj ON proj.ce_id = en.ce_id
|
|---|
| 306 | )
|
|---|
| 307 |
|
|---|
| 308 | SELECT jsonb_agg(
|
|---|
| 309 | jsonb_build_object(
|
|---|
| 310 | 'index', ps.index,
|
|---|
| 311 | 'partial1_pts', ps.partial1_pts,
|
|---|
| 312 | 'partial2_pts', ps.partial2_pts,
|
|---|
| 313 | 'session_pts', ps.session_pts,
|
|---|
| 314 | 'lab_pts', ps.lab_pts,
|
|---|
| 315 | 'project_pts', ps.project_pts,
|
|---|
| 316 | 'total_pts', ps.total_pts,
|
|---|
| 317 | 'max_pts', ps.max_pts,
|
|---|
| 318 | 'percentage', ps.percentage,
|
|---|
| 319 | 'passed', ps.percentage >= 51,
|
|---|
| 320 | 'grade', fn_percentage_to_grade(ps.percentage)
|
|---|
| 321 | )
|
|---|
| 322 | ORDER BY ps.index
|
|---|
| 323 | )
|
|---|
| 324 | INTO v_data
|
|---|
| 325 | FROM per_student ps;
|
|---|
| 326 |
|
|---|
| 327 | IF v_data IS NULL OR jsonb_array_length(v_data) = 0 THEN
|
|---|
| 328 | RAISE EXCEPTION
|
|---|
| 329 | 'No enrolled students found for edition %. Nothing to export.', p_edition_id;
|
|---|
| 330 | END IF;
|
|---|
| 331 |
|
|---|
| 332 | INSERT INTO Aggregated_Course_Edition_Results (
|
|---|
| 333 | exported_at,
|
|---|
| 334 | teaches_teacher_id,
|
|---|
| 335 | teaches_course_edition_id,
|
|---|
| 336 | data
|
|---|
| 337 | )
|
|---|
| 338 | VALUES (
|
|---|
| 339 | CURRENT_TIMESTAMP,
|
|---|
| 340 | p_teacher_id,
|
|---|
| 341 | p_edition_id,
|
|---|
| 342 | v_data
|
|---|
| 343 | )
|
|---|
| 344 | RETURNING id INTO v_new_id;
|
|---|
| 345 |
|
|---|
| 346 | RETURN v_new_id;
|
|---|
| 347 | END;
|
|---|
| 348 | $$; |
|---|