From 16fd09a97b8039265f396f03289b8b886d57949b Mon Sep 17 00:00:00 2001 From: Hermes cc-worker Date: Fri, 24 Jul 2026 18:18:51 +0000 Subject: [PATCH] Add class markbook schema --- volumes/db/cc/79-class-markbook.sql | 115 ++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 volumes/db/cc/79-class-markbook.sql diff --git a/volumes/db/cc/79-class-markbook.sql b/volumes/db/cc/79-class-markbook.sql new file mode 100644 index 0000000..4da410e --- /dev/null +++ b/volumes/db/cc/79-class-markbook.sql @@ -0,0 +1,115 @@ +-- 79-class-markbook.sql +-- G2 running markbook / gradebook: teacher-editable per-class assessment columns and marks. + +create table if not exists public.class_assessments ( + id uuid primary key default gen_random_uuid(), + class_id uuid not null references public.classes(id) on delete cascade, + tenant_id uuid not null references public.institutes(id) on delete cascade, + title text not null check (length(btrim(title)) > 0), + date date, + max_marks numeric not null default 100 check (max_marks > 0), + created_at timestamptz not null default now(), + updated_at timestamptz not null default now() +); + +create table if not exists public.assessment_marks ( + id uuid primary key default gen_random_uuid(), + assessment_id uuid not null references public.class_assessments(id) on delete cascade, + student_id uuid not null references public.profiles(id) on delete cascade, + tenant_id uuid not null references public.institutes(id) on delete cascade, + mark numeric check (mark is null or mark >= 0), + updated_by uuid references public.profiles(id), + updated_at timestamptz not null default now(), + constraint assessment_marks_assessment_student_key unique (assessment_id, student_id) +); + +create index if not exists idx_class_assessments_class on public.class_assessments(class_id); +create index if not exists idx_class_assessments_tenant on public.class_assessments(tenant_id); +create index if not exists idx_assessment_marks_assessment on public.assessment_marks(assessment_id); +create index if not exists idx_assessment_marks_student on public.assessment_marks(student_id); +create index if not exists idx_assessment_marks_tenant on public.assessment_marks(tenant_id); + +create or replace function public.set_class_assessment_tenant() +returns trigger language plpgsql security definer set search_path = public as $$ +begin + select c.institute_id into new.tenant_id from public.classes c where c.id = new.class_id; + if new.tenant_id is null then + raise exception 'class % not found for assessment tenant', new.class_id; + end if; + new.updated_at = now(); + return new; +end; +$$; + +drop trigger if exists trg_set_class_assessment_tenant on public.class_assessments; +create trigger trg_set_class_assessment_tenant +before insert or update of class_id, title, date, max_marks on public.class_assessments +for each row execute function public.set_class_assessment_tenant(); + +create or replace function public.set_assessment_mark_tenant_and_validate() +returns trigger language plpgsql security definer set search_path = public as $$ +declare + a record; +begin + select ca.tenant_id, ca.max_marks, ca.class_id into a + from public.class_assessments ca + where ca.id = new.assessment_id; + if a.tenant_id is null then + raise exception 'assessment % not found for mark tenant', new.assessment_id; + end if; + if new.mark is not null and new.mark > a.max_marks then + raise exception 'mark % exceeds assessment max %', new.mark, a.max_marks; + end if; + if not exists ( + select 1 from public.class_students cs + where cs.class_id = a.class_id and cs.student_id = new.student_id and cs.status = 'active' + ) then + raise exception 'student % is not active in assessment class', new.student_id; + end if; + new.tenant_id = a.tenant_id; + new.updated_at = now(); + return new; +end; +$$; + +drop trigger if exists trg_set_assessment_mark_tenant_validate on public.assessment_marks; +create trigger trg_set_assessment_mark_tenant_validate +before insert or update of assessment_id, student_id, mark on public.assessment_marks +for each row execute function public.set_assessment_mark_tenant_and_validate(); + +alter table public.class_assessments enable row level security; +alter table public.assessment_marks enable row level security; + +-- Assessment columns: class teachers/admins can read/write. +drop policy if exists class_assessments_service_role on public.class_assessments; +create policy class_assessments_service_role on public.class_assessments + using (auth.role() = 'service_role') + with check (auth.role() = 'service_role'); + +drop policy if exists class_assessments_class_staff_rw on public.class_assessments; +create policy class_assessments_class_staff_rw on public.class_assessments for all to authenticated + using (public.is_class_teacher(class_id) or public.is_class_admin(class_id)) + with check (public.is_class_teacher(class_id) or public.is_class_admin(class_id)); + +-- Marks: staff can read/write; students can read their own rows. +drop policy if exists assessment_marks_service_role on public.assessment_marks; +create policy assessment_marks_service_role on public.assessment_marks + using (auth.role() = 'service_role') + with check (auth.role() = 'service_role'); + +drop policy if exists assessment_marks_class_staff_rw on public.assessment_marks; +create policy assessment_marks_class_staff_rw on public.assessment_marks for all to authenticated + using (exists ( + select 1 from public.class_assessments ca + where ca.id = assessment_marks.assessment_id + and (public.is_class_teacher(ca.class_id) or public.is_class_admin(ca.class_id)) + )) + with check (exists ( + select 1 from public.class_assessments ca + where ca.id = assessment_marks.assessment_id + and (public.is_class_teacher(ca.class_id) or public.is_class_admin(ca.class_id)) + )); + +drop policy if exists assessment_marks_student_read on public.assessment_marks; +create policy assessment_marks_student_read on public.assessment_marks for select to authenticated + using (student_id = auth.uid());