merge all migrations into schema.sql

This commit is contained in:
Pontoporeia
2026-04-22 11:30:15 +02:00
parent 38031374c1
commit dbaabaf8a0
17 changed files with 72 additions and 650 deletions

View File

@@ -10,4 +10,5 @@
- [x] Unify form CSS between admin `add.php` and student partage form: move licence/share-badge styles into `admin.css`, remove inline `<style>` from `partage/index.php`, switch to `common.css` + `admin.css`
- [x] Extract form CSS into `form.css`; load it in admin add/edit via `$extraCss` and in student partage form directly; `system.css` now only used by `system.php`; `partage/thanks.php` rewritten to use design-system classes
- [x] Fix student form: add missing `v_smtp_active` view to `schema.sql` (SMTP was silently skipped on fresh installs); fix `thanks.php` redirect (was `/partage/thanks.php` — blocked by nginx PHP deny rule); route `/partage/thanks` through `index.php` special-case handler
- [x] Merge all migration SQL into schema.sql; delete migrations/ folder; simplify migrate.sh (009 share_links, 014 ap_programs, 011 apropos seed, missing semicolon fix)
- [x] Fix student form scroll (add `overflow-y:auto` to `.student-body`); move all remaining inline styles from partage error/password-gate pages into `form.css`

View File

@@ -1,106 +0,0 @@
-- Migration 001: Rename keywords→tags, thesis_keywords→thesis_tags, keyword column→name
-- SQLite does not support ALTER TABLE RENAME COLUMN before 3.25, so we recreate tables.
-- This migration is safe to run after 004 and 005 (no dependency ordering required
-- since SQLite processes this in one transaction).
PRAGMA foreign_keys = OFF;
BEGIN;
-- 1. Create new tags table
CREATE TABLE tags (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- 2. Copy data from keywords → tags (keyword column → name)
INSERT INTO tags (id, name, created_at)
SELECT id, keyword, created_at FROM keywords;
-- 3. Create new junction table thesis_tags
CREATE TABLE 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
);
-- 4. Copy junction data (keyword_id → tag_id)
INSERT INTO thesis_tags (tag_id, thesis_id)
SELECT keyword_id, thesis_id FROM thesis_keywords;
-- 5. Drop old tables
DROP TABLE thesis_keywords;
DROP TABLE keywords;
-- 6. Recreate indexes with canonical names
CREATE INDEX idx_tags_name ON tags(name);
CREATE INDEX idx_thesis_tags_thesis ON thesis_tags(thesis_id);
CREATE INDEX idx_thesis_tags_tag ON thesis_tags(tag_id);
-- 7. Rebuild views to reference new tables
DROP VIEW IF EXISTS v_theses_full;
CREATE VIEW 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.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
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;
DROP VIEW IF EXISTS v_theses_public;
CREATE VIEW v_theses_public AS
SELECT * FROM v_theses_full
WHERE is_published = 1;
COMMIT;
PRAGMA foreign_keys = ON;

View File

@@ -1,9 +0,0 @@
-- Migration 002: Wire visibility states to existing access_type_id column.
-- The access_types table already has Libre (1), Interne (2), Interdit (3).
-- No structural changes needed — this migration is a no-op for the schema.
-- It documents the intent and ensures access_types seed rows exist.
INSERT OR IGNORE INTO access_types (id, name, description) VALUES
(1, 'Libre', 'TFE en libre accès à tout le monde sur la plateforme et en bibliothèque'),
(2, 'Interne', 'TFE accessible uniquement sur place en physique. Une note descriptive est disponible sur le site'),
(3, 'Interdit', 'TFE non disponible en physique ni sur le site. Une note descriptive est disponible sur le site');

View File

@@ -1,19 +0,0 @@
-- Migration 003: Seed license_types table
-- Safe to run on existing databases (INSERT OR IGNORE)
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');

View File

@@ -1,66 +0,0 @@
-- Migration 004: Add jury role and is_external columns to thesis_supervisors
-- Existing rows get role = 'promoteur', is_external = 0 (no data loss)
ALTER TABLE thesis_supervisors ADD COLUMN role TEXT NOT NULL DEFAULT 'promoteur';
ALTER TABLE thesis_supervisors ADD COLUMN is_external INTEGER NOT NULL DEFAULT 0;
-- Recreate v_theses_full to include jury role columns
DROP VIEW IF EXISTS v_theses_full;
CREATE VIEW 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.jury_points,
t.submitted_at,
t.defense_date,
t.published_at,
t.is_published,
t.baiu_link,
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 k.keyword) as keywords
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_keywords tk ON t.id = tk.thesis_id
LEFT JOIN keywords k ON tk.keyword_id = k.id
GROUP BY t.id;
-- Recreate public view
DROP VIEW IF EXISTS v_theses_public;
CREATE VIEW v_theses_public AS
SELECT * FROM v_theses_full
WHERE is_published = 1;

