Add SQLite indexes for contenus page language/tag queries + WIP: Peertube orphans, dialogs, contact decoupling, context note, finality types

This commit is contained in:
Pontoporeia
2026-06-21 13:33:55 +02:00
parent 0d5e9dac19
commit 03c9c3566f
38 changed files with 1432 additions and 333 deletions

View File

@@ -274,6 +274,9 @@ class ExportController
'Licence',
'Points sur 20',
'Lien BAIU',
'CC2r',
'Exemplaire BAIU',
'Exemplaire ERG',
];
/**
@@ -390,6 +393,9 @@ class ExportController
$t['license_name'] ?? '',
isset($t['jury_points']) ? (string) $t['jury_points'] : '',
$t['baiu_link'] ?? '',
!empty($t['cc2r']) ? 'Oui' : 'Non',
!empty($t['exemplaire_baiu']) ? 'Oui' : 'Non',
!empty($t['exemplaire_erg']) ? 'Oui' : 'Non',
];
}

View File

@@ -339,8 +339,13 @@ class ThesisCreateController
if ($contactVisible === '') {
$contactVisible = trim($post['mail'] ?? '');
}
// showContact: whether to show the contact publicly
if (array_key_exists('contact_public', $post)) {
// showContact: whether to show the contact publicly on the TFE page.
// In admin mode, the checkbox always renders — unchecked = not sent.
// In student mode (partage), the checkbox is not shown, so we
// default to true when contact_visible is filled.
if ($adminMode) {
$showContact = !empty($post['contact_public']);
} elseif (array_key_exists('contact_public', $post)) {
$showContact = !empty($post['contact_public']);
} else {
$showContact = $contactVisible !== '';

View File

@@ -249,6 +249,8 @@ class ThesisEditController
// contact_interne = private email of the first author (backoffice field)
$contactInterne = trim($post['contact_interne'] ?? '');
$firstAuthorEmail = $contactInterne !== '' ? $contactInterne : null;
// contact_public: whether to show the public contact on the TFE page
$showContact = !empty($post['contact_public']);
$authorNames = [];
if ($authorsRaw !== '') {
$authorNames = array_values(array_filter(array_map('trim', explode(',', $authorsRaw)), fn ($n) => $n !== ''));
@@ -259,7 +261,7 @@ class ThesisEditController
$authorEntries[] = [
'name' => $name,
'email' => $i === 0 ? $firstAuthorEmail : null,
'show_contact' => $i === 0,
'show_contact' => $i === 0 && $showContact,
];
}
$this->db->setThesisAuthors($thesisId, $authorEntries);

View File

@@ -1297,9 +1297,10 @@ class Database
if ($query === '') {
$stmt = $this->pdo->query('
SELECT tg.id, tg.name,
COUNT(DISTINCT tt.thesis_id) as thesis_count
COUNT(DISTINCT t.id) as thesis_count
FROM tags tg
LEFT JOIN thesis_tags tt ON tg.id = tt.tag_id
LEFT JOIN theses t ON tt.thesis_id = t.id AND t.deleted_at IS NULL
WHERE tg.deleted_at IS NULL
GROUP BY tg.id
ORDER BY thesis_count DESC, tg.name COLLATE NOCASE
@@ -1308,9 +1309,10 @@ class Database
} else {
$stmt = $this->pdo->prepare('
SELECT tg.id, tg.name,
COUNT(DISTINCT tt.thesis_id) as thesis_count
COUNT(DISTINCT t.id) as thesis_count
FROM tags tg
LEFT JOIN thesis_tags tt ON tg.id = tt.tag_id
LEFT JOIN theses t ON tt.thesis_id = t.id AND t.deleted_at IS NULL
WHERE tg.name LIKE ? AND tg.deleted_at IS NULL
GROUP BY tg.id
ORDER BY tg.name = ? DESC, thesis_count DESC, tg.name COLLATE NOCASE
@@ -1328,9 +1330,10 @@ class Database
{
$stmt = $this->pdo->query('
SELECT tg.id, tg.name,
COUNT(DISTINCT tt.thesis_id) as thesis_count
COUNT(DISTINCT t.id) as thesis_count
FROM tags tg
LEFT JOIN thesis_tags tt ON tg.id = tt.tag_id
LEFT JOIN theses t ON tt.thesis_id = t.id AND t.deleted_at IS NULL
WHERE tg.deleted_at IS NULL
GROUP BY tg.id
ORDER BY tg.name COLLATE NOCASE
@@ -1416,9 +1419,10 @@ class Database
$stmt = $this->pdo->query('
SELECT MIN(l.id) as id,
UPPER(SUBSTR(MIN(l.name),1,1)) || SUBSTR(MIN(l.name),2) as name,
COUNT(DISTINCT tl.thesis_id) as thesis_count
COUNT(DISTINCT t.id) as thesis_count
FROM languages l
LEFT JOIN thesis_languages tl ON l.id = tl.language_id
LEFT JOIN theses t ON tl.thesis_id = t.id AND t.deleted_at IS NULL
WHERE l.deleted_at IS NULL
GROUP BY LOWER(l.name)
ORDER BY thesis_count DESC, LOWER(MIN(l.name)) COLLATE NOCASE
@@ -1428,9 +1432,10 @@ class Database
$stmt = $this->pdo->prepare('
SELECT MIN(l.id) as id,
UPPER(SUBSTR(MIN(l.name),1,1)) || SUBSTR(MIN(l.name),2) as name,
COUNT(DISTINCT tl.thesis_id) as thesis_count
COUNT(DISTINCT t.id) as thesis_count
FROM languages l
LEFT JOIN thesis_languages tl ON l.id = tl.language_id
LEFT JOIN theses t ON tl.thesis_id = t.id AND t.deleted_at IS NULL
WHERE LOWER(l.name) LIKE LOWER(?) AND l.deleted_at IS NULL
GROUP BY LOWER(l.name)
ORDER BY LOWER(MIN(l.name)) = LOWER(?) DESC, thesis_count DESC, LOWER(MIN(l.name)) COLLATE NOCASE
@@ -1450,9 +1455,10 @@ class Database
$stmt = $this->pdo->query('
SELECT MIN(l.id) as id,
UPPER(SUBSTR(MIN(l.name),1,1)) || SUBSTR(MIN(l.name),2) as name,
COUNT(DISTINCT tl.thesis_id) as thesis_count
COUNT(DISTINCT t.id) as thesis_count
FROM languages l
LEFT JOIN thesis_languages tl ON l.id = tl.language_id
LEFT JOIN theses t ON tl.thesis_id = t.id AND t.deleted_at IS NULL
WHERE l.deleted_at IS NULL
GROUP BY LOWER(l.name)
ORDER BY LOWER(MIN(l.name)) COLLATE NOCASE
@@ -2683,7 +2689,10 @@ class Database
t.context_note,
t.remarks,
t.jury_points,
t.baiu_link
t.baiu_link,
t.exemplaire_baiu,
t.exemplaire_erg,
t.cc2r
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

View File

@@ -24,6 +24,48 @@ class DatabaseMigrations
$this->migrateRenameFinalityTypes();
$this->migrateShareLinksNameColumn();
$this->migrateShareLinksLockedYearColumn();
$this->migrateThesisLanguagesIndex();
$this->migrateTagsDeletedNameIndex();
}
/**
* 2026-06-21 — Add index on thesis_languages(language_id).
*
* The contenus page queries join languages → thesis_languages on
* language_id. SQLite's PRIMARY KEY on (thesis_id, language_id)
* can't optimize lookups by language_id alone, so it builds an
* AUTOMATIC COVERING INDEX per query. This persisted index
* removes that overhead.
*/
private function migrateThesisLanguagesIndex(): void
{
try {
$this->pdo->exec(
'CREATE INDEX IF NOT EXISTS idx_thesis_languages_language
ON thesis_languages(language_id)'
);
} catch (\PDOException $e) {
// Table may not exist yet on fresh install — ignore
}
}
/**
* 2026-06-21 — Add covering index on tags(deleted_at, name).
*
* getAllTagsWithCount() filters on deleted_at IS NULL and orders
* by name. This covering index avoids a SCAN of the tags table
* and a temp B-tree for sorting.
*/
private function migrateTagsDeletedNameIndex(): void
{
try {
$this->pdo->exec(
'CREATE INDEX IF NOT EXISTS idx_tags_deleted_name
ON tags(deleted_at, name)'
);
} catch (\PDOException $e) {
// Table may not exist yet on fresh install — ignore
}
}
/**
@@ -31,7 +73,7 @@ class DatabaseMigrations
*
* Spécialisé → Spécialisée
* Approfondi → Approfondie
* Enseignement → Didactique
* Didactique → Enseignement
*/
private function migrateRenameFinalityTypes(): void
{
@@ -39,7 +81,7 @@ class DatabaseMigrations
$renames = [
'Spécialisé' => 'Spécialisée',
'Approfondi' => 'Approfondie',
'Enseignement' => 'Didactique',
'Didactique' => 'Enseignement',
];
foreach ($renames as $old => $new) {
// Skip if only canonical row already exists

View File

@@ -108,7 +108,7 @@ class FormBootstrap
$autofocusField = App::consumeAutofocus();
// Controls
$showContact = false;
$showContact = ($mode === 'add' || $mode === 'edit');
$showBackoffice = ($mode === 'add' || $mode === 'edit');
// Licence / access toggles: admin always enables all three
@@ -208,7 +208,7 @@ class FormBootstrap
// Backoffice (empty for add, populated for edit by caller)
'currentRaw' => [],
'contactInterne' => null,
'contactPublic' => false,
'contactPublic' => null,
'currentContextNote' => null,
'currentContactVisible' => null,
'currentDurationValue' => null,

View File

@@ -265,6 +265,74 @@ class PeerTubeService
return rtrim($s['instance_url'], '/') . '/videos/watch/' . $uuid;
}
// -------------------------------------------------------------------------
// List channel videos
// -------------------------------------------------------------------------
/**
* List all videos uploaded by the authenticated user account.
*
* Uses GET /api/v1/users/me/videos which reliably returns only the
* authenticated user's own videos (all on the configured channel).
* The channel-based endpoints proved unreliable on this instance.
*
* @return array<int, array{uuid:string, shortUUID:string, name:string, createdAt:string}>
*/
public static function listChannelVideos(Database $db): array
{
$s = self::getSettings($db);
if ($s['instance_url'] === '') {
return [];
}
$videos = [];
$count = 100;
$start = 0;
try {
$token = self::obtainToken($s);
$baseUrl = rtrim($s['instance_url'], '/');
while (true) {
$url = $baseUrl . '/api/v1/users/me/videos?count=' . $count
. '&start=' . $start . '&sort=-createdAt';
$resp = self::httpRequest($url, 'GET', [
'headers' => ['Authorization' => 'Bearer ' . $token],
'timeout' => 30,
]);
if ($resp['status'] !== 200) {
error_log('PeerTubeService::listChannelVideos failed: status=' . $resp['status']);
break;
}
$json = json_decode($resp['body'], true);
if (!is_array($json) || !isset($json['data'])) {
break;
}
foreach ($json['data'] as $v) {
$videos[] = [
'uuid' => $v['uuid'] ?? '',
'shortUUID' => $v['shortUUID'] ?? '',
'name' => $v['name'] ?? '',
'createdAt' => $v['createdAt'] ?? '',
];
}
$total = (int)($json['total'] ?? 0);
if ($start + $count >= $total) {
break;
}
$start += $count;
}
} catch (\Throwable $e) {
error_log('PeerTubeService::listChannelVideos exception: ' . $e->getMessage());
}
return $videos;
}
// -------------------------------------------------------------------------
// Delete
// -------------------------------------------------------------------------