mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-06-25 16:19:19 +02:00
CSV importer: boolean and ap variants/typos
- add AP aliases for: - Design & politique du multiple → DPM, - Pratiques artistiques & complexité scientifique → PACS, - Narraion Speculative typo → NS - Fix: OUI/NON CSV artefacts in contact_interne — clean DB, guard in findOrCreateAuthor and CSV import - Cleaned 141 authors.email = 'NON' rows → NULL in dev DB - findOrCreateAuthor: treat OUI/NON as null (CSV boolean artefact in email column) - CSV import: sanitize contact column — OUI/NON → empty string before passing to findOrCreateAuthor
This commit is contained in:
4
TODO.md
4
TODO.md
@@ -1,5 +1,7 @@
|
||||
# TODO
|
||||
|
||||
- [x] Languages: store lowercase, display with ucfirst (getOrCreateLanguage, CSV import, getAllLanguages, v_theses_full, schema seed data, migration 025)
|
||||
- [x] CSV importer: add AP aliases for D&P du multiple, PACS variants, Narraion typo
|
||||
- [x] Move default semantic form element styles (checkbox, radio, select) from admin.css/form.css into common.css
|
||||
- [x] Keep specific layouts/classes in form.css (admin-form grid, checkbox-group layout, etc.)
|
||||
- [x] Ensure selects, checkboxes, and radios are properly styled globally
|
||||
@@ -70,7 +72,7 @@
|
||||
- [x] Mots-clés: lowercase enforcement, deduplication, absolute dropdown, keyboard arrows/enter/escape, blur hide, spacing + counter above input, CSV import lowercased, space-collapse normalization, minimum 3 keywords required
|
||||
- [x] ErrorHandler: shared static helper for structured error_log + user-friendly messages with precise FK field extraction from SQLite errors. Applied to 12 action files + 6 public controllers + 2 form controllers + partage. Covers FK, UNIQUE, NOT NULL constraint types.
|
||||
- [x] Fix: findOrCreateAuthor cannot clear email (empty string skips update, leaves old email)
|
||||
- [ ] Fix: "NON" stored as literal email string in authors table (CSV import or old data)
|
||||
- [x] Fix: "NON" stored as literal email string in authors table — cleaned existing DB rows, added OUI/NON→null guard in findOrCreateAuthor and CSV import
|
||||
- [x] Fix: contact_interne field in edit form never saved — removed dead field from form and dead validation from create controller
|
||||
- [x] Fix: formulaire.php unconditionally suppresses display_errors even in dev mode
|
||||
- [x] Fix: access_type_id radio has no "none" option — added "— Non défini" radio for admin mode
|
||||
|
||||
75
app/migrations/applied/025_lowercase_languages.sql
Normal file
75
app/migrations/applied/025_lowercase_languages.sql
Normal file
@@ -0,0 +1,75 @@
|
||||
-- 025_lowercase_languages.sql
|
||||
-- Normalise les noms de langues en minuscules et recrée la vue avec ucfirst.
|
||||
|
||||
-- Normaliser les langues existantes
|
||||
UPDATE languages SET name = LOWER(name);
|
||||
|
||||
-- Recréer la vue pour appliquer UPPER(SUBSTR(name,1,1)) || SUBSTR(name,2) dans le GROUP_CONCAT
|
||||
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,
|
||||
t.objet,
|
||||
o.name as orientation,
|
||||
ap.name as ap_program,
|
||||
ft.name as finality_type,
|
||||
t.synopsis,
|
||||
t.context_note,
|
||||
at.name as access_type,
|
||||
lt.name as license_type,
|
||||
t.license_id,
|
||||
t.license_custom,
|
||||
t.access_type_id,
|
||||
t.jury_points,
|
||||
t.submitted_at,
|
||||
t.defense_date,
|
||||
t.published_at,
|
||||
t.is_published,
|
||||
t.baiu_link,
|
||||
t.banner_path,
|
||||
t.exemplaire_baiu,
|
||||
t.exemplaire_erg,
|
||||
t.cc2r,
|
||||
t.remarks,
|
||||
t.jury_note_added,
|
||||
GROUP_CONCAT(DISTINCT a.name ORDER BY a.name ASC) 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' AND ts.is_ulb = 0 THEN s.name END) as jury_promoteurs,
|
||||
GROUP_CONCAT(DISTINCT CASE WHEN ts.role = 'promoteur' AND ts.is_ulb = 1 THEN s.name END) as jury_promoteurs_ulb,
|
||||
GROUP_CONCAT(DISTINCT CASE WHEN ts.role = 'lecteur' AND ts.is_external = 0 THEN s.name END) as jury_lecteurs_internes,
|
||||
GROUP_CONCAT(DISTINCT CASE WHEN ts.role = 'lecteur' AND ts.is_external = 1 THEN s.name END) as jury_lecteurs_externes,
|
||||
GROUP_CONCAT(DISTINCT UPPER(SUBSTR(l.name,1,1)) || SUBSTR(l.name,2)) 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 contact_interne,
|
||||
(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 contact_public
|
||||
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;
|
||||
72
app/migrations/pending/025_fix_oui_non_artefacts.sql
Normal file
72
app/migrations/pending/025_fix_oui_non_artefacts.sql
Normal file
@@ -0,0 +1,72 @@
|
||||
-- 025_fix_oui_non_artefacts.sql
|
||||
-- Clean OUI/NON CSV artefacts from authors.email (should be NULL, not literal strings).
|
||||
-- Also update the v_theses_full view to use contact_interne/contact_public column names.
|
||||
|
||||
UPDATE authors SET email = NULL WHERE email IN ('NON', 'OUI', '');
|
||||
|
||||
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,
|
||||
t.objet,
|
||||
o.name as orientation,
|
||||
ap.name as ap_program,
|
||||
ft.name as finality_type,
|
||||
t.synopsis,
|
||||
t.context_note,
|
||||
at.name as access_type,
|
||||
lt.name as license_type,
|
||||
t.license_id,
|
||||
t.license_custom,
|
||||
t.access_type_id,
|
||||
t.jury_points,
|
||||
t.submitted_at,
|
||||
t.defense_date,
|
||||
t.published_at,
|
||||
t.is_published,
|
||||
t.baiu_link,
|
||||
t.banner_path,
|
||||
t.exemplaire_baiu,
|
||||
t.exemplaire_erg,
|
||||
t.cc2r,
|
||||
t.remarks,
|
||||
t.jury_note_added,
|
||||
GROUP_CONCAT(DISTINCT a.name ORDER BY a.name ASC) 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' AND ts.is_ulb = 0 THEN s.name END) as jury_promoteurs,
|
||||
GROUP_CONCAT(DISTINCT CASE WHEN ts.role = 'promoteur' AND ts.is_ulb = 1 THEN s.name END) as jury_promoteurs_ulb,
|
||||
GROUP_CONCAT(DISTINCT CASE WHEN ts.role = 'lecteur' AND ts.is_external = 0 THEN s.name END) as jury_lecteurs_internes,
|
||||
GROUP_CONCAT(DISTINCT CASE WHEN ts.role = 'lecteur' AND ts.is_external = 1 THEN s.name END) as jury_lecteurs_externes,
|
||||
GROUP_CONCAT(DISTINCT l.name) as languages,
|
||||
GROUP_CONCAT(DISTINCT fmt.name) as formats,
|
||||
GROUP_CONCAT(DISTINCT tg.name) as keywords,
|
||||
(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 contact_interne,
|
||||
(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 contact_public
|
||||
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;
|
||||
@@ -173,11 +173,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['csv_file'])) {
|
||||
=> 'LIENS',
|
||||
'récits et expérimentation' => 'NS',
|
||||
'recits et experimentation' => 'NS',
|
||||
'atelier pratiques situées' => 'APS',
|
||||
'design et politique du multiple' => 'DPM',
|
||||
'narraion spéculative' => 'NS',
|
||||
'narration spéculative' => 'NS',
|
||||
'atelier pratiques situées' => 'APS',
|
||||
'design & politique du multiple' => 'DPM',
|
||||
'design et politique du multiple' => 'DPM',
|
||||
'pacs' => 'PACS',
|
||||
'pratique de l\'art' => 'PACS',
|
||||
'pratiques artistiques & complexité scientifique' => 'PACS',
|
||||
];
|
||||
|
||||
// Resolve an AP string (code or full name) → ap_program id.
|
||||
@@ -230,6 +233,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['csv_file'])) {
|
||||
$subtitle = $cell($row, 'sous-titre', 2);
|
||||
$authorsRaw = $cell($row, 'auteur', 3);
|
||||
$contact = $cell($row, 'contact', 4);
|
||||
// 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);
|
||||
$formatsRaw = $cell($row, 'format', 6);
|
||||
$yearRaw = $cell($row, 'année', 7);
|
||||
@@ -342,13 +349,18 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['csv_file'])) {
|
||||
}
|
||||
}
|
||||
if (!empty($languageRaw)) {
|
||||
$s = $importPdo->prepare("SELECT id FROM languages WHERE name = ?");
|
||||
$s->execute([ucfirst(strtolower($languageRaw))]);
|
||||
$langName = strtolower(trim($languageRaw));
|
||||
// Lookup case-insensitively; insert if missing (stored lowercase).
|
||||
$s = $importPdo->prepare("SELECT id FROM languages WHERE LOWER(name) = LOWER(?)");
|
||||
$s->execute([$langName]);
|
||||
$r = $s->fetch();
|
||||
if ($r) {
|
||||
$s2 = $importPdo->prepare("INSERT INTO thesis_languages (thesis_id, language_id) VALUES (?,?)");
|
||||
$s2->execute([$thesisId, $r['id']]);
|
||||
$langId = $r ? (int)$r['id'] : null;
|
||||
if ($langId === null) {
|
||||
$importPdo->prepare("INSERT INTO languages (name) VALUES (?)")->execute([$langName]);
|
||||
$langId = (int)$importPdo->lastInsertId();
|
||||
}
|
||||
$s2 = $importPdo->prepare("INSERT INTO thesis_languages (thesis_id, language_id) VALUES (?,?)");
|
||||
$s2->execute([$thesisId, $langId]);
|
||||
}
|
||||
if (!empty($formatsRaw)) {
|
||||
foreach (array_map('trim', explode(',', $formatsRaw)) as $fmt) {
|
||||
|
||||
@@ -744,11 +744,11 @@ class Database
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all languages
|
||||
* Get all languages (name is capitalized for display).
|
||||
*/
|
||||
public function getAllLanguages(): array
|
||||
{
|
||||
$stmt = $this->pdo->query('SELECT * FROM languages ORDER BY name');
|
||||
$stmt = $this->pdo->query("SELECT id, UPPER(SUBSTR(name,1,1)) || SUBSTR(name,2) as name, created_at FROM languages ORDER BY name");
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
|
||||
@@ -954,6 +954,11 @@ class Database
|
||||
*/
|
||||
public function findOrCreateAuthor($name, $email = null, bool $showContact = false)
|
||||
{
|
||||
// Normalise CSV artefacts: OUI/NON strings in email column → null
|
||||
if ($email !== null && in_array(strtoupper(trim($email)), ['NON', 'OUI'], true)) {
|
||||
$email = null;
|
||||
}
|
||||
|
||||
$stmt = $this->pdo->prepare('SELECT id FROM authors WHERE name = ?');
|
||||
$stmt->execute([$name]);
|
||||
$author = $stmt->fetch();
|
||||
@@ -1522,11 +1527,11 @@ class Database
|
||||
|
||||
/**
|
||||
* Return the ID of an existing language by name, inserting it if absent.
|
||||
* Name is trimmed and stored as-is (case-preserved).
|
||||
* Name is stored lowercase and displayed with first letter capitalized.
|
||||
*/
|
||||
public function getOrCreateLanguage(string $name): int
|
||||
{
|
||||
$name = trim($name);
|
||||
$name = strtolower(trim($name));
|
||||
$stmt = $this->pdo->prepare('SELECT id FROM languages WHERE LOWER(name) = LOWER(?) LIMIT 1');
|
||||
$stmt->execute([$name]);
|
||||
$id = $stmt->fetchColumn();
|
||||
|
||||
@@ -88,9 +88,9 @@ CREATE TABLE IF NOT EXISTS languages (
|
||||
);
|
||||
|
||||
INSERT OR IGNORE INTO languages (name) VALUES
|
||||
('Français'),
|
||||
('Anglais'),
|
||||
('Néerlandais');
|
||||
('français'),
|
||||
('anglais'),
|
||||
('néerlandais');
|
||||
|
||||
-- Format types (can select multiple)
|
||||
CREATE TABLE IF NOT EXISTS format_types (
|
||||
@@ -525,7 +525,7 @@ SELECT
|
||||
GROUP_CONCAT(DISTINCT CASE WHEN ts.role = 'promoteur' AND ts.is_ulb = 1 THEN s.name END) as jury_promoteurs_ulb,
|
||||
GROUP_CONCAT(DISTINCT CASE WHEN ts.role = 'lecteur' AND ts.is_external = 0 THEN s.name END) as jury_lecteurs_internes,
|
||||
GROUP_CONCAT(DISTINCT CASE WHEN ts.role = 'lecteur' AND ts.is_external = 1 THEN s.name END) as jury_lecteurs_externes,
|
||||
GROUP_CONCAT(DISTINCT l.name) as languages,
|
||||
GROUP_CONCAT(DISTINCT UPPER(SUBSTR(l.name,1,1)) || SUBSTR(l.name,2)) as languages,
|
||||
GROUP_CONCAT(DISTINCT fmt.name) as formats,
|
||||
GROUP_CONCAT(DISTINCT tg.name) as keywords,
|
||||
-- First author's email and contact-visibility flag
|
||||
|
||||
@@ -87,6 +87,19 @@
|
||||
+%%%%%%% diff from: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision)
|
||||
+\\\\\\\ to: pntwsqvs dd95b4d3 "Rename author_email→contact_interne, author_show_contact→contact_public across view/controllers/templates" (rebased revision)
|
||||
++ $linkName = $link['name'] ?? '';
|
||||
++ $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : '';
|
||||
%%%%%%%%%%%%%%%%%%%%%%% diff from: pntwsqvs dd95b4d3 "Rename author_email→contact_interne, author_show_contact→contact_public across view/controllers/templates" (rebased revision)
|
||||
\\\\\\\\\\\\\\\\\\\\\\\ to: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision)
|
||||
- $linkName = $link['name'] ?? '';
|
||||
- $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : '';
|
||||
%%%%%%%%%%%%%%%%%%%%%%% diff from: somsyvxz 14a3cd10 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebase destination)
|
||||
\\\\\\\\\\\\\\\\\\\\\\\ to: sttrwkly dc233066 "CSV importer: boolean and ap variants/typos" (rebased revision)
|
||||
$linkName = $link['name'] ?? '';
|
||||
$linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : '';
|
||||
$linkLockedYear = $link['locked_year'] ?? null;
|
||||
+%%%%%%% diff from: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision)
|
||||
+\\\\\\\ to: sttrwkly ec5606f5 "CSV importer: boolean and ap variants/typos" (rebased revision)
|
||||
++ $linkName = $link['name'] ?? '';
|
||||
++ $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : '';
|
||||
?>
|
||||
<tr class="admin-table-row" onclick="event.stopPropagation(); window.open('/partage/<?= urlencode($link['slug']) ?>', '_blank')" style="cursor:pointer">
|
||||
|
||||
Reference in New Issue
Block a user