recup sources

This commit is contained in:
Tykayn 2025-09-01 18:28:23 +02:00 committed by tykayn
parent 86622a19ea
commit 65fe2a35f9
155 changed files with 50969 additions and 0 deletions

View file

@ -0,0 +1,57 @@
<nav class="navbar navbar-expand-lg navbar-light bg-light mb-4">
<div class="container-fluid">
<a class="navbar-brand" href="{{ path('app_admin_wiki') }}">Wiki OSM</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#wikiNavbar" aria-controls="wikiNavbar" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="wikiNavbar">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link {% if app.request.get('_route') == 'app_admin_wiki' %}active{% endif %}" href="{{ path('app_admin_wiki') }}">
<i class="bi bi-list-ul"></i> Liste des pages
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if app.request.get('_route') == 'app_admin_wiki_random_suggestion' %}active{% endif %}" href="{{ path('app_admin_wiki_random_suggestion') }}">
<i class="bi bi-shuffle"></i> Suggestion aléatoire
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if app.request.get('_route') == 'app_admin_wiki_tag_proposals' %}active{% endif %}" href="{{ path('app_admin_wiki_tag_proposals') }}">
<i class="bi bi-tag"></i> Propositions de tags
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if app.request.get('_route') == 'app_admin_wiki_archived_proposals' %}active{% endif %}" href="{{ path('app_admin_wiki_archived_proposals') }}">
<i class="bi bi-archive"></i> Propositions archivées
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if app.request.get('_route') == 'app_admin_wiki_suspicious_deletions' %}active{% endif %}" href="{{ path('app_admin_wiki_suspicious_deletions') }}">
<i class="bi bi-exclamation-triangle"></i> Suppressions suspectes
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if app.request.get('_route') == 'app_admin_wiki_missing_translations' %}active{% endif %}" href="{{ path('app_admin_wiki_missing_translations') }}">
<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>
<li class="nav-item">
<a class="nav-link {% if app.request.get('_route') == 'app_admin_wiki_recent_changes' %}active{% endif %}" href="{{ path('app_admin_wiki_recent_changes') }}">
<i class="bi bi-clock-history"></i> Changements récents
</a>
</li>
</ul>
</div>
</div>
</nav>

View file

@ -0,0 +1,506 @@
{% extends 'base.html.twig' %}
{% block title %}Pages Wiki OSM{% endblock %}
{% block body %}
<div class="container mt-4">
{% include 'admin/_wiki_navigation.html.twig' %}
<h1>Pages Wiki OpenStreetMap</h1>
<p class="lead">Outil de qualité des des pages wiki OpenStreetMap en français et en anglais pour les clés OSM
les plus utilisées.
<a href="https://forum.openstreetmap.fr/t/fabriquer-un-outil-de-qualite-pour-le-wiki-osm/36814">
Venez discuter QualiWiki sur le forum
</a>
</p>
<div class="card mb-4">
<div class="card-header">
<h2>Liste des pages wiki</h2>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead class="thead-dark">
<tr>
<th rowspan="2">Image</th>
<th rowspan="2">Clé</th>
<th colspan="4" class="text-center">Différences FR vs EN</th>
<th rowspan="2" class="text-center">Score de<br>décrépitude</th>
<th rowspan="2" class="text-center">Liens</th>
</tr>
<tr>
<th class="text-center">Sections</th>
<th class="text-center">Mots</th>
<th class="text-center">Liens</th>
<th class="text-center">Images</th>
</tr>
</thead>
<tbody>
{% for key, languages in wiki_pages %}
{% if languages['en'] is defined and languages['fr'] is defined %}
<tr>
<td>
<img src="{{ languages['en'].description_img_url }}" alt="image"
style="height: 2rem;">
</td>
<td>
<strong>{{ key }}</strong>
</td>
{% set diff = page_differences[key] %}
<td class="text-center">
{% if diff.section_diff > 0 %}
<span class="badge bg-success">{{ diff.section_diff_formatted }}</span>
{% elseif diff.section_diff < 0 %}
<span class="badge bg-danger">{{ diff.section_diff_formatted }}</span>
{% else %}
<span class="badge bg-secondary">0</span>
{% endif %}
</td>
<td class="text-center">
{% if diff.word_diff > 0 %}
<span class="badge bg-success">{{ diff.word_diff_formatted }}</span>
{% elseif diff.word_diff < 0 %}
<span class="badge bg-danger">{{ diff.word_diff_formatted }}</span>
{% else %}
<span class="badge bg-secondary">0</span>
{% endif %}
</td>
<td class="text-center">
{% if diff.link_diff > 0 %}
<span class="badge bg-success">{{ diff.link_diff_formatted }}</span>
{% elseif diff.link_diff < 0 %}
<span class="badge bg-danger">{{ diff.link_diff_formatted }}</span>
{% else %}
<span class="badge bg-secondary">0</span>
{% endif %}
</td>
<td class="text-center">
{% if diff.media_diff > 0 %}
<span class="badge bg-success">{{ diff.media_diff_formatted }}</span>
{% elseif diff.media_diff < 0 %}
<span class="badge bg-danger">{{ diff.media_diff_formatted }}</span>
{% else %}
<span class="badge bg-secondary">0</span>
{% endif %}
</td>
<td class="text-center">
{% set score = languages['en'].staleness_score|default(0) %}
{% if score > 50 %}
<span class="badge bg-danger">{{ score }}</span>
{% elseif score > 20 %}
<span class="badge bg-warning text-dark">{{ score }}</span>
{% else %}
<span class="badge bg-success">{{ score }}</span>
{% endif %}
</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">
<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">
<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">
<i class="bi bi-arrows-angle-expand"></i> Comparer
</a>
</div>
</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% if missing_translations|length > 0 %}
<div class="card mb-4">
<div class="card-header bg-warning text-dark">
<h2>Pages manquantes en français ({{ missing_translations|length }})</h2>
</div>
<div class="card-body">
<p>Ces pages wiki ont une version anglaise mais pas de traduction française.</p>
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead class="thead-dark">
<tr>
<th>Clé</th>
<th>Sections</th>
<th>Mots</th>
<th>Liens</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for key, page in missing_translations %}
<tr>
<td><strong>{{ key }}</strong></td>
<td>{{ page.sections }}</td>
<td>{{ page.word_count }}</td>
<td>{{ page.link_count }}</td>
<td class="text-center">
<div class="btn-group" role="group">
<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_create_french', {'key': key}) }}"
class="btn btn-sm btn-success"
title="Créer une traduction française">
<i class="bi bi-plus-circle"></i> Traduire
</a>
{# <a href="{{ path('app_admin_wiki_compare', {'key': key}) }}" #}
{# class="btn btn-sm btn-outline-secondary" #}
{# title="Voir les détails et comparer"> #}
{# <i class="bi bi-arrows-angle-expand"></i> Comparer #}
{# </a> #}
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endif %}
{% if specific_pages is defined and specific_pages|length > 0 %}
<div class="card mb-4">
<div class="card-header bg-primary text-white">
<h2>Pages spécifiques ({{ specific_pages|length }})</h2>
</div>
<div class="card-body">
<p>Ces pages wiki sont des pages spécifiques qui ont été sélectionnées pour une comparaison
particulière.</p>
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead class="thead-dark">
<tr>
<th>Titre</th>
<th>Raison</th>
<th>Score de décrépitude</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for page in specific_pages %}
<tr>
<td>
<div class="d-flex align-items-center">
{% if page.en_page.description_img_url is defined and page.en_page.description_img_url %}
<div class="me-3">
<img src="{{ page.en_page.description_img_url }}"
alt="{{ page.key }}"
style="max-width: 80px; max-height: 60px; object-fit: contain;">
</div>
{% endif %}
<div>
<strong>{{ page.key }}</strong>
<span class="badge bg-primary">Spécifique</span>
</div>
</div>
</td>
<td>
{{ page.reason }}
</td>
<td>
{% if page.staleness_score is defined %}
<div class="progress" style="height: 20px;">
{% set score_class = page.staleness_score > 70 ? 'bg-danger' : (page.staleness_score > 40 ? 'bg-warning' : 'bg-success') %}
<div class="progress-bar {{ score_class }}" role="progressbar"
style="width: {{ page.staleness_score }}%;"
aria-valuenow="{{ page.staleness_score }}"
aria-valuemin="0"
aria-valuemax="100">
{{ page.staleness_score }}
</div>
</div>
{% else %}
<span class="text-muted">Non disponible</span>
{% endif %}
</td>
<td class="text-center">
<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>
{% if page.fr_page %}
<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>
{% else %}
<a href="{{ path('app_admin_wiki_create_french', {'key': page.key}) }}"
class="btn btn-sm btn-success"
title="Créer une traduction française">
<i class="bi bi-plus-circle"></i> Traduire
</a>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endif %}
{% if pages_unavailable_in_english|length > 0 %}
<div class="card mb-4">
<div class="card-header bg-info text-dark">
<h2>Pages françaises non disponibles en Anglais ({{ pages_unavailable_in_english|length }})</h2>
</div>
<div class="card-body">
<p>Ces pages wiki ont une version française mais pas de traduction anglaise.</p>
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead class="thead-dark">
<tr>
<th>Titre</th>
<th>Score de décrépitude</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for page in pages_unavailable_in_english %}
<tr>
<td>
<div class="d-flex align-items-center">
{% if page.description_img_url is defined and page.description_img_url %}
<div class="me-3">
<img src="{{ page.description_img_url }}" alt="{{ page.title }}"
style="max-width: 80px; max-height: 60px; object-fit: contain;">
</div>
{% endif %}
<div>
<strong>{{ page.title }}</strong>
</div>
</div>
</td>
<td>
{% if page.outdatedness_score is defined %}
<div class="progress" style="height: 20px;">
{% set score_class = page.outdatedness_score > 70 ? 'bg-danger' : (page.outdatedness_score > 40 ? 'bg-warning' : 'bg-success') %}
<div class="progress-bar {{ score_class }}" role="progressbar"
style="width: {{ page.outdatedness_score }}%;"
aria-valuenow="{{ page.outdatedness_score }}"
aria-valuemin="0"
aria-valuemax="100">
{{ page.outdatedness_score }}
</div>
</div>
{% else %}
<span class="text-muted">Non disponible</span>
{% endif %}
</td>
<td class="text-center">
<div class="btn-group" role="group">
<a href="{{ page.url }}" target="_blank"
class="btn btn-sm btn-outline-info" title="Version française">
<i class="bi bi-flag-fill"></i> FR
</a>
{% set en_url = page.url|replace({'FR:': ''}) %}
<a href="{{ en_url }}" target="_blank"
class="btn btn-sm btn-outline-primary"
title="Créer une traduction anglaise">
<i class="bi bi-translate"></i> créer EN
</a>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endif %}
{% if newly_created_pages is defined and newly_created_pages|length > 0 %}
<div class="card mb-4">
<div class="card-header bg-success text-white">
<h2>Pages françaises récemment créées ({{ newly_created_pages|length }})</h2>
</div>
<div class="card-body">
<p>Ces pages wiki françaises ont été récemment créées et étaient auparavant dans la liste des pages
non disponibles en français.</p>
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead class="thead-dark">
<tr>
<th>Titre</th>
<th>Version anglaise</th>
<th>Date de création</th>
<th>Créée par</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for page in newly_created_pages %}
<tr>
<td>
<div class="d-flex align-items-center">
<div>
<strong>{{ page.title }}</strong>
<span class="badge bg-success">Nouvelle</span>
</div>
</div>
</td>
<td>
{{ page.en_title }}
</td>
<td>
{{ page.created_at }}
</td>
<td>
{{ page.created_by }}
</td>
<td class="text-center">
<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-flag-fill"></i> FR
</a>
<a href="{{ page.en_url }}" target="_blank"
class="btn btn-sm btn-outline-primary" title="Version anglaise">
<i class="bi bi-flag-fill"></i> EN
</a>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endif %}
<p>
le score de fraîcheur prend en compte d'avantage la différence entre le nombre de mots que l'ancienneté de
modification.
On compte aussi le nombre de sections et de liens.
</p>
<div class="mt-3">
<div class="card mb-4">
<div class="card-header">
<h2>Graphe de décrépitude</h2>
</div>
<div class="card-body">
<canvas id="decrepitudeChart" height="300"></canvas>
</div>
</div>
</div>
</div>
{% endblock %}
{% block javascripts %}
{{ parent() }}
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function () {
// Collect data from the table
const labels = [];
const scores = [];
const colors = [];
{% for key, languages in wiki_pages %}
{% if languages['en'] is defined and languages['fr'] is defined %}
labels.push("{{ key }}");
{% set score = languages['en'].staleness_score|default(0) %}
scores.push({{ score }});
// Set color based on score
{% if score > 50 %}
colors.push('rgba(220, 53, 69, 0.7)'); // danger
{% elseif score > 20 %}
colors.push('rgba(255, 193, 7, 0.7)'); // warning
{% else %}
colors.push('rgba(25, 135, 84, 0.7)'); // success
{% endif %}
{% endif %}
{% endfor %}
// Sort data by score (descending)
const indices = Array.from(Array(scores.length).keys())
.sort((a, b) => scores[b] - scores[a]);
const sortedLabels = indices.map(i => labels[i]);
const sortedScores = indices.map(i => scores[i]);
const sortedColors = indices.map(i => colors[i]);
// Limit to top 20 pages for readability
const displayLimit = 20;
const displayLabels = sortedLabels.slice(0, displayLimit);
const displayScores = sortedScores.slice(0, displayLimit);
const displayColors = sortedColors.slice(0, displayLimit);
// Create the chart
const ctx = document.getElementById('decrepitudeChart').getContext('2d');
new Chart(ctx, {
type: 'bar',
data: {
labels: displayLabels,
datasets: [{
label: 'Score de décrépitude',
data: displayScores,
backgroundColor: displayColors,
borderColor: displayColors.map(c => c.replace('0.7', '1')),
borderWidth: 1
}]
},
options: {
indexAxis: 'y',
responsive: true,
plugins: {
legend: {
display: false
},
tooltip: {
callbacks: {
label: function (context) {
return `Score: ${context.raw}`;
}
}
}
},
scales: {
x: {
beginAtZero: true,
max: 100,
title: {
display: true,
text: 'Score de décrépitude (0-100)'
}
}
}
}
});
});
</script>
{% endblock %}

View file

