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

111
front-backend/Database.php Normal file
View File

@@ -0,0 +1,111 @@
<?php
/**
* Database connection class for SQLite
*/
class Database {
private static $instance = null;
private $pdo;
/**
* Private constructor to prevent multiple instances
*/
private function __construct() {
$dbPath = __DIR__ . '/../formulaire/test.db';
if (!file_exists($dbPath)) {
throw new Exception("Database file not found: " . $dbPath);
}
try {
$this->pdo = new PDO('sqlite:' . $dbPath);
$this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$this->pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
} catch (PDOException $e) {
error_log("Database connection failed: " . $e->getMessage());
throw $e;
}
}
/**
* Get singleton instance
*/
public static function getInstance() {
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Get PDO connection
*/
public function getConnection() {
return $this->pdo;
}
/**
* Get all published theses with pagination
*/
public function getPublishedTheses($limit = 10, $offset = 0) {
$sql = "SELECT * FROM v_theses_public ORDER BY year DESC, title ASC LIMIT :limit OFFSET :offset";
$stmt = $this->pdo->prepare($sql);
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
return $stmt->fetchAll();
}
/**
* Count all published theses
*/
public function countPublishedTheses() {
$sql = "SELECT COUNT(*) as count FROM theses WHERE is_published = 1";
$stmt = $this->pdo->query($sql);
$result = $stmt->fetch();
return $result['count'];
}
/**
* Get thesis by ID with all related data
*/
public function getThesisById($id) {
$sql = "SELECT * FROM v_theses_full WHERE id = :id AND is_published = 1";
$stmt = $this->pdo->prepare($sql);
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
$stmt->execute();
$thesis = $stmt->fetch();
if (!$thesis) {
return null;
}
// Get associated files
$thesis['files'] = $this->getThesisFiles($id);
return $thesis;
}
/**
* Get files associated with a thesis
*/
public function getThesisFiles($thesisId) {
$sql = "SELECT * FROM thesis_files WHERE thesis_id = :thesis_id ORDER BY file_type, uploaded_at";
$stmt = $this->pdo->prepare($sql);
$stmt->bindValue(':thesis_id', $thesisId, PDO::PARAM_INT);
$stmt->execute();
return $stmt->fetchAll();
}
/**
* Prevent cloning
*/
private function __clone() {}
/**
* Prevent unserialization
*/
public function __wakeup() {
throw new Exception("Cannot unserialize singleton");
}
}

View File

@@ -3,61 +3,73 @@ ini_set('display_errors', 0);
ini_set('log_errors', 1);
ini_set('error_log', 'error.log');
require_once 'vendor/autoload.php';
use Symfony\Component\Yaml\Yaml;
require_once 'Database.php';
$page = isset($_GET['page']) ? intval($_GET['page']) : 1;
$itemsPerPage = 10;
$dir = "data/yaml/*.yaml";
$yamlFiles = glob($dir);
$data = [];
foreach ($yamlFiles as $yamlFile) {
$data[] = Yaml::parseFile($yamlFile);
try {
$db = Database::getInstance();
$offset = ($page - 1) * $itemsPerPage;
$itemsToLoad = $db->getPublishedTheses($itemsPerPage, $offset);
$totalItems = $db->countPublishedTheses();
$totalPages = ceil($totalItems / $itemsPerPage);
} catch (Exception $e) {
error_log("Error loading theses: " . $e->getMessage());
$itemsToLoad = [];
$totalPages = 0;
}
usort($data, function ($a, $b) {
return $a['année'] <=> $b['année'];
});
$offset = ($page - 1) * $itemsPerPage;
$itemsToLoad = array_slice($data, $offset, $itemsPerPage);
include 'inc/header.php'; ?>
<section class="section">
<div class="container">
<div class="columns is-multiline">
<?php foreach ($itemsToLoad as $key => $item): ?>
<?php foreach ($itemsToLoad as $item): ?>
<div class="column is-one-fifth">
<a href="memoire.php?file=<?= urlencode($yamlFiles[$key]); ?>" class="card-link">
<a href="memoire.php?id=<?= $item['id']; ?>" class="card-link">
<div class="card">
<?php if (isset($item['couverture'])): ?>
<?php
// Get cover image from thesis_files if available
$coverImage = null;
if (!empty($item['id'])) {
$files = $db->getThesisFiles($item['id']);
foreach ($files as $file) {
$ext = strtolower(pathinfo($file['file_path'], PATHINFO_EXTENSION));
if (in_array($ext, ['jpg', 'jpeg', 'png', 'gif']) && $file['file_type'] === 'main') {
$coverImage = $file['file_path'];
break;
}
}
}
?>
<?php if ($coverImage): ?>
<div class="card-image">
<figure class="image ">
<img src="<?= $item['couverture']; ?>" alt="Image preview">
<img src="<?= htmlspecialchars($coverImage); ?>" alt="Image preview">
</figure>
</div>
<?php endif; ?>
<div class="card-content">
<h4 class="title is-4">
<?= $item['titre']; ?>
<?= htmlspecialchars($item['title']); ?>
</h4>
<h2 class="subtitle">
<?= $item['auteurice']; ?>
</p>
<?= htmlspecialchars($item['authors'] ?? 'Auteur inconnu'); ?>
</h2>
<h3 class="tag title is-6 is-link is-light">
<?= $item['année']; ?>
</h3>
<?= htmlspecialchars($item['year']); ?>
</h3>
<p class="block content">
<?php
$excerpt_length = 150;
$description_excerpt = substr($item['description'], 0, $excerpt_length) . '...';
$synopsis = $item['synopsis'] ?? '';
$description_excerpt = strlen($synopsis) > $excerpt_length
? substr($synopsis, 0, $excerpt_length) . '...'
: $synopsis;
?>
<?= $description_excerpt; ?>
<?= htmlspecialchars($description_excerpt); ?>
</p>
</div>

53
front-backend/justfile Normal file
View File

@@ -0,0 +1,53 @@
# Justfile for Post-ERG front-backend website
# Default recipe - show available commands
default:
@just --list
# Start PHP development server
serve:
@echo "Starting PHP development server on http://localhost:8000"
@echo "Using database: ../formulaire/test.db"
@echo "Press Ctrl+C to stop"
@php -S 127.0.0.1:8000
# Test database connection
test:
@echo "Testing database connection..."
@php test_db.php
# Show database statistics
stats:
@echo "=== Database Statistics ==="
@sqlite3 ../formulaire/test.db "SELECT COUNT(*) || ' total theses' FROM theses;"
@sqlite3 ../formulaire/test.db "SELECT COUNT(*) || ' published theses' FROM theses WHERE is_published = 1;"
@sqlite3 ../formulaire/test.db "SELECT COUNT(*) || ' authors' FROM authors;"
@sqlite3 ../formulaire/test.db "SELECT COUNT(*) || ' keywords' FROM keywords;"
# Show recent published theses
recent:
@echo "=== Recent Published Theses ==="
@sqlite3 -column -header ../formulaire/test.db "SELECT id, title, year, authors FROM v_theses_public ORDER BY year DESC, title LIMIT 10;"
# Query database interactively
query:
@sqlite3 ../formulaire/test.db
# Show specific thesis details
show id:
@sqlite3 -column -header ../formulaire/test.db "SELECT * FROM v_theses_full WHERE id = {{id}};"
# Check PHP syntax for all PHP files
check:
@echo "Checking PHP syntax..."
@php -l Database.php
@php -l index.php
@php -l memoire.php
@php -l apropos.php
@php -l contact.php
@php -l licences.php
@echo "✓ All files have valid syntax"
# View error log
logs:
@if [ -f error.log ]; then tail -n 50 error.log; else echo "No error log found"; fi

View File

@@ -5,16 +5,27 @@ ini_set('log_errors', 1);
ini_set('error_log', 'error.log');
// Load required libraries and classes
require_once 'vendor/autoload.php';
use Symfony\Component\Yaml\Yaml;
require_once 'Database.php';
// Check if a file parameter is provided in the URL
if (isset($_GET['file'])) {
// Decode the URL parameter and parse the YAML file
$yamlFile = urldecode($_GET['file']);
$data = Yaml::parseFile($yamlFile);
// Check if an id parameter is provided in the URL
if (isset($_GET['id'])) {
$thesisId = intval($_GET['id']);
try {
$db = Database::getInstance();
$data = $db->getThesisById($thesisId);
if (!$data) {
// Thesis not found or not published
header('Location: index.php');
exit;
}
} catch (Exception $e) {
error_log("Error loading thesis: " . $e->getMessage());
header('Location: index.php');
exit;
}
} else {
// Redirect to the index page if no file parameter is provided
// Redirect to the index page if no id parameter is provided
header('Location: index.php');
exit;
}
@@ -29,74 +40,119 @@ include 'inc/header.php'; ?>
<div class="column is-one-third">
<div class="card">
<div class="card-content">
<!-- Display the title and author from the YAML data -->
<!-- Display the title and author from the database -->
<h1 class="title">
<?= $data['titre']; ?>
<?= htmlspecialchars($data['title']); ?>
<?php if (!empty($data['subtitle'])): ?>
<br><small><?= htmlspecialchars($data['subtitle']); ?></small>
<?php endif; ?>
</h1>
<h2 class="subtitle">par
<?= $data['auteurice']; ?>
<?= htmlspecialchars($data['authors'] ?? 'Auteur inconnu'); ?>
</h2>
<h3 class="subtitle"></h3>
<div class="columns">
<div class="column is-half ">
<h3 class="subtitle">
<?= $data['orientation']; ?> et
<?= $data['ap']; ?>
</h3>
<?php if (!empty($data['orientation']) || !empty($data['ap_program'])): ?>
<h3 class="subtitle">
<?php if (!empty($data['orientation'])): ?>
<?= htmlspecialchars($data['orientation']); ?>
<?php endif; ?>
<?php if (!empty($data['orientation']) && !empty($data['ap_program'])): ?>
et
<?php endif; ?>
<?php if (!empty($data['ap_program'])): ?>
<?= htmlspecialchars($data['ap_program']); ?>
<?php endif; ?>
</h3>
<?php endif; ?>
<p class="block tag subtitle is-6">
<?= $data['année']; ?>
<?= htmlspecialchars($data['year']); ?>
</p>
<?php if (!empty($data['finality_type'])): ?>
<p class="block">
<strong>Finalité:</strong> <?= htmlspecialchars($data['finality_type']); ?>
</p>
<?php endif; ?>
</div>
<div class="column">
<p class="block">
<?= $data['problématique']; ?>
</p>
<?php if (!empty($data['context_note'])): ?>
<p class="block">
<em><?= htmlspecialchars($data['context_note']); ?></em>
</p>
<?php endif; ?>
<p class="block">
<strong>Contact:</strong>
<?= $data['email']; ?>
</p>
<p class="block">
<strong>Promoteur.ice.s:</strong>
<?= $data['promoteurice']; ?>
</p>
<?php if (!empty($data['supervisors'])): ?>
<p class="block">
<strong>Promoteur.ice.s:</strong>
<?= htmlspecialchars($data['supervisors']); ?>
</p>
<?php endif; ?>
<?php if (!empty($data['languages'])): ?>
<p class="block">
<strong>Langue(s):</strong>
<?= htmlspecialchars($data['languages']); ?>
</p>
<?php endif; ?>
<?php if (!empty($data['formats'])): ?>
<p class="block">
<strong>Format(s):</strong>
<?= htmlspecialchars($data['formats']); ?>
</p>
<?php endif; ?>
<?php if (!empty($data['keywords'])): ?>
<p class="block">
<strong>Mots-clés:</strong>
<?= htmlspecialchars($data['keywords']); ?>
</p>
<?php endif; ?>
</div>
</div>
</div>
</div>
<div class="box">
<?= $data['description']; ?>
<?php if (!empty($data['synopsis'])): ?>
<?= nl2br(htmlspecialchars($data['synopsis'])); ?>
<?php endif; ?>
</div>
</div>
<div class="column is-two-third">
<div class="content">
<!-- Check if there are any files in the YAML data -->
<?php if (isset($data['files'])): ?>
<!-- Check if there are any files in the database -->
<?php if (isset($data['files']) && count($data['files']) > 0): ?>
<!-- Loop through the files and display them based on their file type -->
<?php foreach ($data['files'] as $file): ?>
<?php $ext = pathinfo($file, PATHINFO_EXTENSION); ?>
<?php $ext = strtolower(pathinfo($file['file_path'], PATHINFO_EXTENSION)); ?>
<div class="block">
<?php if ($ext === 'pdf'): ?>
<!-- Display PDF files using the embed element -->
<embed src="<?= $file; ?>" type="application/pdf" width="100%" height="600px" />
<embed src="<?= htmlspecialchars($file['file_path']); ?>" type="application/pdf" width="100%" height="600px" />
<?php elseif (in_array($ext, ['jpg', 'jpeg', 'png', 'gif', 'bmp'])): ?>
<!-- Display image files using the img element -->
<figure>
<img src="<?= $file; ?>" alt="Image file">
<img src="<?= htmlspecialchars($file['file_path']); ?>" alt="<?= htmlspecialchars($file['file_name']); ?>">
</figure>
<?php elseif ($ext === 'mp4'): ?>
<!-- Display MP4 video files using the video element -->
<video width="100%" height="auto" controls>
<source src="<?= $file; ?>" type="video/mp4">
<source src="<?= htmlspecialchars($file['file_path']); ?>" type="video/mp4">
Your browser does not support the video tag.
</video>
<?php endif; ?>
<?php if (!empty($file['description'])): ?>
<p class="help"><?= htmlspecialchars($file['description']); ?></p>
<?php endif; ?>
</div>
<?php endforeach; ?>
<?php else: ?>
<p class="notification is-warning">Aucun fichier disponible pour ce mémoire.</p>
<?php endif; ?>
</div>

40
front-backend/test_db.php Normal file
View File

@@ -0,0 +1,40 @@
<?php
// Simple test script to verify database connection and queries
require_once 'Database.php';
try {
$db = Database::getInstance();
echo "✓ Database connection successful\n";
// Test counting theses
$count = $db->countPublishedTheses();
echo "✓ Found {$count} published theses\n";
// Test getting theses
$theses = $db->getPublishedTheses(5, 0);
echo "✓ Retrieved " . count($theses) . " theses\n";
if (count($theses) > 0) {
$first = $theses[0];
echo "\nFirst thesis:\n";
echo " ID: " . $first['id'] . "\n";
echo " Title: " . $first['title'] . "\n";
echo " Author(s): " . ($first['authors'] ?? 'N/A') . "\n";
echo " Year: " . $first['year'] . "\n";
// Test getting single thesis
$thesis = $db->getThesisById($first['id']);
if ($thesis) {
echo "✓ Successfully retrieved thesis details\n";
echo " Orientation: " . ($thesis['orientation'] ?? 'N/A') . "\n";
echo " AP Program: " . ($thesis['ap_program'] ?? 'N/A') . "\n";
echo " Files: " . (count($thesis['files'] ?? [])) . "\n";
}
}
echo "\n✓ All tests passed!\n";
} catch (Exception $e) {
echo "✗ Error: " . $e->getMessage() . "\n";
exit(1);
}