Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions supabase/migrations/20260813002436_stable_learning_tools.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
create table public.review_rubrics (
id uuid primary key default gen_random_uuid(),
class_id uuid not null references public.classes(id) on delete cascade,
title text not null check (char_length(title) between 1 and 80),
criteria jsonb not null default '[]'::jsonb,
created_by uuid not null references public.profiles(id),
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
constraint review_rubrics_criteria_check check (
jsonb_typeof(criteria) = 'array'
and jsonb_array_length(criteria) between 1 and 10
and octet_length(criteria::text) <= 4000
)
);

alter table public.assignments
add column rubric_id uuid references public.review_rubrics(id) on delete set null;

create index review_rubrics_class_idx on public.review_rubrics(class_id);
create index assignments_rubric_idx on public.assignments(rubric_id)
where rubric_id is not null;

alter table public.review_rubrics enable row level security;

create policy "Staff view review rubrics"
on public.review_rubrics for select
to authenticated
using (public.is_class_staff(class_id));

create policy "Staff manage review rubrics"
on public.review_rubrics for all
to authenticated
using (public.is_class_staff(class_id))
with check (
public.is_class_staff(class_id)
and created_by = (select auth.uid())
);

grant select, insert, update, delete on public.review_rubrics to authenticated;
46 changes: 45 additions & 1 deletion supabase/tests/rls.sql
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
begin;

create extension if not exists pgtap with schema extensions;
select plan(45);
select plan(49);

insert into auth.users (
id, instance_id, aud, role, email, encrypted_password,
Expand Down Expand Up @@ -308,6 +308,29 @@ select is(
'students cannot list classroom invitation links'
);

select is(
(select count(*) from public.review_rubrics),
0::bigint,
'students cannot read classroom review guides'
);

select throws_ok(
$$
insert into public.review_rubrics (
class_id, title, criteria, created_by
)
values (
'00000000-0000-0000-0000-000000000201',
'Forged guide',
'[{"id":"answer","label":"Da la respuesta"}]',
'00000000-0000-0000-0000-000000000102'
)
$$,
'42501',
'new row violates row-level security policy for table "review_rubrics"',
'students cannot create review guides'
);

select throws_ok(
$$
select *
Expand Down Expand Up @@ -471,6 +494,27 @@ select set_config(
);
select set_config('request.jwt.claim.role', 'authenticated', true);

select lives_ok(
$$
insert into public.review_rubrics (
class_id, title, criteria, created_by
)
values (
'00000000-0000-0000-0000-000000000201',
'Funciones simples',
'[{"id":"answer","label":"Devuelve el valor pedido"}]',
'00000000-0000-0000-0000-000000000101'
)
$$,
'class staff can create reusable review guides'
);

select is(
(select count(*) from public.review_rubrics),
1::bigint,
'class staff can read their reusable review guides'
);

select is(
(
select count(*)
Expand Down
53 changes: 53 additions & 0 deletions v2/e2e/mentor.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { expect, test, type Page } from "@playwright/test";
import { createDemoSnapshot } from "../src/data/demo-classroom";

async function loginAsMentor(page: Page) {
await page.goto("./");
Expand Down Expand Up @@ -102,6 +103,58 @@ test("mentor creates an assignment for selected students", async ({ page }) => {
).toContainText("Aviso fallido");
});

test("mentor creates a reusable review guide and assigns it to a task", async ({
page,
}) => {
await loginAsMentor(page);
await page.getByRole("link", { name: "Tareas", exact: true }).click();
await page.getByRole("button", { name: "Nueva pauta" }).click();
await page.getByLabel("Nombre").fill("Funciones simples");
await page
.getByLabel("Una pregunta por línea")
.fill("Devuelve el valor pedido\nNo imprime la respuesta");
await page.getByRole("button", { name: "Guardar pauta" }).click();
await expect(page.getByText("Funciones simples")).toBeVisible();

await page.getByRole("button", { name: "Nueva tarea" }).click();
await page.getByLabel("Misión").selectOption({ index: 1 });
await page.getByLabel("Título de la tarea").fill("Tarea con pauta");
await page.getByLabel("Pauta de corrección (opcional)").selectOption({
label: "Funciones simples",
});
await page.getByRole("button", { name: "Publicar tarea" }).click();
await expect(page.getByText("Tarea con pauta")).toBeVisible();
});

test("mentor can approve several available submissions", async ({ page }) => {
await loginAsMentor(page);
await page.evaluate((baseSnapshot) => {
const key = "tomatin.v2.demo-classroom";
const snapshot = JSON.parse(
window.localStorage.getItem(key) ?? JSON.stringify(baseSnapshot),
);
const sourceAttempt = snapshot.attempts[0];
snapshot.progress = snapshot.progress.map((entry: { userId: string; assignmentId: string; status: string }) =>
entry.userId === "student-03" && entry.assignmentId === "assignment-once"
? { ...entry, status: "awaiting_review", submittedAt: new Date().toISOString() }
: entry,
);
snapshot.attempts.push({
...sourceAttempt,
id: "attempt-antonia-once",
userId: "student-03",
createdAt: new Date().toISOString(),
});
window.localStorage.setItem(key, JSON.stringify(snapshot));
}, createDemoSnapshot());
await page.reload();
await page.getByRole("link", { name: /^Revisiones/ }).click();
await page.getByLabel("Seleccionar entrega de Camila Rojas").check();
await page.getByLabel("Seleccionar entrega de Antonia Pérez").check();
await page.getByRole("button", { name: "Aprobar 2" }).click();
await expect(page.getByText("Cola al día")).toBeVisible();
});

test("mentor creates a reward and fulfills a student redemption", async ({
page,
}) => {
Expand Down
14 changes: 14 additions & 0 deletions v2/src/data/demo-classroom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,20 @@ export function createDemoSnapshot(): ClassroomSnapshot {
updatedAt: dateAgo(1),
},
],
reviewRubrics: [
{
id: "rubric-default",
classId: "class-tomatin-2026",
title: "Revisión general",
criteria: [
{ id: "correctness", label: "Da la respuesta correcta" },
{ id: "readability", label: "Se entiende cómo lo resolvió" },
{ id: "edge-cases", label: "Funciona también con otros casos" },
],
createdAt: dateAgo(5),
updatedAt: dateAgo(5),
},
],
};
}

Expand Down
Loading