Files
xamxam/app/storage/schema.sql
Pontoporeia 38031374c1 fix(partage): smtp view missing in schema + thanks redirect broken
- Add v_smtp_active VIEW to schema.sql (was only in migration 012,
  causing SmtpRelay::isConfigured() to always return false on fresh installs)
- Change thanks redirect from /partage/thanks.php to /partage/thanks
  (nginx 'location ~ \.php$ { deny all }' blocked the .php URL)
- Route /partage/thanks in index.php before slug validation
- Guard App::boot() in thanks.php to avoid double-boot when included
2026-04-24 23:03:49 +02:00

520 lines
18 KiB
SQL

-- Post-ERG Thesis Database Schema
-- SQLite Database for managing final thesis projects (TFE) and doctoral theses
-- ============================================================================
-- CORE ENTITIES
-- ============================================================================
-- Students/Authors table
CREATE TABLE IF NOT EXISTS authors (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT,
show_contact INTEGER NOT NULL DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- Supervisors/Promoters table
CREATE TABLE IF NOT EXISTS supervisors (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- ============================================================================
-- PREDEFINED REFERENCE DATA (lookup tables)
-- ============================================================================
-- Orientations (predefined list from specifications)
CREATE TABLE IF NOT EXISTS orientations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- Insert predefined orientations
INSERT OR IGNORE INTO orientations (name) VALUES
('Arts Numériques'),
('Dessin'),
('Cinéma d''animation'),
('Installation-Performance'),
('Peinture'),
('Photographie'),
('Sculpture'),
('Vidéographie'),
('Graphisme'),
('Typographie'),
('Design Numérique'),
('Illustration'),
('Bande-Dessinée'),
('Sérigraphie'),
('Gravure');
-- AP (Ateliers Pratiques) - predefined list
CREATE TABLE IF NOT EXISTS ap_programs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
code TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- Insert predefined AP programs
INSERT OR IGNORE INTO ap_programs (name, code) VALUES
('Narration Spéculative', NULL),
('Design et Politique du Multiple', 'DPM'),
('Atelier Pratiques Situées', 'APS'),
('Lieux, Interdisciplinarités, Écologie, Nécessité, Systèmes', 'LIENS');
-- Master finality types
CREATE TABLE IF NOT EXISTS finality_types (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
INSERT OR IGNORE INTO finality_types (name) VALUES
('Approfondi'),
('Enseignement'),
('Spécialisé');
-- Languages
CREATE TABLE IF NOT EXISTS languages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
INSERT OR IGNORE INTO languages (name) VALUES
('Français'),
('Anglais');
-- Format types (can select multiple)
CREATE TABLE IF NOT EXISTS format_types (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
INSERT OR IGNORE INTO format_types (name) VALUES
('Site web'),
('Audio'),
('Vidéo'),
('Performance'),
('Objet éditorial'),
('Installation'),
('Autre');
-- Tags (keywords — canonical M2M table; formerly 'keywords'/'keyword' column)
CREATE TABLE IF NOT EXISTS tags (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_tags_name ON tags(name);
-- Access authorization types
CREATE TABLE IF NOT EXISTS access_types (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
description TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
INSERT OR IGNORE INTO access_types (name, description) VALUES
('Libre', 'TFE en libre accès à tout le monde sur la plateforme et en bibliothèque'),
('Interne', 'TFE accessible uniquement sur place en physique. Une note descriptive est disponible sur le site'),
('Interdit', 'TFE non disponible en physique ni sur le site. Une note descriptive est disponible sur le site');
-- License types
CREATE TABLE IF NOT EXISTS license_types (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
description TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
INSERT OR IGNORE INTO license_types (name) VALUES
('CC BY 4.0'),
('CC BY-SA 4.0'),
('CC BY-ND 4.0'),
('CC BY-NC 4.0'),
('CC BY-NC-SA 4.0'),
('CC BY-NC-ND 4.0'),
('Tous droits réservés'),
('Domaine public');
-- ============================================================================
-- MAIN THESIS TABLE
-- ============================================================================
CREATE TABLE IF NOT EXISTS theses (
id INTEGER PRIMARY KEY AUTOINCREMENT,
identifier TEXT UNIQUE, -- e.g., "2025-002"
-- Basic information
title TEXT NOT NULL,
subtitle TEXT,
year INTEGER NOT NULL,
-- Type of work
is_doctoral BOOLEAN DEFAULT 0, -- 0 for TFE, 1 for doctoral thesis
-- Academic details
orientation_id INTEGER,
ap_program_id INTEGER,
finality_id INTEGER,
-- Content
synopsis TEXT, -- ~200 words
context_note TEXT, -- Note added by jury president (max 150 words)
remarks TEXT, -- Internal remarks
-- Duration/size
duration_minutes INTEGER, -- For audio/video works
duration_pages INTEGER, -- For written works
file_size_info TEXT, -- e.g., "128 pages", "78 pages + ?? minutes"
-- Access and licensing
access_type_id INTEGER,
license_id INTEGER,
-- Jury information
jury_points DECIMAL(4,2), -- Points out of 20
jury_note_added BOOLEAN DEFAULT 0, -- Whether jury president added a note
-- Publication status
submitted_at DATETIME, -- When student submitted
defense_date DATETIME, -- Date of defense/soutenance
published_at DATETIME, -- When made public (after jury review)
is_published BOOLEAN DEFAULT 0,
-- External links
baiu_link TEXT, -- Link to institutional repository
-- Home page card banner (optional, landscape image)
banner_path TEXT, -- path relative to STORAGE_ROOT (e.g. "banners/abc.jpg")
-- Timestamps
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
-- Foreign keys
FOREIGN KEY (orientation_id) REFERENCES orientations(id),
FOREIGN KEY (ap_program_id) REFERENCES ap_programs(id),
FOREIGN KEY (finality_id) REFERENCES finality_types(id),
FOREIGN KEY (access_type_id) REFERENCES access_types(id),
FOREIGN KEY (license_id) REFERENCES license_types(id)
);
-- ============================================================================
-- JUNCTION TABLES (Many-to-Many relationships)
-- ============================================================================
-- Authors per thesis (can have multiple authors)
CREATE TABLE IF NOT EXISTS thesis_authors (
thesis_id INTEGER NOT NULL,
author_id INTEGER NOT NULL,
author_order INTEGER DEFAULT 1, -- Order of authors if multiple
PRIMARY KEY (thesis_id, author_id),
FOREIGN KEY (thesis_id) REFERENCES theses(id) ON DELETE CASCADE,
FOREIGN KEY (author_id) REFERENCES authors(id) ON DELETE CASCADE
);
-- Supervisors per thesis (jury: president, promoteur, lecteurs)
CREATE TABLE IF NOT EXISTS thesis_supervisors (
thesis_id INTEGER NOT NULL,
supervisor_id INTEGER NOT NULL,
supervisor_order INTEGER DEFAULT 1,
role TEXT NOT NULL DEFAULT 'promoteur', -- 'president'|'promoteur'|'lecteur'
is_external INTEGER NOT NULL DEFAULT 0, -- 0 = internal, 1 = external
PRIMARY KEY (thesis_id, supervisor_id),
FOREIGN KEY (thesis_id) REFERENCES theses(id) ON DELETE CASCADE,
FOREIGN KEY (supervisor_id) REFERENCES supervisors(id) ON DELETE CASCADE
);
-- Languages per thesis (can be multilingual)
CREATE TABLE IF NOT EXISTS thesis_languages (
thesis_id INTEGER NOT NULL,
language_id INTEGER NOT NULL,
PRIMARY KEY (thesis_id, language_id),
FOREIGN KEY (thesis_id) REFERENCES theses(id) ON DELETE CASCADE,
FOREIGN KEY (language_id) REFERENCES languages(id) ON DELETE CASCADE
);
-- Formats per thesis (can have multiple formats)
CREATE TABLE IF NOT EXISTS thesis_formats (
thesis_id INTEGER NOT NULL,
format_id INTEGER NOT NULL,
PRIMARY KEY (thesis_id, format_id),
FOREIGN KEY (thesis_id) REFERENCES theses(id) ON DELETE CASCADE,
FOREIGN KEY (format_id) REFERENCES format_types(id) ON DELETE CASCADE
);
-- Tags per thesis (max 10 as per specs; formerly thesis_keywords)
CREATE TABLE IF NOT EXISTS thesis_tags (
tag_id INTEGER NOT NULL,
thesis_id INTEGER NOT NULL,
PRIMARY KEY (tag_id, thesis_id),
FOREIGN KEY (thesis_id) REFERENCES theses(id) ON DELETE CASCADE,
FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE
);
-- ============================================================================
-- FILE ATTACHMENTS
-- ============================================================================
CREATE TABLE IF NOT EXISTS thesis_files (
id INTEGER PRIMARY KEY AUTOINCREMENT,
thesis_id INTEGER NOT NULL,
file_type TEXT NOT NULL, -- 'main', 'annex', 'written_part', 'other'
file_path TEXT NOT NULL,
file_name TEXT NOT NULL,
file_size INTEGER, -- in bytes
mime_type TEXT,
description TEXT,
uploaded_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (thesis_id) REFERENCES theses(id) ON DELETE CASCADE
);
-- ============================================================================
-- SITE SETTINGS
-- ============================================================================
CREATE TABLE IF NOT EXISTS site_settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL DEFAULT '',
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
INSERT OR IGNORE INTO site_settings (key, value) VALUES
('access_type_interdit_enabled', '1'),
('access_type_interne_enabled', '1'),
('access_type_libre_enabled', '0'),
('admin_password_hash', '');
-- ============================================================================
-- STATIC PAGES / CONTENT MANAGEMENT
-- ============================================================================
-- For managing editable static pages (charte, about, licenses, contact)
CREATE TABLE IF NOT EXISTS system_cache (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS pages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
slug TEXT NOT NULL UNIQUE, -- 'charte', 'about', 'licenses', 'contact'
title TEXT NOT NULL,
content TEXT, -- Markdown or HTML content
is_published BOOLEAN DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- Initialize default pages
INSERT OR IGNORE INTO pages (slug, title, content) VALUES
('charte', 'Charte', 'Contenu à venir'),
('about', 'À propos', 'Contenu à venir'),
('licenses', 'Licences', 'Contenu à venir');
-- ============================================================================
-- SMTP SETTINGS
-- ============================================================================
-- Singleton row — id is always 1. Credentials stored in clear for now.
CREATE TABLE IF NOT EXISTS smtp_settings (
id INTEGER PRIMARY KEY CHECK (id = 1),
host TEXT NOT NULL DEFAULT '',
port INTEGER NOT NULL DEFAULT 587,
encryption TEXT NOT NULL DEFAULT 'tls', -- 'tls' | 'ssl' | 'none'
username TEXT NOT NULL DEFAULT '',
password TEXT NOT NULL DEFAULT '', -- stored in clear for now; encrypt later
from_email TEXT NOT NULL DEFAULT '',
from_name TEXT NOT NULL DEFAULT 'Post-ERG',
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
INSERT OR IGNORE INTO smtp_settings (id) VALUES (1);
CREATE VIEW IF NOT EXISTS v_smtp_active AS
SELECT * FROM smtp_settings WHERE id = 1;
-- ============================================================================
-- APROPOS CONTENTS (structured data for the "À propos" page)
-- ============================================================================
CREATE TABLE IF NOT EXISTS apropos_contents (
id INTEGER PRIMARY KEY AUTOINCREMENT,
key TEXT NOT NULL UNIQUE, -- 'contacts', 'credits', 'erg_url'
value TEXT, -- JSON array or plain string
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
INSERT OR IGNORE INTO apropos_contents (key, value) VALUES
('contacts', '[
{"role":"Bibliothèque d'architecture, d'ingénierie architecturale, d'urbanisme (BAIU) :", "entries":[
{"text":"Laurent Leprince", "email":"laurent.leprince@uclouvain.be"}
]},
{"role":"Responsable des mémoires de l'ERG :", "entries":[
{"text":"Xavier Gorgol", "email":"xavier.gorgol@erg.be"}
]},
{"role":"Cours de suivi de mémoire :", "entries":[
{"text":"Brigitte Ledune", "email":"brigitte.ledune@erg.be"}
]}
]'),
('credits', '[
{"label":"Design & développement", "entries":[
{"text":"Olivia Marly"},
{"text":"Théophile Gerveau-Mercie"},
{"text":"Théo Hennequin"}
]},
{"label":"Typographies", "entries":[
{"text":"Ductus (Amélie Dumont)"},
{"text":"BBB DM Sans"}
]}
]');INSERT OR IGNORE INTO apropos_contents (key, value) VALUES
('contacts', '[
{"role":"Bibliothèque d\u0027architecture, d\u0027ingénierie architecturale, d\u0027urbanisme (BAIU) :", "entries":[
{"text":"Laurent Leprince", "email":"laurent.leprince@uclouvain.be"}
]},
{"role":"Responsable des mémoires de l\u0027ERG :", "entries":[
{"text":"Xavier Gorgol", "email":"xavier.gorgol@erg.be"}
]},
{"role":"Cours de suivi de mémoire :", "entries":[
{"text":"Brigitte Ledune", "email":"brigitte.ledune@erg.be"}
]}
]'),
('credits', '[
{"label":"Design & développement", "entries":[
{"text":"Olivia Marly"},
{"text":"Théophile Gerveau-Mercie"},
{"text":"Théo Hennequin"}
]},
{"label":"Typographies", "entries":[
{"text":"Ductus (Amélie Dumont)"},
{"text":"BBB DM Sans"}
]}
]');
-- ============================================================================
-- INDEXES for performance
-- ============================================================================
CREATE INDEX IF NOT EXISTS idx_theses_year ON theses(year);
CREATE INDEX IF NOT EXISTS idx_theses_published ON theses(is_published);
CREATE INDEX IF NOT EXISTS idx_theses_pub_year ON theses(is_published, year DESC);
CREATE INDEX IF NOT EXISTS idx_theses_identifier ON theses(identifier);
CREATE INDEX IF NOT EXISTS idx_theses_orientation ON theses(orientation_id);
CREATE INDEX IF NOT EXISTS idx_theses_ap_program ON theses(ap_program_id);
CREATE INDEX IF NOT EXISTS idx_theses_access_type ON theses(access_type_id);
CREATE INDEX IF NOT EXISTS idx_authors_email ON authors(email);
CREATE INDEX IF NOT EXISTS idx_thesis_authors_thesis ON thesis_authors(thesis_id);
CREATE INDEX IF NOT EXISTS idx_thesis_authors_author ON thesis_authors(author_id);
CREATE INDEX IF NOT EXISTS idx_thesis_tags_thesis ON thesis_tags(thesis_id);
CREATE INDEX IF NOT EXISTS idx_thesis_tags_tag ON thesis_tags(tag_id);
-- ============================================================================
-- TRIGGERS for automatic timestamp updates
-- ============================================================================
CREATE TRIGGER IF NOT EXISTS update_theses_timestamp
AFTER UPDATE ON theses
BEGIN
UPDATE theses SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
END;
CREATE TRIGGER IF NOT EXISTS update_authors_timestamp
AFTER UPDATE ON authors
BEGIN
UPDATE authors SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
END;
CREATE TRIGGER IF NOT EXISTS update_supervisors_timestamp
AFTER UPDATE ON supervisors
BEGIN
UPDATE supervisors SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
END;
CREATE TRIGGER IF NOT EXISTS update_pages_timestamp
AFTER UPDATE ON pages
BEGIN
UPDATE pages SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
END
CREATE TRIGGER IF NOT EXISTS update_apropos_contents_timestamp
AFTER UPDATE ON apropos_contents
BEGIN
UPDATE apropos_contents SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
END;
-- ============================================================================
-- VIEWS for common queries
-- ============================================================================
-- Full thesis information view
CREATE VIEW IF NOT EXISTS v_theses_full AS
SELECT
t.id,
t.identifier,
t.title,
t.subtitle,
t.year,
t.is_doctoral,
o.name as orientation,
ap.name as ap_program,
ft.name as finality_type,
t.synopsis,
t.context_note,
t.duration_minutes,
t.duration_pages,
t.file_size_info,
at.name as access_type,
lt.name as license_type,
t.license_id,
t.access_type_id,
t.jury_points,
t.submitted_at,
t.defense_date,
t.published_at,
t.is_published,
t.baiu_link,
t.banner_path,
GROUP_CONCAT(DISTINCT a.name) as authors,
GROUP_CONCAT(DISTINCT s.name) as supervisors,
GROUP_CONCAT(DISTINCT CASE WHEN ts.role = 'president' THEN s.name END) as jury_president,
GROUP_CONCAT(DISTINCT CASE WHEN ts.role = 'promoteur' THEN s.name END) as jury_promoteurs,
GROUP_CONCAT(DISTINCT CASE WHEN ts.role = 'lecteur' THEN s.name END) as jury_lecteurs,
GROUP_CONCAT(DISTINCT l.name) as languages,
GROUP_CONCAT(DISTINCT fmt.name) as formats,
GROUP_CONCAT(DISTINCT tg.name) as keywords,
-- First author's email and contact-visibility flag
(SELECT a2.email FROM authors a2 JOIN thesis_authors ta2 ON a2.id = ta2.author_id WHERE ta2.thesis_id = t.id ORDER BY ta2.author_order LIMIT 1) as author_email,
(SELECT a2.show_contact FROM authors a2 JOIN thesis_authors ta2 ON a2.id = ta2.author_id WHERE ta2.thesis_id = t.id ORDER BY ta2.author_order LIMIT 1) as author_show_contact
FROM theses t
LEFT JOIN orientations o ON t.orientation_id = o.id
LEFT JOIN ap_programs ap ON t.ap_program_id = ap.id
LEFT JOIN finality_types ft ON t.finality_id = ft.id
LEFT JOIN access_types at ON t.access_type_id = at.id
LEFT JOIN license_types lt ON t.license_id = lt.id
LEFT JOIN thesis_authors ta ON t.id = ta.thesis_id
LEFT JOIN authors a ON ta.author_id = a.id
LEFT JOIN thesis_supervisors ts ON t.id = ts.thesis_id
LEFT JOIN supervisors s ON ts.supervisor_id = s.id
LEFT JOIN thesis_languages tl ON t.id = tl.thesis_id
LEFT JOIN languages l ON tl.language_id = l.id
LEFT JOIN thesis_formats tf ON t.id = tf.thesis_id
LEFT JOIN format_types fmt ON tf.format_id = fmt.id
LEFT JOIN thesis_tags tt ON t.id = tt.thesis_id
LEFT JOIN tags tg ON tt.tag_id = tg.id
GROUP BY t.id;
-- Published theses only (for public view)
CREATE VIEW IF NOT EXISTS v_theses_public AS
SELECT * FROM v_theses_full
WHERE is_published = 1;