merge: exam-marker Supabase foundation (class-mgmt schema+RLS, exam tables)
Some checks failed
supabase-ci / validate (push) Has been cancelled
Some checks failed
supabase-ci / validate (push) Has been cancelled
Brings 71-class-management.sql (tracks the previously-untracked class schema + as-user RLS helpers) and 72-exam-marker.sql (7 exam tables + RLS). Both applied + verified on dev .94.
This commit is contained in:
commit
feceaf64b6
184
volumes/db/cc/71-class-management.sql
Normal file
184
volumes/db/cc/71-class-management.sql
Normal file
@ -0,0 +1,184 @@
|
||||
-- 71-class-management.sql
|
||||
-- Foundational: capture the (previously untracked) class-management schema and harden its RLS.
|
||||
--
|
||||
-- Background: `classes`, `class_teachers`, `class_students`, `enrollment_requests` existed only on
|
||||
-- live dev (.94), created out-of-band, with NO tracked DDL. Their schema/FKs/uniques are sound,
|
||||
-- but RLS exposed `class_students` / `class_teachers` to service_role ONLY — so any API path that
|
||||
-- calls Supabase AS THE USER (the correct, RLS-enforced pattern) reads ZERO rows. This migration:
|
||||
-- 1. captures the real schema (idempotent; no-op on environments that already have it),
|
||||
-- 2. adds SECURITY DEFINER membership helpers (avoid RLS recursion in policies),
|
||||
-- 3. adds as-user RLS policies so teachers/admins/students can read rosters under RLS,
|
||||
-- while keeping the existing service_role policies intact.
|
||||
-- Verified against live .94 schema 2026-06-06 (all FKs/uniques/checks already present there).
|
||||
|
||||
--==========================================================================================
|
||||
-- 1. Tables (idempotent capture for fresh environments; skipped where they already exist)
|
||||
--==========================================================================================
|
||||
|
||||
create table if not exists public.classes (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
institute_id uuid not null references public.institutes(id) on delete cascade,
|
||||
name varchar not null,
|
||||
class_code text,
|
||||
subject varchar,
|
||||
key_stage text,
|
||||
year_group varchar,
|
||||
academic_year varchar,
|
||||
description text,
|
||||
type varchar not null default 'standard',
|
||||
is_active boolean not null default true,
|
||||
created_by uuid not null references public.profiles(id),
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
create table if not exists public.class_teachers (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
class_id uuid not null references public.classes(id) on delete cascade,
|
||||
teacher_id uuid not null references public.profiles(id) on delete cascade,
|
||||
is_primary boolean not null default false,
|
||||
can_edit boolean not null default true,
|
||||
assigned_at timestamptz not null default now(),
|
||||
assigned_by uuid references public.profiles(id),
|
||||
constraint class_teachers_class_id_teacher_id_key unique (class_id, teacher_id)
|
||||
);
|
||||
|
||||
create table if not exists public.class_students (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
class_id uuid not null references public.classes(id) on delete cascade,
|
||||
student_id uuid not null references public.profiles(id) on delete cascade,
|
||||
status varchar not null default 'active'
|
||||
check (status::text = any (array['active','inactive','pending'])),
|
||||
enrolled_at timestamptz not null default now(),
|
||||
enrolled_by uuid references public.profiles(id),
|
||||
constraint class_students_class_id_student_id_key unique (class_id, student_id)
|
||||
);
|
||||
|
||||
create table if not exists public.enrollment_requests (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
class_id uuid not null references public.classes(id) on delete cascade,
|
||||
student_id uuid not null references public.profiles(id) on delete cascade,
|
||||
status varchar not null default 'pending'
|
||||
check (status::text = any (array['pending','approved','rejected'])),
|
||||
request_message text,
|
||||
requested_at timestamptz not null default now(),
|
||||
responded_at timestamptz,
|
||||
responded_by uuid references public.profiles(id),
|
||||
response_message text
|
||||
);
|
||||
|
||||
create index if not exists idx_classes_institute on public.classes(institute_id);
|
||||
create index if not exists idx_classes_created_by on public.classes(created_by);
|
||||
create index if not exists idx_classes_class_code on public.classes(class_code);
|
||||
create index if not exists idx_class_teachers_class on public.class_teachers(class_id);
|
||||
create index if not exists idx_class_students_class on public.class_students(class_id);
|
||||
|
||||
--==========================================================================================
|
||||
-- 2. SECURITY DEFINER membership helpers
|
||||
-- Run as owner (bypass RLS on the inner tables) → no policy recursion when referenced below.
|
||||
--==========================================================================================
|
||||
|
||||
create or replace function public.user_institute_ids()
|
||||
returns setof uuid language sql stable security definer set search_path = public as $$
|
||||
select institute_id from public.institute_memberships where profile_id = auth.uid()
|
||||
$$;
|
||||
|
||||
create or replace function public.is_class_teacher(p_class uuid)
|
||||
returns boolean language sql stable security definer set search_path = public as $$
|
||||
select exists (select 1 from public.class_teachers
|
||||
where class_id = p_class and teacher_id = auth.uid())
|
||||
$$;
|
||||
|
||||
create or replace function public.is_class_admin(p_class uuid)
|
||||
returns boolean language sql stable security definer set search_path = public as $$
|
||||
select exists (
|
||||
select 1 from public.classes c
|
||||
join public.institute_memberships m on m.institute_id = c.institute_id
|
||||
where c.id = p_class and m.profile_id = auth.uid()
|
||||
and m.role in ('school_admin','department_head'))
|
||||
$$;
|
||||
|
||||
create or replace function public.is_institute_member_of_class(p_class uuid)
|
||||
returns boolean language sql stable security definer set search_path = public as $$
|
||||
select exists (
|
||||
select 1 from public.classes c
|
||||
join public.institute_memberships m on m.institute_id = c.institute_id
|
||||
where c.id = p_class and m.profile_id = auth.uid())
|
||||
$$;
|
||||
|
||||
--==========================================================================================
|
||||
-- 3. RLS — enable + (re)declare every policy so this file is the source of truth.
|
||||
-- Existing service_role / institute_read / er_own policies are preserved verbatim;
|
||||
-- the cs_read/cs_write/ct_read/ct_write policies are the NEW as-user grants.
|
||||
--==========================================================================================
|
||||
|
||||
alter table public.classes enable row level security;
|
||||
alter table public.class_teachers enable row level security;
|
||||
alter table public.class_students enable row level security;
|
||||
alter table public.enrollment_requests enable row level security;
|
||||
|
||||
-- classes ---------------------------------------------------------------------------------
|
||||
drop policy if exists classes_service_role on public.classes;
|
||||
create policy classes_service_role on public.classes
|
||||
using (auth.role() = 'service_role');
|
||||
|
||||
drop policy if exists classes_institute_read on public.classes;
|
||||
create policy classes_institute_read on public.classes for select to authenticated
|
||||
using (institute_id in (select public.user_institute_ids()));
|
||||
|
||||
-- NEW: teachers/admins of a class can update it; admins can insert/delete within their institute
|
||||
drop policy if exists classes_admin_write on public.classes;
|
||||
create policy classes_admin_write on public.classes for all to authenticated
|
||||
using (institute_id in (select public.user_institute_ids())
|
||||
and (public.is_class_admin(id) or public.is_class_teacher(id)))
|
||||
with check (institute_id in (select public.user_institute_ids()));
|
||||
|
||||
-- class_teachers --------------------------------------------------------------------------
|
||||
drop policy if exists ct_service_role on public.class_teachers;
|
||||
create policy ct_service_role on public.class_teachers
|
||||
using (auth.role() = 'service_role');
|
||||
|
||||
-- NEW: institute members can see who teaches a class; the teacher can see their own rows
|
||||
drop policy if exists ct_read on public.class_teachers;
|
||||
create policy ct_read on public.class_teachers for select to authenticated
|
||||
using (teacher_id = auth.uid() or public.is_institute_member_of_class(class_id));
|
||||
|
||||
-- NEW: only school admins assign/unassign teachers
|
||||
drop policy if exists ct_write on public.class_teachers;
|
||||
create policy ct_write on public.class_teachers for all to authenticated
|
||||
using (public.is_class_admin(class_id))
|
||||
with check (public.is_class_admin(class_id));
|
||||
|
||||
-- class_students --------------------------------------------------------------------------
|
||||
drop policy if exists cs_service_role on public.class_students;
|
||||
create policy cs_service_role on public.class_students
|
||||
using (auth.role() = 'service_role');
|
||||
|
||||
-- NEW: a student sees their own enrolment; teachers/admins of the class see the roster
|
||||
drop policy if exists cs_read on public.class_students;
|
||||
create policy cs_read on public.class_students for select to authenticated
|
||||
using (student_id = auth.uid()
|
||||
or public.is_class_teacher(class_id)
|
||||
or public.is_class_admin(class_id));
|
||||
|
||||
-- NEW: teachers (can_edit) and admins of the class manage enrolments
|
||||
drop policy if exists cs_write on public.class_students;
|
||||
create policy cs_write on public.class_students 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));
|
||||
|
||||
-- enrollment_requests ---------------------------------------------------------------------
|
||||
drop policy if exists er_service_role on public.enrollment_requests;
|
||||
create policy er_service_role on public.enrollment_requests
|
||||
using (auth.role() = 'service_role');
|
||||
|
||||
drop policy if exists er_own on public.enrollment_requests;
|
||||
create policy er_own on public.enrollment_requests for all to authenticated
|
||||
using (student_id = auth.uid())
|
||||
with check (student_id = auth.uid());
|
||||
|
||||
-- NEW: teachers/admins of the class can read + respond to requests for their class
|
||||
drop policy if exists er_class_staff on public.enrollment_requests;
|
||||
create policy er_class_staff on public.enrollment_requests 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));
|
||||
238
volumes/db/cc/72-exam-marker.sql
Normal file
238
volumes/db/cc/72-exam-marker.sql
Normal file
@ -0,0 +1,238 @@
|
||||
-- 72-exam-marker.sql
|
||||
-- Exam-marker operational tables (Supabase = source of truth for geometry/marks/submissions).
|
||||
-- Neo4j cc.public.exams holds the knowledge graph; joined by shared UUIDs (see spec §2).
|
||||
--
|
||||
-- Authorization is owned by this layer: the exam API calls Supabase AS THE USER, so these RLS
|
||||
-- policies are enforced (service_role policies cover the Neo4j-projection / seed paths only).
|
||||
-- Depends on: 71-class-management.sql (marking_batches.class_id → classes; user_institute_ids()).
|
||||
|
||||
--==========================================================================================
|
||||
-- 1. Tables
|
||||
--==========================================================================================
|
||||
|
||||
create table if not exists public.exam_templates (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
exam_id uuid references public.eb_exams(id) on delete set null, -- null for ad-hoc upload
|
||||
exam_code text, -- denormalised → Neo4j join
|
||||
institute_id uuid not null references public.institutes(id) on delete cascade,
|
||||
teacher_id uuid not null references public.profiles(id) on delete cascade,
|
||||
title text not null,
|
||||
subject text,
|
||||
source_file_id uuid references public.files(id) on delete set null, -- uploaded PDF (R2.2)
|
||||
page_count int,
|
||||
status text not null default 'draft' check (status in ('draft','ready','archived')),
|
||||
created_at timestamptz not null default timezone('utc', now()),
|
||||
updated_at timestamptz not null default timezone('utc', now())
|
||||
);
|
||||
|
||||
create table if not exists public.exam_questions (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
template_id uuid not null references public.exam_templates(id) on delete cascade,
|
||||
parent_id uuid references public.exam_questions(id) on delete cascade,
|
||||
label text not null,
|
||||
"order" int not null default 0,
|
||||
max_marks numeric not null default 0,
|
||||
answer_type text check (answer_type in ('written','mcq','short','diagram')),
|
||||
mcq_options jsonb,
|
||||
mark_scheme jsonb not null default '{}'::jsonb, -- MarkScheme union from exam-marker types.ts
|
||||
is_container boolean not null default false, -- true → Neo4j Question, false → Part
|
||||
spec_ref text, -- manual spec-point tag → ASSESSES (R3.5.3)
|
||||
created_at timestamptz not null default timezone('utc', now()),
|
||||
updated_at timestamptz not null default timezone('utc', now())
|
||||
);
|
||||
|
||||
create table if not exists public.exam_response_areas (
|
||||
id uuid primary key default gen_random_uuid(), -- == Neo4j Region.uuid_string
|
||||
question_id uuid not null references public.exam_questions(id) on delete cascade,
|
||||
template_id uuid not null references public.exam_templates(id) on delete cascade, -- RLS denorm
|
||||
page int not null,
|
||||
bounds jsonb not null, -- {x,y,w,h}
|
||||
kind text not null check (kind in ('response','context')),
|
||||
response_form text check (response_form in
|
||||
('lines','answer-box','working','diagram','tick-boxes','table','blanks')),
|
||||
source text not null default 'manual' check (source in ('manual','ai')),
|
||||
confirmed boolean not null default true,
|
||||
confidence numeric,
|
||||
created_at timestamptz not null default timezone('utc', now())
|
||||
);
|
||||
|
||||
create table if not exists public.exam_boundaries (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
template_id uuid not null references public.exam_templates(id) on delete cascade,
|
||||
question_id uuid references public.exam_questions(id) on delete set null,
|
||||
label text,
|
||||
page_index int not null,
|
||||
y numeric not null,
|
||||
bounds jsonb,
|
||||
source text not null default 'manual' check (source in ('manual','ai')),
|
||||
confirmed boolean not null default true,
|
||||
created_at timestamptz not null default timezone('utc', now())
|
||||
);
|
||||
|
||||
create table if not exists public.marking_batches (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
template_id uuid not null references public.exam_templates(id) on delete cascade,
|
||||
class_id uuid references public.classes(id) on delete set null, -- roster via class_students
|
||||
institute_id uuid not null references public.institutes(id) on delete cascade,
|
||||
teacher_id uuid not null references public.profiles(id) on delete cascade, -- batch owner (R2.4)
|
||||
title text,
|
||||
status text not null default 'open' check (status in ('open','marking','complete','archived')),
|
||||
created_at timestamptz not null default timezone('utc', now()),
|
||||
updated_at timestamptz not null default timezone('utc', now())
|
||||
);
|
||||
|
||||
create table if not exists public.student_submissions (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
batch_id uuid not null references public.marking_batches(id) on delete cascade,
|
||||
student_id uuid references public.profiles(id) on delete set null, -- null until matched
|
||||
student_name text,
|
||||
scan_file_id uuid references public.files(id) on delete set null,
|
||||
scan_url text,
|
||||
qr_code text,
|
||||
matching_method text check (matching_method in ('ordered','ocr_name','qr_code','manual')),
|
||||
match_confidence numeric,
|
||||
page_start int,
|
||||
page_count int,
|
||||
status text not null default 'unmatched'
|
||||
check (status in ('unmatched','matched','marking','complete','absent')),
|
||||
annotation_snapshot jsonb,
|
||||
created_at timestamptz not null default timezone('utc', now()),
|
||||
updated_at timestamptz not null default timezone('utc', now())
|
||||
);
|
||||
|
||||
create table if not exists public.mark_entries (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
submission_id uuid not null references public.student_submissions(id) on delete cascade,
|
||||
question_id uuid not null references public.exam_questions(id) on delete cascade,
|
||||
batch_id uuid not null references public.marking_batches(id) on delete cascade, -- RLS denorm
|
||||
awarded_marks numeric not null default 0,
|
||||
mark_scheme_detail jsonb not null default '{}'::jsonb,
|
||||
annotation_shape_ids jsonb not null default '[]'::jsonb,
|
||||
comment text,
|
||||
marked_by text not null default 'teacher' check (marked_by in ('teacher','ai')),
|
||||
ai_confidence numeric,
|
||||
confirmed boolean not null default true,
|
||||
marked_at timestamptz not null default timezone('utc', now())
|
||||
);
|
||||
|
||||
create index if not exists idx_exam_templates_institute on public.exam_templates(institute_id);
|
||||
create index if not exists idx_exam_templates_teacher on public.exam_templates(teacher_id);
|
||||
create index if not exists idx_exam_questions_template on public.exam_questions(template_id);
|
||||
create index if not exists idx_exam_questions_parent on public.exam_questions(parent_id);
|
||||
create index if not exists idx_exam_regions_question on public.exam_response_areas(question_id);
|
||||
create index if not exists idx_exam_regions_template on public.exam_response_areas(template_id);
|
||||
create index if not exists idx_exam_boundaries_template on public.exam_boundaries(template_id);
|
||||
create index if not exists idx_batches_template on public.marking_batches(template_id);
|
||||
create index if not exists idx_batches_institute on public.marking_batches(institute_id);
|
||||
create index if not exists idx_submissions_batch on public.student_submissions(batch_id);
|
||||
create index if not exists idx_marks_submission on public.mark_entries(submission_id);
|
||||
create index if not exists idx_marks_batch on public.mark_entries(batch_id);
|
||||
|
||||
--==========================================================================================
|
||||
-- 2. updated_at triggers (reuse public.handle_updated_at from 62-functions-triggers.sql)
|
||||
--==========================================================================================
|
||||
|
||||
drop trigger if exists handle_exam_templates_updated_at on public.exam_templates;
|
||||
create trigger handle_exam_templates_updated_at before update on public.exam_templates
|
||||
for each row execute function public.handle_updated_at();
|
||||
|
||||
drop trigger if exists handle_exam_questions_updated_at on public.exam_questions;
|
||||
create trigger handle_exam_questions_updated_at before update on public.exam_questions
|
||||
for each row execute function public.handle_updated_at();
|
||||
|
||||
drop trigger if exists handle_marking_batches_updated_at on public.marking_batches;
|
||||
create trigger handle_marking_batches_updated_at before update on public.marking_batches
|
||||
for each row execute function public.handle_updated_at();
|
||||
|
||||
drop trigger if exists handle_student_submissions_updated_at on public.student_submissions;
|
||||
create trigger handle_student_submissions_updated_at before update on public.student_submissions
|
||||
for each row execute function public.handle_updated_at();
|
||||
|
||||
--==========================================================================================
|
||||
-- 3. RLS — every table: a service_role passthrough (Neo4j projection / seeds) + as-user policies
|
||||
--==========================================================================================
|
||||
|
||||
alter table public.exam_templates enable row level security;
|
||||
alter table public.exam_questions enable row level security;
|
||||
alter table public.exam_response_areas enable row level security;
|
||||
alter table public.exam_boundaries enable row level security;
|
||||
alter table public.marking_batches enable row level security;
|
||||
alter table public.student_submissions enable row level security;
|
||||
alter table public.mark_entries enable row level security;
|
||||
|
||||
-- exam_templates -------------------------------------------------------------------------
|
||||
drop policy if exists exam_templates_service on public.exam_templates;
|
||||
create policy exam_templates_service on public.exam_templates using (auth.role() = 'service_role');
|
||||
drop policy if exists exam_templates_read on public.exam_templates;
|
||||
create policy exam_templates_read on public.exam_templates for select to authenticated
|
||||
using (institute_id in (select public.user_institute_ids()));
|
||||
drop policy if exists exam_templates_write on public.exam_templates;
|
||||
create policy exam_templates_write on public.exam_templates for all to authenticated
|
||||
using (teacher_id = auth.uid() and institute_id in (select public.user_institute_ids()))
|
||||
with check (teacher_id = auth.uid() and institute_id in (select public.user_institute_ids()));
|
||||
|
||||
-- exam_questions / exam_response_areas / exam_boundaries: cascade authz from owning template
|
||||
drop policy if exists exam_questions_service on public.exam_questions;
|
||||
create policy exam_questions_service on public.exam_questions using (auth.role() = 'service_role');
|
||||
drop policy if exists exam_questions_all on public.exam_questions;
|
||||
create policy exam_questions_all on public.exam_questions for all to authenticated
|
||||
using (exists (select 1 from public.exam_templates t
|
||||
where t.id = exam_questions.template_id
|
||||
and t.institute_id in (select public.user_institute_ids())))
|
||||
with check (exists (select 1 from public.exam_templates t
|
||||
where t.id = exam_questions.template_id and t.teacher_id = auth.uid()));
|
||||
|
||||
drop policy if exists exam_regions_service on public.exam_response_areas;
|
||||
create policy exam_regions_service on public.exam_response_areas using (auth.role() = 'service_role');
|
||||
drop policy if exists exam_regions_all on public.exam_response_areas;
|
||||
create policy exam_regions_all on public.exam_response_areas for all to authenticated
|
||||
using (exists (select 1 from public.exam_templates t
|
||||
where t.id = exam_response_areas.template_id
|
||||
and t.institute_id in (select public.user_institute_ids())))
|
||||
with check (exists (select 1 from public.exam_templates t
|
||||
where t.id = exam_response_areas.template_id and t.teacher_id = auth.uid()));
|
||||
|
||||
drop policy if exists exam_boundaries_service on public.exam_boundaries;
|
||||
create policy exam_boundaries_service on public.exam_boundaries using (auth.role() = 'service_role');
|
||||
drop policy if exists exam_boundaries_all on public.exam_boundaries;
|
||||
create policy exam_boundaries_all on public.exam_boundaries for all to authenticated
|
||||
using (exists (select 1 from public.exam_templates t
|
||||
where t.id = exam_boundaries.template_id
|
||||
and t.institute_id in (select public.user_institute_ids())))
|
||||
with check (exists (select 1 from public.exam_templates t
|
||||
where t.id = exam_boundaries.template_id and t.teacher_id = auth.uid()));
|
||||
|
||||
-- marking_batches: read = same institute (colleagues), write = owning teacher (R2.4)
|
||||
drop policy if exists marking_batches_service on public.marking_batches;
|
||||
create policy marking_batches_service on public.marking_batches using (auth.role() = 'service_role');
|
||||
drop policy if exists marking_batches_read on public.marking_batches;
|
||||
create policy marking_batches_read on public.marking_batches for select to authenticated
|
||||
using (institute_id in (select public.user_institute_ids()));
|
||||
drop policy if exists marking_batches_write on public.marking_batches;
|
||||
create policy marking_batches_write on public.marking_batches for all to authenticated
|
||||
using (teacher_id = auth.uid() and institute_id in (select public.user_institute_ids()))
|
||||
with check (teacher_id = auth.uid() and institute_id in (select public.user_institute_ids()));
|
||||
|
||||
-- student_submissions: authz cascades from batch ownership
|
||||
drop policy if exists submissions_service on public.student_submissions;
|
||||
create policy submissions_service on public.student_submissions using (auth.role() = 'service_role');
|
||||
drop policy if exists submissions_all on public.student_submissions;
|
||||
create policy submissions_all on public.student_submissions for all to authenticated
|
||||
using (exists (select 1 from public.marking_batches b
|
||||
where b.id = student_submissions.batch_id and b.teacher_id = auth.uid()))
|
||||
with check (exists (select 1 from public.marking_batches b
|
||||
where b.id = student_submissions.batch_id and b.teacher_id = auth.uid()));
|
||||
|
||||
-- mark_entries: teacher (batch owner) full; student may read their own marks (R1.5, UI deferred)
|
||||
drop policy if exists marks_service on public.mark_entries;
|
||||
create policy marks_service on public.mark_entries using (auth.role() = 'service_role');
|
||||
drop policy if exists marks_teacher_all on public.mark_entries;
|
||||
create policy marks_teacher_all on public.mark_entries for all to authenticated
|
||||
using (exists (select 1 from public.marking_batches b
|
||||
where b.id = mark_entries.batch_id and b.teacher_id = auth.uid()))
|
||||
with check (exists (select 1 from public.marking_batches b
|
||||
where b.id = mark_entries.batch_id and b.teacher_id = auth.uid()));
|
||||
drop policy if exists marks_student_read on public.mark_entries;
|
||||
create policy marks_student_read on public.mark_entries for select to authenticated
|
||||
using (exists (select 1 from public.student_submissions s
|
||||
where s.id = mark_entries.submission_id and s.student_id = auth.uid()));
|
||||
Loading…
x
Reference in New Issue
Block a user