up wiki controller

This commit is contained in:
Tykayn 2025-08-22 17:58:13 +02:00 committed by tykayn
parent 2f49ef6479
commit 391a212034
13 changed files with 1271297 additions and 866 deletions

View file

@ -18,13 +18,14 @@ body {
#qrcode {
margin-bottom: 8rem;
}
input[data-important]{
border-color: #7a8fbb ;
border-left-width: 5px ;
input[data-important] {
border-color: #7a8fbb;
border-left-width: 5px;
}
input[data-important]:before{
content : ">" !important ;
input[data-important]:before {
content: ">" !important;
}
.filled, .good_filled {
@ -32,7 +33,7 @@ input[data-important]:before{
color: #082b0a !important;
}
.filled:hover , .good_filled:hover {
.filled:hover, .good_filled:hover {
background-color: #d9ffd1 !important;
}
@ -174,13 +175,18 @@ table tbody {
overflow: auto;
}
#table_container, .table-container, #table-container{
#table_container, .table-container, #table-container {
max-height: 700px;
overflow: auto;
display: block;
border: solid 3px rgb(255, 255, 255);
}
#citySuggestions{
#citySuggestions {
z-index: 10;
}
body .card:hover {
transform: none !important;
box-shadow: none !important;
}

104
public/osm_fr_groups.json Normal file
View file

@ -0,0 +1,104 @@
{
"last_updated": "2025-08-22T17:10:41.058478",
"local_groups": [
{
"name": "Liste des groupes locaux se réunissant régulièrement",
"url": "https://framacalc.org/osm-groupes-locaux",
"description": "",
"type": "local_group"
},
{
"name": "Carte des groupes locaux se réunissant régulièrement",
"url": "https://umap.openstreetmap.fr/fr/map/groupes-locaux-openstreetmap_152488",
"description": "",
"type": "local_group"
}
],
"working_groups": [
{
"name": "Que venir faire au sein de l'association ?",
"url": "https://forum.openstreetmap.fr/t/que-venir-faire-au-sein-de-lassociation/15454",
"description": "",
"category": "Général",
"type": "working_group"
},
{
"name": "GT Inclusivité",
"url": "https://wiki.openstreetmap.org/wiki/France/OSM-FR/Groupes_de_travail#GT_Inclusivité",
"description": "",
"category": "Général",
"type": "working_group"
},
{
"name": "GT Technique",
"url": "https://wiki.openstreetmap.org/wiki/France/OSM-FR/Groupes_de_travail#GT_Technique",
"description": "",
"category": "Général",
"type": "working_group"
},
{
"name": "GT Communication externe",
"url": "https://wiki.openstreetmap.org/wiki/France/OSM-FR/Groupes_de_travail#GT_Communication",
"description": "",
"category": "Général",
"type": "working_group"
},
{
"name": "GT Animation de la communauté",
"url": "https://wiki.openstreetmap.org/wiki/France/OSM-FR/Groupes_de_travail#GT_Animation_de_la_communauté",
"description": "",
"category": "Général",
"type": "working_group"
},
{
"name": "GT Communautés locales",
"url": "https://wiki.openstreetmap.org/wiki/France/OSM-FR/Groupes_de_travail#GT_Communautés_locales",
"description": "",
"category": "Général",
"type": "working_group"
},
{
"name": "GT International",
"url": "https://wiki.openstreetmap.org/wiki/France/OSM-FR/Groupes_de_travail#GT_International",
"description": "",
"category": "Général",
"type": "working_group"
},
{
"name": "GT Gestion et comptabilité",
"url": "https://wiki.openstreetmap.org/wiki/France/OSM-FR/Groupes_de_travail#GT_Gestion_et_comptabilité",
"description": "",
"category": "Général",
"type": "working_group"
},
{
"name": "GT Soutiens",
"url": "https://wiki.openstreetmap.org/wiki/France/OSM-FR/Groupes_de_travail#GT_Soutiens",
"description": "",
"category": "Général",
"type": "working_group"
},
{
"name": "GT Conférence SotM-FR",
"url": "https://wiki.openstreetmap.org/wiki/France/OSM-FR/Groupes_de_travail#GT_Conférence_SotM-FR",
"description": "",
"category": "Général",
"type": "working_group"
},
{
"name": "Groupes spéciaux",
"url": "https://wiki.openstreetmap.org/wiki/France/OSM-FR/Groupes_de_travail#Groupes_spéciaux",
"description": "",
"category": "Général",
"type": "working_group"
},
{
"name": "Groupes projets et thématiques",
"url": "https://wiki.openstreetmap.org/wiki/France/OSM-FR/Groupes_de_travail#Groupes_projets_et_thématiques",
"description": "",
"category": "Général",
"type": "working_group"
}
],
"umap_url": "https://umap.openstreetmap.fr/fr/map/groupes-locaux-openstreetmap_152488"
}

File diff suppressed because it is too large Load diff

View file

@ -5,75 +5,302 @@ namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpFoundation\Request;
class WikiController extends AbstractController
{
#[Route('/admin/wiki/missing-translations', name: 'app_admin_wiki_missing_translations')]
#[Route('/wiki/missing-translations', name: 'app_admin_wiki_missing_translations')]
public function missingTranslations(): Response
{
$csvFile = $this->getParameter('kernel.project_dir') . '/wiki_compare/wiki_pages.csv';
$untranslatedFile = $this->getParameter('kernel.project_dir') . '/wiki_compare/untranslated_french_pages.json';
if (!file_exists($csvFile)) {
$this->addFlash('error', 'Le fichier wiki_pages.csv n\'existe pas.');
return $this->redirectToRoute('app_admin_wiki');
// Initialize arrays
$untranslatedPages = [];
$lastUpdated = null;
// Check if the untranslated pages file exists and load it
if (file_exists($untranslatedFile)) {
$untranslatedData = json_decode(file_get_contents($untranslatedFile), true);
if (isset($untranslatedData['untranslated_pages']) && is_array($untranslatedData['untranslated_pages'])) {
$untranslatedPages = $untranslatedData['untranslated_pages'];
$lastUpdated = isset($untranslatedData['last_updated']) ? $untranslatedData['last_updated'] : null;
}
$csvData = array_map('str_getcsv', file($csvFile));
$headers = array_shift($csvData);
// Check if the data is older than 1 hour
if ($lastUpdated) {
$lastUpdatedTime = new \DateTime($lastUpdated);
$now = new \DateTime();
$diff = $now->diff($lastUpdatedTime);
$wikiPages = [];
// Process CSV data
foreach ($csvData as $row) {
$page = array_combine($headers, $row);
$wikiPages[$page['key']][$page['language']] = $page;
}
// Find French pages without English translations
$frenchOnlyPages = [];
foreach ($wikiPages as $key => $languages) {
if (isset($languages['fr']) && !isset($languages['en'])) {
$frenchOnlyPages[$key] = $languages['fr'];
// If older than 1 hour, refresh the data
if ($diff->h >= 1 || $diff->days > 0) {
$this->refreshUntranslatedPagesData();
return $this->redirectToRoute('app_admin_wiki_missing_translations');
}
}
} else {
// If the file doesn't exist, try to create it by running the script
$this->refreshUntranslatedPagesData();
// Sort by key
ksort($frenchOnlyPages);
// Check if the file was created
if (file_exists($untranslatedFile)) {
return $this->redirectToRoute('app_admin_wiki_missing_translations');
} else {
$this->addFlash('error', 'Impossible de générer le fichier des pages sans traduction.');
}
}
return $this->render('admin/wiki_missing_translations.html.twig', [
'french_only_pages' => $frenchOnlyPages
'untranslated_pages' => $untranslatedPages,
'last_updated' => $lastUpdated
]);
}
#[Route('/admin/wiki/suspicious-deletions', name: 'app_admin_wiki_suspicious_deletions')]
/**
* Refresh the untranslated pages data by running the find_untranslated_french_pages.py script
*/
private function refreshUntranslatedPagesData(): void
{
try {
$scriptPath = $this->getParameter('kernel.project_dir') . '/wiki_compare/find_untranslated_french_pages.py';
if (file_exists($scriptPath)) {
exec('python3 ' . $scriptPath . ' --force 2>&1', $output, $returnCode);
if ($returnCode !== 0) {
$this->addFlash('warning', 'Impossible de mettre à jour les pages sans traduction. Erreur: ' . implode("\n", $output));
}
} else {
$this->addFlash('error', 'Le script find_untranslated_french_pages.py n\'existe pas.');
}
} catch (\Exception $e) {
$this->addFlash('error', 'Erreur lors de l\'exécution du script: ' . $e->getMessage());
}
}
#[Route('/wiki/pages-unavailable-in-french', name: 'app_admin_wiki_pages_unavailable_in_french')]
public function pagesUnavailableInFrench(): Response
{
$unavailablePagesFile = $this->getParameter('kernel.project_dir') . '/wiki_compare/pages_unavailable_in_french.json';
// Initialize arrays
$groupedPages = [];
$allPages = [];
$lastUpdated = null;
// Check if the unavailable pages file exists and load it
if (file_exists($unavailablePagesFile)) {
$unavailableData = json_decode(file_get_contents($unavailablePagesFile), true);
if (isset($unavailableData['grouped_pages']) && is_array($unavailableData['grouped_pages'])) {
$groupedPages = $unavailableData['grouped_pages'];
}
if (isset($unavailableData['all_pages']) && is_array($unavailableData['all_pages'])) {
$allPages = $unavailableData['all_pages'];
}
$lastUpdated = isset($unavailableData['last_updated']) ? $unavailableData['last_updated'] : null;
// Check if the data is older than 1 hour
if ($lastUpdated) {
$lastUpdatedTime = new \DateTime($lastUpdated);
$now = new \DateTime();
$diff = $now->diff($lastUpdatedTime);
// If older than 1 hour, refresh the data
if ($diff->h >= 1 || $diff->days > 0) {
$this->refreshPagesUnavailableInFrenchData();
return $this->redirectToRoute('app_admin_wiki_pages_unavailable_in_french');
}
}
} else {
// If the file doesn't exist, try to create it by running the script
$this->refreshPagesUnavailableInFrenchData();
// Check if the file was created
if (file_exists($unavailablePagesFile)) {
return $this->redirectToRoute('app_admin_wiki_pages_unavailable_in_french');
} else {
$this->addFlash('error', 'Impossible de générer le fichier des pages non disponibles en français.');
}
}
// Move English pages to the top of the list
$englishPages = $groupedPages['En'] ?? [];
unset($groupedPages['En']);
// Sort other language groups alphabetically
ksort($groupedPages);
// Reinsert English pages at the beginning
if (!empty($englishPages)) {
$groupedPages = ['En' => $englishPages] + $groupedPages;
}
return $this->render('admin/wiki_pages_unavailable_in_french.html.twig', [
'grouped_pages' => $groupedPages,
'all_pages' => $allPages,
'last_updated' => $lastUpdated
]);
}
/**
* Refresh the pages unavailable in French data by running the find_pages_unavailable_in_french.py script
*/
private function refreshPagesUnavailableInFrenchData(): void
{
try {
$scriptPath = $this->getParameter('kernel.project_dir') . '/wiki_compare/find_pages_unavailable_in_french.py';
if (file_exists($scriptPath)) {
exec('python3 ' . $scriptPath . ' --force 2>&1', $output, $returnCode);
if ($returnCode !== 0) {
$this->addFlash('warning', 'Impossible de mettre à jour les pages non disponibles en français. Erreur: ' . implode("\n", $output));
}
} else {
$this->addFlash('error', 'Le script find_pages_unavailable_in_french.py n\'existe pas.');
}
} catch (\Exception $e) {
$this->addFlash('error', 'Erreur lors de l\'exécution du script: ' . $e->getMessage());
}
}
#[Route('/wiki/osm-fr-groups', name: 'app_admin_wiki_osm_fr_groups')]
public function osmFrGroups(): Response
{
$groupsFile = $this->getParameter('kernel.project_dir') . '/wiki_compare/osm_fr_groups.json';
// Initialize arrays
$workingGroups = [];
$localGroups = [];
$umapUrl = 'https://umap.openstreetmap.fr/fr/map/groupes-locaux-openstreetmap_152488';
$lastUpdated = null;
// Check if the groups file exists and load it
if (file_exists($groupsFile)) {
$groupsData = json_decode(file_get_contents($groupsFile), true);
if (isset($groupsData['working_groups']) && is_array($groupsData['working_groups'])) {
$workingGroups = $groupsData['working_groups'];
}
if (isset($groupsData['local_groups']) && is_array($groupsData['local_groups'])) {
$localGroups = $groupsData['local_groups'];
}
$umapUrl = isset($groupsData['umap_url']) ? $groupsData['umap_url'] : 'https://umap.openstreetmap.fr/fr/map/groupes-locaux-openstreetmap_152488';
$lastUpdated = isset($groupsData['last_updated']) ? $groupsData['last_updated'] : null;
// Check if the data is older than 1 hour
if ($lastUpdated) {
$lastUpdatedTime = new \DateTime($lastUpdated);
$now = new \DateTime();
$diff = $now->diff($lastUpdatedTime);
// If older than 1 hour, refresh the data
// if ($diff->h >= 1 || $diff->days > 0) {
// $this->refreshOsmFrGroupsData();
// return $this->redirectToRoute('app_admin_wiki_osm_fr_groups');
// }
}
} else {
// If the file doesn't exist, try to create it by running the script
$this->refreshOsmFrGroupsData();
// Check if the file was created
if (file_exists($groupsFile)) {
// return $this->redirectToRoute('app_admin_wiki_osm_fr_groups');
} else {
$this->addFlash('error', 'Impossible de générer le fichier des groupes OSM-FR.');
}
}
// Group working groups by category
$groupedWorkingGroups = [];
foreach ($workingGroups as $group) {
$category = $group['category'] ?? 'Autres';
if (!isset($groupedWorkingGroups[$category])) {
$groupedWorkingGroups[$category] = [];
}
$groupedWorkingGroups[$category][] = $group;
}
// Sort categories alphabetically
ksort($groupedWorkingGroups);
return $this->render('admin/wiki_osm_fr_groups.html.twig', [
'working_groups' => $groupedWorkingGroups,
'local_groups' => $localGroups,
'umap_url' => $umapUrl,
'last_updated' => $lastUpdated
]);
}
/**
* Refresh the OSM-FR groups data by running the fetch_osm_fr_groups.py script
*/
private function refreshOsmFrGroupsData(): void
{
try {
$scriptPath = $this->getParameter('kernel.project_dir') . '/wiki_compare/fetch_osm_fr_groups.py';
if (file_exists($scriptPath)) {
exec('python3 ' . $scriptPath . ' --force 2>&1', $output, $returnCode);
if ($returnCode !== 0) {
$this->addFlash('warning', 'Impossible de mettre à jour les groupes OSM-FR. Erreur: ' . implode("\n", $output));
}
} else {
$this->addFlash('error', 'Le script fetch_osm_fr_groups.py n\'existe pas.');
}
} catch (\Exception $e) {
$this->addFlash('error', 'Erreur lors de l\'exécution du script: ' . $e->getMessage());
}
}
#[Route('/wiki/suspicious-deletions', name: 'app_admin_wiki_suspicious_deletions')]
public function suspiciousDeletions(): Response
{
$csvFile = $this->getParameter('kernel.project_dir') . '/wiki_compare/wiki_pages.csv';
$jsonFile = $this->getParameter('kernel.project_dir') . '/wiki_compare/outdated_pages.json';
$suspiciousDeletesFile = $this->getParameter('kernel.project_dir') . '/wiki_compare/suspicious_deletions.json';
$wordDiffFile = $this->getParameter('kernel.project_dir') . '/wiki_compare/outdated_pages.json';
if (!file_exists($csvFile) || !file_exists($jsonFile)) {
$this->addFlash('error', 'Les fichiers nécessaires n\'existent pas.');
return $this->redirectToRoute('app_admin_wiki');
}
// Load CSV data
$csvData = array_map('str_getcsv', file($csvFile));
$headers = array_shift($csvData);
// Process CSV data to find French pages
$frenchPages = [];
foreach ($csvData as $row) {
$page = array_combine($headers, $row);
if ($page['language'] === 'fr') {
$frenchPages[$page['key']] = $page;
}
}
// Load JSON data
$jsonData = json_decode(file_get_contents($jsonFile), true);
// Find pages with suspicious deletions
// Initialize arrays
$suspiciousPages = [];
$wordDiffPages = [];
// Check if the suspicious deletions file exists and load it
if (file_exists($suspiciousDeletesFile)) {
$suspiciousData = json_decode(file_get_contents($suspiciousDeletesFile), true);
if (isset($suspiciousData['deletions']) && is_array($suspiciousData['deletions'])) {
$suspiciousPages = $suspiciousData['deletions'];
$lastUpdated = isset($suspiciousData['last_updated']) ? $suspiciousData['last_updated'] : null;
}
} else {
// If the file doesn't exist, try to create it by running the script
try {
$scriptPath = $this->getParameter('kernel.project_dir') . '/wiki_compare/detect_suspicious_deletions.py';
if (file_exists($scriptPath)) {
exec('python3 ' . $scriptPath . ' 2>&1', $output, $returnCode);
if ($returnCode === 0 && file_exists($suspiciousDeletesFile)) {
$suspiciousData = json_decode(file_get_contents($suspiciousDeletesFile), true);
if (isset($suspiciousData['deletions']) && is_array($suspiciousData['deletions'])) {
$suspiciousPages = $suspiciousData['deletions'];
$lastUpdated = isset($suspiciousData['last_updated']) ? $suspiciousData['last_updated'] : null;
}
} else {
$this->addFlash('warning', 'Impossible de générer le fichier de suppressions suspectes. Erreur: ' . implode("\n", $output));
}
}
} catch (\Exception $e) {
$this->addFlash('error', 'Erreur lors de l\'exécution du script: ' . $e->getMessage());
}
}
// 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) {
if (isset($page['fr_page']) && isset($page['en_page'])) {
// Calculate deletion percentage
@ -84,77 +311,125 @@ class WikiController extends AbstractController
// If English has more words and the difference is significant (>30%)
if ($wordDiff > 0 && $frWordCount > 0 && ($wordDiff / $enWordCount) > 0.3) {
$page['deletion_percentage'] = round(($wordDiff / $enWordCount) * 100, 2);
$suspiciousPages[] = $page;
$wordDiffPages[] = $page;
}
}
}
// Sort by deletion percentage (highest first)
usort($suspiciousPages, function($a, $b) {
usort($wordDiffPages, function ($a, $b) {
return $b['deletion_percentage'] <=> $a['deletion_percentage'];
});
}
return $this->render('admin/wiki_suspicious_deletions.html.twig', [
'suspicious_pages' => $suspiciousPages
'suspicious_pages' => $wordDiffPages,
'recent_deletions' => $suspiciousPages,
'last_updated' => $lastUpdated ?? null
]);
}
#[Route('/admin/wiki/tag-proposals', name: 'app_admin_wiki_tag_proposals')]
#[Route('/wiki/tag-proposals', name: 'app_admin_wiki_tag_proposals')]
public function tagProposals(): Response
{
// URL of the OSM wiki page that lists tag proposals
$url = 'https://wiki.openstreetmap.org/wiki/Proposed_features';
$proposalsFile = $this->getParameter('kernel.project_dir') . '/wiki_compare/proposals.json';
try {
$html = file_get_contents($url);
// Initialize arrays
$votingProposals = [];
$recentProposals = [];
$lastUpdated = null;
if ($html === false) {
throw new \Exception('Failed to fetch the tag proposals page');
// Check if the proposals file exists and load it
if (file_exists($proposalsFile)) {
$proposalsData = json_decode(file_get_contents($proposalsFile), true);
if (isset($proposalsData['voting_proposals']) && is_array($proposalsData['voting_proposals'])) {
$votingProposals = $proposalsData['voting_proposals'];
}
// Create a DOM parser
$dom = new \DOMDocument();
@$dom->loadHTML($html);
$xpath = new \DOMXPath($dom);
if (isset($proposalsData['recent_proposals']) && is_array($proposalsData['recent_proposals'])) {
$recentProposals = $proposalsData['recent_proposals'];
}
// Find the table with proposals
$tables = $xpath->query("//table[contains(@class, 'wikitable')]");
$proposals = [];
$lastUpdated = isset($proposalsData['last_updated']) ? $proposalsData['last_updated'] : null;
if ($tables->length > 0) {
// Get the first table which contains the active proposals
$table = $tables->item(0);
$rows = $xpath->query(".//tr", $table);
// Check if the data is older than 1 hour
if ($lastUpdated) {
$lastUpdatedTime = new \DateTime($lastUpdated);
$now = new \DateTime();
$diff = $now->diff($lastUpdatedTime);
// Skip the header row
for ($i = 1; $i < $rows->length; $i++) {
$row = $rows->item($i);
$cells = $xpath->query(".//td", $row);
// If older than 1 hour, refresh the data
if ($diff->h >= 1 || $diff->days > 0) {
$this->refreshProposalsData();
return $this->redirectToRoute('app_admin_wiki_tag_proposals');
}
}
} else {
// If the file doesn't exist, try to create it by running the script
$this->refreshProposalsData();
if ($cells->length >= 4) {
$proposal = [
'feature' => $xpath->query(".//a", $cells->item(0))->item(0)->textContent,
'url' => 'https://wiki.openstreetmap.org' . $xpath->query(".//a", $cells->item(0))->item(0)->getAttribute('href'),
'description' => $cells->item(1)->textContent,
'proposer' => $cells->item(2)->textContent,
'status' => $cells->item(3)->textContent,
// Check if the file was created
if (file_exists($proposalsFile)) {
return $this->redirectToRoute('app_admin_wiki_tag_proposals');
} else {
$this->addFlash('error', 'Impossible de générer le fichier de propositions.');
}
}
// Format the proposals for the template
$formattedProposals = [];
foreach ($votingProposals as $proposal) {
$formattedProposals[] = [
'feature' => $proposal['title'],
'url' => $proposal['url'],
'description' => 'Proposition en cours de vote',
'proposer' => '',
'status' => 'Voting',
'type' => 'voting'
];
}
$proposals[] = $proposal;
}
}
foreach ($recentProposals as $proposal) {
$formattedProposals[] = [
'feature' => $proposal['title'],
'url' => $proposal['url'],
'description' => 'Dernière modification: ' . $proposal['last_modified'],
'proposer' => $proposal['modified_by'],
'status' => 'Draft',
'type' => 'recent'
];
}
return $this->render('admin/wiki_tag_proposals.html.twig', [
'proposals' => $proposals
'proposals' => $formattedProposals,
'last_updated' => $lastUpdated
]);
}
/**
* Refresh the proposals data by running the fetch_proposals.py script
*/
private function refreshProposalsData(): void
{
try {
$scriptPath = $this->getParameter('kernel.project_dir') . '/wiki_compare/fetch_proposals.py';
if (file_exists($scriptPath)) {
exec('python3 ' . $scriptPath . ' --force 2>&1', $output, $returnCode);
if ($returnCode !== 0) {
$this->addFlash('warning', 'Impossible de mettre à jour les propositions. Erreur: ' . implode("\n", $output));
}
} else {
$this->addFlash('error', 'Le script fetch_proposals.py n\'existe pas.');
}
} catch (\Exception $e) {
$this->addFlash('error', 'Erreur lors de la récupération des propositions de tags : ' . $e->getMessage());
return $this->redirectToRoute('app_admin_wiki');
$this->addFlash('error', 'Erreur lors de l\'exécution du script: ' . $e->getMessage());
}
}
#[Route('/admin/wiki/random-suggestion', name: 'app_admin_wiki_random_suggestion')]
#[Route('/wiki/random-suggestion', name: 'app_admin_wiki_random_suggestion')]
public function randomSuggestion(): Response
{
$jsonFile = $this->getParameter('kernel.project_dir') . '/wiki_compare/outdated_pages.json';
@ -179,7 +454,7 @@ class WikiController extends AbstractController
]);
}
#[Route('/admin/wiki', name: 'app_admin_wiki')]
#[Route('/wiki', name: 'app_admin_wiki')]
public function index(): Response
{
$csvFile = $this->getParameter('kernel.project_dir') . '/wiki_compare/wiki_pages.csv';
@ -266,7 +541,7 @@ class WikiController extends AbstractController
}
// Sort wiki pages by staleness score (descending)
uasort($wikiPages, function($a, $b) {
uasort($wikiPages, function ($a, $b) {
$scoreA = isset($a['en']) && isset($a['fr']) && isset($a['en']['staleness_score']) ? (float)$a['en']['staleness_score'] : 0;
$scoreB = isset($b['en']) && isset($b['fr']) && isset($b['en']['staleness_score']) ? (float)$b['en']['staleness_score'] : 0;
return $scoreB <=> $scoreA;
@ -279,7 +554,7 @@ class WikiController extends AbstractController
]);
}
#[Route('/admin/wiki/compare/{key}', name: 'app_admin_wiki_compare')]
#[Route('/wiki/compare/{key}', name: 'app_admin_wiki_compare')]
public function compare(string $key): Response
{
$csvFile = $this->getParameter('kernel.project_dir') . '/wiki_compare/wiki_pages.csv';
@ -323,10 +598,77 @@ class WikiController extends AbstractController
foreach ($jsonData as $page) {
if ($page['key'] === $key) {
$mediaComparison = $page['media_comparison'] ?? null;
// Deduplicate images by URL in the controller and filter out images that appear in both languages
if ($mediaComparison) {
// Get all image URLs from both languages
$enOnlyImages = $mediaComparison['en_only'] ?? [];
$frOnlyImages = $mediaComparison['fr_only'] ?? [];
$commonImages = $mediaComparison['common'] ?? [];
// Extract all URLs from French images
$frImageUrls = [];
foreach ($frOnlyImages as $media) {
$frImageUrls[] = $media['src'];
}
// Also add URLs from common images (French side)
foreach ($commonImages as $commonMedia) {
if (isset($commonMedia['fr']['src'])) {
$frImageUrls[] = $commonMedia['fr']['src'];
}
}
// Extract all URLs from English images
$enImageUrls = [];
foreach ($enOnlyImages as $media) {
$enImageUrls[] = $media['src'];
}
// Also add URLs from common images (English side)
foreach ($commonImages as $commonMedia) {
if (isset($commonMedia['en']['src'])) {
$enImageUrls[] = $commonMedia['en']['src'];
}
}
// Process English-only images - deduplicate and filter out those that appear in French
$enUniqueImages = [];
$enProcessedUrls = [];
foreach ($enOnlyImages as $media) {
// Skip if this URL is already processed or if it appears in French images
if (!in_array($media['src'], $enProcessedUrls) && !in_array($media['src'], $frImageUrls)) {
$enProcessedUrls[] = $media['src'];
$enUniqueImages[] = $media;
}
}
// Process French-only images - deduplicate and filter out those that appear in English
$frUniqueImages = [];
$frProcessedUrls = [];
foreach ($frOnlyImages as $media) {
// Skip if this URL is already processed or if it appears in English images
if (!in_array($media['src'], $frProcessedUrls) && !in_array($media['src'], $enImageUrls)) {
$frProcessedUrls[] = $media['src'];
$frUniqueImages[] = $media;
}
}
// Replace the arrays with deduplicated and filtered versions
$mediaComparison['en_only'] = $enUniqueImages;
$mediaComparison['fr_only'] = $frUniqueImages;
$mediaComparison['en_only_count'] = count($enOnlyImages);
$mediaComparison['fr_only_count'] = count($frOnlyImages);
}
$detailedComparison = [
'section_comparison' => $page['section_comparison'] ?? null,
'link_comparison' => $page['link_comparison'] ?? null,
'media_comparison' => $page['media_comparison'] ?? null
'media_comparison' => $mediaComparison,
'category_comparison' => $page['category_comparison'] ?? null
];
$mediaDiff = $page['media_diff'] ?? 0;
@ -356,9 +698,9 @@ class WikiController extends AbstractController
// Calculate score components
$dateComponent = abs($dateDiff) * 0.2;
$wordComponent = abs($wordDiff) / 100 * 0.5;
$wordComponent = (abs($wordDiff) / 100) * 0.5;
$sectionComponent = abs($sectionDiff) * 0.15;
$linkComponent = abs($linkDiff) / 10 * 0.15;
$linkComponent = (abs($linkDiff) / 10) * 0.15;
$scoreComponents = [
'date' => [
@ -389,7 +731,7 @@ class WikiController extends AbstractController
// Add media component if available
if (isset($enPage['media_count']) && isset($frPage['media_count'])) {
$mediaComponent = abs($mediaDiff) / 5 * 0.1;
$mediaComponent = (abs($mediaDiff) / 5) * 0.1;
$scoreComponents['media'] = [
'value' => $mediaDiff,
'weight' => 0.1,

28
src/Twig/AppExtension.php Normal file
View file

@ -0,0 +1,28 @@
<?php
namespace App\Twig;
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;
class AppExtension extends AbstractExtension
{
public function getFilters()
{
return [
new TwigFilter('repeat', [$this, 'repeatFilter']),
];
}
/**
* Repeats a string a specified number of times
*
* @param string $string The string to repeat
* @param int $times The number of times to repeat the string
* @return string The repeated string
*/
public function repeatFilter($string, $times)
{
return str_repeat($string, $times);
}
}

View file

@ -31,6 +31,16 @@
<i class="bi bi-translate"></i> Pages sans traduction
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if app.request.get('_route') == 'app_admin_wiki_pages_unavailable_in_french' %}active{% endif %}" href="{{ path('app_admin_wiki_pages_unavailable_in_french') }}">
<i class="bi bi-globe"></i> Pages à traduire en français
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if app.request.get('_route') == 'app_admin_wiki_osm_fr_groups' %}active{% endif %}" href="{{ path('app_admin_wiki_osm_fr_groups') }}">
<i class="bi bi-people"></i> Groupes OSM-FR
</a>
</li>
</ul>
</div>
</div>

View file

@ -3,7 +3,7 @@
{% block title %}Pages Wiki OSM{% endblock %}
{% block body %}
<div class="container mt-4">
<div class="container mt-4">
{% include 'admin/_wiki_navigation.html.twig' %}
<h1>Pages Wiki OpenStreetMap</h1>
@ -92,13 +92,16 @@
</td>
<td class="text-center">
<div class="btn-group" role="group">
<a href="{{ languages['en'].url }}" target="_blank" class="btn btn-sm btn-outline-primary" title="Version anglaise">
<a href="{{ languages['en'].url }}" target="_blank"
class="btn btn-sm btn-outline-primary" title="Version anglaise">
<i class="bi bi-translate"></i> EN
</a>
<a href="{{ languages['fr'].url }}" target="_blank" class="btn btn-sm btn-outline-info" title="Version française">
<a href="{{ languages['fr'].url }}" target="_blank"
class="btn btn-sm btn-outline-info" title="Version française">
<i class="bi bi-translate"></i> FR
</a>
<a href="{{ path('app_admin_wiki_compare', {'key': key}) }}" class="btn btn-sm btn-outline-secondary" title="Comparer les versions">
<a href="{{ path('app_admin_wiki_compare', {'key': key}) }}"
class="btn btn-sm btn-outline-secondary" title="Comparer les versions">
<i class="bi bi-arrows-angle-expand"></i> Comparer
</a>
</div>
@ -143,12 +146,20 @@
</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="Version anglaise">
<i class="bi bi-translate"></i> EN
<a href="{{ page.url }}" target="_blank"
class="btn btn-sm btn-outline-info" title="Version anglaise">
<i class="bi bi-flag-fill"></i> EN
</a>
<a href="{{ path('app_admin_wiki_compare', {'key': key}) }}" class="btn btn-sm btn-outline-secondary" title="Voir les détails et créer la page française">
<i class="bi bi-arrows-angle-expand"></i> Comparer
<a href="https://wiki.openstreetmap.org/w/index.php?title=FR:{{ key }}&action=edit"
target="_blank"
class="btn btn-sm btn-outline-primary" title="Version anglaise">
<i class="bi bi-translate"></i> créer FR
</a>
{# <a href="{{ path('app_admin_wiki_compare', {'key': key}) }}" #}
{# class="btn btn-sm btn-outline-secondary" #}
{# title="Voir les détails et créer la page française"> #}
{# <i class="bi bi-arrows-angle-expand"></i> Comparer #}
{# </a> #}
</div>
</td>
</tr>
@ -160,11 +171,12 @@
</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.
le score de fraîcheur prend en compte d'avantage la différence entre le nombre de mots que l'ancienneté de
modification.
On compte aussi le nombre de sections et de liens.
</p>
<div class="mt-3">
</div>
</div>
</div>
{% endblock %}

File diff suppressed because it is too large Load diff

View file

@ -9,38 +9,38 @@
<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>
{% if last_updated %}
<div class="alert alert-info">
<i class="bi bi-info-circle"></i> Dernière mise à jour : {{ last_updated|date('d/m/Y H:i') }}
</div>
{% endif %}
<div class="card mb-4">
<div class="card-header">
<h2>Pages françaises uniquement</h2>
</div>
<div class="card-body">
{% if french_only_pages|length > 0 %}
{% if untranslated_pages|length > 0 %}
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead class="thead-dark">
<tr>
<th>Titre</th>
<th>Clé</th>
<th>Sections</th>
<th>Mots</th>
<th>Liens</th>
<th>Dernière modification</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for key, page in french_only_pages %}
{% for page in untranslated_pages %}
<tr>
<td><strong>{{ key }}</strong></td>
<td>{{ page.sections }}</td>
<td>{{ page.word_count }}</td>
<td>{{ page.link_count }}</td>
<td>{{ page.last_modified }}</td>
<td><strong>{{ page.title }}</strong></td>
<td>{{ page.key }}</td>
<td>
<div class="btn-group" role="group">
<a href="{{ page.url }}" target="_blank" class="btn btn-sm btn-outline-info" title="Version française">
<i class="bi bi-translate"></i> FR
</a>
<a href="https://wiki.openstreetmap.org/wiki/Key:{{ key }}" target="_blank" class="btn btn-sm btn-success" title="Créer la version anglaise">
<a href="https://wiki.openstreetmap.org/wiki/{{ page.key }}" target="_blank" class="btn btn-sm btn-success" title="Créer la version anglaise">
<i class="bi bi-plus-circle"></i> Créer EN
</a>
</div>

View file

@ -0,0 +1,161 @@
{% extends 'base.html.twig' %}
{% block title %}Groupes OSM-FR{% endblock %}
{% 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>
{% if last_updated %}
<div class="alert alert-info">
<i class="bi bi-info-circle"></i> Dernière mise à jour : {{ last_updated|date('d/m/Y H:i') }}
</div>
{% endif %}
<!-- Carte uMap des groupes locaux -->
<div class="card mb-4">
<div class="card-header bg-primary text-white">
<h2>
<a href="{{ umap_url }}">
Carte des groupes locaux
</a>
</h2>
</div>
<div class="card-body">
<div class="ratio ratio-16x9">
<iframe src="{{ umap_url }}" frameborder="0"></iframe>
</div>
<div class="mt-3">
<a href="{{ umap_url }}" target="_blank" class="btn btn-outline-primary">
<i class="bi bi-box-arrow-up-right"></i> Voir la carte en plein écran
</a>
</div>
</div>
</div>
<!-- Groupes de travail -->
<div class="card mb-4">
<div class="card-header bg-success text-white">
<h2>Groupes de travail</h2>
</div>
<div class="card-body">
{% if working_groups|length > 0 %}
<div class="accordion" id="workingGroupsAccordion">
{% for category, groups in working_groups %}
<div class="accordion-item">
<h2 class="accordion-header" id="heading{{ loop.index }}">
<button class="accordion-button {% if not loop.first %}collapsed{% endif %}"
type="button" data-bs-toggle="collapse"
data-bs-target="#collapse{{ loop.index }}"
aria-expanded="{{ loop.first ? 'true' : 'false' }}"
aria-controls="collapse{{ loop.index }}">
{{ category }} ({{ groups|length }})
</button>
</h2>
<div id="collapse{{ loop.index }}"
class="accordion-collapse collapse {% if loop.first %}show{% endif %}"
aria-labelledby="heading{{ loop.index }}" data-bs-parent="#workingGroupsAccordion">
<div class="accordion-body">
<div class="list-group">
{% for group in groups %}
<a href="{{ group.url }}" target="_blank"
class="list-group-item list-group-item-action">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">{{ group.name }}</h5>
</div>
{% if group.description %}
<p class="mb-1">{{ group.description }}</p>
{% endif %}
<small class="text-muted">
<i class="bi bi-box-arrow-up-right"></i> Voir sur le wiki
</small>
</a>
{% endfor %}
</div>
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="alert alert-info">
<p><i class="bi bi-info-circle"></i> Aucun groupe de travail n'a été trouvé.</p>
</div>
{% endif %}
</div>
</div>
<!-- Groupes locaux -->
<div class="card mb-4">
<div class="card-header bg-info text-white">
<h2>Groupes locaux</h2>
</div>
<div class="card-body">
{% if local_groups|length > 0 %}
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4">
{% for group in local_groups %}
<div class="col">
<div class="card h-100">
<div class="card-body">
<h5 class="card-title">{{ group.name }}</h5>
{% if group.description %}
<p class="card-text">{{ group.description }}</p>
{% endif %}
</div>
<div class="card-footer">
<a href="{{ group.url }}" target="_blank"
class="btn btn-sm btn-outline-primary">
<i class="bi bi-box-arrow-up-right"></i> Voir sur le wiki
</a>
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="alert alert-info">
<p><i class="bi bi-info-circle"></i> Aucun groupe local n'a été trouvé.</p>
</div>
{% endif %}
</div>
</div>
<div class="card mb-4">
<div class="card-header">
<h2>À propos des groupes OSM-FR</h2>
</div>
<div class="card-body">
<h5>Groupes de travail</h5>
<p>Les groupes de travail sont des équipes thématiques qui se concentrent sur des aspects spécifiques
d'OpenStreetMap en France. Ils permettent de coordonner les efforts sur des sujets particuliers
comme l'import de données, la cartographie des transports, etc.</p>
<h5>Groupes locaux</h5>
<p>Les groupes locaux sont des communautés géographiques de contributeurs OpenStreetMap. Ils organisent
des rencontres, des ateliers de cartographie et d'autres événements pour promouvoir OSM dans leur
région.</p>
<div class="d-grid gap-2 col-md-6 mx-auto mt-3">
<a href="https://wiki.openstreetmap.org/wiki/France/OSM-FR/Groupes_de_travail" target="_blank"
class="btn btn-outline-success">
<i class="bi bi-box-arrow-up-right"></i> Voir tous les groupes de travail sur le wiki
</a>
<a href="https://wiki.openstreetmap.org/wiki/France/OSM-FR#Groupes_locaux" target="_blank"
class="btn btn-outline-info">
<i class="bi bi-box-arrow-up-right"></i> Voir tous les groupes locaux sur le wiki
</a>
</div>
</div>
</div>
<div class="mt-3">
<a href="{{ path('app_admin_wiki') }}" class="btn btn-secondary">
<i class="bi bi-arrow-left"></i> Retour à la liste des pages wiki
</a>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,130 @@
{% extends 'base.html.twig' %}
{% block title %}Pages Wiki non disponibles en français{% endblock %}
{% 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>
{% if last_updated %}
<div class="alert alert-info">
<i class="bi bi-info-circle"></i> Dernière mise à jour : {{ last_updated|date('d/m/Y H:i') }}
</div>
{% endif %}
<div class="card mb-4">
<div class="card-header">
<h2>Statistiques</h2>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-4">
<div class="card text-white bg-primary mb-3">
<div class="card-body">
<h5 class="card-title">Total des pages</h5>
<p class="card-text display-4">{{ all_pages|length }}</p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card text-white bg-success mb-3">
<div class="card-body">
<h5 class="card-title">Pages en anglais</h5>
<p class="card-text display-4">{{ grouped_pages['En']|default([])|length }}</p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card text-white bg-info mb-3">
<div class="card-body">
<h5 class="card-title">Langues différentes</h5>
<p class="card-text display-4">{{ grouped_pages|length }}</p>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Pages groupées par langue -->
<div class="accordion" id="languageAccordion">
{% for lang_prefix, pages in grouped_pages %}
<div class="accordion-item">
<h2 class="accordion-header" id="heading{{ lang_prefix }}">
<button class="accordion-button {% if lang_prefix != 'En' %}collapsed{% endif %}" type="button" data-bs-toggle="collapse" data-bs-target="#collapse{{ lang_prefix }}" aria-expanded="{{ lang_prefix == 'En' ? 'true' : 'false' }}" aria-controls="collapse{{ lang_prefix }}">
{% if lang_prefix == 'En' %}
<strong class="text-success">Pages en anglais ({{ pages|length }})</strong>
{% elseif lang_prefix == 'Other' %}
<strong>Autres pages ({{ pages|length }})</strong>
{% else %}
<strong>Pages en {{ lang_prefix }} ({{ pages|length }})</strong>
{% endif %}
</button>
</h2>
<div id="collapse{{ lang_prefix }}" class="accordion-collapse collapse {% if lang_prefix == 'En' %}show{% endif %}" aria-labelledby="heading{{ lang_prefix }}" data-bs-parent="#languageAccordion">
<div class="accordion-body">
<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 pages %}
<tr>
<td>
<strong>{{ page.title }}</strong>
{% if page.is_english %}
<span class="badge bg-success">Priorité</span>
{% endif %}
</td>
<td>
<div class="btn-group" role="group">
<a href="{{ page.url }}" target="_blank" class="btn btn-sm btn-outline-primary" title="Voir la page originale">
<i class="bi bi-eye"></i> Voir
</a>
{% set fr_url = page.url|replace({'/wiki/': '/wiki/FR:'}) %}
<a href="{{ fr_url }}" target="_blank" class="btn btn-sm btn-success" title="Créer la traduction française">
<i class="bi bi-plus-circle"></i> Traduire
</a>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
<div class="card mt-4 mb-4">
<div class="card-header">
<h2>À propos des pages non disponibles en français</h2>
</div>
<div class="card-body">
<p>Ces pages sont des contenus du wiki OpenStreetMap qui n'ont pas encore été traduits en français.</p>
<p>Contribuer à la traduction de ces pages permet de :</p>
<ul>
<li>Rendre la documentation OSM plus accessible aux contributeurs francophones</li>
<li>Améliorer la qualité des contributions en français</li>
<li>Faciliter l'apprentissage et l'utilisation d'OpenStreetMap pour les nouveaux utilisateurs</li>
</ul>
<p><strong>Priorité aux pages anglaises :</strong> Les pages commençant par "En:" sont prioritaires car l'anglais est la langue principale du wiki OSM.</p>
</div>
</div>
<div class="mt-3">
<a href="{{ path('app_admin_wiki') }}" class="btn btn-secondary">
<i class="bi bi-arrow-left"></i> Retour à la liste des pages wiki
</a>
</div>
</div>
{% endblock %}

View file

@ -3,56 +3,66 @@
{% block title %}Pages Wiki avec suppressions suspectes{% endblock %}
{% block body %}
<div class="container mt-4">
<div class="container mt-4">
{% include 'admin/_wiki_navigation.html.twig' %}
<h1>Pages Wiki avec suppressions suspectes</h1>
<p class="lead">Pages françaises du wiki OSM qui ont des suppressions suspectes, avec un grand pourcentage de suppression par rapport à la version anglaise.</p>
<div class="alert alert-info">
<i class="bi bi-info-circle"></i>
Cette page présente deux types de suppressions suspectes :
<ul>
<li><strong>Suppressions récentes</strong> : Détectées en temps réel dans les changements récents du
wiki (suppressions > 20 caractères)
</li>
<li><strong>Différences de contenu</strong> : Pages françaises contenant significativement moins de mots
que leurs équivalents anglais
</li>
</ul>
</div>
<!-- Suppressions récentes -->
<div class="card mb-4">
<div class="card-header">
<h2>Pages avec suppressions suspectes</h2>
<div class="card-header bg-danger text-white">
<h2>Suppressions récentes suspectes</h2>
{% if last_updated %}
<small>Dernière mise à jour : {{ last_updated|date('d/m/Y H:i') }}</small>
{% endif %}
</div>
<div class="card-body">
{% if suspicious_pages|length > 0 %}
{% if recent_deletions is defined and recent_deletions|length > 0 %}
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead class="thead-dark">
<tr>
<th>Clé</th>
<th>Mots (EN)</th>
<th>Mots (FR)</th>
<th>Différence</th>
<th>% Suppression</th>
<th>Page</th>
<th>Suppression</th>
<th>Date</th>
<th>Utilisateur</th>
<th>Commentaire</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for page in suspicious_pages %}
{% for deletion in recent_deletions %}
<tr>
<td><strong>{{ page.key }}</strong></td>
<td>{{ page.en_page.word_count }}</td>
<td>{{ page.fr_page.word_count }}</td>
<td>{{ page.en_page.word_count - page.fr_page.word_count }}</td>
<td><strong>{{ deletion.page_title }}</strong></td>
<td>
{% if page.deletion_percentage > 70 %}
<span class="badge bg-danger">{{ page.deletion_percentage }}%</span>
{% elseif page.deletion_percentage > 50 %}
<span class="badge bg-warning text-dark">{{ page.deletion_percentage }}%</span>
{% else %}
<span class="badge bg-info">{{ page.deletion_percentage }}%</span>
{% endif %}
<span class="badge bg-danger">{{ deletion.deletion_size }} caractères</span>
</td>
<td>{{ deletion.timestamp }}</td>
<td>{{ deletion.user }}</td>
<td>{{ deletion.comment }}</td>
<td>
<div class="btn-group" role="group">
<a href="{{ page.en_page.url }}" target="_blank" class="btn btn-sm btn-outline-primary" title="Version anglaise">
<i class="bi bi-translate"></i> EN
<a href="{{ deletion.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.fr_page.url }}" target="_blank" class="btn btn-sm btn-outline-info" title="Version française">
<i class="bi bi-translate"></i> FR
</a>
<a href="{{ path('app_admin_wiki_compare', {'key': page.key}) }}" class="btn btn-sm btn-outline-secondary" title="Comparer les versions">
<i class="bi bi-arrows-angle-expand"></i> Comparer
<a href="https://wiki.openstreetmap.org/w/index.php?title={{ deletion.page_title|url_encode }}&action=history"
target="_blank" class="btn btn-sm btn-outline-secondary"
title="Historique">
<i class="bi bi-clock-history"></i> Historique
</a>
</div>
</td>
@ -61,25 +71,42 @@
</tbody>
</table>
</div>
<div class="mt-3">
<a href="https://wiki.openstreetmap.org/w/index.php?hidebots=1&hidenewpages=1&hidecategorization=1&hideWikibase=1&hidelog=1&hidenewuserlog=1&namespace=202&limit=250&days=30&enhanced=1&title=Special:RecentChanges&urlversion=2"
target="_blank" class="btn btn-outline-primary">
<i class="bi bi-arrow-right-circle"></i> Voir tous les changements récents sur le wiki
</a>
</div>
{% else %}
<div class="alert alert-info">
<p><i class="bi bi-info-circle"></i> Aucune page avec suppressions suspectes n'a été trouvée.</p>
<p><i class="bi bi-info-circle"></i> Aucune suppression suspecte récente n'a été détectée.</p>
</div>
{% endif %}
</div>
</div>
<!-- Différences de contenu -->
<div class="card mb-4">
<div class="card-header">
<h2>À propos des suppressions suspectes</h2>
</div>
<div class="card-body">
<p>Les suppressions suspectes sont identifiées lorsque la version française d'une page wiki contient significativement moins de mots que la version anglaise (plus de 30% de différence).</p>
<h5>Suppressions récentes</h5>
<p>Les suppressions récentes sont détectées en analysant les changements récents du wiki OSM. Toute
suppression de plus de 20 caractères est considérée comme potentiellement suspecte et mérite une
vérification.</p>
<h5>Différences de contenu</h5>
<p>Les différences de contenu sont identifiées lorsque la version française d'une page wiki contient
significativement moins de mots que la version anglaise (plus de 30% de différence).</p>
<p>Cela peut indiquer :</p>
<ul>
<li>Une traduction incomplète</li>
<li>Des sections manquantes dans la version française</li>
<li>Des mises à jour importantes dans la version anglaise qui n'ont pas été reportées en français</li>
<li>Des mises à jour importantes dans la version anglaise qui n'ont pas été reportées en français
</li>
</ul>
<p>Ces pages sont des candidates prioritaires pour une mise à jour de la traduction française.</p>
</div>
@ -90,5 +117,5 @@
<i class="bi bi-arrow-left"></i> Retour à la liste des pages wiki
</a>
</div>
</div>
</div>
{% endblock %}

View file

@ -6,42 +6,40 @@
<div class="container mt-4">
{% include 'admin/_wiki_navigation.html.twig' %}
<h1>Propositions de tags OSM en cours de vote</h1>
<p class="lead">Liste des propositions de tags OpenStreetMap actuellement en cours de vote ou de discussion.</p>
<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>
{% if last_updated %}
<div class="alert alert-info">
<i class="bi bi-info-circle"></i> Dernière mise à jour : {{ last_updated|date('d/m/Y H:i') }}
</div>
{% endif %}
<!-- Propositions en cours de vote -->
<div class="card mb-4">
<div class="card-header">
<h2>Propositions actives</h2>
<div class="card-header bg-primary text-white">
<h2>Propositions en cours de vote</h2>
</div>
<div class="card-body">
{% if proposals|length > 0 %}
{% set voting_proposals = proposals|filter(p => p.type == 'voting') %}
{% if voting_proposals|length > 0 %}
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead class="thead-dark">
<tr>
<th>Fonctionnalité</th>
<th>Proposition</th>
<th>Description</th>
<th>Proposé par</th>
<th>Statut</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for proposal in proposals %}
{% for proposal in voting_proposals %}
<tr>
<td>{{ proposal.feature }}</td>
<td><strong>{{ proposal.feature }}</strong></td>
<td>{{ proposal.description }}</td>
<td>{{ proposal.proposer }}</td>
<td>
{% if 'voting' in proposal.status|lower %}
<span class="badge bg-primary">En vote</span>
{% elseif 'draft' in proposal.status|lower %}
<span class="badge bg-warning text-dark">Brouillon</span>
{% elseif 'rfc' in proposal.status|lower %}
<span class="badge bg-info">RFC</span>
{% else %}
<span class="badge bg-secondary">{{ proposal.status }}</span>
{% endif %}
</td>
<td>
<a href="{{ proposal.url }}" target="_blank" class="btn btn-sm btn-outline-primary">
@ -55,7 +53,49 @@
</div>
{% else %}
<div class="alert alert-info">
<p><i class="bi bi-info-circle"></i> Aucune proposition de tag n'a été trouvée.</p>
<p><i class="bi bi-info-circle"></i> Aucune proposition en cours de vote n'a été trouvée.</p>
</div>
{% endif %}
</div>
</div>
<!-- Propositions récemment modifiées -->
<div class="card mb-4">
<div class="card-header bg-info text-white">
<h2>Propositions récemment modifiées</h2>
</div>
<div class="card-body">
{% set recent_proposals = proposals|filter(p => p.type == 'recent') %}
{% if recent_proposals|length > 0 %}
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead class="thead-dark">
<tr>
<th>Proposition</th>
<th>Dernière modification</th>
<th>Modifié par</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for proposal in recent_proposals %}
<tr>
<td><strong>{{ proposal.feature }}</strong></td>
<td>{{ proposal.description }}</td>
<td>{{ proposal.proposer }}</td>
<td>
<a href="{{ proposal.url }}" target="_blank" class="btn btn-sm btn-outline-primary">
<i class="bi bi-box-arrow-up-right"></i> Voir
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="alert alert-info">
<p><i class="bi bi-info-circle"></i> Aucune proposition récemment modifiée n'a été trouvée.</p>
</div>
{% endif %}
</div>