View File

@@ -1,65 +0,0 @@
-- Migration 005: Add banner_path column to theses for home page card thumbnails
ALTER TABLE theses ADD COLUMN banner_path TEXT;
-- Recreate v_theses_full to include banner_path
DROP VIEW IF EXISTS v_theses_full;
CREATE VIEW 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.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
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;
-- Recreate public view
DROP VIEW IF EXISTS v_theses_public;
CREATE VIEW v_theses_public AS
SELECT * FROM v_theses_full
WHERE is_published = 1;

View File

@@ -1,8 +0,0 @@
-- Migration 006: Add composite covering index (is_published, year DESC) on theses
--
-- Every public-facing query filters on is_published = 1 AND orders/filters by year.
-- The existing separate idx_theses_published and idx_theses_year force the query
-- planner to pick one index and sort the other via a temp B-tree.
-- This single covering index eliminates the extra sort pass.
CREATE INDEX IF NOT EXISTS idx_theses_pub_year ON theses(is_published, year DESC);

View File

@@ -1,8 +0,0 @@
-- Migration 007: Add system_cache table for admin system page caching
-- Stores JSON-encoded status snapshots keyed by section with a TTL mechanism.
CREATE TABLE IF NOT EXISTS system_cache (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
updated_at INTEGER NOT NULL
);

View File

@@ -1,89 +0,0 @@
-- Migration 008: Formulaire settings + contact visibility
-- Adds site_settings key-value table for admin-configurable options
-- Adds show_contact column to authors table
-- Adds author_email + author_show_contact to views
-- ── 1. site_settings ─────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS site_settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL DEFAULT '',
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- Default formulaire settings:
-- access_type_interdit_enabled = 1 (Interdit is available in the add form)
-- access_type_interne_enabled = 1 (Interne is available in the add form)
-- access_type_libre_enabled = 0 (Libre is NOT yet available — next academic year)
INSERT OR IGNORE INTO site_settings (key, value) VALUES
('access_type_interdit_enabled', '1'),
('access_type_interne_enabled', '1'),
('access_type_libre_enabled', '0');
-- ── 2. show_contact on authors ────────────────────────────────────────────────
-- NOTE: SQLite has no IF NOT EXISTS for ALTER TABLE.
-- The migrate.sh script guards against re-running; ignore errors on existing DBs.
ALTER TABLE authors ADD COLUMN show_contact INTEGER NOT NULL DEFAULT 0;
-- ── 3. Rebuild views to expose author_email and author_show_contact ───────────
DROP VIEW IF EXISTS v_theses_public;
DROP VIEW IF EXISTS v_theses_full;
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.jury_points,
t.submitted_at,
t.defense_date,
t.published_at,
t.is_published,
t.baiu_link,
t.banner_path,
t.access_type_id,
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;
CREATE VIEW IF NOT EXISTS v_theses_public AS
SELECT * FROM v_theses_full
WHERE is_published = 1;

View File

@@ -1,14 +0,0 @@
-- Share links table: enables students to submit TFEs via unique, optional-password-protected URLs
CREATE TABLE IF NOT EXISTS share_links (
id INTEGER PRIMARY KEY AUTOINCREMENT,
slug TEXT NOT NULL UNIQUE, -- Format: YYYYMMDD-<random>, e.g. 20260416-a3f9k2
password_hash TEXT, -- bcrypt hash; NULL = no password required
is_active INTEGER NOT NULL DEFAULT 1, -- 1 = active, 0 = disabled
usage_count INTEGER NOT NULL DEFAULT 0, -- Number of successful submissions via this link
created_by INTEGER NOT NULL, -- admin user ID (references admin_sessions or admin_users)
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
expires_at DATETIME -- NULL = never expires
);
CREATE INDEX IF NOT EXISTS idx_share_links_slug ON share_links(slug);
CREATE INDEX IF NOT EXISTS idx_share_links_active ON share_links(is_active);

View File

