diff --git a/src/Controller/WikiController.php b/src/Controller/WikiController.php index 26b1fda..4cec428 100644 --- a/src/Controller/WikiController.php +++ b/src/Controller/WikiController.php @@ -44,6 +44,49 @@ class WikiController extends AbstractController 'json_exists' => file_exists($outdatedPagesFile) ]); } + + /** + * Displays the evolution of page rankings over time + */ + #[Route('/wiki/rankings', name: 'app_admin_wiki_rankings')] + public function pageRankings(): Response + { + $rankingsFile = $this->getParameter('kernel.project_dir') . '/wiki_compare/page_rankings.json'; + + $timestamps = []; + $pages = []; + $globalMetrics = []; + $lastUpdated = null; + + if (file_exists($rankingsFile)) { + // Load the rankings data + try { + $rankingsData = json_decode(file_get_contents($rankingsFile), true); + + if (json_last_error() === JSON_ERROR_NONE) { + $timestamps = $rankingsData['timestamps'] ?? []; + $pages = $rankingsData['pages'] ?? []; + $globalMetrics = $rankingsData['global_metrics'] ?? []; + + // Get the last timestamp as last_updated + if (!empty($timestamps)) { + $lastUpdated = end($timestamps); + } + } + } catch (\Exception $e) { + // Log the error + error_log("Error loading rankings data: " . $e->getMessage()); + } + } + + return $this->render('admin/wiki_rankings.html.twig', [ + 'timestamps' => $timestamps, + 'pages' => $pages, + 'global_metrics' => $globalMetrics, + 'last_updated' => $lastUpdated, + 'json_exists' => file_exists($rankingsFile) + ]); + } /** * Detects incorrect heading hierarchies in a list of sections * For example, h4 directly under h2 without h3 in between @@ -1306,6 +1349,9 @@ EOT; $detailedComparison = null; $mediaDiff = 0; $historyData = null; + $prevPage = null; + $nextPage = null; + $stalenessDistribution = null; if (file_exists($jsonFile)) { // Use memory-efficient approach to extract only the necessary data @@ -1376,6 +1422,91 @@ EOT; // Combine them into a single array $allPages = array_merge($regularPages, $specificPages); + + // Sort pages by staleness score (descending) + usort($allPages, function($a, $b) { + $scoreA = $a['staleness_score'] ?? 0; + $scoreB = $b['staleness_score'] ?? 0; + return $scoreB <=> $scoreA; // Descending order + }); + + // Find the current page index in the sorted array + $currentIndex = -1; + foreach ($allPages as $index => $page) { + if (isset($page['key']) && $page['key'] === $key) { + $currentIndex = $index; + break; + } + } + + // Determine previous and next pages + if ($currentIndex > 0) { + $prevPage = $allPages[$currentIndex - 1]; + } + + if ($currentIndex < count($allPages) - 1 && $currentIndex >= 0) { + $nextPage = $allPages[$currentIndex + 1]; + } + + // Create staleness score distribution data for histogram + $stalenessScores = []; + foreach ($allPages as $page) { + if (isset($page['staleness_score'])) { + $stalenessScores[] = $page['staleness_score']; + } + } + + if (!empty($stalenessScores)) { + // Calculate statistics + $min = min($stalenessScores); + $max = max($stalenessScores); + $avg = array_sum($stalenessScores) / count($stalenessScores); + $median = $this->calculateMedian($stalenessScores); + + // Create histogram bins (10 bins) + $binCount = 10; + $binSize = ($max - $min) / $binCount; + $bins = []; + $binLabels = []; + + // Initialize bins + for ($i = 0; $i < $binCount; $i++) { + $bins[$i] = 0; + $binStart = $min + ($i * $binSize); + $binEnd = $binStart + $binSize; + $binLabels[$i] = round($binStart, 1) . ' - ' . round($binEnd, 1); + } + + // Count scores in each bin + foreach ($stalenessScores as $score) { + $binIndex = min($binCount - 1, floor(($score - $min) / $binSize)); + $bins[$binIndex]++; + } + + // Find which bin the current page falls into + $currentPageScore = 0; + foreach ($allPages as $page) { + if (isset($page['key']) && $page['key'] === $key && isset($page['staleness_score'])) { + $currentPageScore = $page['staleness_score']; + break; + } + } + + $currentPageBin = min($binCount - 1, floor(($currentPageScore - $min) / $binSize)); + + $stalenessDistribution = [ + 'scores' => $stalenessScores, + 'min' => $min, + 'max' => $max, + 'avg' => $avg, + 'median' => $median, + 'bins' => $bins, + 'binLabels' => $binLabels, + 'currentPageScore' => $currentPageScore, + 'currentPageBin' => $currentPageBin, + 'totalPages' => count($stalenessScores) + ]; + } // Find the page with the matching key foreach ($allPages as $page) { @@ -1792,7 +1923,10 @@ EOT; 'fr_sections' => $frSections, 'en_links' => $enLinks, 'fr_links' => $frLinks, - 'history_data' => $historyData + 'history_data' => $historyData, + 'prev_page' => $prevPage, + 'next_page' => $nextPage, + 'staleness_distribution' => $stalenessDistribution ]); } @@ -2207,4 +2341,30 @@ EOT; { return $this->extractJsonArrayByKey($filePath, 'specific_pages', $maxPages); } + + /** + * Calculate the median value of an array of numbers + * + * @param array $array Array of numbers + * @return float The median value + */ + private function calculateMedian(array $array): float + { + sort($array); + $count = count($array); + + if ($count === 0) { + return 0; + } + + $middle = floor($count / 2); + + if ($count % 2 === 0) { + // Even number of elements, average the two middle values + return ($array[$middle - 1] + $array[$middle]) / 2; + } else { + // Odd number of elements, return the middle value + return $array[$middle]; + } + } } \ No newline at end of file diff --git a/templates/admin/_wiki_navigation.html.twig b/templates/admin/_wiki_navigation.html.twig index faa497c..5081f7d 100644 --- a/templates/admin/_wiki_navigation.html.twig +++ b/templates/admin/_wiki_navigation.html.twig @@ -56,6 +56,11 @@ Scores de décrépitude + diff --git a/templates/admin/wiki_compare.html.twig b/templates/admin/wiki_compare.html.twig index a36ab2f..5d3f8c0 100644 --- a/templates/admin/wiki_compare.html.twig +++ b/templates/admin/wiki_compare.html.twig @@ -105,9 +105,32 @@
{% include 'admin/_wiki_navigation.html.twig' %} +
+
+ {% if prev_page is defined and prev_page is not null %} + + Page précédente: {{ prev_page.key }} + + {% else %} + + {% endif %} +
+
+ {% if next_page is defined and next_page is not null %} + + Page suivante: {{ next_page.key }} + + {% else %} + + {% endif %} +
+
+

