mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-05-06 19:19:19 +02:00
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:
111
front-backend/Database.php
Normal file
111
front-backend/Database.php
Normal 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");
|
||||
}
|
||||
}
|
||||
@@ -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
53
front-backend/justfile
Normal 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
|
||||
@@ -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
40
front-backend/test_db.php
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user