Add comprehensive thesis management system with database migration

This commit introduces a complete thesis management interface and migrates
the system from YAML-based storage to SQLite:

Core Changes:
- Add Database.php helper class with PDO connection and entity management
- Add list.php for viewing all theses with filtering and sorting
- Add edit.php for modifying existing thesis records
- Add import.php for migrating legacy YAML data to SQLite
- Add justfile with development tasks (serve, init-test-db, etc.)

Documentation:
- Add MIGRATION.md with complete migration guide and architecture docs
- Update README.md with database setup and Just recipe instructions
- Update .gitignore to exclude test databases and error logs

Modified Forms:
- Enhanced formulaire.php with transaction-based SQLite processing
- Updated index.php with database-driven form options
- Improved thanks.php to read from database views

The new architecture provides:
- Normalized database schema (19 tables, 2 views)
- Transaction safety and referential integrity
- CRUD operations for thesis management
- Filtering by year, orientation, AP program, publication status
- Secure file handling with metadata tracking

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Théophile Gervreau-Mercier
2026-01-27 15:43:01 +01:00
parent 99ccd60f90
commit 95f52d549e
22 changed files with 3263 additions and 725 deletions

277
formulaire/Database.php Normal file
View File

@@ -0,0 +1,277 @@
<?php
/**
* Database connection and helper class for Post-ERG thesis database
*/
class Database {
private $pdo;
private $dbPath;
/**
* Constructor - establishes database connection
* @param string $dbPath Path to SQLite database file
*/
public function __construct($dbPath = null) {
if ($dbPath === null) {
// Check for test database first (for local development)
$testDb = __DIR__ . '/test.db';
if (file_exists($testDb)) {
$this->dbPath = $testDb;
} else {
// Default to parent directory's db folder (production)
$this->dbPath = __DIR__ . '/../db/posterg.db';
}
} else {
$this->dbPath = $dbPath;
}
try {
$this->pdo = new PDO('sqlite:' . $this->dbPath);
$this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$this->pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
// Enable foreign key constraints
$this->pdo->exec('PRAGMA foreign_keys = ON');
} catch (PDOException $e) {
error_log("Database connection failed: " . $e->getMessage());
throw new Exception("Impossible de se connecter à la base de données.");
}
}
/**
* Get PDO instance for direct queries if needed
* @return PDO
*/
public function getPDO() {
return $this->pdo;
}
/**
* Begin a transaction
*/
public function beginTransaction() {
return $this->pdo->beginTransaction();
}
/**
* Commit a transaction
*/
public function commit() {
return $this->pdo->commit();
}
/**
* Rollback a transaction
*/
public function rollback() {
return $this->pdo->rollback();
}
/**
* Find or create an author
* @param string $name Author name
* @param string $email Author email (optional)
* @return int Author ID
*/
public function findOrCreateAuthor($name, $email = null) {
// Try to find existing author by name
$stmt = $this->pdo->prepare("SELECT id FROM authors WHERE name = ?");
$stmt->execute([$name]);
$author = $stmt->fetch();
if ($author) {
// Update email if provided and different
if ($email && $email !== '') {
$updateStmt = $this->pdo->prepare("UPDATE authors SET email = ? WHERE id = ?");
$updateStmt->execute([$email, $author['id']]);
}
return $author['id'];
}
// Create new author
$stmt = $this->pdo->prepare("INSERT INTO authors (name, email) VALUES (?, ?)");
$stmt->execute([$name, $email]);
return $this->pdo->lastInsertId();
}
/**
* Find or create a supervisor
* @param string $name Supervisor name
* @return int Supervisor ID
*/
public function findOrCreateSupervisor($name) {
// Try to find existing supervisor
$stmt = $this->pdo->prepare("SELECT id FROM supervisors WHERE name = ?");
$stmt->execute([$name]);
$supervisor = $stmt->fetch();
if ($supervisor) {
return $supervisor['id'];
}
// Create new supervisor
$stmt = $this->pdo->prepare("INSERT INTO supervisors (name) VALUES (?)");
$stmt->execute([$name]);
return $this->pdo->lastInsertId();
}
/**
* Find or create a keyword
* @param string $keyword Keyword text
* @return int Keyword ID
*/
public function findOrCreateKeyword($keyword) {
$keyword = trim($keyword);
if (empty($keyword)) {
return null;
}
// Try to find existing keyword
$stmt = $this->pdo->prepare("SELECT id FROM keywords WHERE keyword = ?");
$stmt->execute([$keyword]);
$kw = $stmt->fetch();
if ($kw) {
return $kw['id'];
}
// Create new keyword
$stmt = $this->pdo->prepare("INSERT INTO keywords (keyword) VALUES (?)");
$stmt->execute([$keyword]);
return $this->pdo->lastInsertId();
}
/**
* Get orientation ID by name
* @param string $name Orientation name
* @return int|null Orientation ID or null if not found
*/
public function getOrientationId($name) {
$stmt = $this->pdo->prepare("SELECT id FROM orientations WHERE name = ?");
$stmt->execute([$name]);
$result = $stmt->fetch();
return $result ? $result['id'] : null;
}
/**
* Get AP program ID by name
* @param string $name AP program name
* @return int|null AP program ID or null if not found
*/
public function getAPProgramId($name) {
$stmt = $this->pdo->prepare("SELECT id FROM ap_programs WHERE name = ?");
$stmt->execute([$name]);
$result = $stmt->fetch();
return $result ? $result['id'] : null;
}
/**
* Get finality type ID by name
* @param string $name Finality type name
* @return int|null Finality type ID or null if not found
*/
public function getFinalityId($name) {
$stmt = $this->pdo->prepare("SELECT id FROM finality_types WHERE name = ?");
$stmt->execute([$name]);
$result = $stmt->fetch();
return $result ? $result['id'] : null;
}
/**
* Get language ID by name
* @param string $name Language name
* @return int|null Language ID or null if not found
*/
public function getLanguageId($name) {
$stmt = $this->pdo->prepare("SELECT id FROM languages WHERE name = ?");
$stmt->execute([$name]);
$result = $stmt->fetch();
return $result ? $result['id'] : null;
}
/**
* Get format type ID by name
* @param string $name Format type name
* @return int|null Format type ID or null if not found
*/
public function getFormatId($name) {
$stmt = $this->pdo->prepare("SELECT id FROM format_types WHERE name = ?");
$stmt->execute([$name]);
$result = $stmt->fetch();
return $result ? $result['id'] : null;
}
/**
* Get all orientations
* @return array Array of orientations
*/
public function getAllOrientations() {
$stmt = $this->pdo->query("SELECT id, name FROM orientations ORDER BY name");
return $stmt->fetchAll();
}
/**
* Get all AP programs
* @return array Array of AP programs
*/
public function getAllAPPrograms() {
$stmt = $this->pdo->query("SELECT id, name, code FROM ap_programs ORDER BY name");
return $stmt->fetchAll();
}
/**
* Get all finality types
* @return array Array of finality types
*/
public function getAllFinalityTypes() {
$stmt = $this->pdo->query("SELECT id, name FROM finality_types ORDER BY name");
return $stmt->fetchAll();
}
/**
* Get all languages
* @return array Array of languages
*/
public function getAllLanguages() {
$stmt = $this->pdo->query("SELECT id, name FROM languages ORDER BY name");
return $stmt->fetchAll();
}
/**
* Get all format types
* @return array Array of format types
*/
public function getAllFormatTypes() {
$stmt = $this->pdo->query("SELECT id, name FROM format_types ORDER BY name");
return $stmt->fetchAll();
}
/**
* Get thesis by ID
* @param int $id Thesis ID
* @return array|null Thesis data or null if not found
*/
public function getThesis($id) {
$stmt = $this->pdo->prepare("SELECT * FROM v_theses_full WHERE id = ?");
$stmt->execute([$id]);
return $stmt->fetch();
}
/**
* Insert a thesis file record
* @param int $thesisId Thesis ID
* @param string $fileType File type ('main', 'annex', 'written_part', 'other')
* @param string $filePath Server path to file
* @param string $fileName Original filename
* @param int $fileSize File size in bytes
* @param string $mimeType MIME type
* @return int File ID
*/
public function insertThesisFile($thesisId, $fileType, $filePath, $fileName, $fileSize, $mimeType) {
$stmt = $this->pdo->prepare("
INSERT INTO thesis_files (thesis_id, file_type, file_path, file_name, file_size, mime_type)
VALUES (?, ?, ?, ?, ?, ?)
");
$stmt->execute([$thesisId, $fileType, $filePath, $fileName, $fileSize, $mimeType]);
return $this->pdo->lastInsertId();
}
}