Comparaison Wiki OpenStreetMap - {{ key }} - - {% if en_page.is_specific_page is defined and en_page.is_specific_page %} fr en @@ -117,7 +140,6 @@ {% endif %}

-

{% if en_page.is_specific_page is defined and en_page.is_specific_page %} Comparaison détaillée des pages wiki en français et en anglais pour "{{ key }}". @@ -159,6 +181,63 @@

+ {% if history_data is defined and history_data is not empty %} +
+
+

Évolution du classement

+
+
+
+
+ +
+
+
+
+
+ + + + + + + + + + + + + {% for entry in history_data %} + + + + + + + + + {% endfor %} + +
DateScore de décrépitudeDifférence de motsDifférence de sectionsDifférence de liensDifférence d'images
{{ entry.date }} +
+ {% set score = entry.metrics.staleness_score %} + {% set score_class = score > 70 ? 'bg-danger' : (score > 40 ? 'bg-warning' : 'bg-success') %} +
+ {{ score }} +
+
+
{{ entry.metrics.word_diff }}{{ entry.metrics.section_diff }}{{ entry.metrics.link_diff }}{{ entry.metrics.media_diff }}
+
+
+
+
+
+ {% endif %} + {# suggestions de grammalecte #} {% if fr_page is defined and fr_page is not null %} {% if detailed_comparison is defined and detailed_comparison is not null and detailed_comparison.section_comparison is defined and detailed_comparison.section_comparison is not null %} @@ -225,6 +304,59 @@ +
+
+
+
+

Sections communes

+ + {% if detailed_comparison['section_comparison']['common'] is defined %} + {{ detailed_comparison['section_comparison']['common']|length }} sections communes + {% else %} + 0 sections communes + {% endif %} + +
+
+
+ + + + + + + + + {% if detailed_comparison['section_comparison']['common'] is defined and detailed_comparison['section_comparison']['common'] is iterable %} + {% for section in detailed_comparison['section_comparison']['common'] %} + + + + + {% endfor %} + {% else %} + + + + {% endif %} + +
Section anglaiseSection française
+ h{{ section.en.level|default(1) }} + {% if section.en.title is defined and section.en.title is not empty %} + {{ section.en.title }} + {% endif %} + + h{{ section.fr.level|default(1) }} + {% if section.fr.title is defined and section.fr.title is not empty %} + {{ section.fr.title }} + {% endif %} +
Aucune section commune trouvée
+
+
+
+
+
+
{% if detailed_comparison is defined and detailed_comparison is not null and detailed_comparison.section_comparison is defined and detailed_comparison.section_comparison is not null %}
+ +
+
+
+
+

Images communes

+ + {% if detailed_comparison.media_comparison.common is defined %} + {{ detailed_comparison.media_comparison.common|length }} images communes + {% else %} + 0 images communes + {% endif %} + +
+
+
+ {% if detailed_comparison.media_comparison.common is defined and detailed_comparison.media_comparison.common is iterable %} + {% for media in detailed_comparison.media_comparison.common %} +
+
+
+
Version anglaise
+
+
+ {{ media.en.alt }} +

{{ media.en.alt }}

+
+
+
+
+
+
+
Version française
+
+
+ {{ media.fr.alt }} +

{{ media.fr.alt }}

+
+
+
+ {% endfor %} + {% else %} +
+
+ Aucune image commune trouvée entre les versions anglaise et française. +
+
+ {% endif %} +
+
+
+
+
{% endif %} - {# {% if detailed_comparison and detailed_comparison.link_comparison %} #} - {#
#} - {#
#} - {#

Comparaison des liens

#} - {#
#} - {#
#} - {#
#} - {#
#} - {#
#} - {#
#} - {#

Liens en anglais

#} - {# {{ en_page.link_count }} liens #} - {#
#} - {#
#} + {% if detailed_comparison and detailed_comparison.link_comparison %} +
+
+

Comparaison des liens

+
+
+
+
+
+
+

Liens uniquement en anglais

+ + {% if detailed_comparison.link_comparison.en_only is defined %} + {{ detailed_comparison.link_comparison.en_only|length }} liens + {% else %} + 0 liens + {% endif %} + +
+
+
+ + + + + + + + + {% if detailed_comparison.link_comparison.en_only is defined and detailed_comparison.link_comparison.en_only is iterable %} + {% for link in detailed_comparison.link_comparison.en_only %} + + + + + {% endfor %} + {% else %} + + + + {% endif %} + +
Texte du lienURL
{{ link.text }} + + {{ link.href|slice(0, 30) }}{% if link.href|length > 30 %}...{% endif %} + +
Aucun lien uniquement en anglais
+
+
+
+
+
+
+
+

Liens uniquement en français

+ + {% if detailed_comparison.link_comparison.fr_only is defined %} + {{ detailed_comparison.link_comparison.fr_only|length }} liens + {% else %} + 0 liens + {% endif %} + +
+
+
+ + + + + + + + + {% if detailed_comparison.link_comparison.fr_only is defined and detailed_comparison.link_comparison.fr_only is iterable %} + {% for link in detailed_comparison.link_comparison.fr_only %} + + + + + {% endfor %} + {% else %} + + + + {% endif %} + +
Texte du lienURL
{{ link.text }} + + {{ link.href|slice(0, 30) }}{% if link.href|length > 30 %}...{% endif %} + +
Aucun lien uniquement en français
+
+
+
+
+
+ +
+
+
+
+

Liens communs

+ + {% if detailed_comparison.link_comparison.common is defined %} + {{ detailed_comparison.link_comparison.common|length }} liens communs + {% else %} + 0 liens communs + {% endif %} + +
+
+
+ + + + + + + + + + + {% if detailed_comparison.link_comparison.common is defined and detailed_comparison.link_comparison.common is iterable %} + {% for link in detailed_comparison.link_comparison.common %} + + + + + + + {% endfor %} + {% else %} + + + + {% endif %} + +
Texte ENURL ENTexte FRURL FR
{{ link.en.text }} + + {{ link.en.href|slice(0, 30) }}{% if link.en.href|length > 30 %}...{% endif %} + + {{ link.fr.text }} + + {{ link.fr.href|slice(0, 30) }}{% if link.fr.href|length > 30 %}...{% endif %} + +
Aucun lien commun trouvé
+
+
+
+
+
+
+
+ {% endif %} - - {#

Comparaison des liens côte à côte

#} - {#
#} - {# #} - {# #} - {# #} - {# #} - {# #} - {# #} - {# #} - {# #} - {# #} - {# #} - {# {% set en_links = detailed_comparison.link_comparison.en_only %} #} - {# {% set fr_links = detailed_comparison.link_comparison.fr_only %} #} - {# {% set max_links = max(en_links|length, fr_links|length) %} #} - - {# {% for i in 0..(max_links - 1) %} #} - {# #} - {# {% if i < en_links|length %} #} - {# #} - {# #} - {# {% else %} #} - {# #} - {# #} - {# {% endif %} #} - - {# {% if i < fr_links|length %} #} - {# #} - {# #} - {# {% else %} #} - {# #} - {# #} - {# {% endif %} #} - {# #} - {# {% endfor %} #} - {# #} - {#
Texte ENURL ENTexte FRURL FR
{{ en_links[i].text }}{{ en_links[i].href|slice(0, 30) }} #} - {# ...{{ fr_links[i].text }}{{ fr_links[i].href|slice(0, 30) }} #} - {# ...
#} - {#
#} - {#
#} - {#
#} - {#
#} - {#
#} - {#
#} - {#
#} - {# {% endif %} #} - - {# {% if detailed_comparison and detailed_comparison.category_comparison %} #} - {#
#} - {#
#} - {#

Comparaison des catégories

#} - {#
#} - {#
#} - {#
#} - {#
#} - {#
#} - {#
#} - {#

Catégories en anglais

#} - {# #} - {# {{ (detailed_comparison.category_comparison.en_only|length + detailed_comparison.category_comparison.common|length) }} catégories #} - {# #} - {#
#} - {#
#} - {#

Catégories communes #} - {# ({{ detailed_comparison.category_comparison.common|length }})

#} - {#
    #} - {# {% for category in detailed_comparison.category_comparison.common %} #} - {#
  • {{ category }}
  • #} - {# {% endfor %} #} - {#
#} - - {#

Catégories uniquement en anglais #} - {# ({{ detailed_comparison.category_comparison.en_only|length }})

#} - {#
    #} - {# {% for category in detailed_comparison.category_comparison.en_only %} #} - {#
  • {{ category }}
  • #} - {# {% endfor %} #} - {#
#} - {#
#} - {#
#} - {#
#} - {#
#} - {#
#} - {#
#} - {#

Catégories en français

#} - {# #} - {# {{ (detailed_comparison.category_comparison.fr_only|length + detailed_comparison.category_comparison.common|length) }} catégories #} - {# #} - {#
#} - {#
#} - {#

Catégories communes #} - {# ({{ detailed_comparison.category_comparison.common|length }})

#} - {#
    #} - {# {% for category in detailed_comparison.category_comparison.common %} #} - {#
  • {{ category }}
  • #} - {# {% endfor %} #} - {#
#} - - {#

Catégories uniquement en français #} - {# ({{ detailed_comparison.category_comparison.fr_only|length }})

#} - {#
    #} - {# {% for category in detailed_comparison.category_comparison.fr_only %} #} - {#
  • {{ category }}
  • #} - {# {% endfor %} #} - {#
#} - {#
#} - {#
#} - {#
#} - {#
#} - {#
#} - {#
#} - {# {% endif %} #} + {% if detailed_comparison and detailed_comparison.category_comparison %} +
+
+

Comparaison des catégories

+
+
+
+
+
+
+

Catégories uniquement en anglais

+ + {% if detailed_comparison.category_comparison.en_only is defined %} + {{ detailed_comparison.category_comparison.en_only|length }} catégories + {% else %} + 0 catégories + {% endif %} + +
+
+
    + {% if detailed_comparison.category_comparison.en_only is defined and detailed_comparison.category_comparison.en_only is iterable and detailed_comparison.category_comparison.en_only|length > 0 %} + {% for category in detailed_comparison.category_comparison.en_only %} +
  • {{ category }}
  • + {% endfor %} + {% else %} +
  • Aucune catégorie uniquement en anglais
  • + {% endif %} +
+
+
+
+
+
+
+

Catégories uniquement en français

+ + {% if detailed_comparison.category_comparison.fr_only is defined %} + {{ detailed_comparison.category_comparison.fr_only|length }} catégories + {% else %} + 0 catégories + {% endif %} + +
+
+
    + {% if detailed_comparison.category_comparison.fr_only is defined and detailed_comparison.category_comparison.fr_only is iterable and detailed_comparison.category_comparison.fr_only|length > 0 %} + {% for category in detailed_comparison.category_comparison.fr_only %} +
  • {{ category }}
  • + {% endfor %} + {% else %} +
  • Aucune catégorie uniquement en français
  • + {% endif %} +
+
+
+
+
+ +
+
+
+
+

Catégories communes

+ + {% if detailed_comparison.category_comparison.common is defined %} + {{ detailed_comparison.category_comparison.common|length }} catégories communes + {% else %} + 0 catégories communes + {% endif %} + +
+
+
    + {% if detailed_comparison.category_comparison.common is defined and detailed_comparison.category_comparison.common is iterable and detailed_comparison.category_comparison.common|length > 0 %} + {% for category in detailed_comparison.category_comparison.common %} +
  • {{ category }}
  • + {% endfor %} + {% else %} +
  • Aucune catégorie commune trouvée
  • + {% endif %} +
+
+
+
+
+
+
+ {% endif %} {# {% else %} #} {#
#} {#
#} @@ -796,6 +1097,41 @@
+ {% if staleness_distribution is defined and staleness_distribution is not null %} +
+
+

Répartition des scores de décrépitude

+
+
+

Ce graphique montre la répartition des scores de décrépitude pour toutes les pages wiki et où se situe la page courante :

+ +
+ +
+ +
+

Statistiques de décrépitude :

+
+
+
    +
  • Nombre total de pages : {{ staleness_distribution.totalPages }}
  • +
  • Score minimum : {{ staleness_distribution.min|round(2) }}
  • +
  • Score maximum : {{ staleness_distribution.max|round(2) }}
  • +
+
+
+
    +
  • Score moyen : {{ staleness_distribution.avg|round(2) }}
  • +
  • Score médian : {{ staleness_distribution.median|round(2) }}
  • +
  • Score de cette page : {{ staleness_distribution.currentPageScore|round(2) }}
  • +
+
+
+
+
+
+ {% endif %} + {% if history_data is defined and history_data|length > 0 %}
@@ -1078,6 +1414,162 @@ }); }); + // Create ranking evolution chart if the element exists + const rankingEvolutionChartElement = document.getElementById('rankingEvolutionChart'); + const historyData = {{ history_data|json_encode|raw }}; + if (rankingEvolutionChartElement && historyData && historyData.length > 0) { + // Format the data for the chart + const dates = historyData.map(entry => entry.date); + const stalenessScores = historyData.map(entry => entry.metrics.staleness_score || 0); + const wordDiffs = historyData.map(entry => entry.metrics.word_diff || 0); + const sectionDiffs = historyData.map(entry => entry.metrics.section_diff || 0); + const linkDiffs = historyData.map(entry => entry.metrics.link_diff || 0); + + // Create the chart + new Chart(rankingEvolutionChartElement, { + type: 'line', + data: { + labels: dates, + datasets: [ + { + label: 'Score de décrépitude', + data: stalenessScores, + borderColor: 'rgba(255, 99, 132, 1)', + backgroundColor: 'rgba(255, 99, 132, 0.2)', + fill: false, + tension: 0.1 + }, + { + label: 'Différence de mots / 10', + data: wordDiffs.map(val => val / 10), + borderColor: 'rgba(54, 162, 235, 1)', + backgroundColor: 'rgba(54, 162, 235, 0.2)', + fill: false, + tension: 0.1 + }, + { + label: 'Différence de sections', + data: sectionDiffs, + borderColor: 'rgba(255, 206, 86, 1)', + backgroundColor: 'rgba(255, 206, 86, 0.2)', + fill: false, + tension: 0.1 + }, + { + label: 'Différence de liens', + data: linkDiffs, + borderColor: 'rgba(75, 192, 192, 1)', + backgroundColor: 'rgba(75, 192, 192, 0.2)', + fill: false, + tension: 0.1 + } + ] + }, + options: { + responsive: true, + plugins: { + title: { + display: true, + text: 'Évolution des métriques au fil du temps' + }, + tooltip: { + mode: 'index', + intersect: false + } + }, + scales: { + y: { + beginAtZero: true + } + } + } + }); + } + + // Create distribution histogram if the element exists + const distributionChartElement = document.getElementById('distributionChart'); + if (distributionChartElement) { + // Get staleness distribution data from the template + const distributionData = {{ staleness_distribution|json_encode|raw }}; + + if (distributionData) { + // Prepare data for the histogram + const labels = distributionData.binLabels; + const counts = distributionData.bins; + const currentPageBin = distributionData.currentPageBin; + + // Create colors array with the current page bin highlighted + const colors = counts.map((_, index) => + index === currentPageBin ? 'rgba(255, 99, 132, 0.8)' : 'rgba(54, 162, 235, 0.6)' + ); + + // Create the histogram + const ctx = distributionChartElement.getContext('2d'); + const distributionChart = new Chart(ctx, { + type: 'bar', + data: { + labels: labels, + datasets: [{ + label: 'Nombre de pages', + data: counts, + backgroundColor: colors, + borderColor: colors.map(color => color.replace('0.6', '1').replace('0.8', '1')), + borderWidth: 1 + }] + }, + options: { + responsive: true, + scales: { + y: { + beginAtZero: true, + title: { + display: true, + text: 'Nombre de pages' + } + }, + x: { + title: { + display: true, + text: 'Score de décrépitude' + } + } + }, + plugins: { + title: { + display: true, + text: 'Répartition des scores de décrépitude (la page courante est en rouge)' + }, + tooltip: { + callbacks: { + title: function(tooltipItems) { + const item = tooltipItems[0]; + const binRange = item.label; + return `Score: ${binRange}`; + }, + label: function(context) { + let label = 'Nombre de pages: ' + context.raw; + if (context.dataIndex === currentPageBin) { + label += ' (inclut la page courante)'; + } + return label; + }, + afterLabel: function(context) { + if (context.dataIndex === currentPageBin) { + return `Score de cette page: ${distributionData.currentPageScore.toFixed(2)}`; + } + return ''; + } + } + }, + legend: { + display: false + } + } + } + }); + } + } + // Create history chart if the element exists const historyChartElement = document.getElementById('historyChart'); if (historyChartElement) { diff --git a/templates/admin/wiki_decrepitude.html.twig b/templates/admin/wiki_decrepitude.html.twig index 5fa7d8a..72284e2 100644 --- a/templates/admin/wiki_decrepitude.html.twig +++ b/templates/admin/wiki_decrepitude.html.twig @@ -84,50 +84,72 @@ python3 wiki_compare.py {# {{ page.reason }}#} - {% if page.word_diff > 0 %} - {{ page.word_diff }} - {% elseif page.word_diff < 0 %} - {{ page.word_diff }} + {% if page.word_diff is defined %} + {% if page.word_diff > 0 %} + {{ page.word_diff }} + {% elseif page.word_diff < 0 %} + {{ page.word_diff }} + {% else %} + 0 + {% endif %} {% else %} - 0 + N/A {% endif %} - {% if page.section_diff > 0 %} - {{ page.section_diff }} - {% elseif page.section_diff < 0 %} - {{ page.section_diff }} + {% if page.section_diff is defined %} + {% if page.section_diff > 0 %} + {{ page.section_diff }} + {% elseif page.section_diff < 0 %} + {{ page.section_diff }} + {% else %} + 0 + {% endif %} {% else %} - 0 + N/A {% endif %} - {% if page.link_diff > 0 %} - {{ page.link_diff }} - {% elseif page.link_diff < 0 %} - {{ page.link_diff }} + {% if page.link_diff is defined %} + {% if page.link_diff > 0 %} + {{ page.link_diff }} + {% elseif page.link_diff < 0 %} + {{ page.link_diff }} + {% else %} + 0 + {% endif %} {% else %} - 0 + N/A {% endif %} -
- {% set score_class = page.staleness_score > 70 ? 'bg-danger' : (page.staleness_score > 40 ? 'bg-warning' : 'bg-success') %} -
- {{ page.staleness_score }} + {% if page.staleness_score is defined %} +
+ {% set score_class = page.staleness_score > 70 ? 'bg-danger' : (page.staleness_score > 40 ? 'bg-warning' : 'bg-success') %} +
+ {{ page.staleness_score }} +
-
+ {% else %} + N/A + {% endif %}
- - EN - + {% if page.url is defined and page.url %} + + EN + + {% else %} + + {% endif %} {% if page.fr_page is defined and page.fr_page %} {% if page.fr_page.url is defined %}
- {% if page.en_page.description_img_url is defined and page.en_page.description_img_url %} + {% if page.description_img_url is defined and page.description_img_url %}
- {% if page.key is defined %}{{ page.key }}{% elseif page.title is defined %}{{ page.title }}{% else %}Image{% endif %}
@@ -223,23 +245,33 @@ python3 wiki_compare.py {# {{ page.reason }}#} -
- {% set score_class = page.staleness_score > 70 ? 'bg-danger' : (page.staleness_score > 40 ? 'bg-warning' : 'bg-success') %} -
- {{ page.staleness_score }} + {% if page.staleness_score is defined %} +
+ {% set score_class = page.staleness_score > 70 ? 'bg-danger' : (page.staleness_score > 40 ? 'bg-warning' : 'bg-success') %} +
+ {{ page.staleness_score }} +
-
+ {% else %} + N/A + {% endif %}
- - EN - + {% if page.url is defined and page.url %} + + EN + + {% else %} + + {% endif %} {% if page.fr_page is defined and page.fr_page %} {% if page.fr_page.url is defined %} {% else %} labels.push("Page sans clé"); {% endif %} - scores.push({{ page.staleness_score }}); + {% if page.staleness_score is defined %} + scores.push({{ page.staleness_score }}); - // Set color based on score - {% if page.staleness_score > 80 %} - colors.push('rgba(220, 53, 69, 0.7)'); // danger (red) - {% elseif page.staleness_score > 60 %} - colors.push('rgba(232, 113, 55, 0.7)'); // dark orange - {% elseif page.staleness_score > 40 %} - colors.push('rgba(255, 153, 0, 0.7)'); // orange - {% elseif page.staleness_score > 20 %} - colors.push('rgba(255, 193, 7, 0.7)'); // warning (yellow) - {% elseif page.staleness_score > 10 %} - colors.push('rgba(140, 195, 38, 0.7)'); // light green + // Set color based on score + {% if page.staleness_score > 80 %} + colors.push('rgba(220, 53, 69, 0.7)'); // danger (red) + {% elseif page.staleness_score > 60 %} + colors.push('rgba(232, 113, 55, 0.7)'); // dark orange + {% elseif page.staleness_score > 40 %} + colors.push('rgba(255, 153, 0, 0.7)'); // orange + {% elseif page.staleness_score > 20 %} + colors.push('rgba(255, 193, 7, 0.7)'); // warning (yellow) + {% elseif page.staleness_score > 10 %} + colors.push('rgba(140, 195, 38, 0.7)'); // light green + {% else %} + colors.push('rgba(25, 135, 84, 0.7)'); // success (green) + {% endif %} {% else %} - colors.push('rgba(25, 135, 84, 0.7)'); // success (green) + scores.push(0); + colors.push('rgba(108, 117, 125, 0.7)'); // secondary (gray) {% endif %} {% endfor %} diff --git a/templates/admin/wiki_rankings.html.twig b/templates/admin/wiki_rankings.html.twig new file mode 100644 index 0000000..8ce10af --- /dev/null +++ b/templates/admin/wiki_rankings.html.twig @@ -0,0 +1,566 @@ +{% extends 'base.html.twig' %} + +{% block title %}Évolution des classements Wiki OSM{% endblock %} + +{% block body %} +
+ {% include 'admin/_wiki_navigation.html.twig' %} + +
+

Évolution des classements Wiki OSM

+ {% if last_updated %} +
+ Dernière mise à jour: {{ last_updated|date('d/m/Y H:i') }} +
+ {% endif %} +
+ + {% if not json_exists %} +
+ Aucune donnée de classement n'est disponible. Veuillez exécuter le script de scraping pour générer les données. +
+ {% else %} + +
+
+

Métriques globales

+
+
+
+
+ +
+
+ +
+
+
+
+

Évolution des métriques globales

+
+ + + + + + + + + + + + + + + {% for timestamp in timestamps %} + {% if global_metrics[timestamp] is defined %} + + + + + + + + + + + {% endif %} + {% endfor %} + +
DatePages totalesScore moyenSections (moy.)Mots (moy.)Liens (moy.)Images (moy.)Catégories (moy.)
{{ timestamp|date('d/m/Y') }}{{ global_metrics[timestamp].total_pages }} +
+ {% set score = global_metrics[timestamp].avg_staleness %} + {% set score_class = score > 70 ? 'bg-danger' : (score > 40 ? 'bg-warning' : 'bg-success') %} +
+ {{ score }} +
+
+
{{ global_metrics[timestamp].avg_sections }}{{ global_metrics[timestamp].avg_words }}{{ global_metrics[timestamp].avg_links }}{{ global_metrics[timestamp].avg_images }}{{ global_metrics[timestamp].avg_categories }}
+
+
+
+
+
+ + +
+
+

Classement des pages

+
+
+
+
+
+ Filtrer + +
+
+
+ +
+
+ +
+
+ +
+ + + + + + + + + + + {% for key, page in pages %} + + + + + + + {% endfor %} + +
PageScore actuelÉvolutionActions
{{ page.title }} + {% set latest_timestamp = timestamps|last %} + {% if page.metrics[latest_timestamp] is defined %} + {% set latest_score = page.metrics[latest_timestamp].staleness_score %} +
+ {% set score_class = latest_score > 70 ? 'bg-danger' : (latest_score > 40 ? 'bg-warning' : 'bg-success') %} +
+ {{ latest_score }} +
+
+ {% else %} + N/A + {% endif %} +
+ + + + Comparer + +
+
+
+
+ + +
+ {% endif %} +
+{% endblock %} + +{% block javascripts %} + {{ parent() }} + {% if json_exists %} + + + {% endif %} +{% endblock %} \ No newline at end of file diff --git a/translations.json b/translations.json index 0c6eaf1..618c5fb 100644 --- a/translations.json +++ b/translations.json @@ -47,7 +47,19 @@ "translated_at": "2025-09-05T10:11:14.158066", "model": "mistral:7b", "is_specific_page": false + }, + "https://wiki.openstreetmap.org/wiki/Key:roof:material": { + "key": "https://wiki.openstreetmap.org/wiki/Key:roof:material", + "en_page": { + "url": "https://wiki.openstreetmap.org/wiki/Key:roof:material", + "last_modified": "2025-06-21", + "word_count": 1170 + }, + "translated_content": " v\n·\nd\n·\ne\ntoit:matériau\nDescription\nMatériau extérieur pour la toiture du bâtiment.\nGroupe :\nbâtiments\nUtilisé sur ces éléments\nExigence\nbâtiment\n=*\nou\nbâtiment:partie\n=*\nCombinations utiles\nbâtiment:matériau\n=*\ntoit:forme\n=*\nStatut : de facto\ntoit:matériau\n=\n*\nPlus de détails à la balise\ninfo\nOutils pour cette balise\ntaginfo\n·\nAD\n·\nAT\n·\nBR\n·\nBY\n·\nCH\n·\nCN\n·\nCZ\n·\nDE\n·\nDK\n·\nFI\n·\nFR\n·\nGB\n·\nGR\n·\nHU\n·\nIN\n·\nIR\n·\nIT\n·\nLI\n·\nLU\n·\nJP\n·\nKP\n·\nKR\n·\nNL\n·\nPL\n·\nPT\n·\nRU\n·\nES\n·\nAR\n·\nMX\n·\nCO\n·\nBO\n·\nCL\n·\nEC\n·\nPY\n·\nPE\n·\nUY\n·\nVE\n·\nTW\n·\nUA\n·\nUS\n·\nVN\noverpass-turbo\nHistoire du tag OSM\ntoit:matériau\n=*\n– le matériau extérieur de la toiture d'un\nbâtiment\nou\npartie de bâtiment\n.\nCette information peut être utilisée pour modéliser des bâtiments en 3D.\nCertains rendus 3D définissent la couleur du bâtiment sur la base de cette balise\n[\n1\n]\n.\nValeurs\nValeur\nDescription courte\nExemple d'image\nCompte\ntoit_tuiles\nToit recouvert de tuiles, généralement d'origine céramique (par exemple terracotta). Voir\nTuiles de toit\n.\ntoit:matériau\n=\ntoit_tuiles\n$1%\n$1%\n$1%\n$1%\nPlus de détails à la balise\ninfo\nPlus de détails à la balise\ninfo\nmétal\nToit recouvert de métal, plat ou bosselé (corrugé).\nValeurs similaires :\nmetal_feuille\n,\nzinc\n,\ncuivre\n. Voir\ntoit en métal et\nZinc corrugé\ntoit:matériau\n=\nmétal\n$1%\n$1%\n$1%\n$1%\nPlus de détails à la balise\ninfo\nPlus de détails à la balise\ninfo\nbéton\nToit recouvert de béton visible.\ntoit:matériau\n=\nbéton\n$1%\n$1%\n$1%\n$1%\nPlus de détails à la balise\ninfo\nPlus de détails à la balise\ninfo\npapiers bitumineux\nToit recouvert de papier bitumineux.\ntoit:matériau\n=\npapiers bitumineux\n$1%\n$1%\n$1%\n$1%\nPlus de détails à la balise\ninfo\nPlus de détails à la balise\ninfo\namiante-ciment\nToit recouvert d'amiante ciment corrugé. Cette désignation est inexacte, il est préférable d'utiliser\neternit\n. Les revêtements ne sont pas construits avec de l'amiante lui-même, mais avec eternit, qui est composé de 90% de ciment et 10% d'amiante.\ntoit:matériau\n=\namiante-ciment\n$1%\n$1%\n$1%\n$1%\nPlus de détails à la balise\ninfo\nPlus de détails à la balise\ninfo\neternit\nEternit – désignation commerciale des panneaux en amiante-ciment\ntoit:matériau\n=\neternit\n$1%\n$1%\n$1%\n$1%\nPlus de détails à la balise\ninfo\nPlus de détails à la balise\ninfo\nverre\nToit recouvert de verre.\ntoit:matériau\n=\nverre\n$1%\n$1%\n$1%\n$1%\nPlus de détails à la balise\ninfo\nPlus de détails à la balise\ninfo\nverre acrylique\nToit recouvert de verre acrylique.\ntoit:matériau\n=\nverre acrylique\n$1%\n$1%\n$1%\n$1%\nPlus de détails à la balise\ninfo\nPlus de détails à la balise\ninfo\nmétal feuille, plat ou bosselé\ntoit:matériau\n=\nmétal_feuille\n$1%\n$1%\n$1%\n$1%\nPlus de détails à la balise\ninfo\nPlus de détails à la balise\ninfo\nardoise\nToit recouvert d'ardoise, une sorte de plaques minces de pierre.\ntoit:matériau\n=\nardoise\n$1%\n$1%\n$1%\n$1%\nPlus de détails à la balise\ninfo\nPlus de détails à la balise\ninfo\nétain\nÉtain\ntoit:matériau\n=\nétain\n$1%\n$1%\n$1%\n$1%\nPlus de détails à la balise\ninfo\nPlus de détails à la balise\ninfo\nherbe\nToit recouvert d'une herbe vivante (ou des plantes similaires), scellé en dessous. Voir\nToit vert\n.\ntoit:matériau\n=\nherbe\n$1%\n$1%\n$1%\n$1%\nPlus de détails à la balise\ninfo\nPlus de détails à la balise\ninfo\ncuivre\nToit recouvert de cuivre plaqué. Lorsqu'il est en contact avec l'air, il devient couvert d'un patine verte et s'oxyde progressivement en passant du brun au vert foncé.\ntoit:matériau\n=\ncuivre\n$1%\n$1%\n$1%\n$1%\nPlus de détails à la balise\ninfo\nPlus de détails à la balise\ninfo\ntitane\nTitanium\ntoit:matériau\n=\ntitanium\n$1%\n$1%\n$1%\n$1%\nPlus de détails à la balise\ninfo\nPlus de détails à la balise\ninfo\nmatériau définit par l'utilisateur\nVoir\ntaginfo\npour toutes les valeurs courantes.\nDans certains cas, des valeurs issues de\nbâtiment:matériau =*\npeuvent être également utilisées, car\ntoit:matériau =*\nest également utilisé avec\nparties de bâtiments\n.\nExemples de rendus\ntoit:matériau=toit_tuiles sur OSM2World\ntoit:matériau=herbe sur OSM2World\nMistakes de balisage possibles\nbâtiment:toiture\n=\n*\nPlus de détails à la balise\ninfo\nPlus de détails à la balise\ninfo\nbâtiment:toiture\n=*\nSi vous connaissez des lieux avec cette balise, vérifiez si elle peut être taguée avec une autre balise.\nLes modifications automatisées sont fortement déconseillées\nsauf si vous savez réellement ce que vous faites\n!\nbâtiment:toiture:matériau\n=\n*\nPlus de détails à la balise\ninfo\nPlus de détails à la balise\ninfo\nbâtiment:toiture:matériau\n=*\nSi vous connaissez des lieux avec cette balise, vérifiez si elle peut être taguée avec une autre balise.\nLes modifications automatisées sont fortement déconseillées\nsauf si vous savez réellement ce que vous faites\n!\nVoir également\nbâtiment:matériau\n=*\n– matériau du bâtiment\nmatériau =*\n– matériau\ntoit:forme\n=*\n– forme de toiture\ntoit:couleur\n=*\n– couleur de toiture\nBâtiments simples en 3D\n– vue d'ensemble des balises pour la mise en valeur de bâtiments en 3D\nWikipedia : Liste des matériaux de toiture disponibles sur le marché\nRéférences\n↑\nOSMBuildings sur GitHub\nRétrievé le 9 octobre 2021 à partir de \"\nhttps://wiki.openstreetmap.org/w/index.php?title=Key:toit:matériau&oldid=2868367\n\"", + "translated_at": "2025-09-05T17:15:11.193391", + "model": "mistral:7b", + "is_specific_page": true } }, - "last_updated": "2025-09-05T10:11:14.159855" + "last_updated": "2025-09-05T17:15:11.214240" } \ No newline at end of file diff --git a/wiki_compare/__pycache__/wiki_compare.cpython-313.pyc b/wiki_compare/__pycache__/wiki_compare.cpython-313.pyc index 82e2c47..f3255ad 100644 Binary files a/wiki_compare/__pycache__/wiki_compare.cpython-313.pyc and b/wiki_compare/__pycache__/wiki_compare.cpython-313.pyc differ diff --git a/wiki_compare/wiki_compare.py b/wiki_compare/wiki_compare.py index 5495b13..74fcfe5 100755 --- a/wiki_compare/wiki_compare.py +++ b/wiki_compare/wiki_compare.py @@ -116,7 +116,8 @@ SPECIFIC_PAGES = [ "Mapping_private_information", "Any_tags_you_like", "Organised_Editing/Best_Practices", - "Map_features" + "Map_features", + "Wiki" ] def fetch_desynchronized_pages(): @@ -280,12 +281,104 @@ def save_to_json(data, filename): except IOError as e: logger.error(f"Error saving data to {filename}: {e}") +def calculate_global_metrics(data): + """ + Calculate global metrics for all pages in the data + + Args: + data: Data containing regular_pages and specific_pages + + Returns: + dict: Dictionary with global metrics + """ + # Combine regular and specific pages for global metrics + all_pages = data.get('regular_pages', []) + data.get('specific_pages', []) + + # Initialize metrics + metrics = { + 'total_pages': len(all_pages), + 'avg_sections': 0, + 'avg_words': 0, + 'avg_links': 0, + 'avg_images': 0, + 'avg_categories': 0, + 'avg_staleness': 0, + 'pages_with_en_fr': 0, + 'pages_missing_fr': 0, + 'staleness_distribution': { + '0-20': 0, + '21-40': 0, + '41-60': 0, + '61-80': 0, + '81-100': 0, + '100+': 0 + } + } + + # Skip if no pages + if not all_pages: + return metrics + + # Calculate totals + total_sections = 0 + total_words = 0 + total_links = 0 + total_images = 0 + total_categories = 0 + total_staleness = 0 + + for page in all_pages: + # Count pages with/without French version + if page.get('fr_page'): + metrics['pages_with_en_fr'] += 1 + else: + metrics['pages_missing_fr'] += 1 + + # Add to staleness distribution + staleness = page.get('staleness_score', 0) + total_staleness += staleness + + if staleness <= 20: + metrics['staleness_distribution']['0-20'] += 1 + elif staleness <= 40: + metrics['staleness_distribution']['21-40'] += 1 + elif staleness <= 60: + metrics['staleness_distribution']['41-60'] += 1 + elif staleness <= 80: + metrics['staleness_distribution']['61-80'] += 1 + elif staleness <= 100: + metrics['staleness_distribution']['81-100'] += 1 + else: + metrics['staleness_distribution']['100+'] += 1 + + # Add to totals + total_sections += page.get('section_diff', 0) if 'section_diff' in page else 0 + total_words += page.get('word_diff', 0) if 'word_diff' in page else 0 + total_links += page.get('link_diff', 0) if 'link_diff' in page else 0 + total_images += page.get('media_diff', 0) if 'media_diff' in page else 0 + + # Count categories if available + if page.get('category_comparison'): + cat_count = len(page['category_comparison'].get('en_only', [])) + total_categories += cat_count + + # Calculate averages + metrics['avg_sections'] = round(total_sections / len(all_pages), 2) + metrics['avg_words'] = round(total_words / len(all_pages), 2) + metrics['avg_links'] = round(total_links / len(all_pages), 2) + metrics['avg_images'] = round(total_images / len(all_pages), 2) + metrics['avg_categories'] = round(total_categories / len(all_pages), 2) + metrics['avg_staleness'] = round(total_staleness / len(all_pages), 2) + + return metrics + def save_with_history(data, filename): """ Save data to a JSON file while preserving history This function loads existing data from the file (if it exists), adds the new data to the history, and saves the updated data back to the file. + It also calculates global metrics for the current data. Args: data: New data to save @@ -301,11 +394,15 @@ def save_with_history(data, filename): # Initialize history if it doesn't exist if 'history' not in existing_data: existing_data['history'] = {} + + # Calculate global metrics for the current data + global_metrics = calculate_global_metrics(data) - # Add current regular_pages and specific_pages to history + # Add current regular_pages, specific_pages, and global metrics to history history_entry = { 'regular_pages': data.get('regular_pages', []), - 'specific_pages': data.get('specific_pages', []) + 'specific_pages': data.get('specific_pages', []), + 'global_metrics': global_metrics } # Add the entry to history with timestamp as key @@ -314,6 +411,7 @@ def save_with_history(data, filename): # Update the current data existing_data['regular_pages'] = data.get('regular_pages', []) existing_data['specific_pages'] = data.get('specific_pages', []) + existing_data['global_metrics'] = global_metrics existing_data['last_updated'] = current_timestamp # Save the updated data @@ -321,10 +419,119 @@ def save_with_history(data, filename): json.dump(existing_data, f, indent=2, ensure_ascii=False) logger.info(f"Data with history saved to {filename}") + + # Also save a separate ranking history file + save_ranking_history(existing_data, "page_rankings.json") + except (IOError, json.JSONDecodeError) as e: logger.error(f"Error saving data with history to {filename}: {e}") # Fallback to regular save if there's an error save_to_json(data, filename) + +def save_ranking_history(data, filename): + """ + Save ranking history to a separate JSON file + + This function extracts ranking data from the history and saves it in a format + optimized for displaying ranking evolution over time. + + Args: + data: Data containing history entries + filename (str): Name of the file to save rankings + """ + try: + # Initialize ranking data structure + ranking_data = { + 'timestamps': [], + 'pages': {}, + 'global_metrics': {} + } + + # Extract history entries + history = data.get('history', {}) + + # Sort timestamps chronologically + sorted_timestamps = sorted(history.keys()) + ranking_data['timestamps'] = sorted_timestamps + + # Process each page to track its metrics over time + all_page_keys = set() + + # First, collect all unique page keys across all history entries + for timestamp in sorted_timestamps: + entry = history[timestamp] + + # Add global metrics for this timestamp + if 'global_metrics' in entry: + ranking_data['global_metrics'][timestamp] = entry['global_metrics'] + + # Collect page keys from regular pages + for page in entry.get('regular_pages', []): + all_page_keys.add(page['key']) + + # Collect page keys from specific pages + for page in entry.get('specific_pages', []): + all_page_keys.add(page['key']) + + # Initialize data structure for each page + for page_key in all_page_keys: + ranking_data['pages'][page_key] = { + 'title': page_key, + 'metrics': {} + } + + # Fill in metrics for each page at each timestamp + for timestamp in sorted_timestamps: + entry = history[timestamp] + + # Process regular pages + for page in entry.get('regular_pages', []): + page_key = page['key'] + + # Extract metrics we want to track + metrics = { + 'staleness_score': page.get('staleness_score', 0), + 'word_diff': page.get('word_diff', 0), + 'section_diff': page.get('section_diff', 0), + 'link_diff': page.get('link_diff', 0), + 'media_diff': page.get('media_diff', 0) + } + + # Store metrics for this timestamp + ranking_data['pages'][page_key]['metrics'][timestamp] = metrics + + # Store page title if available + if 'en_page' in page and page['en_page']: + ranking_data['pages'][page_key]['title'] = page['en_page'].get('page_title', page_key) + + # Process specific pages + for page in entry.get('specific_pages', []): + page_key = page['key'] + + # Extract metrics we want to track + metrics = { + 'staleness_score': page.get('staleness_score', 0), + 'word_diff': page.get('word_diff', 0), + 'section_diff': page.get('section_diff', 0), + 'link_diff': page.get('link_diff', 0), + 'media_diff': page.get('media_diff', 0) + } + + # Store metrics for this timestamp + ranking_data['pages'][page_key]['metrics'][timestamp] = metrics + + # Store page title if available + if 'en_page' in page and page['en_page']: + ranking_data['pages'][page_key]['title'] = page['en_page'].get('page_title', page_key) + + # Save the ranking data + with open(filename, 'w', encoding='utf-8') as f: + json.dump(ranking_data, f, indent=2, ensure_ascii=False) + + logger.info(f"Ranking history saved to {filename}") + + except (IOError, json.JSONDecodeError) as e: + logger.error(f"Error saving ranking history to {filename}: {e}") def check_grammar_with_grammalecte(text): """ diff --git a/wiki_compare/wiki_pages.csv b/wiki_compare/wiki_pages.csv index ebfa7be..d4d00c6 100644 --- a/wiki_compare/wiki_pages.csv +++ b/wiki_compare/wiki_pages.csv @@ -12,6 +12,8 @@ Key:harassment_prevention,en,https://wiki.openstreetmap.org/wiki/Key:harassment_ Key:harassment_prevention,fr,https://wiki.openstreetmap.org/wiki/FR:Key:harassment_prevention,2025-07-03,15,328,83,14,66.72,https://wiki.openstreetmap.org/w/images/thumb/7/76/Osm_element_node.svg/30px-Osm_element_node.svg.png Proposal process,en,https://wiki.openstreetmap.org/wiki/Proposal process,2025-08-13,46,5292,202,4,172.34,https://wiki.openstreetmap.org/w/images/thumb/c/c2/Save_proposal_first.png/761px-Save_proposal_first.png Proposal process,fr,https://wiki.openstreetmap.org/wiki/FR:Proposal process,2023-09-22,15,0,0,0,172.34, +Outil de Manipulation et d'Organisation,en,https://wiki.openstreetmap.org/wiki/Outil de Manipulation et d'Organisation,2025-09-02,9,0,0,0,0.6, +Outil de Manipulation et d'Organisation,fr,https://wiki.openstreetmap.org/wiki/FR:Outil de Manipulation et d'Organisation,2025-09-02,13,0,0,0,0.6, Automated_Edits_code_of_conduct,en,https://wiki.openstreetmap.org/wiki/Automated_Edits_code_of_conduct,2025-07-26,19,0,0,0,23.1, Automated_Edits_code_of_conduct,fr,https://wiki.openstreetmap.org/wiki/FR:Automated_Edits_code_of_conduct,2025-04-03,17,0,0,0,23.1, Key:cuisine,en,https://wiki.openstreetmap.org/wiki/Key:cuisine,2025-07-23,17,3422,693,303,107.73,https://upload.wikimedia.org/wikipedia/commons/thumb/f/f0/Food_montage.jpg/200px-Food_montage.jpg @@ -44,6 +46,8 @@ Any_tags_you_like,fr,https://wiki.openstreetmap.org/wiki/FR:Any_tags_you_like,20 Organised_Editing/Best_Practices,en,https://wiki.openstreetmap.org/wiki/Organised_Editing/Best_Practices,2025-07-18,16,501,10,1,100,https://upload.wikimedia.org/wikipedia/commons/thumb/1/15/Ambox_warning_pn.svg/40px-Ambox_warning_pn.svg.png Map_features,en,https://wiki.openstreetmap.org/wiki/Map_features,2025-07-21,125,21926,4255,2222,507.98,https://upload.wikimedia.org/wikipedia/commons/thumb/6/6b/Bar_MXCT.JPG/100px-Bar_MXCT.JPG Map_features,fr,https://wiki.openstreetmap.org/wiki/FR:Map_features,2018-12-27,103,23159,5516,3062,507.98,https://wiki.openstreetmap.org/w/images/c/c4/Aerialway_gondola_render.png +Wiki,en,https://wiki.openstreetmap.org/wiki/Wiki,2025-02-24,16,669,40,1,302.87,https://wiki.openstreetmap.org/w/images/thumb/b/b7/OpenStreetMap_Wiki_MainPage.png/300px-OpenStreetMap_Wiki_MainPage.png +Wiki,fr,https://wiki.openstreetmap.org/wiki/FR:Wiki,2021-01-04,14,645,37,1,302.87,https://wiki.openstreetmap.org/w/images/thumb/b/b7/OpenStreetMap_Wiki_MainPage.png/300px-OpenStreetMap_Wiki_MainPage.png https://wiki.openstreetmap.org/wiki/FR:Quality_Assurance,fr,https://wiki.openstreetmap.org/wiki/FR:Quality_Assurance,2015-05-16,16,0,0,0,0, https://wiki.openstreetmap.org/wiki/Quality_Assurance,en,https://wiki.openstreetmap.org/wiki/Quality_Assurance,2025-06-01,19,0,0,0,100, https://wiki.openstreetmap.org/wiki/FR:Nominatim/Installation,fr,https://wiki.openstreetmap.org/wiki/FR:Nominatim/Installation,2016-08-22,32,0,0,0,0,