change template panel left, create dashboard

This commit is contained in:
Tykayn 2025-09-08 18:40:08 +02:00 committed by tykayn
parent 381f378db4
commit 539b4c094f
24 changed files with 1367 additions and 166 deletions

View file

@ -1,4 +1,63 @@
/* Layout général */
body {
overflow-x: hidden;
}
/* Sidebar styles */
.sidebar {
width: 250px;
min-height: 100vh;
position: fixed;
top: 0;
left: 0;
z-index: 100;
transition: all 0.3s;
}
.sidebar-header {
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.sidebar .nav-link {
padding: 0.75rem 1rem;
border-radius: 0.25rem;
margin-bottom: 0.25rem;
transition: all 0.2s;
}
.sidebar .nav-link:hover {
background-color: rgba(255, 255, 255, 0.1);
}
.sidebar .nav-link.active {
background-color: rgba(255, 255, 255, 0.2);
font-weight: bold;
}
.sidebar .nav-link i {
margin-right: 0.5rem;
}
.content-wrapper {
margin-left: 250px;
width: calc(100% - 250px);
min-height: 100vh;
}
/* For mobile devices */
@media (max-width: 768px) {
.sidebar {
width: 100%;
position: relative;
min-height: auto;
}
.content-wrapper {
margin-left: 0;
width: 100%;
}
}
.body-landing {
background-color: rgb(255, 255, 255);
min-height: 100vh;
@ -9,7 +68,7 @@
.main-header {
background-color: #fff;
box-shadow: 0 2px 4px rgba(0, 0, 0, .1);
padding: 1rem 0;
/*padding: 1rem 0;*/
margin-bottom: 2rem;
}

View file

@ -205,9 +205,16 @@ class WikiController extends AbstractController
if (isset($recentChangesData['recent_changes']) && is_array($recentChangesData['recent_changes'])) {
$recentChanges = $recentChangesData['recent_changes'];
$lastUpdated = isset($recentChangesData['last_updated']) ? $recentChangesData['last_updated'] : null;
// Process team members statistics
$teamMembers = $this->processTeamMembersStats($recentChanges);
// Add avatar URLs to recent changes
foreach ($recentChanges as &$change) {
if (isset($change['user'])) {
$change['avatar_url'] = $this->fetchUserAvatar($change['user']);
}
}
}
// Check if the data is older than 1 hour
@ -241,6 +248,72 @@ class WikiController extends AbstractController
]);
}
/**
* Fetch and cache user avatar from OSM Wiki
*
* @param string $username OSM Wiki username
* @return string|null URL of the avatar or null if not found
*/
private function fetchUserAvatar(string $username): ?string
{
// Create cache directory if it doesn't exist
$cacheDir = $this->getParameter('kernel.project_dir') . '/wiki_compare/avatar_cache';
if (!file_exists($cacheDir)) {
mkdir($cacheDir, 0755, true);
}
// Generate cache filename
$cacheFile = $cacheDir . '/' . md5($username) . '.json';
// Check if avatar is cached and not expired (7 days)
if (file_exists($cacheFile)) {
$cacheData = json_decode(file_get_contents($cacheFile), true);
$cacheTime = new \DateTime($cacheData['timestamp'] ?? '2000-01-01');
$now = new \DateTime();
$diff = $now->diff($cacheTime);
// If cache is less than 7 days old, return cached avatar
if ($diff->days < 7 && isset($cacheData['avatar_url'])) {
return $cacheData['avatar_url'];
}
}
// Avatar not cached or cache expired, fetch from wiki
$userPageUrl = "https://wiki.openstreetmap.org/wiki/User:" . urlencode($username);
try {
$response = file_get_contents($userPageUrl);
if ($response) {
// Parse HTML to find avatar
$avatarUrl = null;
// Look for user avatar in the page
if (preg_match('/<img[^>]*class="[^"]*userImage[^"]*"[^>]*src="([^"]+)"/', $response, $matches)) {
$avatarUrl = $matches[1];
// Make URL absolute if it's relative
if (strpos($avatarUrl, 'http') !== 0) {
$avatarUrl = 'https://wiki.openstreetmap.org' . $avatarUrl;
}
}
// Cache the result
$cacheData = [
'username' => $username,
'avatar_url' => $avatarUrl,
'timestamp' => (new \DateTime())->format('c')
];
file_put_contents($cacheFile, json_encode($cacheData));
return $avatarUrl;
}
} catch (\Exception $e) {
// Log error but continue
error_log("Error fetching avatar for user $username: " . $e->getMessage());
}
return null;
}
/**
* Process team members statistics from recent changes data
*
@ -258,13 +331,17 @@ class WikiController extends AbstractController
// Initialize user data if not exists
if (!isset($teamMembers[$user])) {
// Fetch user avatar
$avatarUrl = $this->fetchUserAvatar($user);
$teamMembers[$user] = [
'username' => $user,
'contributions' => 0,
'chars_added' => 0,
'chars_changed' => 0,
'chars_deleted' => 0,
'user_url' => "https://wiki.openstreetmap.org/wiki/User:" . urlencode($user)
'user_url' => "https://wiki.openstreetmap.org/wiki/User:" . urlencode($user),
'avatar_url' => $avatarUrl
];
}
@ -628,9 +705,18 @@ class WikiController extends AbstractController
// Also load the word-diff based suspicious pages for comparison
if (file_exists($wordDiffFile)) {
$jsonData = json_decode(file_get_contents($wordDiffFile), true);
foreach ($jsonData as $page) {
// Use memory-efficient approach to extract only the necessary data
$maxItems = 50; // Limit the number of items to prevent memory exhaustion
// Extract regular_pages and specific_pages arrays
$regularPages = $this->extractJsonArrayByKey($wordDiffFile, 'regular_pages', $maxItems);
$specificPages = $this->extractJsonArrayByKey($wordDiffFile, 'specific_pages', $maxItems);
// Combine them into a single array
$allPages = array_merge($regularPages, $specificPages);
// Process each page to find suspicious deletions
foreach ($allPages as $page) {
if (isset($page['fr_page']) && isset($page['en_page'])) {
// Calculate deletion percentage
$enWordCount = (int)$page['en_page']['word_count'];
@ -641,6 +727,11 @@ class WikiController extends AbstractController
if ($wordDiff > 0 && $frWordCount > 0 && ($wordDiff / $enWordCount) > 0.3) {
$page['deletion_percentage'] = round(($wordDiff / $enWordCount) * 100, 2);
$wordDiffPages[] = $page;
// Limit the number of suspicious pages to prevent memory issues
if (count($wordDiffPages) >= 20) {
break;
}
}
}
}
@ -794,8 +885,18 @@ class WikiController extends AbstractController
return $this->redirectToRoute('app_admin_wiki');
}
// Select a random page from the combined pages
$randomPage = $allPages[array_rand($allPages)];
// Filter pages to ensure they have an en_page key
$validPages = array_filter($allPages, function($page) {
return isset($page['en_page']) && !empty($page['en_page']);
});
// If no valid pages, use any page but ensure we handle missing en_page in the template
if (empty($validPages)) {
$randomPage = $allPages[array_rand($allPages)];
} else {
// Select a random page from the valid pages
$randomPage = $validPages[array_rand($validPages)];
}
return $this->render('admin/wiki_random_suggestion.html.twig', [
'page' => $randomPage
@ -902,7 +1003,97 @@ EOT;
]);
}
#[Route('/wiki/archived-proposals', name: 'app_admin_wiki_archived_proposals')]
#[Route('/wiki/dashboard', name: 'app_admin_wiki_dashboard')]
public function dashboard(): Response
{
// Get metrics data from JSON files
$metricsData = $this->getDashboardMetrics();
return $this->render('admin/wiki_dashboard.html.twig', [
'metrics' => $metricsData
]);
}
/**
* Get metrics data for the dashboard
*
* @return array Metrics data
*/
private function getDashboardMetrics(): array
{
$metrics = [
'average_scores' => [],
'tracked_pages' => [],
'orphaned_pages' => [],
'uncategorized_pages' => [],
'dates' => []
];
// Get data from outdated_pages.json for average scores and tracked pages
$outdatedPagesFile = $this->getParameter('kernel.project_dir') . '/wiki_compare/outdated_pages.json';
if (file_exists($outdatedPagesFile)) {
$historyEntries = $this->extractJsonArrayByKey($outdatedPagesFile, 'history', 100);
// Process history entries
foreach ($historyEntries as $date => $entry) {
if (isset($entry['global_metrics'])) {
$globalMetrics = $entry['global_metrics'];
// Format date for display
$formattedDate = (new \DateTime($date))->format('Y-m-d');
$metrics['dates'][] = $formattedDate;
// Get average staleness score
$metrics['average_scores'][] = $globalMetrics['avg_staleness'] ?? 0;
// Get number of tracked pages
$totalPages = $globalMetrics['total_pages'] ?? 0;
$metrics['tracked_pages'][] = $totalPages;
}
}
}
// Get data from deadend_pages.json for uncategorized pages
$deadendPagesFile = $this->getParameter('kernel.project_dir') . '/wiki_compare/deadend_pages.json';
if (file_exists($deadendPagesFile)) {
$historyEntries = $this->extractJsonArrayByKey($deadendPagesFile, 'history', 100);
// Process history entries
foreach ($historyEntries as $date => $entry) {
// Format date for display
$formattedDate = (new \DateTime($date))->format('Y-m-d');
// If date already exists in metrics, use the same index
$dateIndex = array_search($formattedDate, $metrics['dates']);
if ($dateIndex === false) {
$dateIndex = count($metrics['dates']);
$metrics['dates'][] = $formattedDate;
// Add placeholder values for other metrics if this is a new date
if (!isset($metrics['average_scores'][$dateIndex])) {
$metrics['average_scores'][$dateIndex] = 0;
}
if (!isset($metrics['tracked_pages'][$dateIndex])) {
$metrics['tracked_pages'][$dateIndex] = 0;
}
}
// Get number of uncategorized pages
$uncategorizedCount = isset($entry['pages']) ? count($entry['pages']) : 0;
$metrics['uncategorized_pages'][$dateIndex] = $uncategorizedCount;
}
}
// Sort dates and reindex arrays
array_multisort($metrics['dates'], SORT_ASC,
$metrics['average_scores'],
$metrics['tracked_pages'],
$metrics['uncategorized_pages']);
return $metrics;
}
#[Route('/wiki/archived-proposals', name: 'app_admin_wiki_archived_proposals')]
public function archivedProposals(\Symfony\Component\HttpFoundation\Request $request): Response
{
$jsonFile = $this->getParameter('kernel.project_dir') . '/wiki_compare/archived_proposals.json';
@ -1233,6 +1424,20 @@ EOT;
$keysWithoutWiki = $keysWithoutWikiData;
}
}
// Load deadend pages (pages starting with "France" from the DeadendPages list)
$deadendPages = [];
$categorizedPages = [];
$deadendPagesFile = $this->getParameter('kernel.project_dir') . '/wiki_compare/deadend_pages.json';
if (file_exists($deadendPagesFile)) {
$deadendPagesData = json_decode(file_get_contents($deadendPagesFile), true);
if (isset($deadendPagesData['pages']) && is_array($deadendPagesData['pages'])) {
$deadendPages = $deadendPagesData['pages'];
}
if (isset($deadendPagesData['categorized_pages']) && is_array($deadendPagesData['categorized_pages'])) {
$categorizedPages = $deadendPagesData['categorized_pages'];
}
}
return $this->render('admin/wiki.html.twig', [
'wiki_pages' => $wikiPages,
@ -1244,7 +1449,9 @@ EOT;
'staleness_stats' => $stalenessStats,
'wiki_pages_stats' => $wikiPagesStats,
'available_translations' => $availableTranslations,
'keys_without_wiki' => $keysWithoutWiki
'keys_without_wiki' => $keysWithoutWiki,
'deadend_pages' => $deadendPages,
'categorized_pages' => $categorizedPages
]);
}

