Nouvelle page /urne : vote quotidien inspiré du vote à l'urne de l'Athènes antique. Chaque Citoyen dépose un jeton par jour sur une autre personne (jamais lui-même), avec confirmation à deux temps avant l'envoi — le vote est définitif pour la journée, aucune RPC de modification. Les jetons s'accumulent sur toute la semaine dans un classement public et cumulatif, identique pour tout le monde y compris les Archontes : contraste volontaire avec Le Mur de la Honte, ici personne (Archontes compris) ne voit qui a voté pour qui, seul le total par personne est public. Nouvelle table urn_votes (append-only, comme points_log) + deux RPC : urn_vote_counts() agrège les votes sans jamais exposer une ligne individuelle, cast_urn_vote() applique les règles (pas de vote pour un juge ni pour soi-même, un vote par jour). Les Archontes ne votent pas et ne peuvent pas recevoir de jetons.
This commit is contained in:
@@ -1131,3 +1131,112 @@ end;
|
||||
$$;
|
||||
|
||||
grant execute on function public.post_wall_note(text) to authenticated;
|
||||
|
||||
-- 15. L'Urne de l'Agora -------------------------------------------------------
|
||||
-- Vote quotidien inspiré du vote à l'urne de l'Athènes antique : chaque jour
|
||||
-- chaque Citoyen dépose un jeton sur une personne de son choix. Contrairement
|
||||
-- au Mur de la Honte, les jetons s'accumulent sur toute la semaine dans un
|
||||
-- classement public et cumulatif — identique pour tout le monde, Archontes
|
||||
-- compris : contrairement à chariot_submissions/wall_notes, les Archontes
|
||||
-- n'ont ici AUCUNE vision privilégiée sur qui a voté pour qui, seul le total
|
||||
-- par personne est public. Les Archontes ne peuvent ni voter ni recevoir de
|
||||
-- jetons.
|
||||
|
||||
create table if not exists public.urn_votes (
|
||||
id bigint generated always as identity primary key,
|
||||
voter_id uuid not null references public.profiles (id) on delete cascade,
|
||||
target_id uuid not null references public.profiles (id) on delete cascade,
|
||||
created_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
alter table public.urn_votes enable row level security;
|
||||
revoke insert, update, delete on public.urn_votes from authenticated, anon;
|
||||
grant select on public.urn_votes to authenticated;
|
||||
|
||||
-- Pas d'exception juge ici (contrairement à chariot_submissions/wall_notes) :
|
||||
-- même les Archontes ne voient que leur propre ligne (pour savoir "j'ai déjà
|
||||
-- voté aujourd'hui") — qui a voté pour qui ne regarde personne d'autre.
|
||||
drop policy if exists "urn_votes readable by voter only" on public.urn_votes;
|
||||
create policy "urn_votes readable by voter only"
|
||||
on public.urn_votes for select to authenticated
|
||||
using (voter_id = auth.uid());
|
||||
|
||||
do $$
|
||||
begin
|
||||
alter publication supabase_realtime add table public.urn_votes;
|
||||
exception
|
||||
when duplicate_object then null;
|
||||
end $$;
|
||||
|
||||
-- Seul moyen de calculer un total public par personne sans jamais exposer
|
||||
-- une ligne individuelle (qui a voté pour qui) à qui que ce soit. Agrégé sur
|
||||
-- tout l'historique (pas de filtre de date, contrairement à wall_notes_today
|
||||
-- qui ne montre que le jour courant) : les jetons s'accumulent toute la
|
||||
-- semaine.
|
||||
create or replace function public.urn_vote_counts()
|
||||
returns table (target_id uuid, votes bigint)
|
||||
language sql
|
||||
security definer
|
||||
set search_path = public
|
||||
as $$
|
||||
select v.target_id, count(*) as votes
|
||||
from public.urn_votes v
|
||||
group by v.target_id;
|
||||
$$;
|
||||
|
||||
grant execute on function public.urn_vote_counts() to authenticated;
|
||||
|
||||
-- Seul point d'entrée pour voter. Un vote par Citoyen par jour, définitif
|
||||
-- (aucune RPC de modification/suppression).
|
||||
create or replace function public.cast_urn_vote(p_target_id uuid)
|
||||
returns public.urn_votes
|
||||
language plpgsql
|
||||
security definer
|
||||
set search_path = public
|
||||
as $$
|
||||
declare
|
||||
v_caller_role text;
|
||||
v_target_role text;
|
||||
v_today date := (now() at time zone 'Europe/Paris')::date;
|
||||
v_already_voted boolean;
|
||||
v_row public.urn_votes;
|
||||
begin
|
||||
if auth.uid() is null then
|
||||
raise exception 'authentication required';
|
||||
end if;
|
||||
|
||||
select role into v_caller_role from public.profiles where id = auth.uid();
|
||||
if v_caller_role = 'judge' then
|
||||
raise exception 'only citizens can vote';
|
||||
end if;
|
||||
|
||||
if p_target_id = auth.uid() then
|
||||
raise exception 'cannot vote for yourself';
|
||||
end if;
|
||||
|
||||
select role into v_target_role from public.profiles where id = p_target_id;
|
||||
if v_target_role is null then
|
||||
raise exception 'target member not found';
|
||||
end if;
|
||||
if v_target_role = 'judge' then
|
||||
raise exception 'cannot vote for a judge';
|
||||
end if;
|
||||
|
||||
select exists (
|
||||
select 1 from public.urn_votes
|
||||
where voter_id = auth.uid()
|
||||
and (created_at at time zone 'Europe/Paris')::date = v_today
|
||||
) into v_already_voted;
|
||||
if v_already_voted then
|
||||
raise exception 'already voted today';
|
||||
end if;
|
||||
|
||||
insert into public.urn_votes (voter_id, target_id)
|
||||
values (auth.uid(), p_target_id)
|
||||
returning * into v_row;
|
||||
|
||||
return v_row;
|
||||
end;
|
||||
$$;
|
||||
|
||||
grant execute on function public.cast_urn_vote(uuid) to authenticated;
|
||||
|
||||
Reference in New Issue
Block a user