Skip to content
Snippets Groups Projects
Select Git revision
  • 57c70632ef7f528fe2ae6b358cbc4d1565213a1b
  • devel default
  • master
  • fo
  • jirka/typing
  • fo-base
  • mj/submit-images
  • jk/issue-96
  • jk/issue-196
  • honza/add-contestant
  • honza/mr7
  • honza/mrf
  • honza/mrd
  • honza/mra
  • honza/mr6
  • honza/submit-images
  • honza/kolo-vs-soutez
  • jh-stress-test-wip
  • shorten-schools
19 results

db.ddl

Blame
  • Martin Mares's avatar
    Martin Mareš authored
    c621430f
    History
    db.ddl 15.83 KiB
    -- CREATE ROLE mo_osmo LOGIN PASSWORD 'pass';
    -- CREATE DATABASE mo_osmo WITH OWNER=mo_osmo;
    -- GRANT mo_osmo TO some_admin;
    -- CREATE EXTENSION unaccent;
    
    SET ROLE mo_osmo;
    
    -- Funkce pro odakcentování textu pomocí extension unaccent.
    -- Je immutable, takže se dá používat i v indexech.
    -- Zdroj: http://stackoverflow.com/questions/11005036/does-postgresql-support-accent-insensitive-collations
    CREATE OR REPLACE FUNCTION f_unaccent(text)
      RETURNS text AS
    $func$
    SELECT unaccent('unaccent', $1)
    $func$  LANGUAGE sql IMMUTABLE SET search_path = public, pg_temp;
    
    -- Uživatelský účet
    CREATE TABLE users (
    	user_id		serial		PRIMARY KEY,
    	email		varchar(255)	UNIQUE NOT NULL,
    	first_name	varchar(255)	COLLATE "cs_CZ" NOT NULL,
    	last_name	varchar(255)	COLLATE "cs_CZ" NOT NULL,
    	is_org		boolean		NOT NULL DEFAULT false,		-- přístup do organizátorské části webu
    	is_admin	boolean		NOT NULL DEFAULT false,		-- globální administrátorské právo
    	is_test		boolean		NOT NULL DEFAULT false,		-- testovací účastník, není vidět ve výsledkovkách
    	created_at	timestamp with time zone	NOT NULL DEFAULT CURRENT_TIMESTAMP,
    	last_login_at	timestamp with time zone	DEFAULT NULL,
    	reset_at	timestamp with time zone	DEFAULT NULL,	-- poslední reset/aktivace nebo žádost o ně
    	password_hash	varchar(255)	DEFAULT NULL,			-- heš hesla (je-li nastaveno)
    	note		text		NOT NULL DEFAULT ''		-- poznámka viditelná pro orgy
    );
    
    -- Hierarchie regionů a organizací
    
    CREATE TYPE place_type AS ENUM (
    	'region',	-- správní celek
    	'school',
    	'site'		-- soutěžní místo
    );
    
    CREATE TABLE places (
    	place_id	serial		PRIMARY KEY,
    	level		int		NOT NULL,			-- úroveň v hierarchii:
    									-- 	0	root
    									-- 	1	kraj
    									-- 	2	okres
    									--	3	město
    									-- 	4	škola / soutěžní místo
    	parent		int		REFERENCES places(place_id),	-- NULL pro root
    	name		varchar(255)	COLLATE "cs_CZ",		-- oficiální jméno
    	code		varchar(255)	UNIQUE COLLATE "cs_CZ",		-- zkratka pro zadávání
    	type		place_type	NOT NULL,
    	nuts		varchar(255)	UNIQUE DEFAULT NULL,		-- evropský NUTS/LAU kód regionu
    	note		text		NOT NULL DEFAULT ''		-- uživatelská poznámka
    );
    
    CREATE INDEX places_parent_index ON places (parent);
    -- XXX: Potřebujeme operator class text_pattern_ops, aby index fungoval na prefixové LIKE
    CREATE INDEX places_noacc_index ON places ((lower(f_unaccent(name))) text_pattern_ops);
    
    -- Rekurzivní dotaz na nadřazené regiony:
    --	WITH RECURSIVE parent_regions(parent, place_id) AS (
    --		SELECT parent, place_id FROM places WHERE place_id=...
    --		UNION select p.parent, p.place_id FROM parent_regions pr, places p
    --			WHERE p.place_id = pr.parent
    --	) SELECT parent FROM parent_regions;
    
    -- Podobně na podřízené:
    --	WITH RECURSIVE descendant_regions(descendant, place_id) AS (
    --		SELECT place_id, parent FROM places WHERE parent=...
    --		UNION select p.place_id, p.parent FROM descendant_regions pr, places p
    --			WHERE p.parent = pr.descendant
    --	) SELECT descendant FROM descendant_regions;
    
    -- Škola je speciální typ místa s doplňujícími informacemi
    CREATE TABLE schools (
    	place_id	int		PRIMARY KEY REFERENCES places(place_id) ON DELETE CASCADE,
    	red_izo		varchar(255)	DEFAULT NULL,
    	ico		varchar(255)	DEFAULT NULL,
    	official_name	varchar(255)	DEFAULT NULL,
    	address		varchar(255)	DEFAULT NULL,
    	is_zs		boolean		NOT NULL DEFAULT false,
    	is_ss		boolean		NOT NULL DEFAULT false
    );
    
    -- Soutěžní kola
    
    CREATE TYPE round_state AS ENUM (
    	'preparing',			-- v přípravě (viditelné pouze organizátorům)
    	'running',			-- je možno odevzdávat
    	'grading',			-- je možno opravovat a vyplňovat body
    	'closed',			-- uzavřeno, není dovoleno nic měnit, zveřejněny výsledky
    	-- Garanta stavy neomezují, vždycky může všechno.
    	-- Ve stavu "running" mohou odevzdávat účastníci i dozor, a to i po termínu,
    	-- jen se odevzdaná řešení zobrazují jako opožděná.
    	'delegate'			-- každá soutěž má svůj stav
    );
    
    CREATE TYPE score_mode AS ENUM (
    	'basic',			-- základní mód výsledkovky se sdílenými místy
    	'mo'				-- jednoznačné pořadí podle pravidel MO
    );
    
    CREATE TYPE enroll_mode AS ENUM (	-- režim přihlašování účastníků
    	'manual',			-- přihlašuje organizátor
    	'register',			-- účastník se registruje
    	'confirm'			-- účastník se registruje, ale org ho musí potvrdit
    );
    
    CREATE TABLE rounds (
    	round_id	serial		PRIMARY KEY,
    	master_round_id	int		DEFAULT NULL REFERENCES rounds(round_id),
    	year		int		NOT NULL,			-- ročník MO
    	category	varchar(2)	NOT NULL,			-- "A", "Z5" apod.
    	seq		int		NOT NULL,			-- 1=domácí kolo atd.
    	part		int		NOT NULL DEFAULT 0,		-- část kola (nenulová u dělených kol)
    	level		int		NOT NULL,			-- úroveň hierarchie míst
    	name		varchar(255)	NOT NULL,			-- zobrazované jméno ("Krajské kolo" apod.)
    	state		round_state	NOT NULL DEFAULT 'preparing',	-- stav kola
    	tasks_file	varchar(255)	DEFAULT NULL,			-- jméno souboru se zadáním úloh
    	ct_tasks_start	timestamp with time zone	DEFAULT NULL,	-- od kdy účastníci vidí zadání
    	ct_submit_end	timestamp with time zone	DEFAULT NULL,	-- do kdy účastníci mohou regulérně odevzdávat
    	pr_tasks_start	timestamp with time zone	DEFAULT NULL,	-- od kdy dozor vidí zadání
    	pr_submit_end	timestamp with time zone	DEFAULT NULL,	-- do kdy dozor může regulérně odevzdávat
    	score_mode	score_mode	NOT NULL DEFAULT 'basic',	-- mód výsledkovky
    	score_winner_limit	int	DEFAULT NULL,			-- bodový limit na označení za vítěze
    	score_successful_limit	int	DEFAULT NULL,			-- bodový limit na označení za úspěšného řešitele
    	points_step	numeric(2,1)	NOT NULL DEFAULT 1,		-- s jakou přesností jsou přidělovány body (celé aneb 1, 0.5, 0.1)
    	has_messages	boolean		NOT NULL DEFAULT false,		-- má zprávičky
    	enroll_mode	enroll_mode	NOT NULL DEFAULT 'manual',	-- režim přihlašování (pro vyšší kola vždy 'manual')
    	enroll_advert	varchar(255)	NOT NULL DEFAULT '',		-- popis v přihlašovacím formuláři
    	UNIQUE (year, category, seq, part)
    );
    
    CREATE INDEX rounds_master_round_id_index ON rounds (master_round_id);
    
    -- Soutěže (instance kola v konkrétním místě)
    CREATE TABLE contests (
    	contest_id		serial		PRIMARY KEY,
    	master_contest_id	int		DEFAULT NULL REFERENCES contests(contest_id),
    	round_id		int		NOT NULL REFERENCES rounds(round_id),
    	place_id		int		NOT NULL REFERENCES places(place_id),
    	state			round_state	NOT NULL DEFAULT 'preparing',	-- používá se, pokud round.state='delegate', jinak kopíruje round.state
    	UNIQUE (round_id, place_id)
    );
    
    CREATE INDEX contests_master_contest_id_index ON contests (master_contest_id);
    
    -- Detaily účastníka
    CREATE TABLE participants (
    	user_id		int		NOT NULL REFERENCES users(user_id),
    	year		int		NOT NULL,			-- ročník MO
    	school		int		NOT NULL REFERENCES places(place_id),
    	birth_year	int		NOT NULL,
    	grade		varchar(20)	NOT NULL,			-- třída ve tvaru "X/Y"
    	registered_on	timestamp with time zone	DEFAULT NULL,	-- kdy se účastník přihlásil (NULL, pokud ho přihlásil organizátor)
    	PRIMARY KEY (user_id, year)
    );
    
    -- Účast v soutěžním kole
    
    CREATE TYPE part_state AS ENUM (
    	'registered',		-- sám se přihlásil, čeká na potvrzení organizátorem
    	-- 'invited',		-- pozván (už nepoužíváme)
    	'active',		-- soutěží (přihlášku zadal/potvrdil organizátor)
    	'refused',		-- organizátor odmítl přihlášku
    	-- 'present',		-- soutěžil (už nepoužíváme)
    	'absent',		-- nedorazil
    	'disqualified'		-- diskvalifikovaný
    );
    
    CREATE TABLE participations (
    	user_id		int		NOT NULL REFERENCES users(user_id),
    	contest_id	int		NOT NULL REFERENCES contests(contest_id),
    	place_id	int		NOT NULL REFERENCES places(place_id),	-- konkrétní soutěžní místo (default: region contestu)
    	state		part_state	NOT NULL,
    	PRIMARY KEY (user_id, contest_id)
    );
    
    CREATE INDEX participations_contest_id_index ON participations (contest_id, place_id);
    
    -- Úloha
    CREATE TABLE tasks (
    	task_id		serial		PRIMARY KEY,
    	round_id	int		NOT NULL REFERENCES rounds(round_id),
    	code		varchar(255)	NOT NULL,			-- např. "P-I-1"
    	name		varchar(255)	NOT NULL,
    	max_points	numeric(5,1)	DEFAULT NULL,			-- maximální počet bodů, pokud je nastaven, nelze zadat více bodů
    	UNIQUE (round_id, code)
    );
    
    -- Spisek (odevzdané řešení, opoznámkované řešení apod.)
    
    CREATE TYPE paper_type AS ENUM (
    	'solution',	-- odevzdané řešení
    	'feedback'	-- komentáře opravovatelů
    );
    
    CREATE TABLE papers (
    	paper_id	serial		PRIMARY KEY,
    	for_task	int		NOT NULL REFERENCES tasks(task_id),
    	for_user	int		NOT NULL REFERENCES users(user_id),
    	type		paper_type	NOT NULL,
    	uploaded_by	int		NOT NULL REFERENCES users(user_id),
    	uploaded_at	timestamp with time zone	NOT NULL DEFAULT CURRENT_TIMESTAMP,
    	pages		int		DEFAULT NULL,			-- počet stránek
    	bytes		int		DEFAULT NULL,			-- velikost souboru
    	file_name	varchar(255)	DEFAULT NULL,			-- relativní cesta k souboru
    	orig_file_name	varchar(255)	DEFAULT NULL,			-- původní cesta, pokud PDF bylo poničené
    	note		text		NOT NULL DEFAULT ''		-- komentář uploadujícího
    	-- Sémantika práce s poničenými soubory:
    	--    - správná PDF mají orig_file_name=NULL
    	--    - pokud někdo odevzdá poničené, vyplní se orig_file_name, ale file_name=NULL
    	--    - časem se spustí oprava, která vyplní i file_name a přepočítá bytes
    );
    
    CREATE INDEX papers_for_task_index ON papers (for_task);
    CREATE INDEX papers_for_user_index ON papers (for_user);
    
    -- Odevzdané řešení
    CREATE TABLE solutions (
    	task_id		int		NOT NULL REFERENCES tasks(task_id),
    	user_id		int		NOT NULL REFERENCES users(user_id),
    	final_submit	int		DEFAULT NULL REFERENCES papers(paper_id),	-- verze odevzdání, která se má hodnotit
    	final_feedback	int		DEFAULT NULL REFERENCES papers(paper_id),	-- verze komentáře opravovatelů, kterou má vidět účastník
    	points		numeric(5,1)	DEFAULT NULL,
    	note		text		NOT NULL DEFAULT '',			-- komentář pro řešitele
    	org_note	text		NOT NULL DEFAULT '',			-- komentář viditelný jen organizátorům
    	PRIMARY KEY (task_id, user_id)
    );
    
    CREATE INDEX solutions_user_id_index ON solutions (user_id);
    
    -- Historie přidělování bodů
    CREATE TABLE points_history (
    	points_history_id	serial	PRIMARY KEY,
    	task_id		int		NOT NULL REFERENCES tasks(task_id),
    	participant_id	int		NOT NULL REFERENCES users(user_id),
    	points		numeric(5,1)	DEFAULT NULL,
    	points_by	int		NOT NULL REFERENCES users(user_id),		-- kdo přidělil body
    	points_at	timestamp with time zone	NOT NULL			-- a kdy
    );
    
    CREATE INDEX points_history_index ON points_history (task_id, participant_id);
    
    -- Organizátorská role
    CREATE TYPE role_type AS ENUM (
    	'garant',		-- celostátní garant (může být omezený na kategorii)
    	'garant_kraj',		-- krajský garant
    	'garant_okres',		-- okresní garant
    	'garant_skola',		-- školní garant
    	'dozor',		-- dozor na soutěži (může odevzdávat řešení za účastníky)
    	'opravovatel'		-- opravovatel
    );
    
    -- Uživatelům majícím is_org=true lze přidělit roli ke konkrétnímu regionu (včetně podregionů) a volitelně kategorii/kolu
    -- HACK: Pokud category='Z', role platí pro všechny kategorie začínající na 'Z' (extra výjimka v mo.rights)
    CREATE TABLE user_roles (
    	user_role_id	serial		PRIMARY KEY,
    	user_id		int		NOT NULL REFERENCES users(user_id),
    	place_id	int		NOT NULL REFERENCES places(place_id),
    	role		role_type	NOT NULL,
    	category	varchar(2)	DEFAULT NULL,
    	year		int		DEFAULT NULL,
    	seq		int		DEFAULT NULL,
    	assigned_by	int		DEFAULT NULL REFERENCES users(user_id),		-- kdo a kdy roli přidělil (NULL=admin DB)
    	assigned_at	timestamp with time zone	NOT NULL DEFAULT CURRENT_TIMESTAMP
    );
    
    -- Společný log změn (pozor, nezaručujeme referenční integritu!)
    
    CREATE TYPE log_type AS ENUM (
    	'general',	-- všeobecné logování, id=0
    	'user',		-- users(user_id)
    	'place',	-- places(place_id)
    	'round',	-- rounds(round_id)
    	'contest',	-- contests(contest_id)
    	'participant',	-- participants(user_id)
    	'task',		-- tasks(task_id)
    	'user_role'	-- user_roles(user_id)
    );
    
    CREATE TABLE log (
    	log_entry_id	serial		PRIMARY KEY,
    	changed_by	int		REFERENCES users(user_id),		-- kdo změnu provedl
    	changed_at	timestamp with time zone	NOT NULL DEFAULT CURRENT_TIMESTAMP,	-- a kdy
    	type		log_type	NOT NULL,
    	id		int		NOT NULL,					-- jakého záznamu se změna týká
    	details		jsonb		NOT NULL					-- detaily (závislé na typu)
    );
    
    CREATE INDEX log_type_id_index ON log (type, id);
    
    -- Asynchronní úlohy
    
    CREATE TYPE job_type AS ENUM (
    	'download_submits',
    	'upload_feedback'
    );
    
    CREATE TYPE job_state AS ENUM (
    	'ready',
    	'running',
    	'done',				-- Hotovo, out_json a out_file jsou platné
    	'failed'			-- Interní chyba při zpracování, viz log
    );
    
    CREATE TABLE jobs (
    	job_id		serial		PRIMARY KEY,
    	type		job_type	NOT NULL,
    	state		job_state	NOT NULL,
    	user_id		int		NOT NULL REFERENCES users(user_id),			-- komu patří
    	created_at	timestamp with time zone	NOT NULL DEFAULT CURRENT_TIMESTAMP,	-- kdy byl založen
    	finished_at	timestamp with time zone	DEFAULT NULL,				-- kdy doběhl
    	expires_at	timestamp with time zone	DEFAULT NULL,				-- kdy bude automaticky smazán
    	description	text		NOT NULL DEFAULT '',					-- popis jobu
    	result		text		NOT NULL DEFAULT '',					-- jednořádková zpráva o výsledku
    	-- Vstupní parametry a výsledky jobu
    	in_json		jsonb		DEFAULT NULL,
    	out_json	jsonb		DEFAULT NULL,
    	-- Soubory jsou součástí úlohy a po její expiraci budou smazány
    	in_file		varchar(255)	DEFAULT NULL,
    	out_file	varchar(255)	DEFAULT NULL
    );
    
    -- Zprávičky k soutěžím
    
    CREATE TABLE messages (
    	message_id	serial		PRIMARY KEY,
    	round_id	int		NOT NULL REFERENCES rounds(round_id),
    	created_at	timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP,	-- čas publikování zprávičky
    	created_by	int		NOT NULL REFERENCES users(user_id),		-- autor zprávičky
    	title		text		NOT NULL,
    	markdown	text		NOT NULL,
    	html		text		NOT NULL
    );
    
    -- Požadavky na registraci a změny vlastností účtu
    
    CREATE TYPE reg_req_type AS ENUM (
    	'register',
    	'change_email',
    	'reset_password'
    );
    
    CREATE TABLE reg_requests (
    	reg_id		serial		PRIMARY KEY,
    	type		reg_req_type	NOT NULL,
    	created_at	timestamp with time zone NOT NULL,
    	expires_at	timestamp with time zone NOT NULL,
    	used_at		timestamp with time zone DEFAULT NULL,
    	email		varchar(255)	DEFAULT NULL,			-- adresa, kterou potvrzujeme
    	captcha_token	varchar(255)	DEFAULT NULL,			-- token pro fázi 1 registrace
    	email_token	varchar(255)	UNIQUE NOT NULL,		-- token pro fázi 2 registrace
    	user_id		int		DEFAULT NULL REFERENCES users(user_id) ON DELETE CASCADE,
    	client		varchar(255)	NOT NULL			-- kdo si registraci vyžádal
    );
    
    -- Statistiky
    
    -- Pro každý region spočítáme všechna podřízená místa
    CREATE VIEW region_descendants AS
    	WITH RECURSIVE descendant_regions(place_id, descendant) AS (
    		SELECT place_id, place_id FROM places
    		UNION SELECT r.place_id, p.place_id
    		      FROM descendant_regions r, places p
    		      WHERE p.parent = r.descendant
    	) SELECT place_id AS region, descendant FROM descendant_regions;
    
    -- Pro každou trojici (kolo, region, stav soutěže) spočítáme soutěže.
    CREATE VIEW region_contest_stats AS
    	SELECT c.round_id, rd.region, c.state, count(*) AS count
    	FROM contests c
    	JOIN region_descendants rd ON rd.descendant = c.place_id
    	WHERE rd.region <> c.place_id
    	GROUP BY c.round_id, rd.region, c.state;
    
    -- Pro každou trojici (kolo, region, stav účasti) spočítáme účastníky.
    -- Pokud se to ukáže být příliš pomalé, přejdeme na materializovaná views.
    CREATE VIEW region_participant_stats AS
    	SELECT c.round_id, rd.region, p.state, count(*) AS count
    	FROM participations p
    	JOIN contests c USING(contest_id)
    	JOIN region_descendants rd ON rd.descendant = c.place_id
    	GROUP BY c.round_id, rd.region, p.state;
    
    -- Pro každou trojici (kolo, region, úloha) spočítáme řešení.
    CREATE VIEW region_task_stats AS
    	SELECT r.round_id, rd.region, t.task_id, count(*) AS count
    	FROM rounds r
    	JOIN contests c USING(round_id)
    	JOIN participations p USING(contest_id)
    	JOIN tasks t USING(round_id)
    	JOIN solutions s USING(user_id, task_id)
    	JOIN region_descendants rd ON rd.descendant = c.place_id
    	GROUP BY r.round_id, rd.region, t.task_id;