View file

@ -4,7 +4,6 @@
{% block body %}
<div class="container mt-4">
{% include 'admin/_wiki_navigation.html.twig' %}
<h1>Pages Wiki OpenStreetMap</h1>
<p class="lead">Outil de qualité des des pages wiki OpenStreetMap en français et en anglais pour les clés OSM
@ -570,6 +569,101 @@
</div>
{% endif %}
{% if deadend_pages is defined and deadend_pages|length > 0 %}
<div class="card mb-4">
<div class="card-header bg-danger text-white">
<h2>Pages "France" sans catégorie ({{ deadend_pages|length }})</h2>
</div>
<div class="card-body">
<p>Ces pages wiki commençant par "France" n'ont pas de catégorie. Vous pouvez contribuer en ajoutant des catégories à ces pages.</p>
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead class="thead-dark">
<tr>
<th>Titre</th>
<th>Catégories suggérées</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for page in deadend_pages %}
<tr>
<td>
<strong>{{ page.title }}</strong>
</td>
<td>
{% if page.suggested_categories is defined and page.suggested_categories|length > 0 %}
{% for category in page.suggested_categories %}
<span class="badge bg-info me-1">{{ category }}</span>
{% endfor %}
{% else %}
<span class="text-muted">Aucune suggestion</span>
{% endif %}
</td>
<td class="text-center">
<div class="btn-group" role="group">
<a href="{{ page.url }}" target="_blank"
class="btn btn-sm btn-outline-primary" title="Voir la page">
<i class="bi bi-eye"></i> Voir
</a>
<a href="{{ page.url }}?action=edit" target="_blank"
class="btn btn-sm btn-success" title="Ajouter des catégories">
<i class="bi bi-tags"></i> Ajouter catégories
</a>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endif %}
{% if categorized_pages is defined and categorized_pages|length > 0 %}
<div class="card mb-4">
<div class="card-header bg-success text-white">
<h2>Pages "France" récemment catégorisées ({{ categorized_pages|length }})</h2>
</div>
<div class="card-body">
<p>Ces pages wiki commençant par "France" ont été récemment catégorisées et ne sont plus dans la liste des pages sans catégorie.</p>
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead class="thead-dark">
<tr>
<th>Titre</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for page in categorized_pages %}
<tr>
<td>
<div class="d-flex align-items-center">
<div>
<strong>{{ page.title }}</strong>
<span class="badge bg-success">Catégorisée</span>
</div>
</div>
</td>
<td class="text-center">
<div class="btn-group" role="group">
<a href="{{ page.url }}" target="_blank"
class="btn btn-sm btn-outline-primary" title="Voir la page">
<i class="bi bi-eye"></i> Voir
</a>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endif %}
<p>
le score de fraîcheur prend en compte d'avantage la différence entre le nombre de mots que l'ancienneté de
modification.

View file

@ -128,8 +128,7 @@
{% block body %}
<div class="container mt-4">
{% include 'admin/_wiki_navigation.html.twig' %}
<h1>Propositions archivées OpenStreetMap</h1>
<p class="lead">Analyse des votes sur les propositions archivées du wiki OSM</p>
@ -182,7 +181,7 @@
<div class="col-md-3 col-sm-6 mb-3">
<div class="card stats-card h-100">
<div class="card-body text-center">
<div class="stats-value">{{ statistics.total_proposals }}</div>
<div class="stats-value">{{ statistics.total_proposals|default(proposals|length) }}</div>
<div class="stats-label">Propositions analysées</div>
</div>
</div>

View file

