mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-05-06 19:19:19 +02:00
merge all migrations into schema.sql
This commit is contained in:
1
TODO.md
1
TODO.md
@@ -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`
|
||||
|
||||
@@ -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;
|
||||
@@ -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');
|
||||
@@ -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');
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
@@ -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
|
||||
);
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
@@ -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');
|
||||
@@ -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';
|
||||
@@ -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;
|
||||
@@ -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', '');
|
||||
@@ -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');
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user