From 381f378db4ee9c7b11427cebb2b66cde3d2daa46 Mon Sep 17 00:00:00 2001 From: Tykayn Date: Mon, 8 Sep 2025 10:20:51 +0200 Subject: [PATCH] add history in articles measures --- src/Controller/WikiController.php | 162 +++- templates/admin/_wiki_navigation.html.twig | 5 + templates/admin/wiki_compare.html.twig | 760 +++++++++++++++--- templates/admin/wiki_decrepitude.html.twig | 149 ++-- templates/admin/wiki_rankings.html.twig | 566 +++++++++++++ translations.json | 14 +- .../__pycache__/wiki_compare.cpython-313.pyc | Bin 53282 -> 59521 bytes wiki_compare/wiki_compare.py | 213 ++++- wiki_compare/wiki_pages.csv | 4 + 9 files changed, 1678 insertions(+), 195 deletions(-) create mode 100644 templates/admin/wiki_rankings.html.twig 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 82e2c4759d4b48817ad4730609131f6b8da815c7..f3255ad28cc07228e973d508a793e9fb7150058e 100644 GIT binary patch delta 9860 zcmdryX;fR+neS=e5CQ~9tO8>|Y&O_zUV{PK*v2@1mP3MLSw;_#EhK!O@RruX=|+;% z+U{+}c9t5aoiSa~;*yy%ZIjR}9XFjcsB&uN)u+d&)1FLbdS>F~G@bOE>3rXP5@4J3 z2nAxvHDvdBWR^3?e@xgC`x(&F6AK&P?oKIVx)J#8wn424~oYm(g~H%Pfpu^ zuI8KRDaXpqtDu6Y5UfV927p^j)?#@b09h}F`hx*!fUL)=2N1L%XhqP5pdG;`YIg3h z-8(^g(3!UvCqz0BEIJ>u1)CPk24A-VkkfF)-Xnp?KzKL;-v<9bN5&go&KGMd;ezCiaH?BCAkjs*w$SfuI=j4T>_~r%a9Y zqyc6q&4etGs~1-R-`WIwe220qgF0E}=S*v;DX$?p|>Q>*hp1=&samsB}H2|0HP zo4>5}4-XDWiE+_OCB=_2iR=LsS$l*8A|ly~lx(53$~g96qk(lQMF{a>I~$u8N;tN} z;4G5wun6nS#z!L1_@9%dOV9GT2~CFzj
4WHW_P(RI4uQ%JwFXQTQ{o%oP`ol7F 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 delta 4211 zcmZu!d2AHd8K2pE*XuRrith{CF!&(&zz1Ms);2f>AFK@_kPPG9S$pwbc{4uNf5e7V zY9-WMuWAD1AVG~(sVYg9G>6nQ1%jHSN}-7fJQ@OR97hhbSxRPd>3Z*h%NtUozBz3;h zEP>SfmIF5URsc5c&h|--_X~pJ5QN>0G;UfZUZH196=|8ZA_^3Q0wG`M`Wf)nru;l8 ztT*QiK~XACE;T=A74MZ?Xs& zWEnyULMZ@C>ucH6(be89xAb(#{oA&X<;b<{)nZXnhO^2MDiA6GBs;T)Ly?e9T9CC0 zVIu&ugmg8+9C|D+5331{)Bsbn0~8n!sTDF{nkT60{vmm8+7_5`IQiY;kIZ5{{UD=e za|1Al384{TEr4Vt>yTa#!0c*tFdWi`NE5P}5jG&;A&3{D6``HBXZHM{!b@)}+`g@) z%iDIB-efbb{Xw-!2M*n>G`SCWEFJZg$3ps0Y($6O;m~d6Q|{277GxEnWc;_}QLKuW1xL1qn%MB>`*{r*~9^dMJx7fQ2^5%45= z5Lfb3SdEQk9CYwqG@Xz=IL;4a1qu%{dm6Wbys^tgLMvfqq>c*j(CipgiXi}ytJ;`}O~6?wbB z)W*{<`fNVC&(UBu2~jIu*HoBZ;&b|3zLW+_f#7rFKN@Y47Nz!!9--PapI(D*+FO%N zKPYn1KQ)zG?-hio?WyMcOb<@wqs zYfsa(H9b8nm8gRwVTH(XWl+_aM~kceP%z{--X#+mg~O9E0Ok%Wnl6vT!If%&>_xue zMWmbn?Zi)yttp~2Yll zR&%E7C#l!nG*sV2U#V~QOgq+pZ5KS%)2UV8{mVgJ4eb?qUP(AnaE~{`e4?%Z=#v>P zZ>jxcksWC%tF_vE%3Ru7X+BkHZLLne(RS03HD{a&OQk-pw@VggiNpdb*-lS(l`Yth zYOX+d6k#L%Z&$6DM{Bx$IYYo;_K2!!;18CfjSPZY^iV9Skrni4cdJ-P|K9!URLlxy zg>VmnA?eYcDsdS-*Hg6pVdPsdFK6=!Gf-nTO%Ff`;B}R(#66*)3(PhW4Tqw`-0Xag zN(ezmFlXDiUyb9wk{dSe>g{fN68ULy$l^hw#E0ZiG@yGRlcTG|L-f|@o}yfokdO16L=B+6 z>sONkc!%KTKFCdujI9*G{zt}3SMttd4n=jzLQdhz(+Gb+_y9ngFL3nw_(qd%qBV(t z>6SnbB~F`83(0|rcg(B!nlmtcA_I>Wjgg2FuJ9{5?yePtLj?+RVZzB=3%dD{cd~y4 z)0ulnkA&q}Rivuhb+B-9s`CLn0qC!c28Q%{h%y-NbO;_|04Fo`_Vev5(#Gtsd zS<-(_Zna6sEThd+_RM<3*9l26wBF{0AI(;9@` z`KQ?eqXVLJo<4`^iD#OZ@!o{iW43WU*$XU=EBwI z7YD1wT6*DNk+luBUGdtXZA-fO#eiG+Hz@sefUEEaPsRp<*18q|m}OmVCTPs7)%-cx zY*;G?gz*AQE(-L;WBI#LCbL19K-j$s~9ewtX9qbLb#G?>9?kzFRCBPVn70Ve`i#5xBCQ z!|67JcM*2tp&vll1pv7?r@rBRo|@=#f{8>vLwFzH8s_4)#R4QES&{tRD^78d4+Q*D zkVGbqYd{Ln@9D)?t0yO5j^+TylyIcw$ljN>l8@k}5BA~)vJ-VwC7LRw&-Wr`#mk~b z%0UjZlGsQza7WQJ!*X&1fLQ`cLStrS067w$ro8d^-#9({g#wYY{N zcN8#QtoVkQK77N4#%A37T;?T#ok@Go|yt?oTk41{t9p>@%w0S$)JG`?ZuAp29e=yj&cOJ&kJt+Dy zx|8LKHFS(Mc)Z9(H)vRu>F@WiTxfiGSRzU&O5TK#M=Z&2*(c7m7_#ks(pqMhVAxVa`s1=9Aoq7Q{b0|dVb$bS&N z5omCxeiM&{O~~j*_yS=Df%~Tzse`!fA%y1