@ -103,7 +103,6 @@
}
</style>
<div class="container mt-4">
{% include 'admin/_wiki_navigation.html.twig' %}
<div class="d-flex justify-content-between align-items-center mb-3">
<div>

View file

@ -67,7 +67,6 @@
{% block body %}
<div class="container-fluid mt-4">
{% include 'admin/_wiki_navigation.html.twig' %}
<h1>Créer une traduction française pour "{{ key }}"</h1>
<p class="lead">Utilisez cette page pour traduire la page wiki en français. La page anglaise est affichée à gauche pour référence, et le formulaire d'édition de la page française est à droite.</p>

View file

@ -0,0 +1,254 @@
{% extends 'base.html.twig' %}
{% block title %}Tableau de bord - Wiki OSM{% endblock %}
{% block body %}
<div class="container mt-4">
<h1>Tableau de bord - Wiki OSM</h1>
<p class="lead">Suivi de l'évolution des métriques du wiki OpenStreetMap</p>
<div class="row mb-4">
<div class="col-md-6">
<div class="card h-100">
<div class="card-header bg-primary text-white">
<h2>Évolution du score moyen de décrépitude</h2>
</div>
<div class="card-body">
<canvas id="averageScoreChart" height="300"></canvas>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card h-100">
<div class="card-header bg-success text-white">
<h2>Évolution du nombre de pages suivies</h2>
</div>
<div class="card-body">
<canvas id="trackedPagesChart" height="300"></canvas>
</div>
</div>
</div>
</div>
<div class="row mb-4">
<div class="col-md-6">
<div class="card h-100">
<div class="card-header bg-danger text-white">
<h2>Évolution des pages sans catégorie</h2>
</div>
<div class="card-body">
<canvas id="uncategorizedPagesChart" height="300"></canvas>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card h-100">
<div class="card-header bg-info text-white">
<h2>Statistiques actuelles</h2>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6 mb-3">
<div class="card bg-light">
<div class="card-body text-center">
<h3 class="card-title">Score moyen</h3>
<p class="display-4">
{% if metrics.average_scores|length > 0 %}
{{ metrics.average_scores|last|number_format(1) }}
{% else %}
0
{% endif %}
</p>
</div>
</div>
</div>
<div class="col-md-6 mb-3">
<div class="card bg-light">
<div class="card-body text-center">
<h3 class="card-title">Pages suivies</h3>
<p class="display-4">
{% if metrics.tracked_pages|length > 0 %}
{{ metrics.tracked_pages|last }}
{% else %}
0
{% endif %}
</p>
</div>
</div>
</div>
<div class="col-md-6 mb-3">
<div class="card bg-light">
<div class="card-body text-center">
<h3 class="card-title">Pages sans catégorie</h3>
<p class="display-4">
{% if metrics.uncategorized_pages|length > 0 %}
{{ metrics.uncategorized_pages|last }}
{% else %}
0
{% endif %}
</p>
</div>
</div>
</div>
<div class="col-md-6 mb-3">
<div class="card bg-light">
<div class="card-body text-center">
<h3 class="card-title">Dernière mise à jour</h3>
<p class="display-4">
{% if metrics.dates|length > 0 %}
{{ metrics.dates|last }}
{% else %}
-
{% endif %}
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header bg-secondary text-white">
<h2>Données brutes</h2>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>Date</th>
<th>Score moyen</th>
<th>Pages suivies</th>
<th>Pages sans catégorie</th>
</tr>
</thead>
<tbody>
{% for i in 0..(metrics.dates|length - 1) %}
<tr>
<td>{{ metrics.dates[i] }}</td>
<td>{{ metrics.average_scores[i]|default(0)|number_format(1) }}</td>
<td>{{ metrics.tracked_pages[i]|default(0) }}</td>
<td>{{ metrics.uncategorized_pages[i]|default(0) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block javascripts %}
{{ parent() }}
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Data from PHP
const dates = {{ metrics.dates|json_encode|raw }};
const averageScores = {{ metrics.average_scores|json_encode|raw }};
const trackedPages = {{ metrics.tracked_pages|json_encode|raw }};
const uncategorizedPages = {{ metrics.uncategorized_pages|json_encode|raw }};
// Average Score Chart
const averageScoreCtx = document.getElementById('averageScoreChart').getContext('2d');
new Chart(averageScoreCtx, {
type: 'line',
data: {
labels: dates,
datasets: [{
label: 'Score moyen de décrépitude',
data: averageScores,
backgroundColor: 'rgba(54, 162, 235, 0.2)',
borderColor: 'rgba(54, 162, 235, 1)',
borderWidth: 2,
tension: 0.1
}]
},
options: {
responsive: true,
scales: {
y: {
beginAtZero: true
}
},
plugins: {
title: {
display: true,
text: 'Évolution du score moyen de décrépitude au fil du temps'
}
}
}
});
// Tracked Pages Chart
const trackedPagesCtx = document.getElementById('trackedPagesChart').getContext('2d');
new Chart(trackedPagesCtx, {
type: 'line',
data: {
labels: dates,
datasets: [{
label: 'Nombre de pages suivies',
data: trackedPages,
backgroundColor: 'rgba(75, 192, 192, 0.2)',
borderColor: 'rgba(75, 192, 192, 1)',
borderWidth: 2,
tension: 0.1
}]
},
options: {
responsive: true,
scales: {
y: {
beginAtZero: true
}
},
plugins: {
title: {
display: true,
text: 'Évolution du nombre de pages suivies au fil du temps'
}
}
}
});
// Uncategorized Pages Chart
const uncategorizedPagesCtx = document.getElementById('uncategorizedPagesChart').getContext('2d');
new Chart(uncategorizedPagesCtx, {
type: 'line',
data: {
labels: dates,
datasets: [{
label: 'Nombre de pages sans catégorie',
data: uncategorizedPages,
backgroundColor: 'rgba(255, 99, 132, 0.2)',
borderColor: 'rgba(255, 99, 132, 1)',
borderWidth: 2,
tension: 0.1
}]
},
options: {
responsive: true,
scales: {
y: {
beginAtZero: true
}
},
plugins: {
title: {
display: true,
text: 'Évolution du nombre de pages sans catégorie au fil du temps'
}
}
}
});
});
</script>
{% endblock %}

View file

@ -4,7 +4,6 @@
{% block body %}
<div class="container mt-4">
{% include 'admin/_wiki_navigation.html.twig' %}
<h1>Évolution des scores de décrépitude</h1>
<p class="lead">

View file

@ -4,7 +4,6 @@
{% block body %}
<div class="container mt-4">
{% include 'admin/_wiki_navigation.html.twig' %}
<h1>Pages Wiki françaises sans traduction anglaise</h1>
<p class="lead">Liste des pages françaises du wiki OSM qui n'ont pas de traduction en anglais.</p>

View file

@ -4,7 +4,6 @@
{% block body %}
<div class="container mt-4">
{% include 'admin/_wiki_navigation.html.twig' %}
<h1>Groupes OSM-FR</h1>
<p class="lead">Liste des groupes de travail et des groupes locaux d'OpenStreetMap France.</p>

View file

@ -4,7 +4,6 @@
{% block body %}
<div class="container mt-4">
{% include 'admin/_wiki_navigation.html.twig' %}
<h1>Pages Wiki non disponibles en français</h1>
<p class="lead">Liste des pages du wiki OSM qui n'ont pas de traduction française, groupées par langue d'origine.</p>

View file