@@ -1,14 +0,0 @@
-- ── apropos_contents table (structured data for the "À propos" page) ───────
-- Replaces config/apropos.php.
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 for contacts/credits, plain string for erg_url
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- Seed with the current defaults from config/apropos.php
INSERT OR IGNORE INTO apropos_contents (key, value) VALUES
('contacts', '[{"name":"Laurent Leprince","role":"Bibliothèque d''architecture, d''ingénierie architecturale, d''urbanisme (BAIU) :","email":"laurent.leprince@uclouvain.be"},{"name":"Xavier Gorgol","role":"Responsable des mémoires de l''ERG :","email":"xavier.gorgol@erg.be"},{"name":"Brigitte Ledune","role":"Cours de suivi de mémoire :","email":"brigitte.ledune@erg.be"}]'),
('credits', '[{"label":"Design & développement","value":"Olivia Marly, Théophile Gerveau-Mercie & Théo Hennequin"},{"label":"Typographies","value":"Ductus (Amélie Dumont) & BBB DM Sans"}]'),
('erg_url', 'https://erg.be');

View File

@@ -1,46 +0,0 @@
-- Transform apropos data: each row has a label/role and an entries[] of {text, url}.
-- Contacts also include email per entry.
UPDATE apropos_contents SET value = '
[
{
"label": "Design & développement",
"entries": [
{"text": "Olivia Marly", "url": ""},
{"text": "Théophile Gerveau-Mercie", "url": ""},
{"text": "Théo Hennequin", "url": ""}
]
},
{
"label": "Typographies",
"entries": [
{"text": "Ductus (Amélie Dumont)", "url": ""},
{"text": "BBB DM Sans", "url": ""}
]
}
]' WHERE key = 'credits';
UPDATE apropos_contents SET value = '
[
{
"role": "Bibliothèque d''architecture, d''ingénierie architecturale, d''urbanisme (BAIU) :",
"entries": [
{"text": "Laurent Leprince", "url": "", "email": "laurent.leprince@uclouvain.be"}
]
},
{
"role": "Responsable des mémoires de l''ERG :",
"entries": [
{"text": "Xavier Gorgol", "url": "", "email": "xavier.gorgol@erg.be"}
]
},
{
"role": "Cours de suivi de mémoire :",
"entries": [
{"text": "Brigitte Ledune", "url": "", "email": "brigitte.ledune@erg.be"}
]
}
]' WHERE key = 'contacts';
-- Remove erg_url from the table (hardcoded in template now)
DELETE FROM apropos_contents WHERE key = 'erg_url';

View File