@ -0,0 +1,701 @@
{% extends 'base.html.twig' %}
{% block title %}Propositions archivées OSM{% endblock %}
{% block stylesheets %}
{{ parent() }}
<style>
.vote-bar {
height: 24px;
border-radius: 4px;
overflow: hidden;
display: flex;
margin-bottom: 10px;
}
.vote-approve {
background-color: #28a745;
height: 100%;
}
.vote-abstain {
background-color: #ffc107;
height: 100%;
}
.vote-oppose {
background-color: #dc3545;
height: 100%;
}
.vote-count {
font-size: 0.85rem;
font-weight: bold;
color: white;
text-align: center;
padding: 2px 5px;
}
.proposal-card {
margin-bottom: 1.5rem;
transition: transform 0.2s;
}
.proposal-card:hover {
transform: translateY(-5px);
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.proposal-metadata {
font-size: 0.85rem;
color: #6c757d;
}
.voter-badge {
display: inline-block;
margin-right: 5px;
margin-bottom: 5px;
padding: 2px 8px;
border-radius: 12px;
font-size: 0.8rem;
}
.voter-approve {
background-color: rgba(40, 167, 69, 0.2);
border: 1px solid rgba(40, 167, 69, 0.4);
}
.voter-abstain {
background-color: rgba(255, 193, 7, 0.2);
border: 1px solid rgba(255, 193, 7, 0.4);
}
.voter-oppose {
background-color: rgba(220, 53, 69, 0.2);
border: 1px solid rgba(220, 53, 69, 0.4);
}
.stats-card {
transition: transform 0.2s;
}
.stats-card:hover {
transform: translateY(-5px);
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.stats-value {
font-size: 2rem;
font-weight: bold;
}
.stats-label {
font-size: 0.9rem;
color: #6c757d;
}
.top-voters-table th, .top-voters-table td {
padding: 0.5rem;
}
.vote-duration {
display: inline-block;
padding: 3px 8px;
border-radius: 4px;
background-color: #e9ecef;
margin-top: 5px;
font-size: 0.85rem;
}
.comment-text {
font-style: italic;
color: #495057;
background-color: #f8f9fa;
padding: 8px;
border-radius: 4px;
margin-top: 5px;
margin-bottom: 10px;
border-left: 3px solid #dee2e6;
}
.chart-container {
position: relative;
height: 300px;
margin-bottom: 20px;
}
</style>
{% endblock %}
{% block body %}
<div class="container mt-4">
{% include 'admin/_wiki_navigation.html.twig' %}
<h1>Propositions archivées OpenStreetMap</h1>
<p class="lead">Analyse des votes sur les propositions archivées du wiki OSM</p>
{% if last_updated %}
<div class="alert alert-info">
<div class="row align-items-center">
<div class="col-md-6">
<i class="bi bi-info-circle"></i> Dernière mise à jour : {{ last_updated|date('d/m/Y H:i') }}
</div>
<div class="col-md-6 text-end">
<form class="d-inline-flex align-items-center" method="get" action="{{ path('app_admin_wiki_archived_proposals') }}">
<div class="me-2">
<label for="limit" class="me-2">Limiter à:</label>
<select name="limit" id="limit" class="form-select form-select-sm d-inline-block" style="width: auto;">
<option value="">Toutes les propositions</option>
<option value="10" {% if limit == 10 %}selected{% endif %}>10 propositions</option>
<option value="20" {% if limit == 20 %}selected{% endif %}>20 propositions</option>
<option value="50" {% if limit == 50 %}selected{% endif %}>50 propositions</option>
<option value="100" {% if limit == 100 %}selected{% endif %}>100 propositions</option>
{% if limit and limit not in [10, 20, 50, 100] %}
<option value="{{ limit }}" selected>{{ limit }} propositions</option>
{% endif %}
</select>
</div>
<input type="hidden" name="refresh" value="1">
<button type="submit" class="btn btn-sm btn-outline-primary">
<i class="bi bi-arrow-clockwise"></i> Rafraîchir les données
</button>
</form>
</div>
</div>
{% if limit %}
<div class="mt-2">
<small class="text-muted">
<i class="bi bi-info-circle"></i> Les données sont limitées à {{ limit }} propositions.
<a href="{{ path('app_admin_wiki_archived_proposals') }}">Voir toutes les propositions</a>
</small>
</div>
{% endif %}
</div>
{% endif %}
{% if statistics %}
<div class="card mb-4">
<div class="card-header">
<h2>Statistiques globales</h2>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-3 col-sm-6 mb-3">
<div class="card stats-card h-100">
<div class="card-body text-center">
<div class="stats-value">{{ statistics.total_proposals }}</div>
<div class="stats-label">Propositions analysées</div>
</div>
</div>
</div>
<div class="col-md-3 col-sm-6 mb-3">
<div class="card stats-card h-100">
<div class="card-body text-center">
<div class="stats-value">{{ statistics.total_votes }}</div>
<div class="stats-label">Votes au total</div>
</div>
</div>
</div>
<div class="col-md-3 col-sm-6 mb-3">
<div class="card stats-card h-100">
<div class="card-body text-center">
<div class="stats-value">{{ statistics.avg_votes_per_proposal }}</div>
<div class="stats-label">Votes par proposition (moyenne)</div>
<div class="small text-muted">Excluant les propositions sans votes</div>
</div>
</div>
</div>
<div class="col-md-3 col-sm-6 mb-3">
<div class="card stats-card h-100">
<div class="card-body text-center">
<div class="stats-value">{{ statistics.unique_voters }}</div>
<div class="stats-label">Votants uniques</div>
</div>
</div>
</div>
</div>
<div class="row mt-3">
<div class="col-md-3 col-sm-6 mb-3">
<div class="card stats-card h-100">
<div class="card-body text-center">
<div class="stats-value">{{ statistics.median_votes_per_proposal }}</div>
<div class="stats-label">Votes par proposition (médiane)</div>
<div class="small text-muted">Excluant les propositions sans votes</div>
</div>
</div>
</div>
<div class="col-md-3 col-sm-6 mb-3">
<div class="card stats-card h-100">
<div class="card-body text-center">
<div class="stats-value">{{ statistics.std_dev_votes_per_proposal }}</div>
<div class="stats-label">Votes par proposition (écart type)</div>
<div class="small text-muted">Excluant les propositions sans votes</div>
</div>
</div>
</div>
</div>
{% if statistics.avg_vote_duration_days is defined %}
<div class="row mt-3">
<div class="col-md-3 col-sm-6 mb-3">
<div class="card stats-card h-100">
<div class="card-body text-center">
<div class="stats-value">{{ statistics.avg_vote_duration_days }}</div>
<div class="stats-label">Durée moyenne des votes (jours)</div>
</div>
</div>
</div>
</div>
{% endif %}
{% if statistics.status_distribution is defined and statistics.status_distribution|length > 0 %}
<div class="row mt-4">
<div class="col-md-6">
<h4>Répartition par statut</h4>
<div class="chart-container">
<canvas id="statusChart"></canvas>
</div>
</div>
<div class="col-md-6">
<h4>Détail des statuts</h4>
<table class="table table-sm table-striped">
<thead>
<tr>
<th>Statut</th>
<th>Nombre</th>
<th>Pourcentage</th>
</tr>
</thead>
<tbody>
{% for status, count in statistics.status_distribution %}
<tr>
<td>{{ status }}</td>
<td>{{ count }}</td>
<td>{{ (count / statistics.total_proposals * 100)|round(1) }}%</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
<div class="row mt-4">
<div class="col-12">
<h4>Répartition des années des propositions</h4>
<div class="chart-container">
<canvas id="yearDistributionChart"></canvas>
</div>
</div>
</div>
</div>
</div>
<div class="card mb-4">
<div class="card-header">
<h2>Contributeurs les plus actifs</h2>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover top-voters-table">
<thead>
<tr>
<th>#</th>
<th>Utilisateur</th>
<th>Total votes</th>
<th>Approbations</th>
<th>Abstentions</th>
<th>Oppositions</th>
<th>Répartition</th>
</tr>
</thead>
<tbody>
{% for voter in statistics.top_voters %}
<tr>
<td>{{ loop.index }}</td>
<td>
<a href="https://wiki.openstreetmap.org/wiki/User:{{ voter.username }}" target="_blank">
{{ voter.username }}
</a>
</td>
<td>{{ voter.total }}</td>
<td>{{ voter.approve }}</td>
<td>{{ voter.abstain }}</td>
<td>{{ voter.oppose }}</td>
<td>
<div class="vote-bar">
{% if voter.approve > 0 %}
<div class="vote-approve" style="width: {{ (voter.approve / voter.total * 100)|round }}%">
<span class="vote-count">{{ (voter.approve / voter.total * 100)|round }}%</span>
</div>
{% endif %}
{% if voter.abstain > 0 %}
<div class="vote-abstain" style="width: {{ (voter.abstain / voter.total * 100)|round }}%">
<span class="vote-count">{{ (voter.abstain / voter.total * 100)|round }}%</span>
</div>
{% endif %}
{% if voter.oppose > 0 %}
<div class="vote-oppose" style="width: {{ (voter.oppose / voter.total * 100)|round }}%">
<span class="vote-count">{{ (voter.oppose / voter.total * 100)|round }}%</span>
</div>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endif %}
<div class="card mb-4">
<div class="card-header d-flex justify-content-between align-items-center">
<h2 class="mb-0">Liste des propositions archivées</h2>
<div>
<div class="btn-group" role="group">
<button type="button" class="btn btn-outline-secondary btn-sm filter-btn active" data-filter="all">Toutes</button>
<button type="button" class="btn btn-outline-success btn-sm filter-btn" data-filter="approved">Approuvées</button>
<button type="button" class="btn btn-outline-danger btn-sm filter-btn" data-filter="rejected">Rejetées</button>
<button type="button" class="btn btn-outline-warning btn-sm filter-btn" data-filter="neutral">Neutres</button>
</div>
</div>
</div>
<div class="card-body">
<div class="row" id="proposals-container">
{% for proposal in proposals %}
{% set total_votes = proposal.votes.approve.count + proposal.votes.oppose.count + proposal.votes.abstain.count %}
{% set is_approved = proposal.votes.approve.count > proposal.votes.oppose.count %}
{% set is_rejected = proposal.votes.approve.count < proposal.votes.oppose.count %}
{% set is_neutral = proposal.votes.approve.count == proposal.votes.oppose.count %}
<div class="col-md-6 mb-4 proposal-item {% if is_approved %}approved{% elseif is_rejected %}rejected{% else %}neutral{% endif %}">
<div class="card proposal-card h-100 {% if is_approved %}border-success{% elseif is_rejected %}border-danger{% else %}border-warning{% endif %}">
<div class="card-header {% if is_approved %}bg-success text-white{% elseif is_rejected %}bg-danger text-white{% else %}bg-warning{% endif %}">
<h5 class="card-title mb-0">
<a href="{{ proposal.url }}" target="_blank" class="text-white">
{{ proposal.title }}
</a>
</h5>
</div>
<div class="card-body">
<div class="proposal-metadata mb-3">
{% if proposal.proposer %}
<div><strong>Proposé par :</strong> {{ proposal.proposer }}</div>
{% endif %}
{% if proposal.last_modified %}
<div><strong>Dernière modification :</strong> {{ proposal.last_modified }}</div>
{% endif %}
<div><strong>Sections :</strong> {{ proposal.section_count }}</div>
<div><strong>Liens :</strong> {{ proposal.link_count }}</div>
<div><strong>Mots :</strong> {{ proposal.word_count }}</div>
{% if proposal.votes.duration_days is defined %}
<div class="mt-2">
<span class="vote-duration">
<i class="bi bi-calendar-range"></i>
<strong>Durée du vote :</strong> {{ proposal.votes.duration_days }} jours
({{ proposal.votes.first_vote }}{{ proposal.votes.last_vote }})
</span>
</div>
{% endif %}
</div>
{% if total_votes > 0 %}
<h6>Résultats des votes ({{ total_votes }} votes)</h6>
<div class="vote-bar">
{% if proposal.votes.approve.count > 0 %}
<div class="vote-approve" style="width: {{ proposal.approve_percentage }}%">
<span class="vote-count">{{ proposal.votes.approve.count }} ({{ proposal.approve_percentage }}%)</span>
</div>
{% endif %}
{% if proposal.votes.abstain.count > 0 %}
<div class="vote-abstain" style="width: {{ proposal.abstain_percentage }}%">
<span class="vote-count">{{ proposal.votes.abstain.count }} ({{ proposal.abstain_percentage }}%)</span>
</div>
{% endif %}
{% if proposal.votes.oppose.count > 0 %}
<div class="vote-oppose" style="width: {{ proposal.oppose_percentage }}%">
<span class="vote-count">{{ proposal.votes.oppose.count }} ({{ proposal.oppose_percentage }}%)</span>
</div>
{% endif %}
</div>
<div class="mt-3">
<div class="accordion" id="votersAccordion{{ loop.index }}">
<div class="accordion-item">
<h2 class="accordion-header" id="headingVoters{{ loop.index }}">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
data-bs-target="#collapseVoters{{ loop.index }}" aria-expanded="false"
aria-controls="collapseVoters{{ loop.index }}">
Voir les votants
</button>
</h2>
<div id="collapseVoters{{ loop.index }}" class="accordion-collapse collapse"
aria-labelledby="headingVoters{{ loop.index }}"
data-bs-parent="#votersAccordion{{ loop.index }}">
<div class="accordion-body">
{% if proposal.votes.approve.users|length > 0 %}
<div class="mb-2">
<strong>Approbations ({{ proposal.votes.approve.count }}):</strong>
<div>
{% for user in proposal.votes.approve.users %}
<div class="mb-2">
<span class="voter-badge voter-approve">{{ user.username }}</span>
{% if user.comment is defined and user.comment is not empty %}
<div class="comment-text">{{ user.comment }}</div>
{% endif %}
</div>
{% endfor %}
</div>
</div>
{% endif %}
{% if proposal.votes.abstain.users|length > 0 %}
<div class="mb-2">
<strong>Abstentions ({{ proposal.votes.abstain.count }}):</strong>
<div>
{% for user in proposal.votes.abstain.users %}
<div class="mb-2">
<span class="voter-badge voter-abstain">{{ user.username }}</span>
{% if user.comment is defined and user.comment is not empty %}
<div class="comment-text">{{ user.comment }}</div>
{% endif %}
</div>
{% endfor %}
</div>
</div>
{% endif %}
{% if proposal.votes.oppose.users|length > 0 %}
<div>
<strong>Oppositions ({{ proposal.votes.oppose.count }}):</strong>
<div>
{% for user in proposal.votes.oppose.users %}
<div class="mb-2">
<span class="voter-badge voter-oppose">{{ user.username }}</span>
{% if user.comment is defined and user.comment is not empty %}
<div class="comment-text">{{ user.comment }}</div>
{% endif %}
</div>
{% endfor %}
</div>
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
{% else %}
<div class="alert alert-secondary">
Aucun vote trouvé pour cette proposition.
</div>
{% endif %}
</div>
<div class="card-footer">
<a href="{{ proposal.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>
{% else %}
<div class="alert alert-warning">
<i class="bi bi-exclamation-triangle"></i> Aucune proposition archivée n'a été trouvée.
</div>
{% endfor %}
</div>
</div>
</div>
</div>
{% endblock %}
{% block javascripts %}
{{ parent() }}
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Filtering functionality
const filterButtons = document.querySelectorAll('.filter-btn');
const proposalItems = document.querySelectorAll('.proposal-item');
filterButtons.forEach(button => {
button.addEventListener('click', function() {
// Remove active class from all buttons
filterButtons.forEach(btn => btn.classList.remove('active'));
// Add active class to clicked button
this.classList.add('active');
const filter = this.getAttribute('data-filter');
// Show/hide proposals based on filter
proposalItems.forEach(item => {
if (filter === 'all') {
item.style.display = 'block';
} else if (filter === 'approved' && item.classList.contains('approved')) {
item.style.display = 'block';
} else if (filter === 'rejected' && item.classList.contains('rejected')) {
item.style.display = 'block';
} else if (filter === 'neutral' && item.classList.contains('neutral')) {
item.style.display = 'block';
} else {
item.style.display = 'none';
}
});
});
});
// Initialize status distribution chart if it exists
const statusChartCanvas = document.getElementById('statusChart');
if (statusChartCanvas) {
// Get status distribution data from the template
const statusDistribution = {{ statistics.status_distribution|json_encode|raw }};
if (statusDistribution && Object.keys(statusDistribution).length > 0) {
const labels = Object.keys(statusDistribution);
const data = Object.values(statusDistribution);
// Generate colors for each status
const backgroundColors = [
'#28a745', // Approved - green
'#dc3545', // Rejected - red
'#ffc107', // Voting/Proposed - yellow
'#6c757d', // Abandoned/Inactive - gray
'#17a2b8', // Other statuses - blue
'#6610f2', // Other statuses - purple
'#fd7e14', // Other statuses - orange
'#20c997', // Other statuses - teal
'#e83e8c' // Other statuses - pink
];
// Create the chart
new Chart(statusChartCanvas, {
type: 'pie',
data: {
labels: labels,
datasets: [{
data: data,
backgroundColor: backgroundColors.slice(0, labels.length),
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'right',
labels: {
font: {
size: 12
}
}
},
tooltip: {
callbacks: {
label: function(context) {
const label = context.label || '';
const value = context.raw || 0;
const total = context.dataset.data.reduce((a, b) => a + b, 0);
const percentage = Math.round((value / total) * 100);
return `${label}: ${value} (${percentage}%)`;
}
}
}
}
}
});
}
}
// Initialize year distribution chart
const yearChartCanvas = document.getElementById('yearDistributionChart');
if (yearChartCanvas) {
// Get proposals data from the template
const proposals = {{ proposals|json_encode|raw }};
if (proposals && proposals.length > 0) {
// Extract years from last_modified dates
const yearCounts = {};
proposals.forEach(proposal => {
if (proposal.last_modified) {
// Extract year from the date string (format: "DD Month YYYY")
const yearMatch = proposal.last_modified.match(/\d{4}$/);
if (yearMatch) {
const year = yearMatch[0];
yearCounts[year] = (yearCounts[year] || 0) + 1;
}
}
});
// Sort years chronologically
const sortedYears = Object.keys(yearCounts).sort();
const counts = sortedYears.map(year => yearCounts[year]);
// Generate a color gradient for the bars
const colors = sortedYears.map((year, index) => {
// Create a gradient from blue to green
const ratio = index / (sortedYears.length - 1 || 1);
return `rgba(${Math.round(33 + (20 * ratio))}, ${Math.round(150 + (50 * ratio))}, ${Math.round(243 - (100 * ratio))}, 0.7)`;
});
// Create the chart
new Chart(yearChartCanvas, {
type: 'bar',
data: {
labels: sortedYears,
datasets: [{
label: 'Nombre de propositions',
data: counts,
backgroundColor: colors,
borderColor: colors.map(color => color.replace('0.7', '1')),
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
ticks: {
precision: 0
},
title: {
display: true,
text: 'Nombre de propositions'
}
},
x: {
title: {
display: true,
text: 'Année'
}
}
},
plugins: {
legend: {
display: false
},
tooltip: {
callbacks: {
title: function(context) {
return `Année ${context[0].label}`;
},
label: function(context) {
const count = context.raw;
return count > 1 ? `${count} propositions` : `${count} proposition`;
}
}
}
}
}
});
}
}
});
</script>
{% endblock %}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,115 @@
{% extends 'base.html.twig' %}
{% block title %}Créer une traduction française pour {{ key }}{% endblock %}
{% block stylesheets %}
{{ parent() }}
<style>
.iframe-container {
display: flex;
flex-direction: row;
height: calc(100vh - 200px);
min-height: 600px;
margin-bottom: 20px;
}
.iframe-container iframe {
flex: 1;
border: 1px solid #dee2e6;
height: 100%;
}
.iframe-container .iframe-separator {
width: 10px;
background-color: #f8f9fa;
border: 1px solid #dee2e6;
display: flex;
justify-content: center;
align-items: center;
}
.iframe-container .iframe-separator::after {
content: "⟺";
font-size: 20px;
color: #6c757d;
}
.iframe-header {
background-color: #f8f9fa;
padding: 10px;
border: 1px solid #dee2e6;
border-bottom: none;
font-weight: bold;
text-align: center;
}
@media (max-width: 768px) {
.iframe-container {
flex-direction: column;
height: auto;
}
.iframe-container iframe {
height: 500px;
}
.iframe-container .iframe-separator {
height: 10px;
width: 100%;
}
.iframe-container .iframe-separator::after {
content: "⟻⟼";
}
}
</style>
{% endblock %}
{% block body %}
<div class="container-fluid mt-4">
{% include 'admin/_wiki_navigation.html.twig' %}
<h1>Créer une traduction française pour "{{ key }}"</h1>
<p class="lead">Utilisez cette page pour traduire la page wiki en français. La page anglaise est affichée à gauche pour référence, et le formulaire d'édition de la page française est à droite.</p>
<div class="alert alert-info">
<i class="bi bi-info-circle"></i> Conseils pour la traduction :
<ul>
<li>Conservez la structure de la page anglaise (sections, sous-sections, etc.)</li>
<li>Traduisez le contenu en français en adaptant les exemples au contexte français si nécessaire</li>
<li>N'hésitez pas à consulter d'autres pages françaises pour vous inspirer du style et de la terminologie</li>
</ul>
</div>
<div class="row">
<div class="col-md-6">
<div class="iframe-header">
<i class="bi bi-flag-fill"></i> Version anglaise (référence)
</div>
</div>
<div class="col-md-6">
<div class="iframe-header">
<i class="bi bi-translate"></i> <a href="{{ french_edit_url }}">Création de la version française</a>
</div>
</div>
</div>
<div class="iframe-container">
<iframe src="{{ english_url }}" title="Version anglaise"></iframe>
<div class="iframe-separator"></div>
<iframe src="{{ french_edit_url }}" title="Édition française"></iframe>
</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>
<a href="{{ english_url }}" target="_blank" class="btn btn-outline-primary">
<i class="bi bi-box-arrow-up-right"></i> Ouvrir la version anglaise dans un nouvel onglet
</a>
<a href="{{ french_edit_url }}" target="_blank" class="btn btn-outline-info">
<i class="bi bi-box-arrow-up-right"></i> Ouvrir l'éditeur français dans un nouvel onglet
</a>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,83 @@
{% extends 'base.html.twig' %}
{% block title %}Pages Wiki françaises sans traduction anglaise{% endblock %}
{% block body %}
<div class="container mt-4">
{% include 'admin/_wiki_navigation.html.twig' %}
<h1>Pages Wiki françaises sans traduction anglaise</h1>
<p class="lead">Liste des pages françaises du wiki OSM qui n'ont pas de traduction en anglais.</p>
{% 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 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>Actions</th>
</tr>
</thead>
<tbody>
{% for page in untranslated_pages %}
<tr>
<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/{{ 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>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="alert alert-info">
<p><i class="bi bi-info-circle"></i> Aucune page française sans traduction anglaise n'a été trouvée.</p>
</div>
{% endif %}
</div>
</div>
<div class="card mb-4">
<div class="card-header">
<h2>À propos des pages sans traduction anglaise</h2>
</div>
<div class="card-body">
<p>Ces pages sont des contenus originaux créés en français qui n'ont pas encore été traduits en anglais.</p>
<p>Bien que la langue principale du wiki OSM soit l'anglais, il est parfois utile de créer d'abord du contenu dans sa langue maternelle, puis de le traduire.</p>
<p>Contribuer à la traduction de ces pages en anglais permet de :</p>
<ul>
<li>Partager les connaissances avec la communauté internationale</li>
<li>Améliorer la visibilité des contributions françaises</li>
<li>Faciliter la collaboration entre contributeurs de différentes langues</li>
</ul>
</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,238 @@
{% 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 %}
<!-- Filtres -->
<div class="mb-4">
<div class="btn-group" role="group" aria-label="Filtres">
<button type="button" class="btn btn-outline-primary active filter-btn" data-filter="all">Tous</button>
<button type="button" class="btn btn-outline-primary filter-btn" data-filter="wiki">Wiki</button>
<button type="button" class="btn btn-outline-primary filter-btn" data-filter="framacalc">Framacalc</button>
<button type="button" class="btn btn-outline-primary filter-btn" data-filter="has-wiki">Avec page wiki</button>
<button type="button" class="btn btn-outline-primary filter-btn" data-filter="no-wiki">Sans page wiki</button>
</div>
</div>
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4">
{% for group in local_groups %}
{% set source = group.source|default('wiki') %}
{% set has_wiki = group.has_wiki_page|default(true) %}
{% set filter_classes = source ~ ' ' ~ (has_wiki ? 'has-wiki' : 'no-wiki') %}
<div class="col group-item {{ filter_classes }}">
<div class="card h-100 {% if source == 'framacalc' and not has_wiki %}border-danger{% endif %}">
{% if source == 'framacalc' %}
<div class="card-header bg-light">
<span class="badge bg-secondary">Framacalc</span>
{% if has_wiki %}
<span class="badge bg-success">Page wiki</span>
{% else %}
<span class="badge bg-danger">Pas de page wiki</span>
{% endif %}
</div>
{% endif %}
<div class="card-body">
<h5 class="card-title">{{ group.name }}</h5>
{% if group.description %}
<p class="card-text">{{ group.description }}</p>
{% endif %}
{% if source == 'framacalc' and group.contact %}
<p class="card-text"><small class="text-muted">Contact: {{ group.contact }}</small></p>
{% endif %}
{% if source == 'framacalc' and group.website %}
<p class="card-text">
<a href="{{ group.website }}" target="_blank" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-globe"></i> Site web
</a>
</p>
{% endif %}
</div>
<div class="card-footer">
{% if source == 'wiki' or has_wiki %}
<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>
{% else %}
<a href="https://wiki.openstreetmap.org/wiki/Special:Search?search={{ group.name|url_encode }}"
target="_blank" class="btn btn-sm btn-outline-danger">
<i class="bi bi-search"></i> Rechercher sur le wiki
</a>
{% endif %}
</div>
</div>
</div>
{% endfor %}
</div>
<!-- JavaScript pour les filtres -->
<script>
document.addEventListener('DOMContentLoaded', function() {
const filterButtons = document.querySelectorAll('.filter-btn');
const groupItems = document.querySelectorAll('.group-item');
filterButtons.forEach(button => {
button.addEventListener('click', function() {
// Remove active class from all buttons
filterButtons.forEach(btn => btn.classList.remove('active'));
// Add active class to clicked button
this.classList.add('active');
const filter = this.getAttribute('data-filter');
// Show/hide items based on filter
groupItems.forEach(item => {
if (filter === 'all') {
item.style.display = 'block';
} else {
if (item.classList.contains(filter)) {
item.style.display = 'block';
} else {
item.style.display = 'none';
}
}
});
});
});
});
</script>
{% 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,215 @@
{% 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">
{% if lang_prefix == 'En' %}
<div class="mb-3">
<button id="copyEnglishTitlesBtn" class="btn btn-outline-primary">
<i class="bi bi-clipboard"></i> Copier les titres au format MediaWiki
</button>
<span id="copyStatus" class="ms-2 text-success" style="display: none;">
<i class="bi bi-check-circle"></i> Copié !
</span>
</div>
{% endif %}
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead class="thead-dark">
<tr>
<th>Titre</th>
<th>Score de décrépitude</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for page in pages %}
<tr>
<td>
<div class="d-flex align-items-center">
{% if page.description_img_url is defined and page.description_img_url %}
<div class="me-3">
<img src="{{ page.description_img_url }}" alt="{{ page.title }}"
style="max-width: 80px; max-height: 60px; object-fit: contain;">
</div>
{% endif %}
<div>
<strong>{{ page.title }}</strong>
{% if page.is_english %}
<span class="badge bg-success">Priorité</span>
{% endif %}
</div>
</div>
</td>
<td>
{% if page.outdatedness_score is defined %}
<div class="progress" style="height: 20px;">
{% set score_class = page.outdatedness_score > 70 ? 'bg-danger' : (page.outdatedness_score > 40 ? 'bg-warning' : 'bg-success') %}
<div class="progress-bar {{ score_class }}" role="progressbar"
style="width: {{ page.outdatedness_score }}%;"
aria-valuenow="{{ page.outdatedness_score }}"
aria-valuemin="0"
aria-valuemax="100">
{{ page.outdatedness_score }}
</div>
</div>
{% else %}
<span class="text-muted">Non disponible</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 %}
{% block javascripts %}
{{ parent() }}
<script>
document.addEventListener('DOMContentLoaded', function() {
const copyButton = document.getElementById('copyEnglishTitlesBtn');
const copyStatus = document.getElementById('copyStatus');
if (copyButton) {
copyButton.addEventListener('click', function() {
// Get all English page titles from the table
const englishSection = document.getElementById('collapseEn');
const titleElements = englishSection.querySelectorAll('tbody tr td:first-child strong');
// Format titles in MediaWiki format
let mediawikiText = '';
const rows = englishSection.querySelectorAll('tbody tr');
rows.forEach(function(row) {
const title = row.querySelector('td:first-child strong').textContent.trim();
const imgElement = row.querySelector('td:first-child img');
if (imgElement) {
const imgSrc = imgElement.getAttribute('src');
mediawikiText += '* [[' + title + ']] - Image: ' + imgSrc + '\n';
} else {
mediawikiText += '* [[' + title + ']]\n';
}
});
// Copy to clipboard
navigator.clipboard.writeText(mediawikiText).then(function() {
// Show success message
copyStatus.style.display = 'inline';
// Hide success message after 3 seconds
setTimeout(function() {
copyStatus.style.display = 'none';
}, 3000);
}).catch(function(err) {
console.error('Erreur lors de la copie: ', err);
alert('Erreur lors de la copie dans le presse-papier. Veuillez réessayer.');
});
});
}
});
</script>
{% endblock %}

View file

@ -0,0 +1,124 @@
{% extends 'base.html.twig' %}
{% block title %}Suggestion de page Wiki à améliorer{% endblock %}
{% block body %}
<div class="container mt-4">
{% include 'admin/_wiki_navigation.html.twig' %}
<h1>Suggestion de page Wiki à améliorer</h1>
<p class="lead">Voici une page wiki qui a besoin d'être améliorée.</p>
<div class="card mb-4">
<div class="card-header bg-primary text-white">
<h2>{{ page.key }}</h2>
</div>
<div class="card-body">
<div class="alert alert-info">
<h3>Raisons d'amélioration</h3>
<p>{{ page.reason }}</p>
</div>
<div class="row">
<div class="col-md-6">
<div class="card h-100">
<div class="card-header bg-primary text-white">
<h3>Version anglaise</h3>
<p class="mb-0">
<small>Dernière modification: {{ page.en_page.last_modified }}</small>
</p>
</div>
<div class="card-body">
<ul class="list-group mb-3">
<li class="list-group-item d-flex justify-content-between align-items-center">
Sections
<span class="badge bg-primary rounded-pill">{{ page.en_page.sections }}</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
Mots
<span class="badge bg-primary rounded-pill">{{ page.en_page.word_count|default(0) }}</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
Liens
<span class="badge bg-primary rounded-pill">{{ page.en_page.link_count|default(0) }}</span>
</li>
</ul>
<div class="d-grid gap-2">
<a href="{{ page.en_page.url }}" target="_blank" class="btn btn-outline-primary">
<i class="bi bi-box-arrow-up-right"></i> Voir la page anglaise
</a>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card h-100">
<div class="card-header bg-info text-white">
<h3>Version française</h3>
{% if page.fr_page %}
<p class="mb-0">
<small>Dernière modification: {{ page.fr_page.last_modified }}</small>
</p>
{% else %}
<p class="mb-0">
<small>Page non existante</small>
</p>
{% endif %}
</div>
<div class="card-body">
{% if page.fr_page %}
<ul class="list-group mb-3">
<li class="list-group-item d-flex justify-content-between align-items-center">
Sections
<span class="badge bg-info rounded-pill">{{ page.fr_page.sections }}</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
Mots
<span class="badge bg-info rounded-pill">{{ page.fr_page.word_count|default(0) }}</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
Liens
<span class="badge bg-info rounded-pill">{{ page.fr_page.link_count|default(0) }}</span>
</li>
</ul>
<div class="d-grid gap-2">
<a href="{{ page.fr_page.url }}" target="_blank" class="btn btn-outline-info">
<i class="bi bi-box-arrow-up-right"></i> Voir la page française
</a>
</div>
{% else %}
<div class="alert alert-warning">
<p><i class="bi bi-exclamation-triangle"></i> <strong>La page wiki pour la clé
"{{ page.key }}" n'existe pas en français.</strong></p>
<p>Vous pouvez contribuer en créant cette page sur le wiki OpenStreetMap.</p>
</div>
<div class="d-grid gap-2">
<a href="https://wiki.openstreetmap.org/w/index.php?title=FR:Key:{{ page.key }}&action=edit"
target="_blank" class="btn btn-success">
<i class="bi bi-plus-circle"></i> Créer la page française
</a>
</div>
{% endif %}
</div>
</div>
</div>
</div>
<div class="mt-4 d-grid gap-2">
<a href="{{ path('app_admin_wiki_compare', {'key': page.key}) }}" class="btn btn-primary">
<i class="bi bi-arrows-angle-expand"></i> Voir la comparaison détaillée
</a>
<a href="{{ path('app_admin_wiki_random_suggestion') }}" class="btn btn-secondary">
<i class="bi bi-shuffle"></i> Autre suggestion aléatoire
</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,156 @@
{% extends 'base.html.twig' %}
{% block title %}Changements récents Wiki OSM{% endblock %}
{% block body %}
<div class="container mt-4">
{% include 'admin/_wiki_navigation.html.twig' %}
<h1>Changements récents Wiki OpenStreetMap</h1>
<p class="lead">Liste des changements récents dans l'espace de noms français du wiki OpenStreetMap.</p>
{% 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 %}
{% if team_members|length > 0 %}
<div class="card mb-4">
<div class="card-header bg-success text-white">
<h2>Équipe Wiki OSM FR</h2>
<p class="mb-0">Contributeurs classés par nombre de modifications</p>
</div>
<div class="card-body">
<div class="row">
{% for member in team_members %}
<div class="col-md-4 mb-3">
<div class="d-flex align-items-center">
<a href="{{ member.user_url }}" target="_blank" class="text-decoration-none">
<span class="fw-bold">{{ member.username }}</span>
</a>
<span class="badge bg-primary ms-2">{{ member.contributions }}</span>
<div class="ms-2 small">
<span class="text-success" title="Caractères ajoutés">+{{ member.chars_added }}</span>
{% if member.chars_changed > 0 %}
<span class="text-warning" title="Caractères modifiés">~{{ member.chars_changed }}</span>
{% endif %}
{% if member.chars_deleted > 0 %}
<span class="text-danger" title="Caractères supprimés">-{{ member.chars_deleted }}</span>
{% endif %}
</div>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
{% endif %}
<div class="card mb-4">
<div class="card-header bg-primary text-white">
<h2>Changements récents</h2>
<p class="mb-0">
<a href="https://wiki.openstreetmap.org/w/index.php?hidebots=1&hidepreviousrevisions=1&hidecategorization=1&hideWikibase=1&hidelog=1&hidenewuserlog=1&namespace=202&limit=500&days=30&enhanced=1&title=Special:RecentChanges&urlversion=2"
target="_blank" class="text-white">
<i class="bi bi-box-arrow-up-right"></i> Voir sur le wiki OSM
</a>
</p>
</div>
<div class="card-body">
{% if recent_changes|length > 0 %}
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead class="thead-dark">
<tr>
<th>Page</th>
<th>Date</th>
<th>Utilisateur</th>
<th>Commentaire</th>
<th>Taille</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for change in recent_changes %}
<tr>
<td>
<strong>
<a href="{{ change.page_url }}" target="_blank" class="text-decoration-none">
{{ change.page_name }}
</a>
</strong>
</td>
<td>{{ change.timestamp }}</td>
<td>
{% if change.user_url %}
<a href="{{ change.user_url }}" target="_blank" class="text-decoration-none">
{{ change.user }}
</a>
{% else %}
{{ change.user }}
{% endif %}
</td>
<td>
{{ change.comment }}
{% if change.added_text or change.removed_text %}
<div class="mt-1 small">
{% if change.added_text %}
<span class="text-success">{{ change.added_text }}</span>
{% endif %}
{% if change.removed_text %}
<span class="text-danger">{{ change.removed_text }}</span>
{% endif %}
</div>
{% endif %}
</td>
<td>
{% if change.change_size starts with '+' or change.change_size > 0 %}
<span class="text-success">{{ change.change_size }}</span>
{% elseif change.change_size starts with '' or change.change_size < 0 %}
<span class="text-danger">{{ change.change_size }}</span>
{% else %}
{{ change.change_size }}
{% endif %}
</td>
<td>
<a href="{{ change.page_url }}" target="_blank" class="btn btn-sm btn-outline-primary">
<i class="bi bi-box-arrow-up-right"></i> Voir
</a>
{% if change.diff_url %}
<a href="{{ change.diff_url }}" target="_blank" class="btn btn-sm btn-outline-secondary mt-1">
<i class="bi bi-file-diff"></i> Diff
</a>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="alert alert-info">
<p><i class="bi bi-info-circle"></i> Aucun changement récent n'a été trouvé.</p>
</div>
{% endif %}
</div>
</div>
<div class="card mb-4">
<div class="card-header">
<h2>À propos des changements récents</h2>
</div>
<div class="card-body">
<p>Cette page affiche les changements récents dans l'espace de noms français (FR:) du wiki OpenStreetMap.</p>
<p>Ces informations sont utiles pour suivre les traductions manquantes et les mises à jour des pages wiki.</p>
<p>Les données sont mises à jour automatiquement toutes les heures.</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

@ -0,0 +1,121 @@
{% extends 'base.html.twig' %}
{% block title %}Pages Wiki avec suppressions suspectes{% endblock %}
{% block body %}
<div class="container mt-4">
{% include 'admin/_wiki_navigation.html.twig' %}
<h1>Pages Wiki avec suppressions suspectes</h1>
<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 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 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>Page</th>
<th>Suppression</th>
<th>Date</th>
<th>Utilisateur</th>
<th>Commentaire</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for deletion in recent_deletions %}
<tr>
<td><strong>{{ deletion.page_title }}</strong></td>
<td>
<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="{{ 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="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>
</tr>
{% endfor %}
</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 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">
<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>
</ul>
<p>Ces pages sont des candidates prioritaires pour une mise à jour de la traduction française.</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

@ -0,0 +1,266 @@
{% extends 'base.html.twig' %}
{% block title %}Propositions de tags OSM{% endblock %}
{% block stylesheets %}
{{ parent() }}
<style>
.vote-bar {
height: 24px;
border-radius: 4px;
overflow: hidden;
display: flex;
margin-bottom: 10px;
}
.vote-approve {
background-color: #28a745;
height: 100%;
}
.vote-abstain {
background-color: #ffc107;
height: 100%;
}
.vote-oppose {
background-color: #dc3545;
height: 100%;
}
.vote-count {
font-size: 0.85rem;
font-weight: bold;
color: white;
text-align: center;
padding: 2px 5px;
}
.voter-badge {
display: inline-block;
margin-right: 5px;
margin-bottom: 5px;
padding: 2px 8px;
border-radius: 12px;
font-size: 0.8rem;
}
.voter-approve {
background-color: rgba(40, 167, 69, 0.2);
border: 1px solid rgba(40, 167, 69, 0.4);
}
.voter-abstain {
background-color: rgba(255, 193, 7, 0.2);
border: 1px solid rgba(255, 193, 7, 0.4);
}
.voter-oppose {
background-color: rgba(220, 53, 69, 0.2);
border: 1px solid rgba(220, 53, 69, 0.4);
}
</style>
{% endblock %}
{% block body %}
<div class="container mt-4">
{% include 'admin/_wiki_navigation.html.twig' %}
<h1>Propositions de tags OSM</h1>
<p class="lead">Liste des propositions de tags OpenStreetMap actuellement en cours de vote ou récemment modifiées.</p>
{% 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 bg-primary text-white">
<h2>Propositions en cours de vote</h2>
</div>
<div class="card-body">
{% 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>Proposition</th>
<th>Proposé par</th>
<th>Votes</th>
<th>Statut</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for proposal in voting_proposals %}
<tr>
<td><strong>{{ proposal.feature }}</strong></td>
<td>
{% if proposal.proposer %}
<a href="https://wiki.openstreetmap.org/wiki/User:{{ proposal.proposer }}" target="_blank">
{{ proposal.proposer }}
</a>
{% else %}
<span class="text-muted">Non spécifié</span>
{% endif %}
</td>
<td>
{% if proposal.votes is defined and proposal.total_votes > 0 %}
<div>
<strong>{{ proposal.total_votes }} votes</strong>
</div>
<div class="vote-bar">
{% if proposal.approve_percentage > 0 %}
<div class="vote-approve" style="width: {{ proposal.approve_percentage }}%">
<span class="vote-count">{{ proposal.votes.approve.count }} ({{ proposal.approve_percentage }}%)</span>
</div>
{% endif %}
{% if proposal.abstain_percentage > 0 %}
<div class="vote-abstain" style="width: {{ proposal.abstain_percentage }}%">
<span class="vote-count">{{ proposal.votes.abstain.count }} ({{ proposal.abstain_percentage }}%)</span>
</div>
{% endif %}
{% if proposal.oppose_percentage > 0 %}
<div class="vote-oppose" style="width: {{ proposal.oppose_percentage }}%">
<span class="vote-count">{{ proposal.votes.oppose.count }} ({{ proposal.oppose_percentage }}%)</span>
</div>
{% endif %}
</div>
<div class="mt-2">
<button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#voters{{ loop.index }}" aria-expanded="false" aria-controls="voters{{ loop.index }}">
Voir les votants
</button>
<div class="collapse mt-2" id="voters{{ loop.index }}">
{% if proposal.votes.approve.users|length > 0 %}
<div class="mb-1">
<strong>Approbations:</strong>
<div>
{% for user in proposal.votes.approve.users %}
<span class="voter-badge voter-approve">{{ user.username }}</span>
{% endfor %}
</div>
</div>
{% endif %}
{% if proposal.votes.abstain.users|length > 0 %}
<div class="mb-1">
<strong>Abstentions:</strong>
<div>
{% for user in proposal.votes.abstain.users %}
<span class="voter-badge voter-abstain">{{ user.username }}</span>
{% endfor %}
</div>
</div>
{% endif %}
{% if proposal.votes.oppose.users|length > 0 %}
<div>
<strong>Oppositions:</strong>
<div>
{% for user in proposal.votes.oppose.users %}
<span class="voter-badge voter-oppose">{{ user.username }}</span>
{% endfor %}
</div>
</div>
{% endif %}
</div>
</div>
{% else %}
<span class="text-muted">Aucun vote</span>
{% endif %}
</td>
<td>
<span class="badge bg-primary">En vote</span>
</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 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>
</div>
<div class="card mb-4">
<div class="card-header">
<h2>À propos des propositions de tags</h2>
</div>
<div class="card-body">
<p>Les propositions de tags sont un processus communautaire pour introduire de nouveaux tags ou modifier des tags existants dans OpenStreetMap.</p>
<p>Le processus typique comprend les étapes suivantes :</p>
<ol>
<li><strong>Brouillon</strong> : La proposition initiale est rédigée et discutée.</li>
<li><strong>RFC (Request for Comments)</strong> : La proposition est ouverte aux commentaires de la communauté.</li>
<li><strong>Vote</strong> : La proposition est soumise au vote de la communauté.</li>
<li><strong>Approbation ou rejet</strong> : Selon les résultats du vote, la proposition est approuvée ou rejetée.</li>
</ol>
<p>Vous pouvez participer à ce processus en commentant les propositions ou en votant lorsqu'elles sont en phase de vote.</p>
<div class="d-grid gap-2 col-md-6 mx-auto mt-3">
<a href="https://wiki.openstreetmap.org/wiki/Proposed_features" target="_blank" class="btn btn-outline-primary">
<i class="bi bi-box-arrow-up-right"></i> Voir toutes les propositions sur le wiki OSM
</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 %}

139
templates/base.html.twig Normal file
View file

@ -0,0 +1,139 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Welcome!{% endblock %}</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 128 128%22><text y=%221.2em%22 font-size=%2296%22>⚫️</text></svg>">
<link rel="icon" type="image/png" href="{{ asset('logo-osm.png') }}">
{# Run `composer require symfony/webpack-encore-bundle` to start using Symfony UX #}
{% block stylesheets %}
{{ encore_entry_link_tags('app') }}
<link href='{{ asset('js/mapbox/mapbox-gl.css') }}' rel='stylesheet' />
<!-- CSS Bootstrap -->
<link rel="stylesheet" href="{{ asset('css/bootstrap-icons.css') }}">
<link rel="stylesheet" href="{{ asset('js/bootstrap/bootstrap-icons.min.css') }}">
<link href="{{ asset('js/bootstrap/bootstrap.min.css') }}" rel="stylesheet">
<!-- CSS personnalisé -->
<link rel="stylesheet" href="{{ asset('css/main.css') }}">
{% endblock %}
</head>
<body>
<header class="main-header">
<div class="container">
<div class="row align-items-center">
<div class="col-12">
<a href="{{ path('app_public_index') }}" class="d-flex align-items-center">
<h1 class="mb-0 mt-2">
<img src="{{ asset('logo-osm.png') }}" alt="Logo OSM" class="me-2" style="width: 30px; height: 30px;">
Qualiwiki OpenStreetMap</h1>
</a>
</div>
</div>
{% for label, messages in app.flashes %}
{% for message in messages %}
<div class="alert alert-{{ label }} is-{{ label }} alert-dismissible fade show mt-3" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endfor %}
<div class="row mt-3">
<div class="col-12">
{% include 'public/nav.html.twig' %}
</div>
</div>
</div>
</header>
<main class="body-landing">
{% block body %}{% endblock %}
</main>
<footer class="main-footer">
<div class="container">
<div class="row mb-4">
<div class="col-12">
{% include 'public/nav.html.twig' %}
</div>
</div>
<div class="row">
<div class="col-12">
<p class="mb-2">OpenStreetMap Mon Commerce</p>
</div>
<div class="col-md-4 col-12">
<p class="mb-2">
Licence AGPLv3+, fait par
<a href="https://mastodon.cipherbliss.com/@tykayn">Tykayn</a> de
<a href="https://www.cipherbliss.com">CipherBliss EI</a>,
membre de la fédération des professionels d'OpenStreetMap
</p>
<p class="mb-2">
<a href="https://www.openstreetmap.org/copyright">OpenStreetMap France</a>
</p>
</div>
<div class="col-md-4 col-12">
<div id="userChangesHistory"></div>
</div>
<div class="col-md-4 col-12">
<div id="qr-share" class="mb-12">
partagez cette page :
<br>
<div id="qrcode"></div>
</div>
<div class="col-md-4 col-12">
<p class="mb-0">
<a href="https://www.openstreetmap.org/copyright">Sources du logiciel</a>
</p>
<p class="mb-2">
Sources des données : <a href="https://www.openstreetmap.org/">OpenStreetMap</a>
</p>
</div>
</div>
</div>
<div class="row">
<div class="col-12">
<p class="mb-2">
<a href="https://forum.openstreetmap.fr/t/osm-mon-commerce/34403/11" class="btn btn-outline-info ms-auto suggestion-float-btn" target="_blank" rel="noopener">
<i class="bi bi-chat-dots"></i> Faire une suggestion
</a>
<a href="https://osm-commerces.cipherbliss.com/api/v1/stats_geojson" target="_blank">Documentation de l'API (GeoJSON)</a>
</p>
</div>
</div>
</div>
</footer>
{% block javascripts %}
{{ encore_entry_script_tags('app') }}
<script src="{{ asset('js/bootstrap/bootstrap.bundle.min.js') }}"></script>
<script src='{{ asset('js/maplibre/maplibre-gl.js') }}'></script>
<!-- Script pour le tri automatique des tableaux -->
{# <script src="{{ asset('js/bootstrap/Sortable.min.js') }}"></script> #}
<script src="{{ asset('js/qrcode/qrcode.min.js') }}"></script>
<script>
new QRCode(document.getElementById('qrcode'), {
text: window.location.href,
width: 100,
height: 100,
colorDark : '#000000',
colorLight : '#ffffff',
correctLevel : QRCode.CorrectLevel.H
});
</script>
{% endblock %}
{% block completion_progress %}
{% endblock %}
</body>
</html>

View file

@ -0,0 +1,51 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Welcome!{% endblock %}</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 128 128%22><text y=%221.2em%22 font-size=%2296%22>⚫️</text></svg>">
<link rel="icon" type="image/png" href="{{ asset('logo-osm.png') }}">
{# Run `composer require symfony/webpack-encore-bundle` to start using Symfony UX #}
{% block stylesheets %}
{{ encore_entry_link_tags('app') }}
<link href='{{ asset('js/mapbox/mapbox-gl.css') }}' rel='stylesheet' />
<!-- CSS Bootstrap -->
<link rel="stylesheet" href="{{ asset('css/bootstrap-icons.css') }}">
<link rel="stylesheet" href="{{ asset('js/bootstrap/bootstrap-icons.min.css') }}">
<link href="{{ asset('js/bootstrap/bootstrap.min.css') }}" rel="stylesheet">
<!-- CSS personnalisé -->
<link rel="stylesheet" href="{{ asset('css/main.css') }}">
{% endblock %}
</head>
<body>
{% for label, messages in app.flashes %}
{% for message in messages %}
<div class="alert alert-{{ label }} is-{{ label }} alert-dismissible fade show mt-3" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endfor %}
<main class="body-landing">
{% block body %}{% endblock %}
</main>
<footer class="main-footer">
{% include 'public/nav.html.twig' %}
</footer>
{% block javascripts %}
{{ encore_entry_script_tags('app') }}
<script src="{{ asset('js/bootstrap/bootstrap.bundle.min.js') }}"></script>
<script src='{{ asset('js/maplibre/maplibre-gl.js') }}'></script>
{% endblock %}
{% block completion_progress %}
{% endblock %}
</body>
</html>

View file

@ -0,0 +1,57 @@
<nav class="navbar navbar-expand-lg navbar-light bg-light mb-4">
<div class="container-fluid">
<a class="navbar-brand" href="{{ path('app_admin_wiki') }}">Wiki OSM</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#wikiNavbar" aria-controls="wikiNavbar" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="wikiNavbar">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link {% if app.request.get('_route') == 'app_admin_wiki' %}active{% endif %}" href="{{ path('app_admin_wiki') }}">
<i class="bi bi-list-ul"></i> Liste des pages
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if app.request.get('_route') == 'app_admin_wiki_random_suggestion' %}active{% endif %}" href="{{ path('app_admin_wiki_random_suggestion') }}">
<i class="bi bi-shuffle"></i> Suggestion aléatoire
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if app.request.get('_route') == 'app_admin_wiki_tag_proposals' %}active{% endif %}" href="{{ path('app_admin_wiki_tag_proposals') }}">
<i class="bi bi-tag"></i> Propositions de tags
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if app.request.get('_route') == 'app_admin_wiki_archived_proposals' %}active{% endif %}" href="{{ path('app_admin_wiki_archived_proposals') }}">
<i class="bi bi-archive"></i> Propositions archivées
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if app.request.get('_route') == 'app_admin_wiki_suspicious_deletions' %}active{% endif %}" href="{{ path('app_admin_wiki_suspicious_deletions') }}">
<i class="bi bi-exclamation-triangle"></i> Suppressions suspectes
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if app.request.get('_route') == 'app_admin_wiki_missing_translations' %}active{% endif %}" href="{{ path('app_admin_wiki_missing_translations') }}">
<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>
<li class="nav-item">
<a class="nav-link {% if app.request.get('_route') == 'app_admin_wiki_recent_changes' %}active{% endif %}" href="{{ path('app_admin_wiki_recent_changes') }}">
<i class="bi bi-clock-history"></i> Changements récents
</a>
</li>
</ul>
</div>
</div>
</nav>