@ -4,7 +4,6 @@
{% block body %}
<div class="container mt-4">
{% include 'admin/_wiki_navigation.html.twig' %}
<h1>Suggestion de page Wiki à améliorer</h1>
<p class="lead">Voici une page wiki qui a besoin d'être améliorée.</p>
@ -25,28 +24,34 @@
<div class="card-header bg-primary text-white">
<h3>Version anglaise</h3>
<p class="mb-0">
<small>Dernière modification: {{ page.en_page.last_modified }}</small>
<small>Dernière modification: {{ page.en_page is defined and page.en_page.last_modified is defined ? page.en_page.last_modified : 'Non disponible' }}</small>
</p>
</div>
<div class="card-body">
<ul class="list-group mb-3">
<li class="list-group-item d-flex justify-content-between align-items-center">
Sections
<span class="badge bg-primary rounded-pill">{{ page.en_page.sections }}</span>
<span class="badge bg-primary rounded-pill">{{ page.en_page is defined ? page.en_page.sections|default(0) : 0 }}</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
Mots
<span class="badge bg-primary rounded-pill">{{ page.en_page.word_count|default(0) }}</span>
<span class="badge bg-primary rounded-pill">{{ page.en_page is defined ? page.en_page.word_count|default(0) : 0 }}</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
Liens
<span class="badge bg-primary rounded-pill">{{ page.en_page.link_count|default(0) }}</span>
<span class="badge bg-primary rounded-pill">{{ page.en_page is defined ? page.en_page.link_count|default(0) : 0 }}</span>
</li>
</ul>
<div class="d-grid gap-2">
<a href="{{ page.en_page.url }}" target="_blank" class="btn btn-outline-primary">
<i class="bi bi-box-arrow-up-right"></i> Voir la page anglaise
</a>
{% if page.en_page is defined and page.en_page.url is defined %}
<a href="{{ page.en_page.url }}" target="_blank" class="btn btn-outline-primary">
<i class="bi bi-box-arrow-up-right"></i> Voir la page anglaise
</a>
{% else %}
<button class="btn btn-outline-secondary" disabled>
<i class="bi bi-box-arrow-up-right"></i> URL non disponible
</button>
{% endif %}
</div>
</div>
</div>

View file

@ -4,7 +4,6 @@
{% block body %}
<div class="container mt-4">
{% include 'admin/_wiki_navigation.html.twig' %}
<div class="d-flex justify-content-between align-items-center mb-3">
<h1>Évolution des classements Wiki OSM</h1>

View file

@ -4,7 +4,6 @@
{% block body %}
<div class="container mt-4">
{% include 'admin/_wiki_navigation.html.twig' %}
<h1>Changements récents Wiki OpenStreetMap</h1>
<p class="lead">Liste des changements récents dans l'espace de noms français du wiki OpenStreetMap.</p>
@ -26,18 +25,27 @@
{% for member in team_members %}
<div class="col-md-4 mb-3">
<div class="d-flex align-items-center">
<a href="{{ member.user_url }}" target="_blank" class="text-decoration-none">
<span class="fw-bold">{{ member.username }}</span>
</a>
<span class="badge bg-primary ms-2">{{ member.contributions }}</span>
<div class="ms-2 small">
<span class="text-success" title="Caractères ajoutés">+{{ member.chars_added }}</span>
{% if member.chars_changed > 0 %}
<span class="text-warning" title="Caractères modifiés">~{{ member.chars_changed }}</span>
{% endif %}
{% if member.chars_deleted > 0 %}
<span class="text-danger" title="Caractères supprimés">-{{ member.chars_deleted }}</span>
{% endif %}
{% if member.avatar_url is defined and member.avatar_url %}
<img src="{{ member.avatar_url }}" alt="{{ member.username }}" class="rounded-circle me-2" style="width: 40px; height: 40px; object-fit: cover;">
{% else %}
<div class="rounded-circle bg-secondary text-white d-flex align-items-center justify-content-center me-2" style="width: 40px; height: 40px;">
<span>{{ member.username|first|upper }}</span>
</div>
{% endif %}
<div>
<a href="{{ member.user_url }}" target="_blank" class="text-decoration-none">
<span class="fw-bold">{{ member.username }}</span>
</a>
<span class="badge bg-primary ms-2">{{ member.contributions }}</span>
<div class="small">
<span class="text-success" title="Caractères ajoutés">+{{ member.chars_added }}</span>
{% if member.chars_changed > 0 %}
<span class="text-warning" title="Caractères modifiés">~{{ member.chars_changed }}</span>
{% endif %}
{% if member.chars_deleted > 0 %}
<span class="text-danger" title="Caractères supprimés">-{{ member.chars_deleted }}</span>
{% endif %}
</div>
</div>
</div>
</div>
@ -83,13 +91,22 @@
</td>
<td>{{ change.timestamp }}</td>
<td>
{% if change.user_url %}
<a href="{{ change.user_url }}" target="_blank" class="text-decoration-none">
<div class="d-flex align-items-center">
{% if change.avatar_url is defined and change.avatar_url %}
<img src="{{ change.avatar_url }}" alt="{{ change.user }}" class="rounded-circle me-2" style="width: 30px; height: 30px; object-fit: cover;">
{% else %}
<div class="rounded-circle bg-secondary text-white d-flex align-items-center justify-content-center me-2" style="width: 30px; height: 30px; font-size: 0.8rem;">
<span>{{ change.user|first|upper }}</span>
</div>
{% endif %}
{% if change.user_url %}
<a href="{{ change.user_url }}" target="_blank" class="text-decoration-none">
{{ change.user }}
</a>
{% else %}
{{ change.user }}
</a>
{% else %}
{{ change.user }}
{% endif %}
{% endif %}
</div>
</td>
<td>
{{ change.comment }}

View file

@ -4,7 +4,6 @@
{% block body %}
<div class="container mt-4">
{% include 'admin/_wiki_navigation.html.twig' %}
<h1>Pages Wiki avec suppressions suspectes</h1>

View file

@ -64,8 +64,7 @@
{% block body %}
<div class="container mt-4">
{% include 'admin/_wiki_navigation.html.twig' %}
<h1>Propositions de tags OSM</h1>
<p class="lead">Liste des propositions de tags OpenStreetMap actuellement en cours de vote ou récemment modifiées.</p>

View file

