mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-06-25 16:19:19 +02:00
Add SQLite indexes for contenus page language/tag queries + WIP: Peertube orphans, dialogs, contact decoupling, context note, finality types
This commit is contained in:
@@ -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',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -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 !== '';
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user