View file

@ -0,0 +1,17 @@
<nav class="navbar navbar-expand-lg navbar-light bg-light mb-4 rounded shadow-sm">
<div class="container-fluid">
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link active" href="{{ path('app_public_index') }}">
<i class="bi bi-house-fill"></i>
{{ 'accueil'|trans }}
</a>
</li>
</ul>
</div>
</div>
</nav>

View file

@ -0,0 +1,506 @@
{% extends 'base.html.twig' %}
{% block title %}Pages Wiki OSM{% endblock %}
{% block body %}
<div class="container mt-4">
{% include 'admin/_wiki_navigation.html.twig' %}
<h1>Pages Wiki OpenStreetMap</h1>
<p class="lead">Outil de qualité des des pages wiki OpenStreetMap en français et en anglais pour les clés OSM
les plus utilisées.
<a href="https://forum.openstreetmap.fr/t/fabriquer-un-outil-de-qualite-pour-le-wiki-osm/36814">
Venez discuter QualiWiki sur le forum
</a>
</p>
<div class="card mb-4">
<div class="card-header">
<h2>Liste des pages wiki</h2>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead class="thead-dark">
<tr>
<th rowspan="2">Image</th>
<th rowspan="2">Clé</th>
<th colspan="4" class="text-center">Différences FR vs EN</th>
<th rowspan="2" class="text-center">Score de<br>décrépitude</th>
<th rowspan="2" class="text-center">Liens</th>
</tr>
<tr>
<th class="text-center">Sections</th>
<th class="text-center">Mots</th>
<th class="text-center">Liens</th>
<th class="text-center">Images</th>
</tr>
</thead>
<tbody>
{% for key, languages in wiki_pages %}
{% if languages['en'] is defined and languages['fr'] is defined %}
<tr>
<td>
<img src="{{ languages['en'].description_img_url }}" alt="image"
style="height: 2rem;">
</td>
<td>
<strong>{{ key }}</strong>
</td>
{% set diff = page_differences[key] %}
<td class="text-center">
{% if diff.section_diff > 0 %}
<span class="badge bg-success">{{ diff.section_diff_formatted }}</span>
{% elseif diff.section_diff < 0 %}
<span class="badge bg-danger">{{ diff.section_diff_formatted }}</span>
{% else %}
<span class="badge bg-secondary">0</span>
{% endif %}
</td>
<td class="text-center">
{% if diff.word_diff > 0 %}
<span class="badge bg-success">{{ diff.word_diff_formatted }}</span>
{% elseif diff.word_diff < 0 %}
<span class="badge bg-danger">{{ diff.word_diff_formatted }}</span>
{% else %}
<span class="badge bg-secondary">0</span>
{% endif %}
</td>
<td class="text-center">
{% if diff.link_diff > 0 %}
<span class="badge bg-success">{{ diff.link_diff_formatted }}</span>
{% elseif diff.link_diff < 0 %}
<span class="badge bg-danger">{{ diff.link_diff_formatted }}</span>
{% else %}
<span class="badge bg-secondary">0</span>
{% endif %}
</td>
<td class="text-center">
{% if diff.media_diff > 0 %}
<span class="badge bg-success">{{ diff.media_diff_formatted }}</span>
{% elseif diff.media_diff < 0 %}
<span class="badge bg-danger">{{ diff.media_diff_formatted }}</span>
{% else %}
<span class="badge bg-secondary">0</span>
{% endif %}
</td>
<td class="text-center">
{% set score = languages['en'].staleness_score|default(0) %}
{% if score > 50 %}
<span class="badge bg-danger">{{ score }}</span>
{% elseif score > 20 %}
<span class="badge bg-warning text-dark">{{ score }}</span>
{% else %}
<span class="badge bg-success">{{ score }}</span>
{% endif %}
</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">
<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">
<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">
<i class="bi bi-arrows-angle-expand"></i> Comparer
</a>
</div>
</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% if missing_translations|length > 0 %}
<div class="card mb-4">
<div class="card-header bg-warning text-dark">
<h2>Pages manquantes en français ({{ missing_translations|length }})</h2>
</div>
<div class="card-body">
<p>Ces pages wiki ont une version anglaise mais pas de traduction française.</p>
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead class="thead-dark">
<tr>
<th>Clé</th>
<th>Sections</th>
<th>Mots</th>
<th>Liens</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for key, page in missing_translations %}
<tr>
<td><strong>{{ key }}</strong></td>
<td>{{ page.sections }}</td>
<td>{{ page.word_count }}</td>
<td>{{ page.link_count }}</td>
<td class="text-center">
<div class="btn-group" role="group">
<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_create_french', {'key': key}) }}"
class="btn btn-sm btn-success"
title="Créer une traduction française">
<i class="bi bi-plus-circle"></i> Traduire
</a>
{# <a href="{{ path('app_admin_wiki_compare', {'key': key}) }}" #}
{# class="btn btn-sm btn-outline-secondary" #}
{# title="Voir les détails et comparer"> #}
{# <i class="bi bi-arrows-angle-expand"></i> Comparer #}
{# </a> #}
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endif %}
{% if specific_pages is defined and specific_pages|length > 0 %}
<div class="card mb-4">
<div class="card-header bg-primary text-white">
<h2>Pages spécifiques ({{ specific_pages|length }})</h2>
</div>
<div class="card-body">
<p>Ces pages wiki sont des pages spécifiques qui ont été sélectionnées pour une comparaison
particulière.</p>
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead class="thead-dark">
<tr>
<th>Titre</th>
<th>Raison</th>
<th>Score de décrépitude</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for page in specific_pages %}
<tr>
<td>
<div class="d-flex align-items-center">
{% if page.en_page.description_img_url is defined and page.en_page.description_img_url %}
<div class="me-3">
<img src="{{ page.en_page.description_img_url }}"
alt="{{ page.key }}"
style="max-width: 80px; max-height: 60px; object-fit: contain;">
</div>
{% endif %}
<div>
<strong>{{ page.key }}</strong>
<span class="badge bg-primary">Spécifique</span>
</div>
</div>
</td>
<td>
{{ page.reason }}
</td>
<td>
{% if page.staleness_score is defined %}
<div class="progress" style="height: 20px;">
{% set score_class = page.staleness_score > 70 ? 'bg-danger' : (page.staleness_score > 40 ? 'bg-warning' : 'bg-success') %}
<div class="progress-bar {{ score_class }}" role="progressbar"
style="width: {{ page.staleness_score }}%;"
aria-valuenow="{{ page.staleness_score }}"
aria-valuemin="0"
aria-valuemax="100">
{{ page.staleness_score }}
</div>
</div>
{% else %}
<span class="text-muted">Non disponible</span>
{% endif %}
</td>
<td class="text-center">
<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>
{% if page.fr_page %}
<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>
{% else %}
<a href="{{ path('app_admin_wiki_create_french', {'key': page.key}) }}"
class="btn btn-sm btn-success"
title="Créer une traduction française">
<i class="bi bi-plus-circle"></i> Traduire
</a>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endif %}
{% if pages_unavailable_in_english|length > 0 %}
<div class="card mb-4">
<div class="card-header bg-info text-dark">
<h2>Pages françaises non disponibles en Anglais ({{ pages_unavailable_in_english|length }})</h2>
</div>
<div class="card-body">
<p>Ces pages wiki ont une version française mais pas de traduction anglaise.</p>
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead class="thead-dark">
<tr>
<th>Titre</th>
<th>Score de décrépitude</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for page in pages_unavailable_in_english %}
<tr>
<td>
<div class="d-flex align-items-center">
{% if page.description_img_url is defined and page.description_img_url %}
<div class="me-3">
<img src="{{ page.description_img_url }}" alt="{{ page.title }}"
style="max-width: 80px; max-height: 60px; object-fit: contain;">
</div>
{% endif %}
<div>
<strong>{{ page.title }}</strong>
</div>
</div>
</td>
<td>
{% if page.outdatedness_score is defined %}
<div class="progress" style="height: 20px;">
{% set score_class = page.outdatedness_score > 70 ? 'bg-danger' : (page.outdatedness_score > 40 ? 'bg-warning' : 'bg-success') %}
<div class="progress-bar {{ score_class }}" role="progressbar"
style="width: {{ page.outdatedness_score }}%;"
aria-valuenow="{{ page.outdatedness_score }}"
aria-valuemin="0"
aria-valuemax="100">
{{ page.outdatedness_score }}
</div>
</div>
{% else %}
<span class="text-muted">Non disponible</span>
{% endif %}
</td>
<td class="text-center">
<div class="btn-group" role="group">
<a href="{{ page.url }}" target="_blank"
class="btn btn-sm btn-outline-info" title="Version française">
<i class="bi bi-flag-fill"></i> FR
</a>
{% set en_url = page.url|replace({'FR:': ''}) %}
<a href="{{ en_url }}" target="_blank"
class="btn btn-sm btn-outline-primary"
title="Créer une traduction anglaise">
<i class="bi bi-translate"></i> créer EN
</a>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endif %}
{% if newly_created_pages is defined and newly_created_pages|length > 0 %}
<div class="card mb-4">
<div class="card-header bg-success text-white">
<h2>Pages françaises récemment créées ({{ newly_created_pages|length }})</h2>
</div>
<div class="card-body">
<p>Ces pages wiki françaises ont été récemment créées et étaient auparavant dans la liste des pages
non disponibles en français.</p>
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead class="thead-dark">
<tr>
<th>Titre</th>
<th>Version anglaise</th>
<th>Date de création</th>
<th>Créée par</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for page in newly_created_pages %}
<tr>
<td>
<div class="d-flex align-items-center">
<div>
<strong>{{ page.title }}</strong>
<span class="badge bg-success">Nouvelle</span>
</div>
</div>
</td>
<td>
{{ page.en_title }}
</td>
<td>
{{ page.created_at }}
</td>
<td>
{{ page.created_by }}
</td>
<td class="text-center">
<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-flag-fill"></i> FR
</a>
<a href="{{ page.en_url }}" target="_blank"
class="btn btn-sm btn-outline-primary" title="Version anglaise">
<i class="bi bi-flag-fill"></i> EN
</a>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endif %}
<p>
le score de fraîcheur prend en compte d'avantage la différence entre le nombre de mots que l'ancienneté de
modification.
On compte aussi le nombre de sections et de liens.
</p>
<div class="mt-3">
<div class="card mb-4">
<div class="card-header">
<h2>Graphe de décrépitude</h2>
</div>
<div class="card-body">
<canvas id="decrepitudeChart" height="300"></canvas>
</div>
</div>
</div>
</div>
{% endblock %}
{% block javascripts %}
{{ parent() }}
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function () {
// Collect data from the table
const labels = [];
const scores = [];
const colors = [];
{% for key, languages in wiki_pages %}
{% if languages['en'] is defined and languages['fr'] is defined %}
labels.push("{{ key }}");
{% set score = languages['en'].staleness_score|default(0) %}
scores.push({{ score }});
// Set color based on score
{% if score > 50 %}
colors.push('rgba(220, 53, 69, 0.7)'); // danger
{% elseif score > 20 %}
colors.push('rgba(255, 193, 7, 0.7)'); // warning
{% else %}
colors.push('rgba(25, 135, 84, 0.7)'); // success
{% endif %}
{% endif %}
{% endfor %}
// Sort data by score (descending)
const indices = Array.from(Array(scores.length).keys())
.sort((a, b) => scores[b] - scores[a]);
const sortedLabels = indices.map(i => labels[i]);
const sortedScores = indices.map(i => scores[i]);
const sortedColors = indices.map(i => colors[i]);
// Limit to top 20 pages for readability
const displayLimit = 20;
const displayLabels = sortedLabels.slice(0, displayLimit);
const displayScores = sortedScores.slice(0, displayLimit);
const displayColors = sortedColors.slice(0, displayLimit);
// Create the chart
const ctx = document.getElementById('decrepitudeChart').getContext('2d');
new Chart(ctx, {
type: 'bar',
data: {
labels: displayLabels,
datasets: [{
label: 'Score de décrépitude',
data: displayScores,
backgroundColor: displayColors,
borderColor: displayColors.map(c => c.replace('0.7', '1')),
borderWidth: 1
}]
},
options: {
indexAxis: 'y',
responsive: true,
plugins: {
legend: {
display: false
},
tooltip: {
callbacks: {
label: function (context) {
return `Score: ${context.raw}`;
}
}
}
},
scales: {
x: {
beginAtZero: true,
max: 100,
title: {
display: true,
text: 'Score de décrépitude (0-100)'
}
}
}
}
});
});
</script>
{% endblock %}