@ -21,97 +21,141 @@
{% endblock %}
</head>
<body>
<header class="main-header">
<div class="container">
<div class="row align-items-center">
<div class="col-12">
<a href="{{ path('app_public_index') }}" class="d-flex align-items-center">
<h1 class="mb-0 mt-2">
<img src="{{ asset('logo-osm.png') }}" alt="Logo OSM" class="me-2" style="width: 30px; height: 30px;">
Qualiwiki OpenStreetMap</h1>
</a>
</div>
<div class="d-flex">
<!-- Dark Sidebar -->
<div class="sidebar bg-dark text-white" id="sidebar">
{# {% include 'admin/_wiki_navigation.html.twig' %}#}
<div class="sidebar-header p-3">
<a href="{{ path('app_public_index') }}" class="d-flex align-items-center text-white text-decoration-none">
<img src="{{ asset('logo-osm.png') }}" alt="Logo OSM" class="me-2" style="width: 30px; height: 30px;">
<h5 class="mb-0">Qualiwiki OSM</h5>
</a>
</div>
{% for label, messages in app.flashes %}
{% for message in messages %}
<div class="alert alert-{{ label }} is-{{ label }} alert-dismissible fade show mt-3" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endfor %}
<div class="row mt-3">
<div class="col-12">
{% include 'public/nav.html.twig' %}
</div>
<div class="sidebar-content p-2">
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link text-white {% if app.request.get('_route') == 'app_public_index' %}active{% endif %}" href="{{ path('app_public_index') }}">
<i class="bi bi-house-fill"></i>
{{ 'accueil'|trans }}
</a>
</li>
<li class="nav-item">
<a class="nav-link text-white {% if app.request.get('_route') == 'app_admin_wiki_dashboard' %}active{% endif %}" href="{{ path('app_admin_wiki_dashboard') }}">
<i class="bi bi-speedometer2"></i>
Tableau de bord
</a>
</li>
<li class="nav-item">
<a class="nav-link text-white {% if app.request.get('_route') == 'app_admin_wiki_decrepitude' %}active{% endif %}" href="{{ path('app_admin_wiki_decrepitude') }}">
<i class="bi bi-graph-up"></i>
Scores de décrépitude
</a>
</li>
<li class="nav-item">
<a class="nav-link text-white {% if app.request.get('_route') == 'app_admin_wiki' %}active{% endif %}" href="{{ path('app_admin_wiki') }}">
<i class="bi bi-list-ul"></i>
Pages Wiki
</a>
</li>
<li class="nav-item">
<a class="nav-link text-white {% if app.request.get('_route') == 'app_admin_wiki_archived_proposals' %}active{% endif %}" href="{{ path('app_admin_wiki_archived_proposals') }}">
<i class="bi bi-archive"></i>
Propositions archivées
</a>
</li>
<li class="nav-item">
<a class="nav-link text-white {% if app.request.get('_route') == 'app_admin_wiki_random_suggestion' %}active{% endif %}" href="{{ path('app_admin_wiki_random_suggestion') }}">
<i class="bi bi-shuffle"></i>
Suggestion aléatoire
</a>
</li>
<li class="nav-item">
<a class="nav-link text-white {% if app.request.get('_route') == 'app_admin_wiki_recent_changes' %}active{% endif %}" href="{{ path('app_admin_wiki_recent_changes') }}">
<i class="bi bi-clock-history"></i>
Changements récents
</a>
</li>
<li class="nav-item">
<a class="nav-link text-white {% if app.request.get('_route') == 'app_admin_wiki_suspicious_deletions' %}active{% endif %}" href="{{ path('app_admin_wiki_suspicious_deletions') }}">
<i class="bi bi-exclamation-triangle"></i>
Suppressions suspectes
</a>
</li>
</ul>
</div>
</div>
</header>
<main class="body-landing">
{% block body %}{% endblock %}
</main>
<footer class="main-footer">
<div class="container">
<div class="row mb-4">
<div class="col-12">
{% include 'public/nav.html.twig' %}
</div>
</div>
<div class="row">
<div class="col-12">
<p class="mb-2">OpenStreetMap Mon Commerce</p>
</div>
<div class="col-md-4 col-12">
<p class="mb-2">
Licence AGPLv3+, fait par
<a href="https://mastodon.cipherbliss.com/@tykayn">Tykayn</a> de
<a href="https://www.cipherbliss.com">CipherBliss EI</a>,
membre de la fédération des professionels d'OpenStreetMap
</p>
<p class="mb-2">
<a href="https://www.openstreetmap.org/copyright">OpenStreetMap France</a>
</p>
</div>
<div class="col-md-4 col-12">
<div id="userChangesHistory"></div>
</div>
<div class="col-md-4 col-12">
<div id="qr-share" class="mb-12">
partagez cette page :
<br>
<div id="qrcode"></div>
</div>
<div class="col-md-4 col-12">
<p class="mb-0">
<a href="https://www.openstreetmap.org/copyright">Sources du logiciel</a>
</p>
<p class="mb-2">
Sources des données : <a href="https://www.openstreetmap.org/">OpenStreetMap</a>
</p>
</div>
</div>
</div>
<div class="row">
<div class="col-12">
<p class="mb-2">
<a href="https://forum.openstreetmap.fr/t/osm-mon-commerce/34403/11" class="btn btn-outline-info ms-auto suggestion-float-btn" target="_blank" rel="noopener">
<i class="bi bi-chat-dots"></i> Faire une suggestion
</a>
<a href="https://osm-commerces.cipherbliss.com/api/v1/stats_geojson" target="_blank">Documentation de l'API (GeoJSON)</a>
</p>
<!-- Main Content -->
<div class="content-wrapper">
<header class="main-header">
<div class="container">
{% for label, messages in app.flashes %}
{% for message in messages %}
<div class="alert alert-{{ label }} is-{{ label }} alert-dismissible fade show mt-3" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endfor %}
</div>
</div>
</header>
<main class="body-landing">
{% block body %}{% endblock %}
</main>
<footer class="main-footer">
<div class="container">
<div class="row">
<div class="col-12">
<p class="mb-2">OpenStreetMap Mon Commerce</p>
</div>
<div class="col-md-4 col-12">
<p class="mb-2">
Licence AGPLv3+, fait par
<a href="https://mastodon.cipherbliss.com/@tykayn">Tykayn</a> de
<a href="https://www.cipherbliss.com">CipherBliss EI</a>,
membre de la fédération des professionels d'OpenStreetMap
</p>
<p class="mb-2">
<a href="https://www.openstreetmap.org/copyright">OpenStreetMap France</a>
</p>
</div>
<div class="col-md-4 col-12">
<div id="userChangesHistory"></div>
</div>
<div class="col-md-4 col-12">
<div id="qr-share" class="mb-12">
partagez cette page :
<br>
<div id="qrcode"></div>
</div>
<p class="mb-0">
<a href="https://www.openstreetmap.org/copyright">Sources du logiciel</a>
</p>
<p class="mb-2">
Sources des données : <a href="https://www.openstreetmap.org/">OpenStreetMap</a>
</p>
</div>
</div>
<div class="row">
<div class="col-12">
<p class="mb-2">
<a href="https://forum.openstreetmap.fr/t/osm-mon-commerce/34403/11" class="btn btn-outline-info ms-auto suggestion-float-btn" target="_blank" rel="noopener">
<i class="bi bi-chat-dots"></i> Faire une suggestion
</a>
<a href="https://osm-commerces.cipherbliss.com/api/v1/stats_geojson" target="_blank">Documentation de l'API (GeoJSON)</a>
</p>
</div>
</div>
</div>
</footer>
</div>
</footer>
</div>
{% block javascripts %}
{{ encore_entry_script_tags('app') }}

View file

@ -4,7 +4,7 @@
{% block body %}
<div class="container mt-4">
{% include 'admin/_wiki_navigation.html.twig' %}
<h1>Pages Wiki OpenStreetMap</h1>
<p class="lead">Outil de qualité des des pages wiki OpenStreetMap en français et en anglais pour les clés OSM

View file

@ -25,28 +25,34 @@
<div class="card-header bg-primary text-white">
<h3>Version anglaise</h3>
<p class="mb-0">
<small>Dernière modification: {{ page.en_page.last_modified }}</small>
<small>Dernière modification: {{ page.en_page is defined and page.en_page.last_modified is defined ? page.en_page.last_modified : 'Non disponible' }}</small>
</p>
</div>
<div class="card-body">
<ul class="list-group mb-3">
<li class="list-group-item d-flex justify-content-between align-items-center">
Sections
<span class="badge bg-primary rounded-pill">{{ page.en_page.sections }}</span>
<span class="badge bg-primary rounded-pill">{{ page.en_page is defined ? page.en_page.sections|default(0) : 0 }}</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
Mots
<span class="badge bg-primary rounded-pill">{{ page.en_page.word_count|default(0) }}</span>
<span class="badge bg-primary rounded-pill">{{ page.en_page is defined ? page.en_page.word_count|default(0) : 0 }}</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
Liens
<span class="badge bg-primary rounded-pill">{{ page.en_page.link_count|default(0) }}</span>
<span class="badge bg-primary rounded-pill">{{ page.en_page is defined ? page.en_page.link_count|default(0) : 0 }}</span>
</li>
</ul>
<div class="d-grid gap-2">
<a href="{{ page.en_page.url }}" target="_blank" class="btn btn-outline-primary">
<i class="bi bi-box-arrow-up-right"></i> Voir la page anglaise
</a>
{% if page.en_page is defined and page.en_page.url is defined %}
<a href="{{ page.en_page.url }}" target="_blank" class="btn btn-outline-primary">
<i class="bi bi-box-arrow-up-right"></i> Voir la page anglaise
</a>
{% else %}
<button class="btn btn-outline-secondary" disabled>
<i class="bi bi-box-arrow-up-right"></i> URL non disponible
</button>
{% endif %}
</div>
</div>
</div>