@@ -1,22 +0,0 @@
-- SMTP relay credentials stored in the database.
-- A single active row is read at send-time for flexibility (change provider,
-- rotate passwords, etc. without touching code or env vars).
CREATE TABLE IF NOT EXISTS smtp_settings (
id INTEGER PRIMARY KEY CHECK (id = 1), -- singleton row
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 default empty row so the settings form can start working immediately.
INSERT OR IGNORE INTO smtp_settings (id) VALUES (1);
-- Helper view so callers always read the same row.
CREATE VIEW IF NOT EXISTS v_smtp_active AS
SELECT * FROM smtp_settings WHERE id = 1;

View File

@@ -1,11 +0,0 @@
-- Migration 013: Store admin password hash in site_settings
--
-- Previously stored in config/admin_credentials.php as the constant ADMIN_PASSWORD_HASH.
-- Now stored alongside SMTP credentials in the site_settings key-value table.
--
-- After applying this migration, import your existing hash manually:
-- UPDATE site_settings SET value = '$2y$12$...' WHERE key = 'admin_password_hash';
-- Or simply set a new one via the admin panel UI.
INSERT OR IGNORE INTO site_settings (key, value) VALUES
('admin_password_hash', '');

View File

@@ -1,9 +0,0 @@
-- Migration 014: seed missing AP programs and fix codes
-- Adds programs present in real CSV data but absent from the initial seed.
-- Fill in missing code for Narration Spéculative
UPDATE ap_programs SET code = 'NS' WHERE name = 'Narration Spéculative' AND (code IS NULL OR code = '');
-- Add missing AP programs (INSERT OR IGNORE is safe to re-run)
INSERT OR IGNORE INTO ap_programs (name, code) VALUES ('Récits et expérimentation', 'RE');
INSERT OR IGNORE INTO ap_programs (name, code) VALUES ('PACS', 'PACS');

View File

@@ -62,10 +62,12 @@ CREATE TABLE IF NOT EXISTS ap_programs (
-- Insert predefined AP programs
INSERT OR IGNORE INTO ap_programs (name, code) VALUES
('Narration Spéculative', NULL),
('Narration Spéculative', 'NS'),
('Design et Politique du Multiple', 'DPM'),
('Atelier Pratiques Situées', 'APS'),
('Lieux, Interdisciplinarités, Écologie, Nécessité, Systèmes', 'LIENS');
('Lieux, Interdisciplinarités, Écologie, Nécessité, Systèmes', 'LIENS'),
('Récits et expérimentation', 'RE'),
('PACS', 'PACS');
-- Master finality types
CREATE TABLE IF NOT EXISTS finality_types (
@@ -322,6 +324,24 @@ INSERT OR IGNORE INTO pages (slug, title, content) VALUES
('about', 'À propos', 'Contenu à venir'),
('licenses', 'Licences', 'Contenu à venir');
-- ============================================================================
-- SHARE LINKS
-- ============================================================================
CREATE TABLE IF NOT EXISTS share_links (
id INTEGER PRIMARY KEY AUTOINCREMENT,
slug TEXT NOT NULL UNIQUE, -- Format: YYYYMMDD-<random>, e.g. 20260416-a3f9k2
password_hash TEXT, -- bcrypt hash; NULL = no password required
is_active INTEGER NOT NULL DEFAULT 1, -- 1 = active, 0 = disabled
usage_count INTEGER NOT NULL DEFAULT 0, -- Number of successful submissions via this link
created_by INTEGER NOT NULL, -- admin user ID
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
expires_at DATETIME -- NULL = never expires
);
CREATE INDEX IF NOT EXISTS idx_share_links_slug ON share_links(slug);
CREATE INDEX IF NOT EXISTS idx_share_links_active ON share_links(is_active);
-- ============================================================================
-- SMTP SETTINGS
-- ============================================================================
@@ -357,48 +377,41 @@ CREATE TABLE IF NOT EXISTS apropos_contents (
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"}
]}
{
"role": "Bibliothèque d''architecture, d''ingénierie architecturale, d''urbanisme (BAIU) :",
"entries": [
{"text": "Laurent Leprince", "url": "", "email": "laurent.leprince@uclouvain.be"}
]
},
{
"role": "Responsable des mémoires de l''ERG :",
"entries": [
{"text": "Xavier Gorgol", "url": "", "email": "xavier.gorgol@erg.be"}
]
},
{
"role": "Cours de suivi de mémoire :",
"entries": [
{"text": "Brigitte Ledune", "url": "", "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"}
]}
('credits', '[
{
"label": "Design & développement",
"entries": [
{"text": "Olivia Marly", "url": ""},
{"text": "Théophile Gerveau-Mercie", "url": ""},
{"text": "Théo Hennequin", "url": ""}
]
},
{
"label": "Typographies",
"entries": [
{"text": "Ductus (Amélie Dumont)", "url": ""},
{"text": "BBB DM Sans", "url": ""}
]
}
]');
-- ============================================================================
@@ -444,7 +457,7 @@ 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
END;
CREATE TRIGGER IF NOT EXISTS update_apropos_contents_timestamp
AFTER UPDATE ON apropos_contents

View File

@@ -1,141 +1,35 @@
#!/usr/bin/env bash
# Apply pending SQL migrations to one or both SQLite databases.
# Initialise one or both SQLite databases from schema.sql.
# Safe to run on existing databases — schema uses IF NOT EXISTS / INSERT OR IGNORE.
# Usage:
# scripts/migrate.sh # migrates both test.db and posterg.db
# scripts/migrate.sh test # migrates storage/test.db only
# scripts/migrate.sh prod # migrates storage/posterg.db only
# scripts/migrate.sh # both test.db and posterg.db
# scripts/migrate.sh test # storage/test.db only
# scripts/migrate.sh prod # storage/posterg.db only
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
APP_DIR="$REPO_ROOT/app"
MIGRATIONS_DIR="$APP_DIR/storage/migrations"
SCHEMA="$APP_DIR/storage/schema.sql"
TEST_DB="$APP_DIR/storage/test.db"
PROD_DB="$APP_DIR/storage/posterg.db"
# ---------------------------------------------------------------------------
# Check whether a migration's effects are already present in the DB so that
# legacy databases (created before the migrations table existed) can be
# bootstrapped correctly without re-running non-idempotent SQL.
# ---------------------------------------------------------------------------
already_applied_structurally() {
local db="$1"
local name="$2"
case "$name" in
001_rename_keywords_to_tags.sql)
# Effect: table 'tags' and 'thesis_tags' exist
count=$(sqlite3 "$db" "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name IN ('tags','thesis_tags');")
[ "$count" -eq 2 ]
;;
002_add_visibility.sql)
# Effect: access_types seed rows (always safe to re-run with OR IGNORE, but mark done if rows exist)
count=$(sqlite3 "$db" "SELECT COUNT(*) FROM access_types WHERE id IN (1,2,3);" 2>/dev/null || echo 0)
[ "$count" -eq 3 ]
;;
003_seed_license_types.sql)
# Effect: at least one row in license_types
count=$(sqlite3 "$db" "SELECT COUNT(*) FROM license_types;" 2>/dev/null || echo 0)
[ "$count" -gt 0 ]
;;
004_jury_roles.sql)
# Effect: 'role' column on thesis_supervisors
count=$(sqlite3 "$db" "SELECT COUNT(*) FROM pragma_table_info('thesis_supervisors') WHERE name='role';")
[ "$count" -eq 1 ]
;;
005_add_banner.sql)
# Effect: 'banner_path' column on theses
count=$(sqlite3 "$db" "SELECT COUNT(*) FROM pragma_table_info('theses') WHERE name='banner_path';")
[ "$count" -eq 1 ]
;;
006_add_composite_index.sql)
# Effect: index idx_theses_pub_year exists (CREATE INDEX IF NOT EXISTS — safe to re-run anyway)
count=$(sqlite3 "$db" "SELECT COUNT(*) FROM sqlite_master WHERE type='index' AND name='idx_theses_pub_year';")
[ "$count" -eq 1 ]
;;
008_formulaire_settings.sql)
# Effect: site_settings table exists + show_contact column on authors
tbl=$(sqlite3 "$db" "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='site_settings';")
col=$(sqlite3 "$db" "SELECT COUNT(*) FROM pragma_table_info('authors') WHERE name='show_contact';")
[ "$tbl" -eq 1 ] && [ "$col" -eq 1 ]
;;
012_smtp_settings.sql)
tbl=$(sqlite3 "$db" "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='smtp_settings';")
[ "$tbl" -eq 1 ]
;;
013_admin_password.sql)
# Effect: admin_password_hash key exists in site_settings (INSERT OR IGNORE — safe to re-run)
count=$(sqlite3 "$db" "SELECT COUNT(*) FROM site_settings WHERE key='admin_password_hash';")
[ "$count" -eq 1 ]
;;
*)
# Unknown migration — assume not applied
return 1
;;
esac
}
migrate_db() {
init_db() {
local db="$1"
local label="$2"
# Auto-create from schema only when the file is absent or truly empty (no tables)
local table_count
table_count=$(sqlite3 "$db" "SELECT COUNT(*) FROM sqlite_master WHERE type='table';" 2>/dev/null || echo 0)
if [ "$table_count" -eq 0 ]; then
echo " [$label] initialising from schema…"
sqlite3 "$db" < "$APP_DIR/storage/schema.sql"
echo " [$label] schema applied."
fi
# Ensure tracking table exists
sqlite3 "$db" "CREATE TABLE IF NOT EXISTS schema_migrations (
name TEXT PRIMARY KEY,
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
);"
local applied=0
local seeded=0
local skipped=0
for migration in "$MIGRATIONS_DIR"/*.sql; do
name="$(basename "$migration")"
already=$(sqlite3 "$db" "SELECT COUNT(*) FROM schema_migrations WHERE name='$name';")
if [ "$already" -eq 1 ]; then
skipped=$((skipped + 1))
continue
fi
# Not in tracking table — check if it was already applied before we started tracking
if already_applied_structurally "$db" "$name"; then
sqlite3 "$db" "INSERT OR IGNORE INTO schema_migrations (name) VALUES ('$name');"
seeded=$((seeded + 1))
echo " [$label] seeded $name (already applied)"
continue
fi
echo " [$label] applying $name"
if sqlite3 "$db" < "$migration"; then
sqlite3 "$db" "INSERT OR IGNORE INTO schema_migrations (name) VALUES ('$name');"
applied=$((applied + 1))
else
echo " [$label] ERROR applying $name — aborting" >&2
exit 1
fi
done
echo " [$label] done — $applied applied, $seeded seeded, $skipped already up-to-date"
echo " [$label] applying schema…"
sqlite3 "$db" < "$SCHEMA"
echo " [$label] done"
}
TARGET="${1:-both}"
case "$TARGET" in
test) migrate_db "$TEST_DB" "test.db" ;;
prod) migrate_db "$PROD_DB" "posterg.db" ;;
test) init_db "$TEST_DB" "test.db" ;;
prod) init_db "$PROD_DB" "posterg.db" ;;
both)
migrate_db "$TEST_DB" "test.db"
migrate_db "$PROD_DB" "posterg.db"
init_db "$TEST_DB" "test.db"
init_db "$PROD_DB" "posterg.db"
;;
*)
echo "Usage: $0 [test|prod|both]" >&2