Boost Azure Demo
This commit is contained in:
6
Backend/db/embed.go
Normal file
6
Backend/db/embed.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package db
|
||||
|
||||
import "embed"
|
||||
|
||||
//go:embed migrations/*.sql
|
||||
var Migrations embed.FS
|
||||
160
Backend/db/migrations/001_init.sql
Normal file
160
Backend/db/migrations/001_init.sql
Normal file
@@ -0,0 +1,160 @@
|
||||
-- +goose Up
|
||||
CREATE TYPE user_role AS ENUM ('student', 'teacher');
|
||||
CREATE TYPE question_status AS ENUM ('draft', 'published', 'archived');
|
||||
CREATE TYPE assignment_status AS ENUM ('draft', 'assigned', 'closed');
|
||||
CREATE TYPE answer_status AS ENUM ('not_started', 'in_progress', 'submitted', 'reviewed');
|
||||
|
||||
CREATE TABLE users (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
email VARCHAR(255) NOT NULL UNIQUE,
|
||||
password_hash TEXT,
|
||||
role user_role NOT NULL,
|
||||
full_name VARCHAR(255) NOT NULL,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE classrooms (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
teacher_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
code VARCHAR(50) UNIQUE,
|
||||
description TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE classroom_students (
|
||||
classroom_id BIGINT NOT NULL REFERENCES classrooms(id) ON DELETE CASCADE,
|
||||
student_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
joined_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
PRIMARY KEY (classroom_id, student_id)
|
||||
);
|
||||
|
||||
CREATE TABLE questions (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
author_teacher_id BIGINT NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
prompt TEXT NOT NULL,
|
||||
subject VARCHAR(100),
|
||||
source TEXT,
|
||||
status question_status NOT NULL DEFAULT 'draft',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE tags (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL UNIQUE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE question_tags (
|
||||
question_id BIGINT NOT NULL REFERENCES questions(id) ON DELETE CASCADE,
|
||||
tag_id BIGINT NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (question_id, tag_id)
|
||||
);
|
||||
|
||||
CREATE TABLE assignments (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
classroom_id BIGINT NOT NULL REFERENCES classrooms(id) ON DELETE CASCADE,
|
||||
teacher_id BIGINT NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
instructions TEXT,
|
||||
status assignment_status NOT NULL DEFAULT 'draft',
|
||||
due_at TIMESTAMPTZ,
|
||||
published_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE assignment_assignees (
|
||||
assignment_id BIGINT NOT NULL REFERENCES assignments(id) ON DELETE CASCADE,
|
||||
student_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
assigned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
PRIMARY KEY (assignment_id, student_id)
|
||||
);
|
||||
|
||||
CREATE TABLE assignment_questions (
|
||||
assignment_id BIGINT NOT NULL REFERENCES assignments(id) ON DELETE CASCADE,
|
||||
question_id BIGINT NOT NULL REFERENCES questions(id) ON DELETE RESTRICT,
|
||||
position INTEGER NOT NULL,
|
||||
PRIMARY KEY (assignment_id, question_id),
|
||||
CONSTRAINT assignment_questions_assignment_position_key UNIQUE (assignment_id, position)
|
||||
);
|
||||
|
||||
CREATE TABLE student_answers (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
assignment_id BIGINT NOT NULL REFERENCES assignments(id) ON DELETE CASCADE,
|
||||
question_id BIGINT NOT NULL REFERENCES questions(id) ON DELETE RESTRICT,
|
||||
student_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
answer_text TEXT,
|
||||
ai_feedback TEXT,
|
||||
teacher_feedback TEXT,
|
||||
status answer_status NOT NULL DEFAULT 'not_started',
|
||||
submitted_at TIMESTAMPTZ,
|
||||
reviewed_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT student_answers_assignment_question_student_key UNIQUE (assignment_id, question_id, student_id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_users_role ON users(role);
|
||||
CREATE INDEX idx_classrooms_teacher_id ON classrooms(teacher_id);
|
||||
CREATE INDEX idx_questions_author_teacher_id ON questions(author_teacher_id);
|
||||
CREATE INDEX idx_questions_status ON questions(status);
|
||||
CREATE INDEX idx_assignments_classroom_id ON assignments(classroom_id);
|
||||
CREATE INDEX idx_assignments_teacher_id ON assignments(teacher_id);
|
||||
CREATE INDEX idx_assignment_assignees_student_id ON assignment_assignees(student_id);
|
||||
CREATE INDEX idx_student_answers_student_id ON student_answers(student_id);
|
||||
CREATE INDEX idx_student_answers_assignment_id ON student_answers(assignment_id);
|
||||
|
||||
-- +goose StatementBegin
|
||||
CREATE OR REPLACE FUNCTION update_updated_at()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = NOW();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
-- +goose StatementEnd
|
||||
|
||||
CREATE TRIGGER users_updated_at BEFORE UPDATE ON users
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
||||
|
||||
CREATE TRIGGER classrooms_updated_at BEFORE UPDATE ON classrooms
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
||||
|
||||
CREATE TRIGGER questions_updated_at BEFORE UPDATE ON questions
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
||||
|
||||
CREATE TRIGGER assignments_updated_at BEFORE UPDATE ON assignments
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
||||
|
||||
CREATE TRIGGER student_answers_updated_at BEFORE UPDATE ON student_answers
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
||||
|
||||
-- +goose Down
|
||||
DROP TRIGGER IF EXISTS student_answers_updated_at ON student_answers;
|
||||
DROP TRIGGER IF EXISTS assignments_updated_at ON assignments;
|
||||
DROP TRIGGER IF EXISTS questions_updated_at ON questions;
|
||||
DROP TRIGGER IF EXISTS classrooms_updated_at ON classrooms;
|
||||
DROP TRIGGER IF EXISTS users_updated_at ON users;
|
||||
DROP FUNCTION IF EXISTS update_updated_at();
|
||||
|
||||
DROP TABLE IF EXISTS student_answers;
|
||||
DROP TABLE IF EXISTS assignment_questions;
|
||||
DROP TABLE IF EXISTS assignment_assignees;
|
||||
DROP TABLE IF EXISTS assignments;
|
||||
DROP TABLE IF EXISTS question_tags;
|
||||
DROP TABLE IF EXISTS tags;
|
||||
DROP TABLE IF EXISTS questions;
|
||||
DROP TABLE IF EXISTS classroom_students;
|
||||
DROP TABLE IF EXISTS classrooms;
|
||||
DROP TABLE IF EXISTS users;
|
||||
|
||||
DROP TYPE IF EXISTS answer_status;
|
||||
DROP TYPE IF EXISTS assignment_status;
|
||||
DROP TYPE IF EXISTS question_status;
|
||||
DROP TYPE IF EXISTS user_role;
|
||||
44
Backend/db/migrations/002_profiles.sql
Normal file
44
Backend/db/migrations/002_profiles.sql
Normal file
@@ -0,0 +1,44 @@
|
||||
-- +goose Up
|
||||
CREATE TABLE profiles (
|
||||
user_id BIGINT PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
|
||||
preferred_name VARCHAR(100),
|
||||
profile_icon_url TEXT,
|
||||
headline VARCHAR(255),
|
||||
bio TEXT,
|
||||
timezone VARCHAR(100),
|
||||
locale VARCHAR(20),
|
||||
grade_level VARCHAR(100),
|
||||
learning_goal TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
INSERT INTO profiles (user_id)
|
||||
SELECT id
|
||||
FROM users
|
||||
ON CONFLICT (user_id) DO NOTHING;
|
||||
|
||||
-- +goose StatementBegin
|
||||
CREATE OR REPLACE FUNCTION ensure_profile_for_user()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
INSERT INTO profiles (user_id)
|
||||
VALUES (NEW.id)
|
||||
ON CONFLICT (user_id) DO NOTHING;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
-- +goose StatementEnd
|
||||
|
||||
CREATE TRIGGER profiles_updated_at BEFORE UPDATE ON profiles
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
||||
|
||||
CREATE TRIGGER users_ensure_profile_after_insert AFTER INSERT ON users
|
||||
FOR EACH ROW EXECUTE FUNCTION ensure_profile_for_user();
|
||||
|
||||
-- +goose Down
|
||||
DROP TRIGGER IF EXISTS users_ensure_profile_after_insert ON users;
|
||||
DROP TRIGGER IF EXISTS profiles_updated_at ON profiles;
|
||||
DROP FUNCTION IF EXISTS ensure_profile_for_user();
|
||||
DROP TABLE IF EXISTS profiles;
|
||||
48
Backend/db/migrations/003_messages.sql
Normal file
48
Backend/db/migrations/003_messages.sql
Normal file
@@ -0,0 +1,48 @@
|
||||
-- +goose Up
|
||||
CREATE TABLE message_threads (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
created_by_user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
subject VARCHAR(255) NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT message_threads_subject_not_blank CHECK (length(btrim(subject)) > 0)
|
||||
);
|
||||
|
||||
CREATE TABLE message_thread_participants (
|
||||
thread_id BIGINT NOT NULL REFERENCES message_threads(id) ON DELETE CASCADE,
|
||||
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
joined_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
last_read_at TIMESTAMPTZ,
|
||||
archived_at TIMESTAMPTZ,
|
||||
PRIMARY KEY (thread_id, user_id)
|
||||
);
|
||||
|
||||
CREATE TABLE messages (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
thread_id BIGINT NOT NULL REFERENCES message_threads(id) ON DELETE CASCADE,
|
||||
sender_user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
body TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT messages_body_not_blank CHECK (length(btrim(body)) > 0)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_message_threads_created_by_user_id ON message_threads(created_by_user_id);
|
||||
CREATE INDEX idx_message_threads_updated_at ON message_threads(updated_at DESC);
|
||||
CREATE INDEX idx_message_thread_participants_user_id ON message_thread_participants(user_id);
|
||||
CREATE INDEX idx_messages_thread_id_created_at ON messages(thread_id, created_at DESC);
|
||||
CREATE INDEX idx_messages_sender_user_id ON messages(sender_user_id);
|
||||
|
||||
CREATE TRIGGER message_threads_updated_at BEFORE UPDATE ON message_threads
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
||||
|
||||
CREATE TRIGGER messages_updated_at BEFORE UPDATE ON messages
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
||||
|
||||
-- +goose Down
|
||||
DROP TRIGGER IF EXISTS messages_updated_at ON messages;
|
||||
DROP TRIGGER IF EXISTS message_threads_updated_at ON message_threads;
|
||||
|
||||
DROP TABLE IF EXISTS messages;
|
||||
DROP TABLE IF EXISTS message_thread_participants;
|
||||
DROP TABLE IF EXISTS message_threads;
|
||||
12
Backend/db/migrations/004_student_answer_workspace.sql
Normal file
12
Backend/db/migrations/004_student_answer_workspace.sql
Normal file
@@ -0,0 +1,12 @@
|
||||
-- +goose Up
|
||||
ALTER TABLE student_answers
|
||||
ADD COLUMN solve_mode TEXT NOT NULL DEFAULT 'just_answer',
|
||||
ADD COLUMN working_steps TEXT,
|
||||
ADD CONSTRAINT student_answers_solve_mode_check
|
||||
CHECK (solve_mode IN ('just_answer', 'step_by_step', 'solve_together', 'handwritten'));
|
||||
|
||||
-- +goose Down
|
||||
ALTER TABLE student_answers
|
||||
DROP CONSTRAINT IF EXISTS student_answers_solve_mode_check,
|
||||
DROP COLUMN IF EXISTS working_steps,
|
||||
DROP COLUMN IF EXISTS solve_mode;
|
||||
@@ -0,0 +1,13 @@
|
||||
-- +goose Up
|
||||
ALTER TABLE questions
|
||||
ADD COLUMN correct_answer TEXT;
|
||||
|
||||
ALTER TABLE student_answers
|
||||
ADD COLUMN is_correct BOOLEAN;
|
||||
|
||||
-- +goose Down
|
||||
ALTER TABLE student_answers
|
||||
DROP COLUMN IF EXISTS is_correct;
|
||||
|
||||
ALTER TABLE questions
|
||||
DROP COLUMN IF EXISTS correct_answer;
|
||||
53
Backend/db/migrations/006_assignment_level_feedback.sql
Normal file
53
Backend/db/migrations/006_assignment_level_feedback.sql
Normal file
@@ -0,0 +1,53 @@
|
||||
-- +goose Up
|
||||
|
||||
ALTER TABLE assignment_assignees
|
||||
ADD COLUMN ai_feedback TEXT,
|
||||
ADD COLUMN teacher_feedback TEXT;
|
||||
|
||||
UPDATE assignment_assignees aa
|
||||
SET ai_feedback = aggregated.ai_feedback
|
||||
FROM (
|
||||
SELECT
|
||||
sa.assignment_id,
|
||||
sa.student_id,
|
||||
string_agg(
|
||||
format('Question %s: %s', aq.position, btrim(sa.ai_feedback)),
|
||||
E'\n\n'
|
||||
ORDER BY aq.position ASC
|
||||
) AS ai_feedback
|
||||
FROM student_answers sa
|
||||
JOIN assignment_questions aq
|
||||
ON aq.assignment_id = sa.assignment_id
|
||||
AND aq.question_id = sa.question_id
|
||||
WHERE NULLIF(btrim(sa.ai_feedback), '') IS NOT NULL
|
||||
GROUP BY sa.assignment_id, sa.student_id
|
||||
) AS aggregated
|
||||
WHERE aa.assignment_id = aggregated.assignment_id
|
||||
AND aa.student_id = aggregated.student_id;
|
||||
|
||||
UPDATE assignment_assignees aa
|
||||
SET teacher_feedback = aggregated.teacher_feedback
|
||||
FROM (
|
||||
SELECT
|
||||
sa.assignment_id,
|
||||
sa.student_id,
|
||||
string_agg(
|
||||
format('Question %s: %s', aq.position, btrim(sa.teacher_feedback)),
|
||||
E'\n\n'
|
||||
ORDER BY aq.position ASC
|
||||
) AS teacher_feedback
|
||||
FROM student_answers sa
|
||||
JOIN assignment_questions aq
|
||||
ON aq.assignment_id = sa.assignment_id
|
||||
AND aq.question_id = sa.question_id
|
||||
WHERE NULLIF(btrim(sa.teacher_feedback), '') IS NOT NULL
|
||||
GROUP BY sa.assignment_id, sa.student_id
|
||||
) AS aggregated
|
||||
WHERE aa.assignment_id = aggregated.assignment_id
|
||||
AND aa.student_id = aggregated.student_id;
|
||||
|
||||
-- +goose Down
|
||||
|
||||
ALTER TABLE assignment_assignees
|
||||
DROP COLUMN IF EXISTS teacher_feedback,
|
||||
DROP COLUMN IF EXISTS ai_feedback;
|
||||
66
Backend/db/migrations/007_review_contract.sql
Normal file
66
Backend/db/migrations/007_review_contract.sql
Normal file
@@ -0,0 +1,66 @@
|
||||
-- +goose Up
|
||||
|
||||
CREATE TYPE assignment_pass_status AS ENUM ('pending', 'pass', 'no_pass');
|
||||
|
||||
ALTER TABLE student_answers
|
||||
ADD COLUMN review_needs_attention BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
ADD COLUMN review_issue_reason TEXT,
|
||||
ADD COLUMN review_correctness_score NUMERIC(4,3),
|
||||
ADD COLUMN review_understanding_score NUMERIC(4,3),
|
||||
ADD COLUMN review_question_score NUMERIC(4,3),
|
||||
ADD COLUMN review_confidence NUMERIC(4,3),
|
||||
ADD COLUMN review_tags TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
|
||||
ADD CONSTRAINT student_answers_review_correctness_score_range_check
|
||||
CHECK (review_correctness_score IS NULL OR (review_correctness_score >= 0 AND review_correctness_score <= 1)),
|
||||
ADD CONSTRAINT student_answers_review_understanding_score_range_check
|
||||
CHECK (review_understanding_score IS NULL OR (review_understanding_score >= 0 AND review_understanding_score <= 1)),
|
||||
ADD CONSTRAINT student_answers_review_question_score_range_check
|
||||
CHECK (review_question_score IS NULL OR (review_question_score >= 0 AND review_question_score <= 1)),
|
||||
ADD CONSTRAINT student_answers_review_confidence_range_check
|
||||
CHECK (review_confidence IS NULL OR (review_confidence >= 0 AND review_confidence <= 1));
|
||||
|
||||
UPDATE student_answers
|
||||
SET review_correctness_score = CASE
|
||||
WHEN is_correct IS TRUE THEN 1.000
|
||||
WHEN is_correct IS FALSE THEN 0.000
|
||||
ELSE NULL
|
||||
END,
|
||||
review_question_score = CASE
|
||||
WHEN is_correct IS TRUE THEN 1.000
|
||||
WHEN is_correct IS FALSE THEN 0.000
|
||||
ELSE NULL
|
||||
END
|
||||
WHERE is_correct IS NOT NULL;
|
||||
|
||||
ALTER TABLE assignment_assignees
|
||||
ADD COLUMN overall_score NUMERIC(5,2),
|
||||
ADD COLUMN pass_threshold NUMERIC(5,2) NOT NULL DEFAULT 8.00,
|
||||
ADD COLUMN pass_status assignment_pass_status NOT NULL DEFAULT 'pending',
|
||||
ADD CONSTRAINT assignment_assignees_overall_score_range_check
|
||||
CHECK (overall_score IS NULL OR (overall_score >= 0 AND overall_score <= 10)),
|
||||
ADD CONSTRAINT assignment_assignees_pass_threshold_range_check
|
||||
CHECK (pass_threshold >= 0 AND pass_threshold <= 10);
|
||||
|
||||
-- +goose Down
|
||||
|
||||
ALTER TABLE assignment_assignees
|
||||
DROP CONSTRAINT IF EXISTS assignment_assignees_pass_threshold_range_check,
|
||||
DROP CONSTRAINT IF EXISTS assignment_assignees_overall_score_range_check,
|
||||
DROP COLUMN IF EXISTS pass_status,
|
||||
DROP COLUMN IF EXISTS pass_threshold,
|
||||
DROP COLUMN IF EXISTS overall_score;
|
||||
|
||||
ALTER TABLE student_answers
|
||||
DROP CONSTRAINT IF EXISTS student_answers_review_confidence_range_check,
|
||||
DROP CONSTRAINT IF EXISTS student_answers_review_question_score_range_check,
|
||||
DROP CONSTRAINT IF EXISTS student_answers_review_understanding_score_range_check,
|
||||
DROP CONSTRAINT IF EXISTS student_answers_review_correctness_score_range_check,
|
||||
DROP COLUMN IF EXISTS review_tags,
|
||||
DROP COLUMN IF EXISTS review_confidence,
|
||||
DROP COLUMN IF EXISTS review_question_score,
|
||||
DROP COLUMN IF EXISTS review_understanding_score,
|
||||
DROP COLUMN IF EXISTS review_correctness_score,
|
||||
DROP COLUMN IF EXISTS review_issue_reason,
|
||||
DROP COLUMN IF EXISTS review_needs_attention;
|
||||
|
||||
DROP TYPE IF EXISTS assignment_pass_status;
|
||||
22
Backend/db/migrations/008_assignment_pass_threshold.sql
Normal file
22
Backend/db/migrations/008_assignment_pass_threshold.sql
Normal file
@@ -0,0 +1,22 @@
|
||||
-- +goose Up
|
||||
|
||||
ALTER TABLE assignments
|
||||
ADD COLUMN pass_threshold NUMERIC(5,2) NOT NULL DEFAULT 8.00,
|
||||
ADD CONSTRAINT assignments_pass_threshold_range_check
|
||||
CHECK (pass_threshold >= 0 AND pass_threshold <= 10);
|
||||
|
||||
UPDATE assignments a
|
||||
SET pass_threshold = COALESCE(
|
||||
(
|
||||
SELECT MAX(aa.pass_threshold)
|
||||
FROM assignment_assignees aa
|
||||
WHERE aa.assignment_id = a.id
|
||||
),
|
||||
8.00
|
||||
);
|
||||
|
||||
-- +goose Down
|
||||
|
||||
ALTER TABLE assignments
|
||||
DROP CONSTRAINT IF EXISTS assignments_pass_threshold_range_check,
|
||||
DROP COLUMN IF EXISTS pass_threshold;
|
||||
@@ -0,0 +1,9 @@
|
||||
-- +goose Up
|
||||
|
||||
ALTER TABLE assignment_assignees
|
||||
ADD COLUMN pass_status_override assignment_pass_status;
|
||||
|
||||
-- +goose Down
|
||||
|
||||
ALTER TABLE assignment_assignees
|
||||
DROP COLUMN IF EXISTS pass_status_override;
|
||||
13
Backend/db/migrations/010_assignment_next_step_outcome.sql
Normal file
13
Backend/db/migrations/010_assignment_next_step_outcome.sql
Normal file
@@ -0,0 +1,13 @@
|
||||
-- +goose Up
|
||||
|
||||
CREATE TYPE assignment_next_step_outcome AS ENUM ('redo', 'accept', 'support');
|
||||
|
||||
ALTER TABLE assignment_assignees
|
||||
ADD COLUMN next_step_outcome assignment_next_step_outcome;
|
||||
|
||||
-- +goose Down
|
||||
|
||||
ALTER TABLE assignment_assignees
|
||||
DROP COLUMN IF EXISTS next_step_outcome;
|
||||
|
||||
DROP TYPE IF EXISTS assignment_next_step_outcome;
|
||||
@@ -0,0 +1,80 @@
|
||||
-- +goose Up
|
||||
|
||||
UPDATE questions AS q
|
||||
SET correct_answer = seeded.correct_answer
|
||||
FROM (
|
||||
VALUES
|
||||
(1001, '700'),
|
||||
(1002, '25000'),
|
||||
(1003, '7/100'),
|
||||
(1004, '0.37'),
|
||||
(1101, '383'),
|
||||
(1102, '456'),
|
||||
(1103, '2627'),
|
||||
(1104, '196'),
|
||||
(1105, '24'),
|
||||
(1106, '3744'),
|
||||
(1201, '3'),
|
||||
(1202, '-5'),
|
||||
(1203, '-4'),
|
||||
(1301, '11'),
|
||||
(1302, '22'),
|
||||
(1303, '19'),
|
||||
(1401, '1/2'),
|
||||
(1402, '2/3'),
|
||||
(1403, '5/8'),
|
||||
(1411, '5/6'),
|
||||
(1412, '3/4'),
|
||||
(1413, '11/15'),
|
||||
(1414, '11/12'),
|
||||
(1415, '1/2'),
|
||||
(1416, '29/24'),
|
||||
(1421, '1/6'),
|
||||
(1422, '1/2'),
|
||||
(1423, '28'),
|
||||
(1501, '5'),
|
||||
(1502, '7'),
|
||||
(1503, '6'),
|
||||
(1511, '14'),
|
||||
(1512, '31'),
|
||||
(1513, '3n+2'),
|
||||
(1601, '28'),
|
||||
(1602, '40'),
|
||||
(1603, '27'),
|
||||
(1611, '70'),
|
||||
(1612, '75'),
|
||||
(1613, '720'),
|
||||
(1701, '8'),
|
||||
(1702, '5'),
|
||||
(1703, '7'),
|
||||
(1711, '3/8'),
|
||||
(1712, '1/3'),
|
||||
(1801, '15'),
|
||||
(1802, '3/8'),
|
||||
(1803, '51'),
|
||||
(1804, '23'),
|
||||
(1805, '96')
|
||||
) AS seeded(id, correct_answer)
|
||||
WHERE q.id = seeded.id
|
||||
AND (q.correct_answer IS NULL OR BTRIM(q.correct_answer) = '');
|
||||
|
||||
-- +goose Down
|
||||
|
||||
UPDATE questions
|
||||
SET correct_answer = NULL
|
||||
WHERE id IN (
|
||||
1001, 1002, 1003, 1004,
|
||||
1101, 1102, 1103, 1104, 1105, 1106,
|
||||
1201, 1202, 1203,
|
||||
1301, 1302, 1303,
|
||||
1401, 1402, 1403,
|
||||
1411, 1412, 1413, 1414, 1415, 1416,
|
||||
1421, 1422, 1423,
|
||||
1501, 1502, 1503,
|
||||
1511, 1512, 1513,
|
||||
1601, 1602, 1603,
|
||||
1611, 1612, 1613,
|
||||
1701, 1702, 1703,
|
||||
1711, 1712,
|
||||
1801, 1802, 1803, 1804, 1805
|
||||
);
|
||||
@@ -0,0 +1,123 @@
|
||||
-- +goose Up
|
||||
|
||||
CREATE TYPE question_topic AS ENUM (
|
||||
'place_value',
|
||||
'arithmetic',
|
||||
'negative_numbers',
|
||||
'bidmas',
|
||||
'fractions',
|
||||
'algebra',
|
||||
'geometry',
|
||||
'data'
|
||||
);
|
||||
|
||||
CREATE TYPE question_difficulty AS ENUM ('easy', 'medium', 'hard');
|
||||
|
||||
ALTER TABLE questions
|
||||
ADD COLUMN topic question_topic,
|
||||
ADD COLUMN difficulty question_difficulty;
|
||||
|
||||
UPDATE questions
|
||||
SET topic = CASE BTRIM(COALESCE(subject, ''))
|
||||
WHEN 'Place Value' THEN 'place_value'::question_topic
|
||||
WHEN 'Arithmetic' THEN 'arithmetic'::question_topic
|
||||
WHEN 'Negative Numbers' THEN 'negative_numbers'::question_topic
|
||||
WHEN 'BIDMAS' THEN 'bidmas'::question_topic
|
||||
WHEN 'Fractions' THEN 'fractions'::question_topic
|
||||
WHEN 'Algebra' THEN 'algebra'::question_topic
|
||||
WHEN 'Geometry' THEN 'geometry'::question_topic
|
||||
WHEN 'Data' THEN 'data'::question_topic
|
||||
ELSE NULL
|
||||
END
|
||||
WHERE topic IS NULL;
|
||||
|
||||
UPDATE questions AS q
|
||||
SET difficulty = seeded.difficulty::question_difficulty
|
||||
FROM (
|
||||
VALUES
|
||||
(1001, 'easy'),
|
||||
(1002, 'medium'),
|
||||
(1003, 'medium'),
|
||||
(1004, 'hard'),
|
||||
(1101, 'easy'),
|
||||
(1102, 'medium'),
|
||||
(1103, 'hard'),
|
||||
(1104, 'medium'),
|
||||
(1105, 'medium'),
|
||||
(1106, 'hard'),
|
||||
(1201, 'easy'),
|
||||
(1202, 'medium'),
|
||||
(1203, 'hard'),
|
||||
(1301, 'easy'),
|
||||
(1302, 'medium'),
|
||||
(1303, 'hard'),
|
||||
(1401, 'easy'),
|
||||
(1402, 'medium'),
|
||||
(1403, 'hard'),
|
||||
(1411, 'easy'),
|
||||
(1412, 'easy'),
|
||||
(1413, 'medium'),
|
||||
(1414, 'medium'),
|
||||
(1415, 'medium'),
|
||||
(1416, 'hard'),
|
||||
(1421, 'easy'),
|
||||
(1422, 'medium'),
|
||||
(1423, 'hard'),
|
||||
(1501, 'easy'),
|
||||
(1502, 'medium'),
|
||||
(1503, 'hard'),
|
||||
(1511, 'easy'),
|
||||
(1512, 'medium'),
|
||||
(1513, 'hard'),
|
||||
(1601, 'easy'),
|
||||
(1602, 'medium'),
|
||||
(1603, 'hard'),
|
||||
(1611, 'easy'),
|
||||
(1612, 'medium'),
|
||||
(1613, 'hard'),
|
||||
(1701, 'easy'),
|
||||
(1702, 'medium'),
|
||||
(1703, 'easy'),
|
||||
(1711, 'medium'),
|
||||
(1712, 'hard'),
|
||||
(1801, 'easy'),
|
||||
(1802, 'medium'),
|
||||
(1803, 'medium'),
|
||||
(1804, 'hard'),
|
||||
(1805, 'hard')
|
||||
) AS seeded(id, difficulty)
|
||||
WHERE q.id = seeded.id
|
||||
AND q.difficulty IS NULL;
|
||||
|
||||
ALTER TABLE assignments
|
||||
ALTER COLUMN pass_threshold SET DEFAULT 6.00;
|
||||
|
||||
UPDATE assignments
|
||||
SET pass_threshold = 6.00;
|
||||
|
||||
ALTER TABLE assignment_assignees
|
||||
ALTER COLUMN pass_threshold SET DEFAULT 6.00;
|
||||
|
||||
UPDATE assignment_assignees
|
||||
SET pass_threshold = 6.00;
|
||||
|
||||
-- +goose Down
|
||||
|
||||
UPDATE assignment_assignees
|
||||
SET pass_threshold = 8.00;
|
||||
|
||||
ALTER TABLE assignment_assignees
|
||||
ALTER COLUMN pass_threshold SET DEFAULT 8.00;
|
||||
|
||||
UPDATE assignments
|
||||
SET pass_threshold = 8.00;
|
||||
|
||||
ALTER TABLE assignments
|
||||
ALTER COLUMN pass_threshold SET DEFAULT 8.00;
|
||||
|
||||
ALTER TABLE questions
|
||||
DROP COLUMN IF EXISTS difficulty,
|
||||
DROP COLUMN IF EXISTS topic;
|
||||
|
||||
DROP TYPE IF EXISTS question_difficulty;
|
||||
DROP TYPE IF EXISTS question_topic;
|
||||
11
Backend/db/migrations/013_redo_assignment_plan.sql
Normal file
11
Backend/db/migrations/013_redo_assignment_plan.sql
Normal file
@@ -0,0 +1,11 @@
|
||||
-- +goose Up
|
||||
|
||||
ALTER TABLE assignment_assignees
|
||||
ADD COLUMN redo_plan TEXT,
|
||||
ADD COLUMN redo_plan_generated_at TIMESTAMPTZ;
|
||||
|
||||
-- +goose Down
|
||||
|
||||
ALTER TABLE assignment_assignees
|
||||
DROP COLUMN IF EXISTS redo_plan_generated_at,
|
||||
DROP COLUMN IF EXISTS redo_plan;
|
||||
30
Backend/db/migrations/014_assignment_student_questions.sql
Normal file
30
Backend/db/migrations/014_assignment_student_questions.sql
Normal file
@@ -0,0 +1,30 @@
|
||||
-- +goose Up
|
||||
|
||||
CREATE TABLE assignment_student_questions (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
assignment_id BIGINT NOT NULL,
|
||||
student_id BIGINT NOT NULL,
|
||||
question_id BIGINT NOT NULL REFERENCES questions(id) ON DELETE CASCADE,
|
||||
position INTEGER NOT NULL CHECK (position > 0),
|
||||
source_bucket TEXT NOT NULL CHECK (btrim(source_bucket) <> ''),
|
||||
source_topic question_topic,
|
||||
source_difficulty question_difficulty,
|
||||
generator_seed BIGINT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT assignment_student_questions_assignment_student_fkey
|
||||
FOREIGN KEY (assignment_id, student_id)
|
||||
REFERENCES assignment_assignees(assignment_id, student_id)
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT assignment_student_questions_assignment_student_question_key
|
||||
UNIQUE (assignment_id, student_id, question_id),
|
||||
CONSTRAINT assignment_student_questions_assignment_student_position_key
|
||||
UNIQUE (assignment_id, student_id, position)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_assignment_student_questions_assignment_student
|
||||
ON assignment_student_questions (assignment_id, student_id);
|
||||
|
||||
-- +goose Down
|
||||
|
||||
DROP INDEX IF EXISTS idx_assignment_student_questions_assignment_student;
|
||||
DROP TABLE IF EXISTS assignment_student_questions;
|
||||
391
Backend/db/queries/assignments.sql
Normal file
391
Backend/db/queries/assignments.sql
Normal file
@@ -0,0 +1,391 @@
|
||||
-- name: CreateAssignment :one
|
||||
INSERT INTO assignments (
|
||||
classroom_id,
|
||||
teacher_id,
|
||||
title,
|
||||
instructions,
|
||||
status,
|
||||
due_at,
|
||||
published_at,
|
||||
pass_threshold
|
||||
) VALUES (
|
||||
$1,
|
||||
$2,
|
||||
$3,
|
||||
$4,
|
||||
$5,
|
||||
$6,
|
||||
$7,
|
||||
$8
|
||||
)
|
||||
RETURNING *;
|
||||
|
||||
-- name: AssignStudentToAssignment :exec
|
||||
INSERT INTO assignment_assignees (
|
||||
assignment_id,
|
||||
student_id
|
||||
) VALUES (
|
||||
$1,
|
||||
$2
|
||||
)
|
||||
ON CONFLICT (assignment_id, student_id) DO NOTHING;
|
||||
|
||||
-- name: DeleteAssignmentAssignee :exec
|
||||
DELETE FROM assignment_assignees
|
||||
WHERE assignment_id = $1
|
||||
AND student_id = $2;
|
||||
|
||||
-- name: GetAssignmentAssignee :one
|
||||
SELECT *
|
||||
FROM assignment_assignees
|
||||
WHERE assignment_id = $1
|
||||
AND student_id = $2;
|
||||
|
||||
-- name: UpdateAssignmentAIReview :one
|
||||
UPDATE assignment_assignees
|
||||
SET ai_feedback = $3,
|
||||
next_step_outcome = NULLIF($4::text, '')::assignment_next_step_outcome
|
||||
WHERE assignment_id = $1
|
||||
AND student_id = $2
|
||||
RETURNING *;
|
||||
|
||||
-- name: UpdateAssignmentRedoPlan :one
|
||||
UPDATE assignment_assignees
|
||||
SET redo_plan = NULLIF($3::text, ''),
|
||||
redo_plan_generated_at = CASE
|
||||
WHEN NULLIF($3::text, '') IS NULL THEN NULL
|
||||
ELSE NOW()
|
||||
END
|
||||
WHERE assignment_id = $1
|
||||
AND student_id = $2
|
||||
RETURNING *;
|
||||
|
||||
-- name: GetAssignmentRedoPlan :one
|
||||
SELECT
|
||||
assignment_id,
|
||||
student_id,
|
||||
redo_plan,
|
||||
redo_plan_generated_at
|
||||
FROM assignment_assignees
|
||||
WHERE assignment_id = $1
|
||||
AND student_id = $2;
|
||||
|
||||
-- name: UpdateAssignmentTeacherFeedback :one
|
||||
WITH student_question_set AS (
|
||||
SELECT asq.assignment_id, asq.question_id, asq.position
|
||||
FROM assignment_student_questions asq
|
||||
WHERE asq.assignment_id = $1
|
||||
AND asq.student_id = $2
|
||||
), selected_questions AS (
|
||||
SELECT assignment_id, question_id, position
|
||||
FROM student_question_set
|
||||
UNION ALL
|
||||
SELECT aq.assignment_id, aq.question_id, aq.position
|
||||
FROM assignment_questions aq
|
||||
WHERE aq.assignment_id = $1
|
||||
AND NOT EXISTS (SELECT 1 FROM student_question_set)
|
||||
), score_summary AS (
|
||||
SELECT CASE
|
||||
WHEN COUNT(sa.id) = 0 THEN NULL
|
||||
ELSE ROUND((AVG(
|
||||
CASE
|
||||
WHEN sa.is_correct IS NULL THEN COALESCE(sa.review_understanding_score, 0)::NUMERIC
|
||||
ELSE (
|
||||
((CASE WHEN sa.is_correct THEN 1 ELSE 0 END)::NUMERIC) + COALESCE(sa.review_understanding_score, 0)::NUMERIC
|
||||
) / 2
|
||||
END
|
||||
) * 10)::NUMERIC, 2)
|
||||
END AS overall_score
|
||||
FROM selected_questions aq
|
||||
LEFT JOIN student_answers sa
|
||||
ON sa.assignment_id = aq.assignment_id
|
||||
AND sa.question_id = aq.question_id
|
||||
AND sa.student_id = $2
|
||||
WHERE aq.assignment_id = $1
|
||||
), updated AS (
|
||||
UPDATE assignment_assignees aa
|
||||
SET teacher_feedback = $3,
|
||||
pass_status_override = NULLIF($4::text, '')::assignment_pass_status,
|
||||
next_step_outcome = NULLIF($5::text, '')::assignment_next_step_outcome,
|
||||
overall_score = (SELECT overall_score FROM score_summary),
|
||||
pass_status = COALESCE(
|
||||
NULLIF($4::text, '')::assignment_pass_status,
|
||||
CASE
|
||||
WHEN (SELECT overall_score FROM score_summary) IS NULL THEN 'pending'::assignment_pass_status
|
||||
WHEN (SELECT overall_score FROM score_summary) >= a.pass_threshold THEN 'pass'::assignment_pass_status
|
||||
ELSE 'no_pass'::assignment_pass_status
|
||||
END
|
||||
)
|
||||
FROM assignments a
|
||||
WHERE aa.assignment_id = $1
|
||||
AND aa.student_id = $2
|
||||
AND a.id = aa.assignment_id
|
||||
RETURNING aa.*
|
||||
)
|
||||
SELECT *
|
||||
FROM updated;
|
||||
|
||||
-- name: AddQuestionToAssignment :exec
|
||||
INSERT INTO assignment_questions (
|
||||
assignment_id,
|
||||
question_id,
|
||||
position
|
||||
) VALUES (
|
||||
$1,
|
||||
$2,
|
||||
$3
|
||||
)
|
||||
ON CONFLICT (assignment_id, question_id) DO UPDATE
|
||||
SET position = EXCLUDED.position;
|
||||
|
||||
-- name: DeleteAssignmentStudentQuestions :exec
|
||||
DELETE FROM assignment_student_questions
|
||||
WHERE assignment_id = $1
|
||||
AND student_id = $2;
|
||||
|
||||
-- name: AddAssignmentStudentQuestion :one
|
||||
INSERT INTO assignment_student_questions (
|
||||
assignment_id,
|
||||
student_id,
|
||||
question_id,
|
||||
position,
|
||||
source_bucket,
|
||||
source_topic,
|
||||
source_difficulty,
|
||||
generator_seed
|
||||
) VALUES (
|
||||
$1,
|
||||
$2,
|
||||
$3,
|
||||
$4,
|
||||
$5,
|
||||
$6,
|
||||
$7,
|
||||
$8
|
||||
)
|
||||
RETURNING *;
|
||||
|
||||
-- name: ListAssignmentStudentQuestions :many
|
||||
SELECT *
|
||||
FROM assignment_student_questions
|
||||
WHERE assignment_id = $1
|
||||
AND student_id = $2
|
||||
ORDER BY position ASC, id ASC;
|
||||
|
||||
-- name: ListGeneratedQuestionsForAssignmentStudent :many
|
||||
SELECT
|
||||
asq.id,
|
||||
asq.assignment_id,
|
||||
asq.student_id,
|
||||
asq.question_id,
|
||||
asq.position,
|
||||
asq.source_bucket,
|
||||
asq.source_topic,
|
||||
asq.source_difficulty,
|
||||
asq.generator_seed,
|
||||
asq.created_at,
|
||||
q.author_teacher_id,
|
||||
q.title,
|
||||
q.prompt,
|
||||
q.subject,
|
||||
q.source,
|
||||
q.status,
|
||||
q.created_at AS question_created_at,
|
||||
q.updated_at AS question_updated_at,
|
||||
q.correct_answer,
|
||||
q.topic,
|
||||
q.difficulty
|
||||
FROM assignment_student_questions asq
|
||||
JOIN questions q ON q.id = asq.question_id
|
||||
WHERE asq.assignment_id = $1
|
||||
AND asq.student_id = $2
|
||||
ORDER BY asq.position ASC, asq.id ASC;
|
||||
|
||||
-- name: ListAssignmentsByTeacher :many
|
||||
SELECT *
|
||||
FROM assignments
|
||||
WHERE teacher_id = $1
|
||||
ORDER BY created_at DESC;
|
||||
|
||||
-- name: ListAssignmentsForStudent :many
|
||||
SELECT a.*
|
||||
FROM assignment_assignees aa
|
||||
JOIN assignments a ON a.id = aa.assignment_id
|
||||
WHERE aa.student_id = $1
|
||||
ORDER BY a.created_at DESC;
|
||||
|
||||
-- name: GetAssignmentByID :one
|
||||
SELECT *
|
||||
FROM assignments
|
||||
WHERE id = $1;
|
||||
|
||||
-- name: UpdateAssignmentDraft :one
|
||||
UPDATE assignments
|
||||
SET classroom_id = $2,
|
||||
title = $3,
|
||||
instructions = $4,
|
||||
due_at = $5,
|
||||
pass_threshold = $6,
|
||||
updated_at = NOW()
|
||||
WHERE id = $1
|
||||
RETURNING *;
|
||||
|
||||
-- name: CloseAssignment :one
|
||||
UPDATE assignments
|
||||
SET status = 'closed'::assignment_status,
|
||||
updated_at = NOW()
|
||||
WHERE id = $1
|
||||
RETURNING *;
|
||||
|
||||
-- name: ListQuestionsForAssignment :many
|
||||
SELECT
|
||||
aq.assignment_id,
|
||||
aq.question_id,
|
||||
aq.position,
|
||||
q.author_teacher_id,
|
||||
q.title,
|
||||
q.prompt,
|
||||
q.subject,
|
||||
q.source,
|
||||
q.status,
|
||||
q.created_at,
|
||||
q.updated_at
|
||||
FROM assignment_questions aq
|
||||
JOIN questions q ON q.id = aq.question_id
|
||||
WHERE aq.assignment_id = $1
|
||||
ORDER BY aq.position ASC, aq.question_id ASC;
|
||||
|
||||
-- name: GetAssignmentReviewSummary :one
|
||||
WITH student_question_set AS (
|
||||
SELECT asq.student_id, asq.question_id, asq.position
|
||||
FROM assignment_student_questions asq
|
||||
WHERE asq.assignment_id = $1
|
||||
),
|
||||
students_with_personalized AS (
|
||||
SELECT DISTINCT student_id
|
||||
FROM student_question_set
|
||||
),
|
||||
selected_questions AS (
|
||||
SELECT student_id, question_id, position
|
||||
FROM student_question_set
|
||||
UNION ALL
|
||||
SELECT aa.student_id, aq.question_id, aq.position
|
||||
FROM assignment_assignees aa
|
||||
JOIN assignment_questions aq ON aq.assignment_id = aa.assignment_id
|
||||
WHERE aa.assignment_id = $1
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM students_with_personalized swp
|
||||
WHERE swp.student_id = aa.student_id
|
||||
)
|
||||
),
|
||||
student_states AS (
|
||||
SELECT
|
||||
aa.student_id,
|
||||
COUNT(sq.question_id)::BIGINT AS total_questions,
|
||||
CASE
|
||||
WHEN COUNT(sa.id) = 0 THEN 'not_started'::answer_status
|
||||
WHEN COUNT(sq.question_id) > 0
|
||||
AND COUNT(sa.id) FILTER (WHERE sa.status = 'reviewed') = COUNT(sq.question_id)
|
||||
THEN 'reviewed'::answer_status
|
||||
WHEN COUNT(sa.id) FILTER (WHERE sa.status = 'submitted') > 0 THEN 'submitted'::answer_status
|
||||
WHEN COUNT(sa.id) FILTER (WHERE sa.status = 'in_progress') > 0 THEN 'in_progress'::answer_status
|
||||
WHEN COUNT(sa.id) FILTER (WHERE sa.status = 'reviewed') > 0 THEN 'in_progress'::answer_status
|
||||
ELSE 'not_started'::answer_status
|
||||
END AS review_status
|
||||
FROM assignment_assignees aa
|
||||
LEFT JOIN selected_questions sq
|
||||
ON sq.student_id = aa.student_id
|
||||
LEFT JOIN student_answers sa
|
||||
ON sa.assignment_id = aa.assignment_id
|
||||
AND sa.question_id = sq.question_id
|
||||
AND sa.student_id = aa.student_id
|
||||
WHERE aa.assignment_id = $1
|
||||
GROUP BY aa.student_id
|
||||
)
|
||||
SELECT
|
||||
$1::BIGINT AS assignment_id,
|
||||
COALESCE(MAX(student_states.total_questions), 0)::BIGINT AS total_questions,
|
||||
COUNT(*)::BIGINT AS total_assigned,
|
||||
COUNT(*) FILTER (WHERE review_status = 'not_started')::BIGINT AS not_started,
|
||||
COUNT(*) FILTER (WHERE review_status = 'in_progress')::BIGINT AS in_progress,
|
||||
COUNT(*) FILTER (WHERE review_status = 'submitted')::BIGINT AS submitted,
|
||||
COUNT(*) FILTER (WHERE review_status = 'reviewed')::BIGINT AS reviewed
|
||||
FROM student_states;
|
||||
|
||||
-- name: ListAssignmentReviewQueue :many
|
||||
WITH student_question_set AS (
|
||||
SELECT asq.student_id, asq.question_id, asq.position
|
||||
FROM assignment_student_questions asq
|
||||
WHERE asq.assignment_id = $1
|
||||
),
|
||||
students_with_personalized AS (
|
||||
SELECT DISTINCT student_id
|
||||
FROM student_question_set
|
||||
),
|
||||
selected_questions AS (
|
||||
SELECT student_id, question_id, position
|
||||
FROM student_question_set
|
||||
UNION ALL
|
||||
SELECT aa.student_id, aq.question_id, aq.position
|
||||
FROM assignment_assignees aa
|
||||
JOIN assignment_questions aq ON aq.assignment_id = aa.assignment_id
|
||||
WHERE aa.assignment_id = $1
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM students_with_personalized swp
|
||||
WHERE swp.student_id = aa.student_id
|
||||
)
|
||||
),
|
||||
student_states AS (
|
||||
SELECT
|
||||
aa.assignment_id,
|
||||
aa.student_id,
|
||||
aa.next_step_outcome,
|
||||
u.full_name AS student_name,
|
||||
u.email AS student_email,
|
||||
COUNT(sq.question_id)::BIGINT AS total_questions,
|
||||
COUNT(sa.id)::BIGINT AS answered_questions,
|
||||
COUNT(sa.id) FILTER (WHERE sa.status = 'reviewed')::BIGINT AS reviewed_questions,
|
||||
COUNT(sa.id) FILTER (WHERE sa.status = 'submitted')::BIGINT AS submitted_questions,
|
||||
COUNT(sa.id) FILTER (WHERE sa.status = 'in_progress')::BIGINT AS in_progress_questions,
|
||||
MAX(sa.submitted_at)::timestamptz AS latest_submitted_at,
|
||||
MAX(sa.reviewed_at)::timestamptz AS latest_reviewed_at,
|
||||
CASE
|
||||
WHEN COUNT(sa.id) = 0 THEN 'not_started'::answer_status
|
||||
WHEN COUNT(sq.question_id) > 0
|
||||
AND COUNT(sa.id) FILTER (WHERE sa.status = 'reviewed') = COUNT(sq.question_id)
|
||||
THEN 'reviewed'::answer_status
|
||||
WHEN COUNT(sa.id) FILTER (WHERE sa.status = 'submitted') > 0 THEN 'submitted'::answer_status
|
||||
WHEN COUNT(sa.id) FILTER (WHERE sa.status = 'in_progress') > 0 THEN 'in_progress'::answer_status
|
||||
WHEN COUNT(sa.id) FILTER (WHERE sa.status = 'reviewed') > 0 THEN 'in_progress'::answer_status
|
||||
ELSE 'not_started'::answer_status
|
||||
END AS review_status
|
||||
FROM assignment_assignees aa
|
||||
JOIN users u ON u.id = aa.student_id
|
||||
LEFT JOIN selected_questions sq
|
||||
ON sq.student_id = aa.student_id
|
||||
LEFT JOIN student_answers sa
|
||||
ON sa.assignment_id = aa.assignment_id
|
||||
AND sa.question_id = sq.question_id
|
||||
AND sa.student_id = aa.student_id
|
||||
WHERE aa.assignment_id = $1
|
||||
GROUP BY aa.assignment_id, aa.student_id, aa.next_step_outcome, u.full_name, u.email
|
||||
)
|
||||
SELECT
|
||||
student_states.assignment_id,
|
||||
student_states.student_id,
|
||||
student_states.next_step_outcome,
|
||||
student_states.student_name,
|
||||
student_states.student_email,
|
||||
student_states.total_questions,
|
||||
student_states.answered_questions,
|
||||
student_states.reviewed_questions,
|
||||
student_states.submitted_questions,
|
||||
student_states.in_progress_questions,
|
||||
student_states.review_status,
|
||||
student_states.latest_submitted_at,
|
||||
student_states.latest_reviewed_at
|
||||
FROM student_states
|
||||
WHERE ($2::text = '' OR review_status::text = $2::text)
|
||||
ORDER BY student_states.student_name ASC, student_states.student_id ASC;
|
||||
36
Backend/db/queries/classrooms.sql
Normal file
36
Backend/db/queries/classrooms.sql
Normal file
@@ -0,0 +1,36 @@
|
||||
-- name: CreateClassroom :one
|
||||
INSERT INTO classrooms (
|
||||
teacher_id,
|
||||
name,
|
||||
code,
|
||||
description
|
||||
) VALUES (
|
||||
$1,
|
||||
$2,
|
||||
$3,
|
||||
$4
|
||||
)
|
||||
RETURNING *;
|
||||
|
||||
-- name: ListClassroomsByTeacher :many
|
||||
SELECT *
|
||||
FROM classrooms
|
||||
WHERE teacher_id = $1
|
||||
ORDER BY created_at DESC;
|
||||
|
||||
-- name: AddStudentToClassroom :exec
|
||||
INSERT INTO classroom_students (
|
||||
classroom_id,
|
||||
student_id
|
||||
) VALUES (
|
||||
$1,
|
||||
$2
|
||||
)
|
||||
ON CONFLICT (classroom_id, student_id) DO NOTHING;
|
||||
|
||||
-- name: ListStudentsForClassroom :many
|
||||
SELECT u.*
|
||||
FROM classroom_students cs
|
||||
JOIN users u ON u.id = cs.student_id
|
||||
WHERE cs.classroom_id = $1
|
||||
ORDER BY u.full_name ASC;
|
||||
266
Backend/db/queries/messages.sql
Normal file
266
Backend/db/queries/messages.sql
Normal file
@@ -0,0 +1,266 @@
|
||||
-- name: ListMessageRecipientsForUser :many
|
||||
SELECT
|
||||
u.id AS user_id,
|
||||
u.email AS user_email,
|
||||
u.role AS user_role,
|
||||
u.full_name AS user_full_name,
|
||||
p.preferred_name,
|
||||
p.profile_icon_url,
|
||||
p.headline
|
||||
FROM users u
|
||||
LEFT JOIN profiles p ON p.user_id = u.id
|
||||
WHERE u.id <> $1
|
||||
AND u.is_active = TRUE
|
||||
AND (
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM classrooms c
|
||||
JOIN classroom_students cs ON cs.classroom_id = c.id
|
||||
WHERE c.teacher_id = u.id
|
||||
AND cs.student_id = $1
|
||||
)
|
||||
OR EXISTS (
|
||||
SELECT 1
|
||||
FROM classrooms c
|
||||
JOIN classroom_students cs ON cs.classroom_id = c.id
|
||||
WHERE c.teacher_id = $1
|
||||
AND cs.student_id = u.id
|
||||
)
|
||||
)
|
||||
ORDER BY COALESCE(NULLIF(p.preferred_name, ''), u.full_name) ASC, u.id ASC;
|
||||
|
||||
-- name: GetMessageRecipientByIDForUser :one
|
||||
SELECT
|
||||
u.id AS user_id,
|
||||
u.email AS user_email,
|
||||
u.role AS user_role,
|
||||
u.full_name AS user_full_name,
|
||||
p.preferred_name,
|
||||
p.profile_icon_url,
|
||||
p.headline
|
||||
FROM users u
|
||||
LEFT JOIN profiles p ON p.user_id = u.id
|
||||
WHERE u.id = $2
|
||||
AND u.id <> $1
|
||||
AND u.is_active = TRUE
|
||||
AND (
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM classrooms c
|
||||
JOIN classroom_students cs ON cs.classroom_id = c.id
|
||||
WHERE c.teacher_id = u.id
|
||||
AND cs.student_id = $1
|
||||
)
|
||||
OR EXISTS (
|
||||
SELECT 1
|
||||
FROM classrooms c
|
||||
JOIN classroom_students cs ON cs.classroom_id = c.id
|
||||
WHERE c.teacher_id = $1
|
||||
AND cs.student_id = u.id
|
||||
)
|
||||
)
|
||||
LIMIT 1;
|
||||
|
||||
-- name: ListMessageThreadsForUser :many
|
||||
SELECT
|
||||
t.id AS thread_id,
|
||||
t.subject,
|
||||
t.created_by_user_id,
|
||||
t.created_at AS thread_created_at,
|
||||
t.updated_at AS thread_updated_at,
|
||||
COALESCE(last_message.id, 0)::bigint AS last_message_id,
|
||||
COALESCE(last_message.body, '') AS last_message_body,
|
||||
last_message.created_at AS last_message_created_at,
|
||||
COALESCE(last_message.sender_user_id, 0)::bigint AS last_message_sender_user_id,
|
||||
sender.full_name AS last_message_sender_full_name,
|
||||
sender_profile.preferred_name AS last_message_sender_preferred_name,
|
||||
sender_profile.profile_icon_url AS last_message_sender_profile_icon_url,
|
||||
COALESCE((
|
||||
SELECT COUNT(*)::bigint
|
||||
FROM messages unread
|
||||
WHERE unread.thread_id = t.id
|
||||
AND unread.sender_user_id <> $1
|
||||
AND (participant.last_read_at IS NULL OR unread.created_at > participant.last_read_at)
|
||||
), 0)::bigint AS unread_count
|
||||
FROM message_thread_participants participant
|
||||
JOIN message_threads t ON t.id = participant.thread_id
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT m.id, m.body, m.created_at, m.sender_user_id
|
||||
FROM messages m
|
||||
WHERE m.thread_id = t.id
|
||||
ORDER BY m.created_at DESC, m.id DESC
|
||||
LIMIT 1
|
||||
) AS last_message ON TRUE
|
||||
LEFT JOIN users sender ON sender.id = last_message.sender_user_id
|
||||
LEFT JOIN profiles sender_profile ON sender_profile.user_id = sender.id
|
||||
WHERE participant.user_id = $1
|
||||
AND participant.archived_at IS NULL
|
||||
ORDER BY COALESCE(last_message.created_at, t.updated_at) DESC, t.id DESC;
|
||||
|
||||
-- name: ListMessageThreadParticipantsForUser :many
|
||||
SELECT
|
||||
mtp.thread_id,
|
||||
u.id AS user_id,
|
||||
u.email AS user_email,
|
||||
u.role AS user_role,
|
||||
u.full_name AS user_full_name,
|
||||
p.preferred_name,
|
||||
p.profile_icon_url,
|
||||
p.headline,
|
||||
mtp.joined_at,
|
||||
mtp.last_read_at,
|
||||
mtp.archived_at
|
||||
FROM message_thread_participants mtp
|
||||
JOIN users u ON u.id = mtp.user_id
|
||||
LEFT JOIN profiles p ON p.user_id = u.id
|
||||
WHERE mtp.thread_id IN (
|
||||
SELECT participant.thread_id
|
||||
FROM message_thread_participants participant
|
||||
WHERE participant.user_id = $1
|
||||
AND participant.archived_at IS NULL
|
||||
)
|
||||
ORDER BY mtp.thread_id ASC, COALESCE(NULLIF(p.preferred_name, ''), u.full_name) ASC, u.id ASC;
|
||||
|
||||
-- name: GetMessageThreadForUser :one
|
||||
SELECT
|
||||
t.id,
|
||||
t.subject,
|
||||
t.created_by_user_id,
|
||||
t.created_at,
|
||||
t.updated_at,
|
||||
participant.last_read_at,
|
||||
COALESCE((
|
||||
SELECT COUNT(*)::bigint
|
||||
FROM messages unread
|
||||
WHERE unread.thread_id = t.id
|
||||
AND unread.sender_user_id <> $2
|
||||
AND (participant.last_read_at IS NULL OR unread.created_at > participant.last_read_at)
|
||||
), 0)::bigint AS unread_count
|
||||
FROM message_threads t
|
||||
JOIN message_thread_participants participant ON participant.thread_id = t.id
|
||||
WHERE t.id = $1
|
||||
AND participant.user_id = $2
|
||||
AND participant.archived_at IS NULL;
|
||||
|
||||
-- name: ListMessagesForThreadForUser :many
|
||||
SELECT
|
||||
m.id,
|
||||
m.thread_id,
|
||||
m.sender_user_id,
|
||||
m.body,
|
||||
m.created_at,
|
||||
m.updated_at,
|
||||
sender.email AS sender_email,
|
||||
sender.role AS sender_role,
|
||||
sender.full_name AS sender_full_name,
|
||||
sender_profile.preferred_name AS sender_preferred_name,
|
||||
sender_profile.profile_icon_url AS sender_profile_icon_url,
|
||||
sender_profile.headline AS sender_headline
|
||||
FROM messages m
|
||||
JOIN message_thread_participants participant ON participant.thread_id = m.thread_id
|
||||
JOIN users sender ON sender.id = m.sender_user_id
|
||||
LEFT JOIN profiles sender_profile ON sender_profile.user_id = sender.id
|
||||
WHERE m.thread_id = $1
|
||||
AND participant.user_id = $2
|
||||
AND participant.archived_at IS NULL
|
||||
ORDER BY m.created_at ASC, m.id ASC;
|
||||
|
||||
-- name: ListParticipantsForThreadForUser :many
|
||||
SELECT
|
||||
mtp.thread_id,
|
||||
u.id AS user_id,
|
||||
u.email AS user_email,
|
||||
u.role AS user_role,
|
||||
u.full_name AS user_full_name,
|
||||
p.preferred_name,
|
||||
p.profile_icon_url,
|
||||
p.headline,
|
||||
mtp.joined_at,
|
||||
mtp.last_read_at,
|
||||
mtp.archived_at
|
||||
FROM message_thread_participants mtp
|
||||
JOIN users u ON u.id = mtp.user_id
|
||||
LEFT JOIN profiles p ON p.user_id = u.id
|
||||
WHERE mtp.thread_id = $1
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM message_thread_participants participant
|
||||
WHERE participant.thread_id = mtp.thread_id
|
||||
AND participant.user_id = $2
|
||||
AND participant.archived_at IS NULL
|
||||
)
|
||||
ORDER BY COALESCE(NULLIF(p.preferred_name, ''), u.full_name) ASC, u.id ASC;
|
||||
|
||||
-- name: CreateMessageThread :one
|
||||
INSERT INTO message_threads (
|
||||
created_by_user_id,
|
||||
subject
|
||||
) VALUES (
|
||||
$1,
|
||||
$2
|
||||
)
|
||||
RETURNING *;
|
||||
|
||||
-- name: AddMessageThreadParticipant :exec
|
||||
INSERT INTO message_thread_participants (
|
||||
thread_id,
|
||||
user_id,
|
||||
last_read_at
|
||||
) VALUES (
|
||||
$1,
|
||||
$2,
|
||||
$3
|
||||
)
|
||||
ON CONFLICT (thread_id, user_id) DO NOTHING;
|
||||
|
||||
-- name: CreateThreadMessage :one
|
||||
INSERT INTO messages (
|
||||
thread_id,
|
||||
sender_user_id,
|
||||
body
|
||||
) VALUES (
|
||||
$1,
|
||||
$2,
|
||||
$3
|
||||
)
|
||||
RETURNING *;
|
||||
|
||||
-- name: TouchMessageThread :exec
|
||||
UPDATE message_threads
|
||||
SET updated_at = NOW()
|
||||
WHERE id = $1;
|
||||
|
||||
-- name: UpdateMessageThreadSubject :one
|
||||
UPDATE message_threads
|
||||
SET subject = sqlc.arg(subject),
|
||||
updated_at = NOW()
|
||||
WHERE id = sqlc.arg(thread_id)
|
||||
RETURNING *;
|
||||
|
||||
-- name: UpdateThreadMessageBody :one
|
||||
UPDATE messages
|
||||
SET body = sqlc.arg(body),
|
||||
updated_at = NOW()
|
||||
WHERE id = sqlc.arg(message_id)
|
||||
AND thread_id = sqlc.arg(thread_id)
|
||||
AND sender_user_id = sqlc.arg(user_id)
|
||||
RETURNING *;
|
||||
|
||||
-- name: DeleteThreadMessage :one
|
||||
DELETE FROM messages
|
||||
WHERE id = sqlc.arg(message_id)
|
||||
AND thread_id = sqlc.arg(thread_id)
|
||||
AND sender_user_id = sqlc.arg(user_id)
|
||||
RETURNING *;
|
||||
|
||||
-- name: DeleteMessageThread :one
|
||||
DELETE FROM message_threads
|
||||
WHERE id = sqlc.arg(thread_id)
|
||||
RETURNING *;
|
||||
|
||||
-- name: MarkMessageThreadRead :one
|
||||
UPDATE message_thread_participants
|
||||
SET last_read_at = COALESCE((SELECT MAX(m.created_at) FROM messages m WHERE m.thread_id = $1), NOW())
|
||||
WHERE message_thread_participants.thread_id = $1
|
||||
AND message_thread_participants.user_id = $2
|
||||
RETURNING *;
|
||||
55
Backend/db/queries/questions.sql
Normal file
55
Backend/db/queries/questions.sql
Normal file
@@ -0,0 +1,55 @@
|
||||
-- name: CreateQuestion :one
|
||||
INSERT INTO questions (
|
||||
author_teacher_id,
|
||||
title,
|
||||
prompt,
|
||||
topic,
|
||||
subject,
|
||||
difficulty,
|
||||
source,
|
||||
status,
|
||||
correct_answer
|
||||
) VALUES (
|
||||
$1,
|
||||
$2,
|
||||
$3,
|
||||
$4,
|
||||
$5,
|
||||
$6,
|
||||
$7,
|
||||
$8,
|
||||
$9
|
||||
)
|
||||
RETURNING *;
|
||||
|
||||
-- name: ListQuestionsByTeacher :many
|
||||
SELECT *
|
||||
FROM questions
|
||||
WHERE author_teacher_id = $1
|
||||
ORDER BY created_at DESC;
|
||||
|
||||
-- name: GetQuestionByID :one
|
||||
SELECT *
|
||||
FROM questions
|
||||
WHERE id = $1;
|
||||
|
||||
-- name: CreateTag :one
|
||||
INSERT INTO tags (name)
|
||||
VALUES ($1)
|
||||
ON CONFLICT (name) DO UPDATE SET name = EXCLUDED.name
|
||||
RETURNING *;
|
||||
|
||||
-- name: AttachTagToQuestion :exec
|
||||
INSERT INTO question_tags (
|
||||
question_id,
|
||||
tag_id
|
||||
) VALUES (
|
||||
$1,
|
||||
$2
|
||||
)
|
||||
ON CONFLICT (question_id, tag_id) DO NOTHING;
|
||||
|
||||
-- name: ListTags :many
|
||||
SELECT *
|
||||
FROM tags
|
||||
ORDER BY name ASC;
|
||||
228
Backend/db/queries/student_answers.sql
Normal file
228
Backend/db/queries/student_answers.sql
Normal file
@@ -0,0 +1,228 @@
|
||||
-- name: UpsertStudentAnswer :one
|
||||
INSERT INTO student_answers (
|
||||
assignment_id,
|
||||
question_id,
|
||||
student_id,
|
||||
answer_text,
|
||||
solve_mode,
|
||||
working_steps,
|
||||
ai_feedback,
|
||||
teacher_feedback,
|
||||
status,
|
||||
submitted_at,
|
||||
reviewed_at,
|
||||
is_correct
|
||||
) VALUES (
|
||||
$1,
|
||||
$2,
|
||||
$3,
|
||||
$4,
|
||||
$5,
|
||||
$6,
|
||||
$7,
|
||||
$8,
|
||||
$9,
|
||||
$10,
|
||||
$11,
|
||||
$12
|
||||
)
|
||||
ON CONFLICT (assignment_id, question_id, student_id) DO UPDATE
|
||||
SET
|
||||
answer_text = EXCLUDED.answer_text,
|
||||
solve_mode = EXCLUDED.solve_mode,
|
||||
working_steps = EXCLUDED.working_steps,
|
||||
ai_feedback = EXCLUDED.ai_feedback,
|
||||
teacher_feedback = EXCLUDED.teacher_feedback,
|
||||
status = EXCLUDED.status,
|
||||
submitted_at = EXCLUDED.submitted_at,
|
||||
reviewed_at = EXCLUDED.reviewed_at,
|
||||
is_correct = EXCLUDED.is_correct,
|
||||
updated_at = NOW()
|
||||
RETURNING *;
|
||||
|
||||
-- name: UpdateAnswerAIReview :one
|
||||
UPDATE student_answers
|
||||
SET
|
||||
ai_feedback = $2,
|
||||
review_needs_attention = $3,
|
||||
review_issue_reason = $4,
|
||||
review_correctness_score = $5,
|
||||
review_understanding_score = $6,
|
||||
review_question_score = $7,
|
||||
review_confidence = $8,
|
||||
updated_at = NOW()
|
||||
WHERE id = $1
|
||||
RETURNING *;
|
||||
|
||||
-- name: ListAnswersForAssignment :many
|
||||
SELECT *
|
||||
FROM student_answers
|
||||
WHERE assignment_id = $1
|
||||
ORDER BY created_at ASC;
|
||||
|
||||
-- name: ListAnswersForStudent :many
|
||||
SELECT *
|
||||
FROM student_answers
|
||||
WHERE student_id = $1
|
||||
ORDER BY created_at DESC;
|
||||
|
||||
-- name: ListQuestionDetailsForAssignmentStudent :many
|
||||
WITH student_question_set AS (
|
||||
SELECT
|
||||
asq.assignment_id,
|
||||
asq.question_id,
|
||||
asq.position
|
||||
FROM assignment_student_questions asq
|
||||
WHERE asq.assignment_id = $1
|
||||
AND asq.student_id = $2
|
||||
),
|
||||
selected_questions AS (
|
||||
SELECT
|
||||
sq.assignment_id,
|
||||
sq.question_id,
|
||||
sq.position
|
||||
FROM student_question_set sq
|
||||
UNION ALL
|
||||
SELECT
|
||||
aq.assignment_id,
|
||||
aq.question_id,
|
||||
aq.position
|
||||
FROM assignment_questions aq
|
||||
WHERE aq.assignment_id = $1
|
||||
AND NOT EXISTS (SELECT 1 FROM student_question_set)
|
||||
)
|
||||
SELECT
|
||||
aq.assignment_id,
|
||||
aq.question_id,
|
||||
aq.position,
|
||||
q.title,
|
||||
q.prompt,
|
||||
q.subject,
|
||||
q.source,
|
||||
COALESCE(
|
||||
ARRAY(
|
||||
SELECT t.name
|
||||
FROM question_tags qt
|
||||
JOIN tags t ON t.id = qt.tag_id
|
||||
WHERE qt.question_id = aq.question_id
|
||||
ORDER BY t.name ASC
|
||||
),
|
||||
ARRAY[]::TEXT[]
|
||||
)::TEXT[] AS question_tags,
|
||||
q.status AS question_status,
|
||||
q.correct_answer,
|
||||
aa.ai_feedback AS assignment_ai_feedback,
|
||||
aa.teacher_feedback AS assignment_teacher_feedback,
|
||||
review_summary.overall_score,
|
||||
a.pass_threshold,
|
||||
aa.next_step_outcome,
|
||||
aa.pass_status_override,
|
||||
COALESCE(
|
||||
aa.pass_status_override,
|
||||
CASE
|
||||
WHEN review_summary.overall_score IS NULL THEN 'pending'::assignment_pass_status
|
||||
WHEN review_summary.overall_score >= a.pass_threshold THEN 'pass'::assignment_pass_status
|
||||
ELSE 'no_pass'::assignment_pass_status
|
||||
END
|
||||
) AS pass_status,
|
||||
sa.id AS answer_id,
|
||||
sa.student_id,
|
||||
sa.answer_text,
|
||||
sa.solve_mode,
|
||||
sa.working_steps,
|
||||
sa.is_correct,
|
||||
sa.ai_feedback,
|
||||
sa.teacher_feedback,
|
||||
sa.status AS answer_status,
|
||||
sa.review_needs_attention,
|
||||
sa.review_issue_reason,
|
||||
sa.review_correctness_score,
|
||||
sa.review_understanding_score,
|
||||
sa.review_question_score,
|
||||
sa.review_confidence,
|
||||
sa.review_tags,
|
||||
sa.submitted_at,
|
||||
sa.reviewed_at,
|
||||
sa.created_at AS answer_created_at,
|
||||
sa.updated_at AS answer_updated_at
|
||||
FROM selected_questions aq
|
||||
JOIN assignments a ON a.id = aq.assignment_id
|
||||
JOIN questions q ON q.id = aq.question_id
|
||||
LEFT JOIN assignment_assignees aa
|
||||
ON aa.assignment_id = aq.assignment_id
|
||||
AND aa.student_id = $2
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT CASE
|
||||
WHEN COUNT(sa2.id) = 0 THEN NULL::NUMERIC(5,2)
|
||||
ELSE ROUND((AVG(
|
||||
CASE
|
||||
WHEN sa2.is_correct IS NULL THEN COALESCE(sa2.review_understanding_score, 0)::NUMERIC
|
||||
ELSE (
|
||||
((CASE WHEN sa2.is_correct THEN 1 ELSE 0 END)::NUMERIC) + COALESCE(sa2.review_understanding_score, 0)::NUMERIC
|
||||
) / 2
|
||||
END
|
||||
) * 10)::NUMERIC, 2)::NUMERIC(5,2)
|
||||
END AS overall_score
|
||||
FROM selected_questions aq2
|
||||
LEFT JOIN student_answers sa2
|
||||
ON sa2.assignment_id = aq2.assignment_id
|
||||
AND sa2.question_id = aq2.question_id
|
||||
AND sa2.student_id = $2
|
||||
WHERE aq2.assignment_id = aq.assignment_id
|
||||
) review_summary ON TRUE
|
||||
LEFT JOIN student_answers sa
|
||||
ON sa.assignment_id = aq.assignment_id
|
||||
AND sa.question_id = aq.question_id
|
||||
AND sa.student_id = $2
|
||||
WHERE aq.assignment_id = $1
|
||||
ORDER BY aq.position ASC, aq.question_id ASC;
|
||||
|
||||
-- name: UpdateAnswerReview :one
|
||||
UPDATE student_answers
|
||||
SET
|
||||
status = $2,
|
||||
review_needs_attention = $3,
|
||||
review_issue_reason = $4,
|
||||
review_correctness_score = $5,
|
||||
review_understanding_score = $6,
|
||||
review_question_score = $7,
|
||||
review_confidence = $8,
|
||||
review_tags = $9,
|
||||
reviewed_at = CASE
|
||||
WHEN $2::answer_status = 'reviewed' THEN NOW()
|
||||
ELSE NULL
|
||||
END,
|
||||
updated_at = NOW()
|
||||
WHERE id = $1
|
||||
RETURNING *;
|
||||
|
||||
-- name: ListStudentPlanningPerformance :many
|
||||
SELECT
|
||||
sa.assignment_id,
|
||||
sa.question_id,
|
||||
q.topic,
|
||||
q.subject,
|
||||
q.difficulty,
|
||||
COALESCE(
|
||||
ARRAY(
|
||||
SELECT t.name
|
||||
FROM question_tags qt
|
||||
JOIN tags t ON t.id = qt.tag_id
|
||||
WHERE qt.question_id = sa.question_id
|
||||
ORDER BY t.name ASC
|
||||
),
|
||||
ARRAY[]::TEXT[]
|
||||
)::TEXT[] AS question_tags,
|
||||
sa.is_correct,
|
||||
sa.review_understanding_score,
|
||||
sa.review_needs_attention,
|
||||
sa.review_issue_reason,
|
||||
sa.status,
|
||||
sa.submitted_at,
|
||||
sa.reviewed_at,
|
||||
sa.updated_at
|
||||
FROM student_answers sa
|
||||
JOIN questions q ON q.id = sa.question_id
|
||||
WHERE sa.student_id = $1
|
||||
AND sa.status IN ('submitted'::answer_status, 'reviewed'::answer_status)
|
||||
ORDER BY COALESCE(sa.reviewed_at, sa.submitted_at, sa.updated_at) DESC, sa.id DESC;
|
||||
178
Backend/db/queries/users.sql
Normal file
178
Backend/db/queries/users.sql
Normal file
@@ -0,0 +1,178 @@
|
||||
-- name: CreateUser :one
|
||||
INSERT INTO users (
|
||||
email,
|
||||
password_hash,
|
||||
role,
|
||||
full_name
|
||||
) VALUES (
|
||||
$1,
|
||||
$2,
|
||||
$3,
|
||||
$4
|
||||
)
|
||||
RETURNING *;
|
||||
|
||||
-- name: GetUserByID :one
|
||||
SELECT *
|
||||
FROM users
|
||||
WHERE id = $1;
|
||||
|
||||
-- name: GetAuthUserByID :one
|
||||
SELECT
|
||||
u.id AS user_id,
|
||||
u.email AS user_email,
|
||||
u.role AS user_role,
|
||||
u.full_name AS user_full_name,
|
||||
u.is_active AS user_is_active,
|
||||
u.created_at AS user_created_at,
|
||||
u.updated_at AS user_updated_at,
|
||||
p.user_id AS profile_user_id,
|
||||
p.preferred_name,
|
||||
p.profile_icon_url,
|
||||
p.headline,
|
||||
p.bio,
|
||||
p.timezone,
|
||||
p.locale,
|
||||
p.grade_level,
|
||||
p.learning_goal,
|
||||
p.created_at AS profile_created_at,
|
||||
p.updated_at AS profile_updated_at
|
||||
FROM users u
|
||||
LEFT JOIN profiles p ON p.user_id = u.id
|
||||
WHERE u.id = $1;
|
||||
|
||||
-- name: GetUserByEmail :one
|
||||
SELECT *
|
||||
FROM users
|
||||
WHERE email = $1;
|
||||
|
||||
-- name: GetAuthUserByEmail :one
|
||||
SELECT
|
||||
u.id AS user_id,
|
||||
u.email AS user_email,
|
||||
u.role AS user_role,
|
||||
u.full_name AS user_full_name,
|
||||
u.is_active AS user_is_active,
|
||||
u.password_hash AS user_password_hash,
|
||||
u.created_at AS user_created_at,
|
||||
u.updated_at AS user_updated_at,
|
||||
p.user_id AS profile_user_id,
|
||||
p.preferred_name,
|
||||
p.profile_icon_url,
|
||||
p.headline,
|
||||
p.bio,
|
||||
p.timezone,
|
||||
p.locale,
|
||||
p.grade_level,
|
||||
p.learning_goal,
|
||||
p.created_at AS profile_created_at,
|
||||
p.updated_at AS profile_updated_at
|
||||
FROM users u
|
||||
LEFT JOIN profiles p ON p.user_id = u.id
|
||||
WHERE u.email = $1;
|
||||
|
||||
-- name: ListUsersByRole :many
|
||||
SELECT *
|
||||
FROM users
|
||||
WHERE role = $1
|
||||
ORDER BY full_name ASC;
|
||||
|
||||
-- name: ListUsersWithProfileByRole :many
|
||||
SELECT
|
||||
u.id AS user_id,
|
||||
u.email AS user_email,
|
||||
u.role AS user_role,
|
||||
u.full_name AS user_full_name,
|
||||
u.is_active AS user_is_active,
|
||||
u.created_at AS user_created_at,
|
||||
u.updated_at AS user_updated_at,
|
||||
p.user_id AS profile_user_id,
|
||||
p.preferred_name,
|
||||
p.profile_icon_url,
|
||||
p.headline,
|
||||
p.bio,
|
||||
p.timezone,
|
||||
p.locale,
|
||||
p.grade_level,
|
||||
p.learning_goal,
|
||||
p.created_at AS profile_created_at,
|
||||
p.updated_at AS profile_updated_at
|
||||
FROM users u
|
||||
LEFT JOIN profiles p ON p.user_id = u.id
|
||||
WHERE u.role = $1
|
||||
ORDER BY u.full_name ASC;
|
||||
|
||||
-- name: GetUserWithProfileByID :one
|
||||
SELECT
|
||||
u.id AS user_id,
|
||||
u.email AS user_email,
|
||||
u.role AS user_role,
|
||||
u.full_name AS user_full_name,
|
||||
u.is_active AS user_is_active,
|
||||
u.created_at AS user_created_at,
|
||||
u.updated_at AS user_updated_at,
|
||||
p.user_id AS profile_user_id,
|
||||
p.preferred_name,
|
||||
p.profile_icon_url,
|
||||
p.headline,
|
||||
p.bio,
|
||||
p.timezone,
|
||||
p.locale,
|
||||
p.grade_level,
|
||||
p.learning_goal,
|
||||
p.created_at AS profile_created_at,
|
||||
p.updated_at AS profile_updated_at
|
||||
FROM users u
|
||||
LEFT JOIN profiles p ON p.user_id = u.id
|
||||
WHERE u.id = $1;
|
||||
|
||||
-- name: UpdateUserActiveStatus :one
|
||||
UPDATE users
|
||||
SET
|
||||
is_active = $2,
|
||||
updated_at = NOW()
|
||||
WHERE id = $1
|
||||
RETURNING *;
|
||||
|
||||
-- name: UpdateUserFullName :one
|
||||
UPDATE users
|
||||
SET
|
||||
full_name = $2,
|
||||
updated_at = NOW()
|
||||
WHERE id = $1
|
||||
RETURNING *;
|
||||
|
||||
-- name: UpsertUserProfile :one
|
||||
INSERT INTO profiles (
|
||||
user_id,
|
||||
preferred_name,
|
||||
profile_icon_url,
|
||||
headline,
|
||||
bio,
|
||||
timezone,
|
||||
locale,
|
||||
grade_level,
|
||||
learning_goal
|
||||
) VALUES (
|
||||
$1,
|
||||
$2,
|
||||
$3,
|
||||
$4,
|
||||
$5,
|
||||
$6,
|
||||
$7,
|
||||
$8,
|
||||
$9
|
||||
)
|
||||
ON CONFLICT (user_id) DO UPDATE
|
||||
SET
|
||||
preferred_name = EXCLUDED.preferred_name,
|
||||
profile_icon_url = EXCLUDED.profile_icon_url,
|
||||
headline = EXCLUDED.headline,
|
||||
bio = EXCLUDED.bio,
|
||||
timezone = EXCLUDED.timezone,
|
||||
locale = EXCLUDED.locale,
|
||||
grade_level = EXCLUDED.grade_level,
|
||||
learning_goal = EXCLUDED.learning_goal,
|
||||
updated_at = NOW()
|
||||
RETURNING *;
|
||||
12
Backend/db/sqlc.yaml
Normal file
12
Backend/db/sqlc.yaml
Normal file
@@ -0,0 +1,12 @@
|
||||
version: "2"
|
||||
sql:
|
||||
- engine: "postgresql"
|
||||
queries: "queries/"
|
||||
schema: "migrations/"
|
||||
gen:
|
||||
go:
|
||||
package: "sqlc"
|
||||
out: "../internal/sqlc"
|
||||
sql_package: "pgx/v5"
|
||||
emit_json_tags: true
|
||||
emit_empty_slices: true
|
||||
Reference in New Issue
Block a user