View file

@ -26,18 +26,27 @@
{% for member in team_members %}
<div class="col-md-4 mb-3">
<div class="d-flex align-items-center">
<a href="{{ member.user_url }}" target="_blank" class="text-decoration-none">
<span class="fw-bold">{{ member.username }}</span>
</a>
<span class="badge bg-primary ms-2">{{ member.contributions }}</span>
<div class="ms-2 small">
<span class="text-success" title="Caractères ajoutés">+{{ member.chars_added }}</span>
{% if member.chars_changed > 0 %}
<span class="text-warning" title="Caractères modifiés">~{{ member.chars_changed }}</span>
{% endif %}
{% if member.chars_deleted > 0 %}
<span class="text-danger" title="Caractères supprimés">-{{ member.chars_deleted }}</span>
{% endif %}
{% if member.avatar_url is defined and member.avatar_url %}
<img src="{{ member.avatar_url }}" alt="{{ member.username }}" class="rounded-circle me-2" style="width: 40px; height: 40px; object-fit: cover;">
{% else %}
<div class="rounded-circle bg-secondary text-white d-flex align-items-center justify-content-center me-2" style="width: 40px; height: 40px;">
<span>{{ member.username|first|upper }}</span>
</div>
{% endif %}
<div>
<a href="{{ member.user_url }}" target="_blank" class="text-decoration-none">
<span class="fw-bold">{{ member.username }}</span>
</a>
<span class="badge bg-primary ms-2">{{ member.contributions }}</span>
<div class="small">
<span class="text-success" title="Caractères ajoutés">+{{ member.chars_added }}</span>
{% if member.chars_changed > 0 %}
<span class="text-warning" title="Caractères modifiés">~{{ member.chars_changed }}</span>
{% endif %}
{% if member.chars_deleted > 0 %}
<span class="text-danger" title="Caractères supprimés">-{{ member.chars_deleted }}</span>
{% endif %}
</div>
</div>
</div>
</div>
@ -83,13 +92,22 @@
</td>
<td>{{ change.timestamp }}</td>
<td>
{% if change.user_url %}
<a href="{{ change.user_url }}" target="_blank" class="text-decoration-none">
<div class="d-flex align-items-center">
{% if change.avatar_url is defined and change.avatar_url %}
<img src="{{ change.avatar_url }}" alt="{{ change.user }}" class="rounded-circle me-2" style="width: 30px; height: 30px; object-fit: cover;">
{% else %}
<div class="rounded-circle bg-secondary text-white d-flex align-items-center justify-content-center me-2" style="width: 30px; height: 30px; font-size: 0.8rem;">
<span>{{ change.user|first|upper }}</span>
</div>
{% endif %}
{% if change.user_url %}
<a href="{{ change.user_url }}" target="_blank" class="text-decoration-none">
{{ change.user }}
</a>
{% else %}
{{ change.user }}
</a>
{% else %}
{{ change.user }}
{% endif %}
{% endif %}
</div>
</td>
<td>
{{ change.comment }}

View file

@ -0,0 +1,199 @@
# Ajout de la fonctionnalité de suivi des pages "France" sans catégorie
## Description
Cette fonctionnalité permet de suivre les pages du wiki OpenStreetMap commençant par "France" qui n'ont pas de catégorie (pages sans issues). Ces pages sont identifiées à partir de la liste des "DeadendPages" du wiki OSM. La fonctionnalité suggère également des catégories pour ces pages et suit leur évolution lorsqu'elles sont catégorisées.
## Modifications apportées
### 1. Ajout de constantes dans `wiki_compare.py`
```python
WIKI_DEADEND_PAGES_URL = "https://wiki.openstreetmap.org/w/index.php?title=Special:DeadendPages&limit=500&offset=1000"
DEADEND_PAGES_FILE = "deadend_pages.json"
```
### 2. Ajout de la fonction `suggest_categories` dans `wiki_compare.py`
Cette fonction analyse le titre et le contenu d'une page pour suggérer des catégories pertinentes :
```python
def suggest_categories(page_title, page_url):
"""
Suggest categories for an uncategorized page based on its title and content
Args:
page_title (str): Title of the page
page_url (str): URL of the page
Returns:
list: List of suggested categories
"""
# Logique de suggestion de catégories basée sur le titre et le contenu de la page
```
### 3. Ajout de la fonction `fetch_deadend_pages` dans `wiki_compare.py`
Cette fonction récupère les pages commençant par "France" depuis la liste des DeadendPages :
```python
def fetch_deadend_pages():
"""
Fetch pages starting with "France" from the DeadendPages list
Returns:
list: List of dictionaries containing page information
"""
# Logique de récupération des pages depuis la liste des DeadendPages
# Filtrage des pages commençant par "France"
# Suggestion de catégories pour chaque page
```
### 4. Modification de la fonction `main` dans `wiki_compare.py`
Ajout d'une section pour traiter les pages sans catégorie et suivre leur évolution :
```python
# Fetch pages starting with "France" from the DeadendPages list
deadend_pages = fetch_deadend_pages()
if deadend_pages:
# Load existing deadend pages data to compare with history
existing_data = load_json_data(DEADEND_PAGES_FILE)
# Initialize history if it doesn't exist
if 'history' not in existing_data:
existing_data['history'] = {}
# Get the most recent history entry
sorted_timestamps = sorted(existing_data.get('history', {}).keys())
previous_pages = []
if sorted_timestamps:
latest_timestamp = sorted_timestamps[-1]
previous_pages = existing_data['history'][latest_timestamp].get('pages', [])
# Find pages that were in the previous list but are no longer in the current list
previous_urls = [page['url'] for page in previous_pages]
current_urls = [page['url'] for page in deadend_pages]
categorized_pages = []
for url in previous_urls:
if url not in current_urls:
# Find the page in previous_pages
for page in previous_pages:
if page['url'] == url:
# This page is no longer in the DeadendPages list, which means it has been categorized
categorized_pages.append(page)
break
# Create a timestamp for the current data
current_timestamp = datetime.now().isoformat()
# Create the history entry
history_entry = {
'pages': deadend_pages,
'categorized_pages': categorized_pages
}
# Add the entry to history with timestamp as key
existing_data['history'][current_timestamp] = history_entry
# Update the current data
existing_data['pages'] = deadend_pages
existing_data['categorized_pages'] = categorized_pages
existing_data['last_updated'] = current_timestamp
# Save the updated data
save_to_json(existing_data, DEADEND_PAGES_FILE)
```
### 5. Modification du contrôleur `WikiController.php`
Ajout du chargement des données des pages sans catégorie :
```php
// Load deadend pages (pages starting with "France" from the DeadendPages list)
$deadendPages = [];
$categorizedPages = [];
$deadendPagesFile = $this->getParameter('kernel.project_dir') . '/wiki_compare/deadend_pages.json';
if (file_exists($deadendPagesFile)) {
$deadendPagesData = json_decode(file_get_contents($deadendPagesFile), true);
if (isset($deadendPagesData['pages']) && is_array($deadendPagesData['pages'])) {
$deadendPages = $deadendPagesData['pages'];
}
if (isset($deadendPagesData['categorized_pages']) && is_array($deadendPagesData['categorized_pages'])) {
$categorizedPages = $deadendPagesData['categorized_pages'];
}
}
```
Ajout des données à la vue :
```php
return $this->render('admin/wiki.html.twig', [
// Autres données...
'deadend_pages' => $deadendPages,
'categorized_pages' => $categorizedPages
]);
```
### 6. Ajout de sections dans le template `admin/wiki.html.twig`
Ajout d'une section pour afficher les pages sans catégorie :
```twig
{% if deadend_pages is defined and deadend_pages|length > 0 %}
<div class="card mb-4">
<div class="card-header bg-danger text-white">
<h2>Pages "France" sans catégorie ({{ deadend_pages|length }})</h2>
</div>
<div class="card-body">
<p>Ces pages wiki commençant par "France" n'ont pas de catégorie. Vous pouvez contribuer en ajoutant des catégories à ces pages.</p>
<div class="table-responsive">
<table class="table table-striped table-hover">
<!-- Contenu de la table -->
</table>
</div>
</div>
</div>
{% endif %}
```
Ajout d'une section pour afficher les pages récemment catégorisées :
```twig
{% if categorized_pages is defined and categorized_pages|length > 0 %}
<div class="card mb-4">
<div class="card-header bg-success text-white">
<h2>Pages "France" récemment catégorisées ({{ categorized_pages|length }})</h2>
</div>
<div class="card-body">
<p>Ces pages wiki commençant par "France" ont été récemment catégorisées et ne sont plus dans la liste des pages sans catégorie.</p>
<div class="table-responsive">
<table class="table table-striped table-hover">
<!-- Contenu de la table -->
</table>
</div>
</div>
</div>
{% endif %}
```
## Fonctionnalités
1. **Scraping des pages sans catégorie** : Récupération des pages commençant par "France" depuis la liste des DeadendPages du wiki OSM.
2. **Suggestion de catégories** : Analyse du titre et du contenu des pages pour suggérer des catégories pertinentes.
3. **Suivi des pages catégorisées** : Détection des pages qui ont été catégorisées depuis le dernier scraping.
4. **Affichage dans l'interface** : Affichage des pages sans catégorie et des pages récemment catégorisées dans l'interface utilisateur.
## Utilisation
1. Le script `wiki_compare.py` est exécuté périodiquement pour mettre à jour les données.
2. Les utilisateurs peuvent consulter les pages sans catégorie et les pages récemment catégorisées dans l'interface.
3. Les utilisateurs peuvent cliquer sur les boutons d'action pour voir les pages et ajouter des catégories.
## Avantages
1. **Amélioration de la qualité du wiki** : Aide à identifier et à catégoriser les pages qui devraient avoir des catégories.
2. **Suivi des progrès** : Permet de suivre les progrès dans la catégorisation des pages.
3. **Suggestion de catégories** : Facilite le travail des contributeurs en suggérant des catégories pertinentes.

