- Migration: student role, students.user_id, grade_categories/grade_scale (school defaults + per-class override), assignments, grades + RLS - Classes area (nav): list/create classes, class page with Gradebook / Roster / Grading setup tabs; teachers see their classes, admins all - Weighted-category grade calc -> letter scale (shared helper) - Roster assignment (admin), assignment + grade entry (teacher/admin) - Student profile: view-only Grades tab (parents/students) - Portal access: create parent OR student logins linked to the student Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
91 lines
5.6 KiB
PL/PgSQL
91 lines
5.6 KiB
PL/PgSQL
-- Gradebook: weighted-category grading, per-class assignments & grades, student role.
|
|
-- (Applied to the remote via the Supabase MCP; kept here as the source of record.)
|
|
|
|
ALTER TYPE public.app_role ADD VALUE IF NOT EXISTS 'student';
|
|
|
|
ALTER TABLE public.students ADD COLUMN IF NOT EXISTS user_id UUID REFERENCES auth.users(id) ON DELETE SET NULL;
|
|
CREATE INDEX IF NOT EXISTS idx_students_user ON public.students(user_id);
|
|
|
|
CREATE OR REPLACE FUNCTION public.teaches_class(_class UUID)
|
|
RETURNS BOOLEAN LANGUAGE SQL STABLE SECURITY DEFINER SET search_path = public AS $$
|
|
SELECT EXISTS (SELECT 1 FROM public.classes c WHERE c.id = _class AND c.teacher_id = auth.uid())
|
|
$$;
|
|
CREATE OR REPLACE FUNCTION public.is_student_account(_student UUID)
|
|
RETURNS BOOLEAN LANGUAGE SQL STABLE SECURITY DEFINER SET search_path = public AS $$
|
|
SELECT EXISTS (SELECT 1 FROM public.students s WHERE s.id = _student AND s.user_id = auth.uid())
|
|
$$;
|
|
|
|
CREATE TABLE IF NOT EXISTS public.grade_categories (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
class_id UUID REFERENCES public.classes(id) ON DELETE CASCADE,
|
|
name TEXT NOT NULL, weight NUMERIC NOT NULL DEFAULT 0, sort_order INT NOT NULL DEFAULT 0,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
ALTER TABLE public.grade_categories ENABLE ROW LEVEL SECURITY;
|
|
|
|
CREATE TABLE IF NOT EXISTS public.grade_scale (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
class_id UUID REFERENCES public.classes(id) ON DELETE CASCADE,
|
|
letter TEXT NOT NULL, min_percent NUMERIC NOT NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
ALTER TABLE public.grade_scale ENABLE ROW LEVEL SECURITY;
|
|
|
|
CREATE TABLE IF NOT EXISTS public.assignments (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
class_id UUID NOT NULL REFERENCES public.classes(id) ON DELETE CASCADE,
|
|
category_id UUID REFERENCES public.grade_categories(id) ON DELETE SET NULL,
|
|
name TEXT NOT NULL, max_points NUMERIC NOT NULL DEFAULT 100, date DATE,
|
|
created_by UUID REFERENCES auth.users(id), created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_assignments_class ON public.assignments(class_id);
|
|
ALTER TABLE public.assignments ENABLE ROW LEVEL SECURITY;
|
|
|
|
CREATE TABLE IF NOT EXISTS public.grades (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
assignment_id UUID NOT NULL REFERENCES public.assignments(id) ON DELETE CASCADE,
|
|
student_id UUID NOT NULL REFERENCES public.students(id) ON DELETE CASCADE,
|
|
points NUMERIC, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
UNIQUE (assignment_id, student_id)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_grades_student ON public.grades(student_id);
|
|
ALTER TABLE public.grades ENABLE ROW LEVEL SECURITY;
|
|
|
|
CREATE POLICY "grade_categories read" ON public.grade_categories FOR SELECT TO authenticated USING (TRUE);
|
|
CREATE POLICY "grade_categories admin" ON public.grade_categories FOR ALL TO authenticated
|
|
USING (public.current_user_has_role('admin')) WITH CHECK (public.current_user_has_role('admin'));
|
|
CREATE POLICY "grade_categories teacher" ON public.grade_categories FOR ALL TO authenticated
|
|
USING (class_id IS NOT NULL AND public.teaches_class(class_id)) WITH CHECK (class_id IS NOT NULL AND public.teaches_class(class_id));
|
|
|
|
CREATE POLICY "grade_scale read" ON public.grade_scale FOR SELECT TO authenticated USING (TRUE);
|
|
CREATE POLICY "grade_scale admin" ON public.grade_scale FOR ALL TO authenticated
|
|
USING (public.current_user_has_role('admin')) WITH CHECK (public.current_user_has_role('admin'));
|
|
CREATE POLICY "grade_scale teacher" ON public.grade_scale FOR ALL TO authenticated
|
|
USING (class_id IS NOT NULL AND public.teaches_class(class_id)) WITH CHECK (class_id IS NOT NULL AND public.teaches_class(class_id));
|
|
|
|
CREATE POLICY "assignments read" ON public.assignments FOR SELECT TO authenticated USING (
|
|
public.current_user_has_role('admin') OR public.teaches_class(class_id)
|
|
OR EXISTS (SELECT 1 FROM public.students s WHERE s.class_id = assignments.class_id AND (public.is_parent_of(s.id) OR public.is_student_account(s.id)))
|
|
);
|
|
CREATE POLICY "assignments write" ON public.assignments FOR ALL TO authenticated
|
|
USING (public.current_user_has_role('admin') OR public.teaches_class(class_id))
|
|
WITH CHECK (public.current_user_has_role('admin') OR public.teaches_class(class_id));
|
|
|
|
CREATE POLICY "grades read" ON public.grades FOR SELECT TO authenticated USING (
|
|
public.current_user_has_role('admin') OR public.is_parent_of(student_id) OR public.is_student_account(student_id)
|
|
OR EXISTS (SELECT 1 FROM public.assignments a WHERE a.id = grades.assignment_id AND public.teaches_class(a.class_id))
|
|
);
|
|
CREATE POLICY "grades write" ON public.grades FOR ALL TO authenticated
|
|
USING (public.current_user_has_role('admin') OR EXISTS (SELECT 1 FROM public.assignments a WHERE a.id = grades.assignment_id AND public.teaches_class(a.class_id)))
|
|
WITH CHECK (public.current_user_has_role('admin') OR EXISTS (SELECT 1 FROM public.assignments a WHERE a.id = grades.assignment_id AND public.teaches_class(a.class_id)));
|
|
|
|
INSERT INTO public.grade_categories (class_id, name, weight, sort_order)
|
|
SELECT v.class_id, v.name, v.weight, v.sort_order
|
|
FROM (VALUES (NULL::UUID, 'Homework', 20::NUMERIC, 0), (NULL, 'Quizzes', 30, 1), (NULL, 'Tests', 50, 2)) AS v(class_id, name, weight, sort_order)
|
|
WHERE NOT EXISTS (SELECT 1 FROM public.grade_categories WHERE class_id IS NULL);
|
|
|
|
INSERT INTO public.grade_scale (class_id, letter, min_percent)
|
|
SELECT v.class_id, v.letter, v.min_percent
|
|
FROM (VALUES (NULL::UUID, 'A', 90::NUMERIC), (NULL, 'B', 80), (NULL, 'C', 70), (NULL, 'D', 60), (NULL, 'F', 0)) AS v(class_id, letter, min_percent)
|
|
WHERE NOT EXISTS (SELECT 1 FROM public.grade_scale WHERE class_id IS NULL);
|