View file

@ -0,0 +1,701 @@
{% extends 'base.html.twig' %}
{% block title %}Propositions archivées OSM{% endblock %}
{% block stylesheets %}
{{ parent() }}
<style>
.vote-bar {
height: 24px;
border-radius: 4px;
overflow: hidden;
display: flex;
margin-bottom: 10px;
}
.vote-approve {
background-color: #28a745;
height: 100%;
}
.vote-abstain {
background-color: #ffc107;
height: 100%;
}
.vote-oppose {
background-color: #dc3545;
height: 100%;
}
.vote-count {
font-size: 0.85rem;
font-weight: bold;
color: white;
text-align: center;
padding: 2px 5px;
}
.proposal-card {
margin-bottom: 1.5rem;
transition: transform 0.2s;
}
.proposal-card:hover {
transform: translateY(-5px);
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.proposal-metadata {
font-size: 0.85rem;
color: #6c757d;
}
.voter-badge {
display: inline-block;
margin-right: 5px;
margin-bottom: 5px;
padding: 2px 8px;
border-radius: 12px;
font-size: 0.8rem;
}
.voter-approve {
background-color: rgba(40, 167, 69, 0.2);
border: 1px solid rgba(40, 167, 69, 0.4);
}
.voter-abstain {
background-color: rgba(255, 193, 7, 0.2);
border: 1px solid rgba(255, 193, 7, 0.4);
}
.voter-oppose {
background-color: rgba(220, 53, 69, 0.2);
border: 1px solid rgba(220, 53, 69, 0.4);
}
.stats-card {
transition: transform 0.2s;
}
.stats-card:hover {
transform: translateY(-5px);
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.stats-value {
font-size: 2rem;
font-weight: bold;
}
.stats-label {
font-size: 0.9rem;
color: #6c757d;
}
.top-voters-table th, .top-voters-table td {
padding: 0.5rem;
}
.vote-duration {
display: inline-block;
padding: 3px 8px;
border-radius: 4px;
background-color: #e9ecef;
margin-top: 5px;
font-size: 0.85rem;
}
.comment-text {
font-style: italic;
color: #495057;
background-color: #f8f9fa;
padding: 8px;
border-radius: 4px;
margin-top: 5px;
margin-bottom: 10px;
border-left: 3px solid #dee2e6;
}
.chart-container {
position: relative;
height: 300px;
margin-bottom: 20px;
}
</style>
{% endblock %}
{% block body %}
<div class="container mt-4">
{% include 'admin/_wiki_navigation.html.twig' %}
<h1>Propositions archivées OpenStreetMap</h1>
<p class="lead">Analyse des votes sur les propositions archivées du wiki OSM</p>
{% if last_updated %}
<div class="alert alert-info">
<div class="row align-items-center">
<div class="col-md-6">
<i class="bi bi-info-circle"></i> Dernière mise à jour : {{ last_updated|date('d/m/Y H:i') }}
</div>
<div class="col-md-6 text-end">
<form class="d-inline-flex align-items-center" method="get" action="{{ path('app_admin_wiki_archived_proposals') }}">
<div class="me-2">
<label for="limit" class="me-2">Limiter à:</label>
<select name="limit" id="limit" class="form-select form-select-sm d-inline-block" style="width: auto;">
<option value="">Toutes les propositions</option>
<option value="10" {% if limit == 10 %}selected{% endif %}>10 propositions</option>
<option value="20" {% if limit == 20 %}selected{% endif %}>20 propositions</option>
<option value="50" {% if limit == 50 %}selected{% endif %}>50 propositions</option>
<option value="100" {% if limit == 100 %}selected{% endif %}>100 propositions</option>
{% if limit and limit not in [10, 20, 50, 100] %}
<option value="{{ limit }}" selected>{{ limit }} propositions</option>
{% endif %}
</select>
</div>
<input type="hidden" name="refresh" value="1">
<button type="submit" class="btn btn-sm btn-outline-primary">
<i class="bi bi-arrow-clockwise"></i> Rafraîchir les données
</button>
</form>
</div>
</div>
{% if limit %}
<div class="mt-2">
<small class="text-muted">
<i class="bi bi-info-circle"></i> Les données sont limitées à {{ limit }} propositions.
<a href="{{ path('app_admin_wiki_archived_proposals') }}">Voir toutes les propositions</a>
</small>
</div>
{% endif %}
</div>
{% endif %}
{% if statistics %}
<div class="card mb-4">
<div class="card-header">
<h2>Statistiques globales</h2>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-3 col-sm-6 mb-3">
<div class="card stats-card h-100">
<div class="card-body text-center">
<div class="stats-value">{{ statistics.total_proposals }}</div>
<div class="stats-label">Propositions analysées</div>
</div>
</div>
</div>
<div class="col-md-3 col-sm-6 mb-3">
<div class="card stats-card h-100">
<div class="card-body text-center">
<div class="stats-value">{{ statistics.total_votes }}</div>
<div class="stats-label">Votes au total</div>
</div>
</div>
</div>
<div class="col-md-3 col-sm-6 mb-3">
<div class="card stats-card h-100">
<div class="card-body text-center">
<div class="stats-value">{{ statistics.avg_votes_per_proposal }}</div>
<div class="stats-label">Votes par proposition (moyenne)</div>
<div class="small text-muted">Excluant les propositions sans votes</div>
</div>
</div>
</div>
<div class="col-md-3 col-sm-6 mb-3">
<div class="card stats-card h-100">
<div class="card-body text-center">
<div class="stats-value">{{ statistics.unique_voters }}</div>
<div class="stats-label">Votants uniques</div>
</div>
</div>
</div>
</div>
<div class="row mt-3">
<div class="col-md-3 col-sm-6 mb-3">
<div class="card stats-card h-100">
<div class="card-body text-center">
<div class="stats-value">{{ statistics.median_votes_per_proposal }}</div>
<div class="stats-label">Votes par proposition (médiane)</div>
<div class="small text-muted">Excluant les propositions sans votes</div>
</div>
</div>
</div>
<div class="col-md-3 col-sm-6 mb-3">
<div class="card stats-card h-100">
<div class="card-body text-center">
<div class="stats-value">{{ statistics.std_dev_votes_per_proposal }}</div>
<div class="stats-label">Votes par proposition (écart type)</div>
<div class="small text-muted">Excluant les propositions sans votes</div>
</div>
</div>
</div>
</div>
{% if statistics.avg_vote_duration_days is defined %}
<div class="row mt-3">
<div class="col-md-3 col-sm-6 mb-3">
<div class="card stats-card h-100">
<div class="card-body text-center">
<div class="stats-value">{{ statistics.avg_vote_duration_days }}</div>
<div class="stats-label">Durée moyenne des votes (jours)</div>
</div>
</div>
</div>
</div>
{% endif %}
{% if statistics.status_distribution is defined and statistics.status_distribution|length > 0 %}
<div class="row mt-4">
<div class="col-md-6">
<h4>Répartition par statut</h4>
<div class="chart-container">
<canvas id="statusChart"></canvas>
</div>
</div>
<div class="col-md-6">
<h4>Détail des statuts</h4>
<table class="table table-sm table-striped">
<thead>
<tr>
<th>Statut</th>
<th>Nombre</th>
<th>Pourcentage</th>
</tr>
</thead>
<tbody>
{% for status, count in statistics.status_distribution %}
<tr>
<td>{{ status }}</td>
<td>{{ count }}</td>
<td>{{ (count / statistics.total_proposals * 100)|round(1) }}%</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
<div class="row mt-4">
<div class="col-12">
<h4>Répartition des années des propositions</h4>
<div class="chart-container">
<canvas id="yearDistributionChart"></canvas>
</div>
</div>
</div>
</div>
</div>
<div class="card mb-4">
<div class="card-header">
<h2>Contributeurs les plus actifs</h2>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover top-voters-table">
<thead>
<tr>
<th>#</th>
<th>Utilisateur</th>
<th>Total votes</th>
<th>Approbations</th>
<th>Abstentions</th>
<th>Oppositions</th>
<th>Répartition</th>
</tr>
</thead>
<tbody>
{% for voter in statistics.top_voters %}
<tr>
<td>{{ loop.index }}</td>
<td>
<a href="https://wiki.openstreetmap.org/wiki/User:{{ voter.username }}" target="_blank">
{{ voter.username }}
</a>
</td>
<td>{{ voter.total }}</td>
<td>{{ voter.approve }}</td>
<td>{{ voter.abstain }}</td>
<td>{{ voter.oppose }}</td>
<td>
<div class="vote-bar">
{% if voter.approve > 0 %}
<div class="vote-approve" style="width: {{ (voter.approve / voter.total * 100)|round }}%">
<span class="vote-count">{{ (voter.approve / voter.total * 100)|round }}%</span>
</div>
{% endif %}
{% if voter.abstain > 0 %}
<div class="vote-abstain" style="width: {{ (voter.abstain / voter.total * 100)|round }}%">
<span class="vote-count">{{ (voter.abstain / voter.total * 100)|round }}%</span>
</div>
{% endif %}
{% if voter.oppose > 0 %}
<div class="vote-oppose" style="width: {{ (voter.oppose / voter.total * 100)|round }}%">
<span class="vote-count">{{ (voter.oppose / voter.total * 100)|round }}%</span>
</div>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endif %}
<div class="card mb-4">
<div class="card-header d-flex justify-content-between align-items-center">
<h2 class="mb-0">Liste des propositions archivées</h2>
<div>
<div class="btn-group" role="group">
<button type="button" class="btn btn-outline-secondary btn-sm filter-btn active" data-filter="all">Toutes</button>
<button type="button" class="btn btn-outline-success btn-sm filter-btn" data-filter="approved">Approuvées</button>
<button type="button" class="btn btn-outline-danger btn-sm filter-btn" data-filter="rejected">Rejetées</button>
<button type="button" class="btn btn-outline-warning btn-sm filter-btn" data-filter="neutral">Neutres</button>
</div>
</div>
</div>
<div class="card-body">
<div class="row" id="proposals-container">
{% for proposal in proposals %}
{% set total_votes = proposal.votes.approve.count + proposal.votes.oppose.count + proposal.votes.abstain.count %}
{% set is_approved = proposal.votes.approve.count > proposal.votes.oppose.count %}
{% set is_rejected = proposal.votes.approve.count < proposal.votes.oppose.count %}
{% set is_neutral = proposal.votes.approve.count == proposal.votes.oppose.count %}
<div class="col-md-6 mb-4 proposal-item {% if is_approved %}approved{% elseif is_rejected %}rejected{% else %}neutral{% endif %}">
<div class="card proposal-card h-100 {% if is_approved %}border-success{% elseif is_rejected %}border-danger{% else %}border-warning{% endif %}">
<div class="card-header {% if is_approved %}bg-success text-white{% elseif is_rejected %}bg-danger text-white{% else %}bg-warning{% endif %}">
<h5 class="card-title mb-0">
<a href="{{ proposal.url }}" target="_blank" class="text-white">
{{ proposal.title }}
</a>
</h5>
</div>
<div class="card-body">
<div class="proposal-metadata mb-3">
{% if proposal.proposer %}
<div><strong>Proposé par :</strong> {{ proposal.proposer }}</div>
{% endif %}
{% if proposal.last_modified %}
<div><strong>Dernière modification :</strong> {{ proposal.last_modified }}</div>
{% endif %}
<div><strong>Sections :</strong> {{ proposal.section_count }}</div>
<div><strong>Liens :</strong> {{ proposal.link_count }}</div>
<div><strong>Mots :</strong> {{ proposal.word_count }}</div>
{% if proposal.votes.duration_days is defined %}
<div class="mt-2">
<span class="vote-duration">
<i class="bi bi-calendar-range"></i>
<strong>Durée du vote :</strong> {{ proposal.votes.duration_days }} jours
({{ proposal.votes.first_vote }}{{ proposal.votes.last_vote }})
</span>
</div>
{% endif %}
</div>
{% if total_votes > 0 %}
<h6>Résultats des votes ({{ total_votes }} votes)</h6>
<div class="vote-bar">
{% if proposal.votes.approve.count > 0 %}
<div class="vote-approve" style="width: {{ proposal.approve_percentage }}%">
<span class="vote-count">{{ proposal.votes.approve.count }} ({{ proposal.approve_percentage }}%)</span>
</div>
{% endif %}
{% if proposal.votes.abstain.count > 0 %}
<div class="vote-abstain" style="width: {{ proposal.abstain_percentage }}%">
<span class="vote-count">{{ proposal.votes.abstain.count }} ({{ proposal.abstain_percentage }}%)</span>
</div>
{% endif %}
{% if proposal.votes.oppose.count > 0 %}
<div class="vote-oppose" style="width: {{ proposal.oppose_percentage }}%">
<span class="vote-count">{{ proposal.votes.oppose.count }} ({{ proposal.oppose_percentage }}%)</span>
</div>
{% endif %}
</div>
<div class="mt-3">
<div class="accordion" id="votersAccordion{{ loop.index }}">
<div class="accordion-item">
<h2 class="accordion-header" id="headingVoters{{ loop.index }}">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
data-bs-target="#collapseVoters{{ loop.index }}" aria-expanded="false"
aria-controls="collapseVoters{{ loop.index }}">
Voir les votants
</button>
</h2>
<div id="collapseVoters{{ loop.index }}" class="accordion-collapse collapse"
aria-labelledby="headingVoters{{ loop.index }}"
data-bs-parent="#votersAccordion{{ loop.index }}">
<div class="accordion-body">
{% if proposal.votes.approve.users|length > 0 %}
<div class="mb-2">
<strong>Approbations ({{ proposal.votes.approve.count }}):</strong>
<div>
{% for user in proposal.votes.approve.users %}
<div class="mb-2">
<span class="voter-badge voter-approve">{{ user.username }}</span>
{% if user.comment is defined and user.comment is not empty %}
<div class="comment-text">{{ user.comment }}</div>
{% endif %}
</div>
{% endfor %}
</div>
</div>
{% endif %}
{% if proposal.votes.abstain.users|length > 0 %}
<div class="mb-2">
<strong>Abstentions ({{ proposal.votes.abstain.count }}):</strong>
<div>
{% for user in proposal.votes.abstain.users %}
<div class="mb-2">
<span class="voter-badge voter-abstain">{{ user.username }}</span>
{% if user.comment is defined and user.comment is not empty %}
<div class="comment-text">{{ user.comment }}</div>
{% endif %}
</div>
{% endfor %}
</div>
</div>
{% endif %}
{% if proposal.votes.oppose.users|length > 0 %}
<div>
<strong>Oppositions ({{ proposal.votes.oppose.count }}):</strong>
<div>
{% for user in proposal.votes.oppose.users %}
<div class="mb-2">
<span class="voter-badge voter-oppose">{{ user.username }}</span>
{% if user.comment is defined and user.comment is not empty %}
<div class="comment-text">{{ user.comment }}</div>
{% endif %}
</div>
{% endfor %}
</div>
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
{% else %}
<div class="alert alert-secondary">
Aucun vote trouvé pour cette proposition.
</div>
{% endif %}
</div>
<div class="card-footer">
<a href="{{ proposal.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>
{% else %}
<div class="alert alert-warning">
<i class="bi bi-exclamation-triangle"></i> Aucune proposition archivée n'a été trouvée.
</div>
{% endfor %}
</div>
</div>
</div>
</div>
{% endblock %}
{% block javascripts %}
{{ parent() }}
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Filtering functionality
const filterButtons = document.querySelectorAll('.filter-btn');
const proposalItems = document.querySelectorAll('.proposal-item');
filterButtons.forEach(button => {
button.addEventListener('click', function() {
// Remove active class from all buttons
filterButtons.forEach(btn => btn.classList.remove('active'));
// Add active class to clicked button
this.classList.add('active');
const filter = this.getAttribute('data-filter');
// Show/hide proposals based on filter
proposalItems.forEach(item => {
if (filter === 'all') {
item.style.display = 'block';
} else if (filter === 'approved' && item.classList.contains('approved')) {
item.style.display = 'block';
} else if (filter === 'rejected' && item.classList.contains('rejected')) {
item.style.display = 'block';
} else if (filter === 'neutral' && item.classList.contains('neutral')) {
item.style.display = 'block';
} else {
item.style.display = 'none';
}
});
});
});
// Initialize status distribution chart if it exists
const statusChartCanvas = document.getElementById('statusChart');
if (statusChartCanvas) {
// Get status distribution data from the template
const statusDistribution = {{ statistics.status_distribution|json_encode|raw }};
if (statusDistribution && Object.keys(statusDistribution).length > 0) {
const labels = Object.keys(statusDistribution);
const data = Object.values(statusDistribution);
// Generate colors for each status
const backgroundColors = [
'#28a745', // Approved - green
'#dc3545', // Rejected - red
'#ffc107', // Voting/Proposed - yellow
'#6c757d', // Abandoned/Inactive - gray
'#17a2b8', // Other statuses - blue
'#6610f2', // Other statuses - purple
'#fd7e14', // Other statuses - orange
'#20c997', // Other statuses - teal
'#e83e8c' // Other statuses - pink
];
// Create the chart
new Chart(statusChartCanvas, {
type: 'pie',
data: {
labels: labels,
datasets: [{
data: data,
backgroundColor: backgroundColors.slice(0, labels.length),
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'right',
labels: {
font: {
size: 12
}
}
},
tooltip: {
callbacks: {
label: function(context) {
const label = context.label || '';
const value = context.raw || 0;
const total = context.dataset.data.reduce((a, b) => a + b, 0);
const percentage = Math.round((value / total) * 100);
return `${label}: ${value} (${percentage}%)`;
}
}
}
}
}
});
}
}
// Initialize year distribution chart
const yearChartCanvas = document.getElementById('yearDistributionChart');
if (yearChartCanvas) {
// Get proposals data from the template
const proposals = {{ proposals|json_encode|raw }};
if (proposals && proposals.length > 0) {
// Extract years from last_modified dates
const yearCounts = {};
proposals.forEach(proposal => {
if (proposal.last_modified) {
// Extract year from the date string (format: "DD Month YYYY")
const yearMatch = proposal.last_modified.match(/\d{4}$/);
if (yearMatch) {
const year = yearMatch[0];
yearCounts[year] = (yearCounts[year] || 0) + 1;
}
}
});
// Sort years chronologically
const sortedYears = Object.keys(yearCounts).sort();
const counts = sortedYears.map(year => yearCounts[year]);
// Generate a color gradient for the bars
const colors = sortedYears.map((year, index) => {
// Create a gradient from blue to green
const ratio = index / (sortedYears.length - 1 || 1);
return `rgba(${Math.round(33 + (20 * ratio))}, ${Math.round(150 + (50 * ratio))}, ${Math.round(243 - (100 * ratio))}, 0.7)`;
});
// Create the chart
new Chart(yearChartCanvas, {
type: 'bar',
data: {
labels: sortedYears,
datasets: [{
label: 'Nombre de propositions',
data: counts,
backgroundColor: colors,
borderColor: colors.map(color => color.replace('0.7', '1')),
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
ticks: {
precision: 0
},
title: {
display: true,
text: 'Nombre de propositions'
}
},
x: {
title: {
display: true,
text: 'Année'
}
}
},
plugins: {
legend: {
display: false
},
tooltip: {
callbacks: {
title: function(context) {
return `Année ${context[0].label}`;
},
label: function(context) {
const count = context.raw;
return count > 1 ? `${count} propositions` : `${count} proposition`;
}
}
}
}
}
});
}
}
});
</script>
{% endblock %}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,115 @@
{% extends 'base.html.twig' %}
{% block title %}Créer une traduction française pour {{ key }}{% endblock %}
{% block stylesheets %}
{{ parent() }}
<style>
.iframe-container {
display: flex;
flex-direction: row;
height: calc(100vh - 200px);
min-height: 600px;
margin-bottom: 20px;
}
.iframe-container iframe {
flex: 1;
border: 1px solid #dee2e6;
height: 100%;
}
.iframe-container .iframe-separator {
width: 10px;
background-color: #f8f9fa;
border: 1px solid #dee2e6;
display: flex;
justify-content: center;
align-items: center;
}
.iframe-container .iframe-separator::after {
content: "⟺";
font-size: 20px;
color: #6c757d;
}
.iframe-header {
background-color: #f8f9fa;
padding: 10px;
border: 1px solid #dee2e6;
border-bottom: none;
font-weight: bold;
text-align: center;
}
@media (max-width: 768px) {
.iframe-container {
flex-direction: column;
height: auto;
}
.iframe-container iframe {
height: 500px;
}
.iframe-container .iframe-separator {
height: 10px;
width: 100%;
}
.iframe-container .iframe-separator::after {
content: "⟻⟼";
}
}
</style>
{% endblock %}
{% block body %}
<div class="container-fluid mt-4">
{% include 'admin/_wiki_navigation.html.twig' %}
<h1>Créer une traduction française pour "{{ key }}"</h1>
<p class="lead">Utilisez cette page pour traduire la page wiki en français. La page anglaise est affichée à gauche pour référence, et le formulaire d'édition de la page française est à droite.</p>
<div class="alert alert-info">
<i class="bi bi-info-circle"></i> Conseils pour la traduction :
<ul>
<li>Conservez la structure de la page anglaise (sections, sous-sections, etc.)</li>
<li>Traduisez le contenu en français en adaptant les exemples au contexte français si nécessaire</li>
<li>N'hésitez pas à consulter d'autres pages françaises pour vous inspirer du style et de la terminologie</li>
</ul>
</div>
<div class="row">
<div class="col-md-6">
<div class="iframe-header">
<i class="bi bi-flag-fill"></i> Version anglaise (référence)
</div>
</div>
<div class="col-md-6">
<div class="iframe-header">
<i class="bi bi-translate"></i> <a href="{{ french_edit_url }}">Création de la version française</a>
</div>
</div>
</div>
<div class="iframe-container">
<iframe src="{{ english_url }}" title="Version anglaise"></iframe>
<div class="iframe-separator"></div>
<iframe src="{{ french_edit_url }}" title="Édition française"></iframe>
</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>
<a href="{{ english_url }}" target="_blank" class="btn btn-outline-primary">
<i class="bi bi-box-arrow-up-right"></i> Ouvrir la version anglaise dans un nouvel onglet
</a>
<a href="{{ french_edit_url }}" target="_blank" class="btn btn-outline-info">
<i class="bi bi-box-arrow-up-right"></i> Ouvrir l'éditeur français dans un nouvel onglet
</a>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,83 @@
{% extends 'base.html.twig' %}
{% block title %}Pages Wiki françaises sans traduction anglaise{% endblock %}
{% block body %}
<div class="container mt-4">
{% include 'admin/_wiki_navigation.html.twig' %}
<h1>Pages Wiki françaises sans traduction anglaise</h1>
<p class="lead">Liste des pages françaises du wiki OSM qui n'ont pas de traduction en anglais.</p>
{% 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 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>Actions</th>
</tr>
</thead>
<tbody>
{% for page in untranslated_pages %}
<tr>
<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/{{ 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>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="alert alert-info">
<p><i class="bi bi-info-circle"></i> Aucune page française sans traduction anglaise n'a été trouvée.</p>
</div>
{% endif %}
</div>
</div>
<div class="card mb-4">
<div class="card-header">
<h2>À propos des pages sans traduction anglaise</h2>
</div>
<div class="card-body">
<p>Ces pages sont des contenus originaux créés en français qui n'ont pas encore été traduits en anglais.</p>
<p>Bien que la langue principale du wiki OSM soit l'anglais, il est parfois utile de créer d'abord du contenu dans sa langue maternelle, puis de le traduire.</p>
<p>Contribuer à la traduction de ces pages en anglais permet de :</p>
<ul>
<li>Partager les connaissances avec la communauté internationale</li>
<li>Améliorer la visibilité des contributions françaises</li>
<li>Faciliter la collaboration entre contributeurs de différentes langues</li>
</ul>
</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,238 @@
{% 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 %}
<!-- Filtres -->
<div class="mb-4">
<div class="btn-group" role="group" aria-label="Filtres">
<button type="button" class="btn btn-outline-primary active filter-btn" data-filter="all">Tous</button>
<button type="button" class="btn btn-outline-primary filter-btn" data-filter="wiki">Wiki</button>
<button type="button" class="btn btn-outline-primary filter-btn" data-filter="framacalc">Framacalc</button>
<button type="button" class="btn btn-outline-primary filter-btn" data-filter="has-wiki">Avec page wiki</button>
<button type="button" class="btn btn-outline-primary filter-btn" data-filter="no-wiki">Sans page wiki</button>
</div>
</div>
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4">
{% for group in local_groups %}
{% set source = group.source|default('wiki') %}
{% set has_wiki = group.has_wiki_page|default(true) %}
{% set filter_classes = source ~ ' ' ~ (has_wiki ? 'has-wiki' : 'no-wiki') %}
<div class="col group-item {{ filter_classes }}">
<div class="card h-100 {% if source == 'framacalc' and not has_wiki %}border-danger{% endif %}">
{% if source == 'framacalc' %}
<div class="card-header bg-light">
<span class="badge bg-secondary">Framacalc</span>
{% if has_wiki %}
<span class="badge bg-success">Page wiki</span>
{% else %}
<span class="badge bg-danger">Pas de page wiki</span>
{% endif %}
</div>
{% endif %}
<div class="card-body">
<h5 class="card-title">{{ group.name }}</h5>
{% if group.description %}
<p class="card-text">{{ group.description }}</p>
{% endif %}
{% if source == 'framacalc' and group.contact %}
<p class="card-text"><small class="text-muted">Contact: {{ group.contact }}</small></p>
{% endif %}
{% if source == 'framacalc' and group.website %}
<p class="card-text">
<a href="{{ group.website }}" target="_blank" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-globe"></i> Site web
</a>
</p>
{% endif %}
</div>
<div class="card-footer">
{% if source == 'wiki' or has_wiki %}
<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>
{% else %}
<a href="https://wiki.openstreetmap.org/wiki/Special:Search?search={{ group.name|url_encode }}"
target="_blank" class="btn btn-sm btn-outline-danger">
<i class="bi bi-search"></i> Rechercher sur le wiki
</a>
{% endif %}
</div>
</div>
</div>
{% endfor %}
</div>
<!-- JavaScript pour les filtres -->
<script>
document.addEventListener('DOMContentLoaded', function() {
const filterButtons = document.querySelectorAll('.filter-btn');
const groupItems = document.querySelectorAll('.group-item');
filterButtons.forEach(button => {
button.addEventListener('click', function() {
// Remove active class from all buttons
filterButtons.forEach(btn => btn.classList.remove('active'));
// Add active class to clicked button
this.classList.add('active');
const filter = this.getAttribute('data-filter');
// Show/hide items based on filter
groupItems.forEach(item => {
if (filter === 'all') {
item.style.display = 'block';
} else {
if (item.classList.contains(filter)) {
item.style.display = 'block';
} else {
item.style.display = 'none';
}
}
});
});
});
});
</script>
{% 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,215 @@
{% 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">
{% if lang_prefix == 'En' %}
<div class="mb-3">
<button id="copyEnglishTitlesBtn" class="btn btn-outline-primary">
<i class="bi bi-clipboard"></i> Copier les titres au format MediaWiki
</button>
<span id="copyStatus" class="ms-2 text-success" style="display: none;">
<i class="bi bi-check-circle"></i> Copié !
</span>
</div>
{% endif %}
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead class="thead-dark">
<tr>
<th>Titre</th>
<th>Score de décrépitude</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for page in pages %}
<tr>
<td>
<div class="d-flex align-items-center">
{% if page.description_img_url is defined and page.description_img_url %}
<div class="me-3">
<img src="{{ page.description_img_url }}" alt="{{ page.title }}"
style="max-width: 80px; max-height: 60px; object-fit: contain;">
</div>
{% endif %}
<div>
<strong>{{ page.title }}</strong>
{% if page.is_english %}
<span class="badge bg-success">Priorité</span>
{% endif %}
</div>
</div>
</td>
<td>
{% if page.outdatedness_score is defined %}
<div class="progress" style="height: 20px;">
{% set score_class = page.outdatedness_score > 70 ? 'bg-danger' : (page.outdatedness_score > 40 ? 'bg-warning' : 'bg-success') %}
<div class="progress-bar {{ score_class }}" role="progressbar"
style="width: {{ page.outdatedness_score }}%;"
aria-valuenow="{{ page.outdatedness_score }}"
aria-valuemin="0"
aria-valuemax="100">
{{ page.outdatedness_score }}
</div>
</div>
{% else %}
<span class="text-muted">Non disponible</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 %}
{% block javascripts %}
{{ parent() }}
<script>
document.addEventListener('DOMContentLoaded', function() {
const copyButton = document.getElementById('copyEnglishTitlesBtn');
const copyStatus = document.getElementById('copyStatus');
if (copyButton) {
copyButton.addEventListener('click', function() {
// Get all English page titles from the table
const englishSection = document.getElementById('collapseEn');
const titleElements = englishSection.querySelectorAll('tbody tr td:first-child strong');
// Format titles in MediaWiki format
let mediawikiText = '';
const rows = englishSection.querySelectorAll('tbody tr');
rows.forEach(function(row) {
const title = row.querySelector('td:first-child strong').textContent.trim();
const imgElement = row.querySelector('td:first-child img');
if (imgElement) {
const imgSrc = imgElement.getAttribute('src');
mediawikiText += '* [[' + title + ']] - Image: ' + imgSrc + '\n';
} else {
mediawikiText += '* [[' + title + ']]\n';
}
});
// Copy to clipboard
navigator.clipboard.writeText(mediawikiText).then(function() {
// Show success message
copyStatus.style.display = 'inline';
// Hide success message after 3 seconds
setTimeout(function() {
copyStatus.style.display = 'none';
}, 3000);
}).catch(function(err) {
console.error('Erreur lors de la copie: ', err);
alert('Erreur lors de la copie dans le presse-papier. Veuillez réessayer.');
});
});
}
});
</script>
{% endblock %}

View file

@ -0,0 +1,124 @@
{% extends 'base.html.twig' %}
{% block title %}Suggestion de page Wiki à améliorer{% endblock %}
{% block body %}
<div class="container mt-4">
{% include 'admin/_wiki_navigation.html.twig' %}
<h1>Suggestion de page Wiki à améliorer</h1>
<p class="lead">Voici une page wiki qui a besoin d'être améliorée.</p>
<div class="card mb-4">
<div class="card-header bg-primary text-white">
<h2>{{ page.key }}</h2>
</div>
<div class="card-body">
<div class="alert alert-info">
<h3>Raisons d'amélioration</h3>
<p>{{ page.reason }}</p>
</div>
<div class="row">
<div class="col-md-6">
<div class="card h-100">
<div class="card-header bg-primary text-white">
<h3>Version anglaise</h3>
<p class="mb-0">
<small>Dernière modification: {{ page.en_page.last_modified }}</small>
</p>
</div>
<div class="card-body">
<ul class="list-group mb-3">
<li class="list-group-item d-flex justify-content-between align-items-center">
Sections
<span class="badge bg-primary rounded-pill">{{ page.en_page.sections }}</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
Mots
<span class="badge bg-primary rounded-pill">{{ page.en_page.word_count|default(0) }}</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
Liens
<span class="badge bg-primary rounded-pill">{{ page.en_page.link_count|default(0) }}</span>
</li>
</ul>
<div class="d-grid gap-2">
<a href="{{ page.en_page.url }}" target="_blank" class="btn btn-outline-primary">
<i class="bi bi-box-arrow-up-right"></i> Voir la page anglaise
</a>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card h-100">
<div class="card-header bg-info text-white">
<h3>Version française</h3>
{% if page.fr_page %}
<p class="mb-0">
<small>Dernière modification: {{ page.fr_page.last_modified }}</small>
</p>
{% else %}
<p class="mb-0">
<small>Page non existante</small>
</p>
{% endif %}
</div>
<div class="card-body">
{% if page.fr_page %}
<ul class="list-group mb-3">
<li class="list-group-item d-flex justify-content-between align-items-center">
Sections
<span class="badge bg-info rounded-pill">{{ page.fr_page.sections }}</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
Mots
<span class="badge bg-info rounded-pill">{{ page.fr_page.word_count|default(0) }}</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
Liens
<span class="badge bg-info rounded-pill">{{ page.fr_page.link_count|default(0) }}</span>
</li>
</ul>
<div class="d-grid gap-2">
<a href="{{ page.fr_page.url }}" target="_blank" class="btn btn-outline-info">
<i class="bi bi-box-arrow-up-right"></i> Voir la page française
</a>
</div>
{% else %}
<div class="alert alert-warning">
<p><i class="bi bi-exclamation-triangle"></i> <strong>La page wiki pour la clé
"{{ page.key }}" n'existe pas en français.</strong></p>
<p>Vous pouvez contribuer en créant cette page sur le wiki OpenStreetMap.</p>
</div>
<div class="d-grid gap-2">
<a href="https://wiki.openstreetmap.org/w/index.php?title=FR:Key:{{ page.key }}&action=edit"
target="_blank" class="btn btn-success">
<i class="bi bi-plus-circle"></i> Créer la page française
</a>
</div>
{% endif %}
</div>
</div>
</div>
</div>
<div class="mt-4 d-grid gap-2">
<a href="{{ path('app_admin_wiki_compare', {'key': page.key}) }}" class="btn btn-primary">
<i class="bi bi-arrows-angle-expand"></i> Voir la comparaison détaillée
</a>
<a href="{{ path('app_admin_wiki_random_suggestion') }}" class="btn btn-secondary">
<i class="bi bi-shuffle"></i> Autre suggestion aléatoire
</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,156 @@
{% extends 'base.html.twig' %}
{% block title %}Changements récents Wiki OSM{% endblock %}
{% block body %}
<div class="container mt-4">
{% include 'admin/_wiki_navigation.html.twig' %}
<h1>Changements récents Wiki OpenStreetMap</h1>
<p class="lead">Liste des changements récents dans l'espace de noms français du wiki OpenStreetMap.</p>
{% 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 %}
{% if team_members|length > 0 %}
<div class="card mb-4">
<div class="card-header bg-success text-white">
<h2>Équipe Wiki OSM FR</h2>
<p class="mb-0">Contributeurs classés par nombre de modifications</p>
</div>
<div class="card-body">
<div class="row">
{% for member in team_members %}
<div class="col-md-4 mb-3">
<div class="d-flex align-items-center">
<a href="{{ member.user_url }}" target="_blank" class="text-decoration-none">
<span class="fw-bold">{{ member.username }}</span>
</a>
<span class="badge bg-primary ms-2">{{ member.contributions }}</span>
<div class="ms-2 small">
<span class="text-success" title="Caractères ajoutés">+{{ member.chars_added }}</span>
{% if member.chars_changed > 0 %}
<span class="text-warning" title="Caractères modifiés">~{{ member.chars_changed }}</span>
{% endif %}
{% if member.chars_deleted > 0 %}
<span class="text-danger" title="Caractères supprimés">-{{ member.chars_deleted }}</span>
{% endif %}
</div>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
{% endif %}
<div class="card mb-4">
<div class="card-header bg-primary text-white">
<h2>Changements récents</h2>
<p class="mb-0">
<a href="https://wiki.openstreetmap.org/w/index.php?hidebots=1&hidepreviousrevisions=1&hidecategorization=1&hideWikibase=1&hidelog=1&hidenewuserlog=1&namespace=202&limit=500&days=30&enhanced=1&title=Special:RecentChanges&urlversion=2"
target="_blank" class="text-white">
<i class="bi bi-box-arrow-up-right"></i> Voir sur le wiki OSM
</a>
</p>
</div>
<div class="card-body">
{% if recent_changes|length > 0 %}
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead class="thead-dark">
<tr>
<th>Page</th>
<th>Date</th>
<th>Utilisateur</th>
<th>Commentaire</th>
<th>Taille</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for change in recent_changes %}
<tr>
<td>
<strong>
<a href="{{ change.page_url }}" target="_blank" class="text-decoration-none">
{{ change.page_name }}
</a>
</strong>
</td>
<td>{{ change.timestamp }}</td>
<td>
{% if change.user_url %}
<a href="{{ change.user_url }}" target="_blank" class="text-decoration-none">
{{ change.user }}
</a>
{% else %}
{{ change.user }}
{% endif %}
</td>
<td>
{{ change.comment }}
{% if change.added_text or change.removed_text %}
<div class="mt-1 small">
{% if change.added_text %}
<span class="text-success">{{ change.added_text }}</span>
{% endif %}
{% if change.removed_text %}
<span class="text-danger">{{ change.removed_text }}</span>
{% endif %}
</div>
{% endif %}
</td>
<td>
{% if change.change_size starts with '+' or change.change_size > 0 %}
<span class="text-success">{{ change.change_size }}</span>
{% elseif change.change_size starts with '' or change.change_size < 0 %}
<span class="text-danger">{{ change.change_size }}</span>
{% else %}
{{ change.change_size }}
{% endif %}
</td>
<td>
<a href="{{ change.page_url }}" target="_blank" class="btn btn-sm btn-outline-primary">
<i class="bi bi-box-arrow-up-right"></i> Voir
</a>
{% if change.diff_url %}
<a href="{{ change.diff_url }}" target="_blank" class="btn btn-sm btn-outline-secondary mt-1">
<i class="bi bi-file-diff"></i> Diff
</a>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="alert alert-info">
<p><i class="bi bi-info-circle"></i> Aucun changement récent n'a été trouvé.</p>
</div>
{% endif %}
</div>
</div>
<div class="card mb-4">
<div class="card-header">
<h2>À propos des changements récents</h2>
</div>
<div class="card-body">
<p>Cette page affiche les changements récents dans l'espace de noms français (FR:) du wiki OpenStreetMap.</p>
<p>Ces informations sont utiles pour suivre les traductions manquantes et les mises à jour des pages wiki.</p>
<p>Les données sont mises à jour automatiquement toutes les heures.</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

@ -0,0 +1,121 @@
{% extends 'base.html.twig' %}
{% block title %}Pages Wiki avec suppressions suspectes{% endblock %}
{% block body %}
<div class="container mt-4">
{% include 'admin/_wiki_navigation.html.twig' %}
<h1>Pages Wiki avec suppressions suspectes</h1>
<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 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 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>Page</th>
<th>Suppression</th>
<th>Date</th>
<th>Utilisateur</th>
<th>Commentaire</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for deletion in recent_deletions %}
<tr>
<td><strong>{{ deletion.page_title }}</strong></td>
<td>
<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="{{ 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="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>
</tr>
{% endfor %}
</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 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">
<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>
</ul>
<p>Ces pages sont des candidates prioritaires pour une mise à jour de la traduction française.</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

@ -0,0 +1,266 @@
{% extends 'base.html.twig' %}
{% block title %}Propositions de tags OSM{% endblock %}
{% block stylesheets %}
{{ parent() }}
<style>
.vote-bar {
height: 24px;
border-radius: 4px;
overflow: hidden;
display: flex;
margin-bottom: 10px;
}
.vote-approve {
background-color: #28a745;
height: 100%;
}
.vote-abstain {
background-color: #ffc107;
height: 100%;
}
.vote-oppose {
background-color: #dc3545;
height: 100%;
}
.vote-count {
font-size: 0.85rem;
font-weight: bold;
color: white;
text-align: center;
padding: 2px 5px;
}
.voter-badge {
display: inline-block;
margin-right: 5px;
margin-bottom: 5px;
padding: 2px 8px;
border-radius: 12px;
font-size: 0.8rem;
}
.voter-approve {
background-color: rgba(40, 167, 69, 0.2);
border: 1px solid rgba(40, 167, 69, 0.4);
}
.voter-abstain {
background-color: rgba(255, 193, 7, 0.2);
border: 1px solid rgba(255, 193, 7, 0.4);
}
.voter-oppose {
background-color: rgba(220, 53, 69, 0.2);
border: 1px solid rgba(220, 53, 69, 0.4);
}
</style>
{% endblock %}
{% block body %}
<div class="container mt-4">
{% include 'admin/_wiki_navigation.html.twig' %}
<h1>Propositions de tags OSM</h1>
<p class="lead">Liste des propositions de tags OpenStreetMap actuellement en cours de vote ou récemment modifiées.</p>
{% 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 bg-primary text-white">
<h2>Propositions en cours de vote</h2>
</div>
<div class="card-body">
{% 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>Proposition</th>
<th>Proposé par</th>
<th>Votes</th>
<th>Statut</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for proposal in voting_proposals %}
<tr>
<td><strong>{{ proposal.feature }}</strong></td>
<td>
{% if proposal.proposer %}
<a href="https://wiki.openstreetmap.org/wiki/User:{{ proposal.proposer }}" target="_blank">
{{ proposal.proposer }}
</a>
{% else %}
<span class="text-muted">Non spécifié</span>
{% endif %}
</td>
<td>
{% if proposal.votes is defined and proposal.total_votes > 0 %}
<div>
<strong>{{ proposal.total_votes }} votes</strong>
</div>
<div class="vote-bar">
{% if proposal.approve_percentage > 0 %}
<div class="vote-approve" style="width: {{ proposal.approve_percentage }}%">
<span class="vote-count">{{ proposal.votes.approve.count }} ({{ proposal.approve_percentage }}%)</span>
</div>
{% endif %}
{% if proposal.abstain_percentage > 0 %}
<div class="vote-abstain" style="width: {{ proposal.abstain_percentage }}%">
<span class="vote-count">{{ proposal.votes.abstain.count }} ({{ proposal.abstain_percentage }}%)</span>
</div>
{% endif %}
{% if proposal.oppose_percentage > 0 %}
<div class="vote-oppose" style="width: {{ proposal.oppose_percentage }}%">
<span class="vote-count">{{ proposal.votes.oppose.count }} ({{ proposal.oppose_percentage }}%)</span>
</div>
{% endif %}
</div>
<div class="mt-2">
<button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#voters{{ loop.index }}" aria-expanded="false" aria-controls="voters{{ loop.index }}">
Voir les votants
</button>
<div class="collapse mt-2" id="voters{{ loop.index }}">
{% if proposal.votes.approve.users|length > 0 %}
<div class="mb-1">
<strong>Approbations:</strong>
<div>
{% for user in proposal.votes.approve.users %}
<span class="voter-badge voter-approve">{{ user.username }}</span>
{% endfor %}
</div>
</div>
{% endif %}
{% if proposal.votes.abstain.users|length > 0 %}
<div class="mb-1">
<strong>Abstentions:</strong>
<div>
{% for user in proposal.votes.abstain.users %}
<span class="voter-badge voter-abstain">{{ user.username }}</span>
{% endfor %}
</div>
</div>
{% endif %}
{% if proposal.votes.oppose.users|length > 0 %}
<div>
<strong>Oppositions:</strong>
<div>
{% for user in proposal.votes.oppose.users %}
<span class="voter-badge voter-oppose">{{ user.username }}</span>
{% endfor %}
</div>
</div>
{% endif %}
</div>
</div>
{% else %}
<span class="text-muted">Aucun vote</span>
{% endif %}
</td>
<td>
<span class="badge bg-primary">En vote</span>
</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 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>
</div>
<div class="card mb-4">
<div class="card-header">
<h2>À propos des propositions de tags</h2>
</div>
<div class="card-body">
<p>Les propositions de tags sont un processus communautaire pour introduire de nouveaux tags ou modifier des tags existants dans OpenStreetMap.</p>
<p>Le processus typique comprend les étapes suivantes :</p>
<ol>
<li><strong>Brouillon</strong> : La proposition initiale est rédigée et discutée.</li>
<li><strong>RFC (Request for Comments)</strong> : La proposition est ouverte aux commentaires de la communauté.</li>
<li><strong>Vote</strong> : La proposition est soumise au vote de la communauté.</li>
<li><strong>Approbation ou rejet</strong> : Selon les résultats du vote, la proposition est approuvée ou rejetée.</li>
</ol>
<p>Vous pouvez participer à ce processus en commentant les propositions ou en votant lorsqu'elles sont en phase de vote.</p>
<div class="d-grid gap-2 col-md-6 mx-auto mt-3">
<a href="https://wiki.openstreetmap.org/wiki/Proposed_features" target="_blank" class="btn btn-outline-primary">
<i class="bi bi-box-arrow-up-right"></i> Voir toutes les propositions sur le wiki OSM
</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 %}