View file

@ -0,0 +1,94 @@
# Corrections apportées à QualiWiki
Ce document décrit les corrections apportées pour résoudre les problèmes mentionnés dans la description des issues.
## 1. Correction de l'erreur dans wiki_archived_proposals.html.twig
### Problème
```
Key "total_proposals" for sequence/mapping with keys "0, 1, 2, 3, ..." does not exist in admin/wiki_archived_proposals.html.twig at line 193.
```
### Solution
Ajout d'une valeur par défaut pour `total_proposals` dans le template :
```twig
<div class="stats-value">{{ statistics.total_proposals|default(proposals|length) }}</div>
```
Cette modification permet d'utiliser le nombre de propositions dans le tableau `proposals` comme valeur par défaut si `statistics.total_proposals` n'est pas défini.
## 2. Implémentation d'un panneau latéral sombre pour la navigation
### Modifications
- Ajout d'un panneau latéral sombre dans `templates/base.html.twig`
- Ajout de styles CSS dans `public/css/main.css` pour le panneau latéral
- Réorganisation de la structure HTML pour intégrer le panneau latéral
- Ajout de liens vers toutes les sections principales de l'application
### Avantages
- Navigation plus claire et plus accessible
- Meilleure organisation visuelle de l'application
- Accès rapide à toutes les fonctionnalités principales
## 3. Création d'une page dashboard avec graphiques
### Fonctionnalités
- Ajout d'une méthode `dashboard` dans `WikiController.php`
- Création d'un template `admin/wiki_dashboard.html.twig`
- Implémentation de graphiques montrant :
- L'évolution du score moyen de décrépitude
- L'évolution du nombre de pages suivies
- L'évolution du nombre de pages sans catégorie
### Données suivies
- Score moyen de décrépitude des pages
- Nombre de pages suivies
- Nombre de pages sans catégorie (pages orphelines)
## 4. Correction de l'erreur dans wiki_random_suggestion.html.twig
### Problème
```
Key "en_page" for sequence/mapping with keys "title, level" does not exist in admin/wiki_random_suggestion.html.twig at line 28.
```
### Solution
- Ajout de vérifications pour l'existence de `en_page` dans le template
- Ajout de valeurs par défaut pour les propriétés de `en_page`
- Filtrage des pages pour s'assurer qu'elles ont une clé `en_page` dans le contrôleur
## 5. Ajout d'avatars utilisateurs dans la page des changements récents
### Fonctionnalités
- Ajout d'une fonction `fetchUserAvatar` dans `WikiController.php` pour récupérer et mettre en cache les avatars des utilisateurs
- Mise à jour des templates `admin/wiki_recent_changes.html.twig` et `public/wiki_recent_changes.html.twig` pour afficher les avatars
- Mise en cache des avatars pour éviter de les récupérer à chaque fois
## 6. Correction de l'erreur de mémoire dans la page des suppressions suspectes
### Problème
```
Error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 175260864 bytes)
```
### Solution
- Modification de la méthode `suspiciousDeletions` dans `WikiController.php` pour utiliser une approche plus efficace en mémoire
- Utilisation de la méthode `extractJsonArrayByKey` au lieu de `json_decode(file_get_contents())` pour traiter le fichier JSON
- Limitation du nombre d'éléments extraits à 50 avec le paramètre `$maxItems`
- Ajout d'une limite pour arrêter le traitement après avoir trouvé 20 pages suspectes
## 7. Implémentation du suivi des pages "France" sans catégorie
### Fonctionnalités
- Ajout de constantes pour l'URL des DeadendPages et le fichier de sortie dans `wiki_compare.py`
- Création d'une fonction `fetch_deadend_pages` pour récupérer les pages commençant par "France" depuis la liste des DeadendPages
- Création d'une fonction `suggest_categories` pour suggérer des catégories pour les pages sans catégorie
- Modification de la fonction `main` pour traiter et sauvegarder les données des DeadendPages
- Mise à jour du contrôleur pour charger et afficher les données des DeadendPages
- Ajout de sections dans le template pour afficher les pages sans catégorie et les pages récemment catégorisées
### Avantages
- Suivi des pages qui devraient avoir des catégories
- Suggestion de catégories pour faciliter le travail des contributeurs
- Suivi des progrès dans la catégorisation des pages

View file

