fix: unify CSV import/export columns, fix import JS error, fix createThesis VALUES

- Fix createThesis SQL: extra ? placeholder in VALUES (27 values for 26 columns)
- Add createThesis integration tests to catch column/value mismatches
- Add CSV_COLUMNS as single source of truth in ExportController, deriving
  csvHeaders() and positional fallback from it
- Add missing columns to export query + CSV: duration_pages, duration_minutes,
  has_annexes, license_custom, contact_visible, objet
- Add missing columns to import INSERT: license_id, license_custom, cc2r,
  exemplaire_baiu, exemplaire_erg, objet, contact_visible, duration_pages,
  duration_minutes, has_annexes
- Resolve license name → license_id during import
- Fix XamxamInitFilePonds: add file-upload-filepond.js to admin-entry.js
  so FilePond initialization code is available on the admin list page
- Add FilePond vendor CSS to admin.min.css bundle (import dialog styling)
- Generate .admin-file-hint from csvHeaders() instead of hardcoded stale list
- Use positional fallback from CSV_COLUMNS for import cell parsing
This commit is contained in:
Pontoporeia
2026-07-10 11:56:27 +02:00
parent ce936e3b71
commit 22a49f7cb6
11 changed files with 355 additions and 42 deletions
+1 -1
View File
@@ -27,7 +27,7 @@ echo "\xEF\xBB\xBF";
$out = fopen('php://output', 'w');
// Column headers
fputcsv($out, ExportController::CSV_HEADERS, ',', '"', '');
fputcsv($out, ExportController::csvHeaders(), ',', '"', '');
// Data rows
$rows = $controller->exportAllTheses();
+102 -35
View File
@@ -37,14 +37,17 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['csv_file'])) {
// Build colIdx[name] → position map; fall back to positional if header not found.
// Matching uses prefix + variant logic so "contact.visible" matches "contact",
// "promoteur·ice(s)" matches "promoteur", "Licence" matches "license", etc.
require_once APP_ROOT . '/src/Controllers/ExportController.php';
$csvCols = ExportController::CSV_COLUMNS;
$csvHeaders = ExportController::csvHeaders();
$colCount = count($csvCols);
$colIdx = null;
$headerRowNum = 0;
$knownHeaders = [
'identifiant', 'titre', 'sous-titre', 'auteur', 'contact',
'promoteur', 'lecteur', 'ulb', 'externe', 'format', 'année', 'ap', 'orientation', 'finalité',
'mots-clés', 'synopsis', 'contexte', 'remarques', 'langue',
'autorisation', 'licence', 'license', 'points', 'lien baiu',
];
// Build knownHeaders from CSV_COLUMNS import_key column (index 2)
$knownHeaders = array_unique(array_column($csvCols, 2));
// Add licence/license cross-match aliases
$knownHeaders[] = 'license';
for ($scan = 0; $scan < 8; $scan++) {
$hrow = fgetcsv($handle, 0, ',', '"', '');
if ($hrow === false) break;
@@ -101,10 +104,20 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['csv_file'])) {
}
}
// Build positional fallback from CSV_COLUMNS (keyed by import_key → position)
$fallbackPositions = [];
foreach ($csvCols as $pos => $col) {
$key = $col[2];
if (!isset($fallbackPositions[$key])) {
$fallbackPositions[$key] = $pos;
}
}
// Helper: get cell value by column name.
// When header was found: only use mapped column (returns '' if missing from header).
// When no header found: use positional fallback index.
$cell = function(array $row, string $name, int $fallbackPos) use ($colIdx): string {
// When header was found: use mapped column.
// When header was found but key is missing: use positional fallback.
// When no header found: use positional fallback.
$cell = function(array $row, string $name) use ($colIdx, $fallbackPositions): string {
if ($colIdx !== null) {
$pos = $colIdx[$name] ?? null;
if ($pos === null) {
@@ -112,13 +125,24 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['csv_file'])) {
if ($name === 'license') $pos = $colIdx['licence'] ?? null;
elseif ($name === 'licence') $pos = $colIdx['license'] ?? null;
}
// Fall back to positional if header didn't have this key
if ($pos === null) {
$pos = $fallbackPositions[$name] ?? null;
}
if ($pos === null) return '';
} else {
$pos = $fallbackPos;
$pos = $fallbackPositions[$name] ?? null;
if ($pos === null) return '';
}
return isset($row[$pos]) ? trim((string)$row[$pos]) : '';
};
// Helper: parse "Oui"/"Non"/1/0 strings to int
$parseBool = function(string $raw): int {
$raw = strtolower(trim($raw));
return ($raw === 'oui' || $raw === '1' || $raw === 'yes' || $raw === 'true') ? 1 : 0;
};
// Code → canonical name (legacy short-code CSV format)
$orientationCodeMap = [
'SC'=>'Sculpture','VI'=>'Vidéographie','CA'=>"Cinéma d'animation",
@@ -232,39 +256,55 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['csv_file'])) {
try {
$importDb->beginTransaction();
$identifier = $cell($row, 'identifiant', 0);
$title = $cell($row, 'titre', 1);
$subtitle = $cell($row, 'sous-titre', 2);
$authorsRaw = $cell($row, 'auteur', 3);
$contact = $cell($row, 'contact', 4);
$identifier = $cell($row, 'identifiant');
$title = $cell($row, 'titre');
$subtitle = $cell($row, 'sous-titre');
$authorsRaw = $cell($row, 'auteur');
$contact = $cell($row, 'contact');
// Normalise CSV artefacts: OUI/NON → empty (not a valid email)
if ($contact !== '' && in_array(strtoupper(trim($contact)), ['NON', 'OUI'], true)) {
$contact = '';
}
$supervisorsRaw = $cell($row, 'promoteur', 5);
$lecteursInternesRaw = $cell($row, 'lecteur', 6); // first "lecteur" col = interne
$lecteursExternesRaw = $cell($row, 'externe', 7); // contains "externe"
$promoteursUlbRaw = $cell($row, 'ulb', 8); // contains "ulb"
$formatsRaw = $cell($row, 'format', 9);
$yearRaw = $cell($row, 'année', 10);
$supervisorsRaw = $cell($row, 'promoteur');
$lecteursInternesRaw = $cell($row, 'lecteur'); // first "lecteur" col = interne
$lecteursExternesRaw = $cell($row, 'externe'); // contains "externe"
$promoteursUlbRaw = $cell($row, 'ulb'); // contains "ulb"
$formatsRaw = $cell($row, 'format');
$yearRaw = $cell($row, 'année');
$year = $yearRaw !== '' ? intval($yearRaw) : 0;
// Fallback: derive year from identifier (e.g. "2024-003" → 2024)
if ($year === 0 && $identifier !== '' && preg_match('/^(\d{4})-/', $identifier, $m)) {
$year = (int)$m[1];
}
$apCode = $cell($row, 'ap', 11);
$orientationCode = $cell($row, 'orientation', 12);
$finalityName = $cell($row, 'finalité', 13);
$keywordsRaw = $cell($row, 'mots-clés', 14);
$synopsis = $cell($row, 'synopsis', 15);
$context = $cell($row, 'contexte', 16);
$remarks = $cell($row, 'remarques', 17);
$languageRaw = $cell($row, 'langue', 18);
$access = $cell($row, 'autorisation', 19);
$license = $cell($row, 'license', 20);
$juryPointsRaw = $cell($row, 'points', 21);
$apCode = $cell($row, 'ap');
$orientationCode = $cell($row, 'orientation');
$finalityName = $cell($row, 'finalité');
$keywordsRaw = $cell($row, 'mots-clés');
$synopsis = $cell($row, 'synopsis');
$context = $cell($row, 'contexte');
$remarks = $cell($row, 'remarques');
$languageRaw = $cell($row, 'langue');
$access = $cell($row, 'autorisation');
$license = $cell($row, 'licence');
$juryPointsRaw = $cell($row, 'points');
$juryPoints = $juryPointsRaw !== '' ? floatval($juryPointsRaw) : null;
$baiuLink = $cell($row, 'lien baiu', 22);
$baiuLink = $cell($row, 'lien baiu');
$cc2rRaw = $cell($row, 'cc2r');
$cc2r = $parseBool($cc2rRaw);
$exempBaiuRaw = $cell($row, 'exemplaire'); // first "exemplaire" col = BAIU
$exempBaiu = $parseBool($exempBaiuRaw);
$exempErgRaw = $cell($row, 'exemplaire-erg'); // second "exemplaire", key won't match header → positional
$exempErg = $parseBool($exempErgRaw);
$durationPagesRaw = $cell($row, 'pages');
$durationPages = $durationPagesRaw !== '' ? (int)$durationPagesRaw : null;
$durationMinutesRaw = $cell($row, 'durée');
$durationMinutes = $durationMinutesRaw !== '' ? (int)$durationMinutesRaw : null;
$hasAnnexesRaw = $cell($row, 'annexes');
$hasAnnexes = $parseBool($hasAnnexesRaw);
$licenseCustom = $cell($row, 'licence-perso'); // second "licence", key won't match header → positional
$contactVisible = $cell($row, 'contact-visible'); // second "contact", key won't match header → positional
$objetRaw = $cell($row, 'objet');
$objet = in_array($objetRaw, ['tfe', 'thèse', 'frart'], true) ? $objetRaw : 'tfe';
if ($title === '' || $year === 0) {
$missing = [];
@@ -303,14 +343,31 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['csv_file'])) {
}
}
// Resolve license name → license_id
$licenseId = null;
if (!empty($license)) {
$ls = $importPdo->prepare("SELECT id FROM license_types WHERE LOWER(name) = LOWER(?)");
$ls->execute([trim($license)]);
$lr = $ls->fetch();
$licenseId = $lr ? (int)$lr['id'] : null;
}
// If a custom license string is provided and no DB license matched, store as custom
if ($licenseId === null && !empty($license) && empty($licenseCustom)) {
$licenseCustom = $license;
}
$s = $importPdo->prepare("
INSERT INTO theses (
identifier, title, subtitle, year,
orientation_id, ap_program_id, finality_id,
synopsis, context_note, remarks,
jury_points, baiu_link,
access_type_id, is_published, submitted_at
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,0,CURRENT_TIMESTAMP)
access_type_id, license_id, license_custom,
cc2r, exemplaire_baiu, exemplaire_erg,
objet, contact_visible,
duration_pages, duration_minutes, has_annexes,
is_published, status, submitted_at
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,0,'active',CURRENT_TIMESTAMP)
");
$s->execute([
!empty($identifier) ? $identifier : null, $title,
@@ -322,6 +379,16 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['csv_file'])) {
$juryPoints,
!empty($baiuLink) ? $baiuLink : null,
$accessTypeId,
$licenseId,
!empty($licenseCustom) ? $licenseCustom : null,
$cc2r,
$exempBaiu,
$exempErg,
$objet,
!empty($contactVisible) ? $contactVisible : null,
$durationPages,
$durationMinutes,
$hasAnnexes,
]);
$thesisId = $importPdo->lastInsertId();