@ -61,10 +61,12 @@ WIKI_BASE_URL_EN = "https://wiki.openstreetmap.org/wiki/Key:"
WIKI_BASE_URL_FR = "https://wiki.openstreetmap.org/wiki/FR:Key:"
WIKI_BASE_URL = "https://wiki.openstreetmap.org/wiki/"
WIKI_CATEGORY_URL = "https://wiki.openstreetmap.org/wiki/Category:FR:Traductions_d%C3%A9synchronis%C3%A9es"
WIKI_DEADEND_PAGES_URL = "https://wiki.openstreetmap.org/w/index.php?title=Special:DeadendPages&limit=500&offset=1000"
TOP_KEYS_FILE = "top_keys.json"
KEYS_WITHOUT_WIKI_FILE = "keys_without_wiki.json"
WIKI_PAGES_CSV = "wiki_pages.csv"
OUTDATED_PAGES_FILE = "outdated_pages.json"
DEADEND_PAGES_FILE = "deadend_pages.json"
STALENESS_HISTOGRAM_FILE = "staleness_histogram.png"
# Number of wiki pages to examine
NUM_WIKI_PAGES = 2
@ -154,6 +156,161 @@ def fetch_desynchronized_pages():
logger.error(f"Error fetching category page: {e}")
return []
def suggest_categories(page_title, page_url):
"""
Suggest categories for an uncategorized page based on its title and content
Args:
page_title (str): Title of the page
page_url (str): URL of the page
Returns:
list: List of suggested categories
"""
logger.info(f"Suggesting categories for page: {page_title}")
suggested_categories = []
# Common categories for French OSM wiki pages
common_categories = [
"Documentation OSM en français",
"Cartographie",
"Contributeurs",
"Développeurs",
"Éléments cartographiés",
"Imports",
"Logiciels",
"Projets",
"Rencontres",
"Utilisateurs"
]
# Add geography-related categories for pages about France
if "France" in page_title:
suggested_categories.append("France")
# Check for specific regions or departments
regions = [
"Auvergne-Rhône-Alpes", "Bourgogne-Franche-Comté", "Bretagne",
"Centre-Val de Loire", "Corse", "Grand Est", "Hauts-de-France",
"Île-de-France", "Normandie", "Nouvelle-Aquitaine",
"Occitanie", "Pays de la Loire", "Provence-Alpes-Côte d'Azur"
]
for region in regions:
if region in page_title:
suggested_categories.append(region)
# Try to fetch the page content to make better suggestions
try:
response = requests.get(page_url)
response.raise_for_status()
soup = BeautifulSoup(response.text, 'html.parser')
# Get the main content
content = soup.select_one('#mw-content-text')
if content:
text = content.get_text(separator=' ', strip=True).lower()
# Check for keywords related to common categories
if any(keyword in text for keyword in ["carte", "cartographie", "mapper"]):
suggested_categories.append("Cartographie")
if any(keyword in text for keyword in ["contribuer", "contributeur", "éditer"]):
suggested_categories.append("Contributeurs")
if any(keyword in text for keyword in ["développeur", "programmer", "code", "api"]):
suggested_categories.append("Développeurs")
if any(keyword in text for keyword in ["tag", "clé", "valeur", "élément", "nœud", "way", "relation"]):
suggested_categories.append("Éléments cartographiés")
if any(keyword in text for keyword in ["import", "données", "dataset"]):
suggested_categories.append("Imports")
if any(keyword in text for keyword in ["logiciel", "application", "outil"]):
suggested_categories.append("Logiciels")
if any(keyword in text for keyword in ["projet", "initiative"]):
suggested_categories.append("Projets")
if any(keyword in text for keyword in ["rencontre", "réunion", "événement", "conférence"]):
suggested_categories.append("Rencontres")
if any(keyword in text for keyword in ["utiliser", "utilisateur", "usage"]):
suggested_categories.append("Utilisateurs")
except requests.exceptions.RequestException as e:
logger.warning(f"Error fetching page content for category suggestions: {e}")
# If we can't fetch the content, suggest common categories based on title only
if "projet" in page_title.lower():
suggested_categories.append("Projets")
elif "logiciel" in page_title.lower() or "application" in page_title.lower():
suggested_categories.append("Logiciels")
elif "rencontre" in page_title.lower() or "réunion" in page_title.lower():
suggested_categories.append("Rencontres")
# Always suggest the general French documentation category
suggested_categories.append("Documentation OSM en français")
# Remove duplicates while preserving order
seen = set()
unique_categories = []
for cat in suggested_categories:
if cat not in seen:
seen.add(cat)
unique_categories.append(cat)
logger.info(f"Suggested {len(unique_categories)} categories for {page_title}: {', '.join(unique_categories)}")
return unique_categories
def fetch_deadend_pages():
"""
Fetch pages starting with "France" from the DeadendPages list
Returns:
list: List of dictionaries containing page information
"""
logger.info(f"Fetching pages from DeadendPages list: {WIKI_DEADEND_PAGES_URL}")
try:
response = requests.get(WIKI_DEADEND_PAGES_URL)
response.raise_for_status()
soup = BeautifulSoup(response.text, 'html.parser')
# Find all links in the DeadendPages list
page_links = []
for link in soup.select('.mw-spcontent li a'):
href = link.get('href', '')
title = link.get_text(strip=True)
# Skip if it's not a wiki page or if it's a special page
if not href.startswith('/wiki/') or 'Special:' in href:
continue
# Filter pages that start with "France"
if title.startswith('France'):
# Get the full URL
full_url = 'https://wiki.openstreetmap.org' + href
# Suggest categories for this page
suggested_categories = suggest_categories(title, full_url)
page_links.append({
'title': title,
'url': full_url,
'suggested_categories': suggested_categories
})
logger.info(f"Found {len(page_links)} pages starting with 'France' in the DeadendPages list")
return page_links
except requests.exceptions.RequestException as e:
logger.error(f"Error fetching DeadendPages list: {e}")
return []
def fetch_top_keys(limit=NUM_WIKI_PAGES):
"""
Fetch the most used OSM keys from TagInfo API
@ -1365,10 +1522,11 @@ def main():
3. Fetches and processes wiki pages for these keys
4. Processes specific wiki pages listed in SPECIFIC_PAGES
5. Processes pages from the FR:Traductions_désynchronisées category
6. Calculates staleness scores for all pages
7. Generates a histogram of staleness scores
8. Saves the results to CSV and JSON files
9. Prints a list of pages that need updating
6. Processes pages starting with "France" from the DeadendPages list
7. Calculates staleness scores for all pages
8. Generates a histogram of staleness scores
9. Saves the results to CSV and JSON files
10. Prints a list of pages that need updating
"""
# Parse command-line arguments
parser = argparse.ArgumentParser(description='Compare OpenStreetMap wiki pages in English and French.')
@ -1404,6 +1562,62 @@ def main():
logger.info(f"Saved {len(keys_without_wiki)} keys without wiki pages to {KEYS_WITHOUT_WIKI_FILE}")
else:
logger.warning("No keys without wiki pages were fetched.")
# Fetch pages starting with "France" from the DeadendPages list
deadend_pages = fetch_deadend_pages()
if deadend_pages:
# Load existing deadend pages data to compare with history
existing_data = load_json_data(DEADEND_PAGES_FILE)
# Initialize history if it doesn't exist
if 'history' not in existing_data:
existing_data['history'] = {}
# Get the most recent history entry
sorted_timestamps = sorted(existing_data.get('history', {}).keys())
previous_pages = []
if sorted_timestamps:
latest_timestamp = sorted_timestamps[-1]
previous_pages = existing_data['history'][latest_timestamp].get('pages', [])
# Find pages that were in the previous list but are no longer in the current list
previous_urls = [page['url'] for page in previous_pages]
current_urls = [page['url'] for page in deadend_pages]
categorized_pages = []
for url in previous_urls:
if url not in current_urls:
# Find the page in previous_pages
for page in previous_pages:
if page['url'] == url:
# This page is no longer in the DeadendPages list, which means it has been categorized
categorized_pages.append(page)
break
# Create a timestamp for the current data
current_timestamp = datetime.now().isoformat()
# Create the history entry
history_entry = {
'pages': deadend_pages,
'categorized_pages': categorized_pages
}
# Add the entry to history with timestamp as key
existing_data['history'][current_timestamp] = history_entry
# Update the current data
existing_data['pages'] = deadend_pages
existing_data['categorized_pages'] = categorized_pages
existing_data['last_updated'] = current_timestamp
# Save the updated data
save_to_json(existing_data, DEADEND_PAGES_FILE)
logger.info(f"Saved {len(deadend_pages)} deadend pages to {DEADEND_PAGES_FILE}")
logger.info(f"Found {len(categorized_pages)} pages that have been categorized since the last run")
else:
logger.warning("No deadend pages were fetched.")
# Fetch wiki pages for each key
wiki_pages = []