mirror of
https://forge.chapril.org/tykayn/osm-commerces
synced 2025-10-04 17:04:53 +02:00
enrich exports lat et lon
This commit is contained in:
parent
46d3b21cf6
commit
b5b2880637
6 changed files with 606 additions and 53 deletions
124
docs/import-stats.md
Normal file
124
docs/import-stats.md
Normal file
|
@ -0,0 +1,124 @@
|
|||
# Import d'objets Stats
|
||||
|
||||
Cette fonctionnalité permet d'importer des objets Stats à partir d'un fichier JSON via l'interface d'administration.
|
||||
|
||||
## Accès
|
||||
|
||||
La page d'import est accessible via :
|
||||
- L'URL : `/admin/import-stats`
|
||||
- Le menu de navigation : "Import Stats"
|
||||
|
||||
## Fonctionnalités
|
||||
|
||||
### Sécurité
|
||||
- **Aucune modification** des objets Stats existants
|
||||
- Seuls les nouveaux objets sont créés
|
||||
- Vérification de l'existence par le code INSEE (`zone`)
|
||||
|
||||
### Validation
|
||||
- Vérification du format JSON
|
||||
- Validation des champs requis (`zone` et `name`)
|
||||
- Gestion des erreurs par ligne
|
||||
- Rapport détaillé des résultats
|
||||
|
||||
### Champs supportés
|
||||
|
||||
#### Champs requis
|
||||
- `zone` : Code INSEE de la zone (ex: "75056")
|
||||
- `name` : Nom de la ville/zone (ex: "Paris")
|
||||
|
||||
#### Champs optionnels
|
||||
- `population` : Population de la zone (nombre)
|
||||
- `budgetAnnuel` : Budget annuel de la collectivité (chaîne)
|
||||
- `siren` : Code SIREN (nombre)
|
||||
- `codeEpci` : Code EPCI (nombre)
|
||||
- `codesPostaux` : Codes postaux séparés par des points-virgules (ex: "75001;75002;75003")
|
||||
|
||||
#### Objet `decomptes` (optionnel)
|
||||
- `placesCount` : Nombre total de lieux
|
||||
- `avecHoraires` : Nombre de lieux avec horaires
|
||||
- `avecAdresse` : Nombre de lieux avec adresse
|
||||
- `avecSite` : Nombre de lieux avec site web
|
||||
- `avecAccessibilite` : Nombre de lieux avec accessibilité
|
||||
- `avecNote` : Nombre de lieux avec note
|
||||
- `completionPercent` : Pourcentage de complétion
|
||||
|
||||
## Format JSON attendu
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"zone": "75056",
|
||||
"name": "Paris",
|
||||
"population": 2161000,
|
||||
"budgetAnnuel": "8500000000",
|
||||
"siren": 200054781,
|
||||
"codeEpci": 200054781,
|
||||
"codesPostaux": "75001;75002;75003",
|
||||
"decomptes": {
|
||||
"placesCount": 1250,
|
||||
"avecHoraires": 980,
|
||||
"avecAdresse": 1200,
|
||||
"avecSite": 850,
|
||||
"avecAccessibilite": 450,
|
||||
"avecNote": 320,
|
||||
"completionPercent": 75
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Utilisation
|
||||
|
||||
1. **Préparer le fichier JSON**
|
||||
- Créer un tableau d'objets Stats
|
||||
- Inclure au minimum les champs `zone` et `name`
|
||||
- Valider le format JSON
|
||||
|
||||
2. **Accéder à la page d'import**
|
||||
- Aller sur `/admin/import-stats`
|
||||
- Ou cliquer sur "Import Stats" dans le menu
|
||||
|
||||
3. **Importer le fichier**
|
||||
- Sélectionner le fichier JSON
|
||||
- Cliquer sur "Importer"
|
||||
- Vérifier les messages de résultat
|
||||
|
||||
4. **Vérifier les résultats**
|
||||
- Nombre d'objets créés
|
||||
- Nombre d'objets ignorés (déjà existants)
|
||||
- Liste des erreurs éventuelles
|
||||
|
||||
## Messages de retour
|
||||
|
||||
### Succès
|
||||
```
|
||||
Import terminé : X objet(s) créé(s), Y objet(s) ignoré(s) (déjà existants).
|
||||
```
|
||||
|
||||
### Erreurs possibles
|
||||
- "Aucun fichier JSON n'a été fourni."
|
||||
- "Le fichier doit être au format JSON."
|
||||
- "Erreur lors du décodage JSON: [message]"
|
||||
- "Le fichier JSON doit contenir un tableau d'objets Stats."
|
||||
- "Ligne X: Champs 'zone' et 'name' requis"
|
||||
- "Ligne X: [message d'erreur spécifique]"
|
||||
|
||||
## Logs
|
||||
|
||||
Toutes les actions d'import sont loggées via le service `ActionLogger` :
|
||||
- `admin/import_stats` : Accès à la page
|
||||
- `admin/import_stats_success` : Import réussi avec statistiques
|
||||
- `admin/import_stats_error` : Erreur lors de l'import
|
||||
|
||||
## Exemple de fichier de test
|
||||
|
||||
Un fichier `test_import_stats.json` est fourni avec des exemples pour Paris, Lyon et Marseille.
|
||||
|
||||
## Notes importantes
|
||||
|
||||
- Les objets existants ne sont jamais modifiés
|
||||
- Seuls les nouveaux objets sont créés
|
||||
- Les dates de création et modification sont automatiquement définies
|
||||
- Les erreurs sont affichées par ligne pour faciliter le débogage
|
||||
- Le système est conçu pour être sûr et non destructif
|
|
@ -1320,60 +1320,306 @@ final class AdminController extends AbstractController
|
|||
#[Route('/admin/podium-contributeurs-osm', name: 'app_admin_podium_contributeurs_osm')]
|
||||
public function podiumContributeursOsm(): Response
|
||||
{
|
||||
// Ajout d'un log d'action avec le service ActionLogger
|
||||
$this->actionLogger->log('podium_contributeurs_osm', []);
|
||||
// On suppose que le champ "osmUser" existe sur l'entité Place
|
||||
$placeRepo = $this->entityManager->getRepository(\App\Entity\Place::class);
|
||||
$this->actionLogger->log('admin/podium_contributeurs_osm', []);
|
||||
|
||||
// Nouvelle requête groupée pour tout calculer d'un coup
|
||||
$qb = $placeRepo->createQueryBuilder('p')
|
||||
->select(
|
||||
'p.osm_user',
|
||||
'COUNT(p.id) as nb',
|
||||
'AVG((CASE WHEN p.has_opening_hours = true THEN 1 ELSE 0 END) +'
|
||||
. ' (CASE WHEN p.has_address = true THEN 1 ELSE 0 END) +'
|
||||
. ' (CASE WHEN p.has_website = true THEN 1 ELSE 0 END) +'
|
||||
. ' (CASE WHEN p.has_wheelchair = true THEN 1 ELSE 0 END) +'
|
||||
. ' (CASE WHEN p.has_note = true THEN 1 ELSE 0 END)) / 5 * 100 as completion_moyen'
|
||||
)
|
||||
->where('p.osm_user IS NOT NULL')
|
||||
->andWhere("p.osm_user != ''")
|
||||
->groupBy('p.osm_user')
|
||||
->orderBy('nb', 'DESC')
|
||||
->setMaxResults(100);
|
||||
// Récupérer tous les lieux avec un utilisateur OSM
|
||||
$places = $this->entityManager->getRepository(Place::class)->findBy(['osm_user' => null], ['osm_user' => 'ASC']);
|
||||
$places = array_filter($places, fn($place) => $place->getOsmUser() !== null);
|
||||
|
||||
$podium = $qb->getQuery()->getResult();
|
||||
|
||||
// Calcul du score pondéré et normalisation
|
||||
$maxPondere = 0;
|
||||
foreach ($podium as &$row) {
|
||||
$row['completion_moyen'] = $row['completion_moyen'] !== null ? round($row['completion_moyen'], 1) : null;
|
||||
$row['completion_pondere'] = ($row['completion_moyen'] !== null && $row['nb'] > 0)
|
||||
? round($row['completion_moyen'] * $row['nb'], 1)
|
||||
: null;
|
||||
if ($row['completion_pondere'] !== null && $row['completion_pondere'] > $maxPondere) {
|
||||
$maxPondere = $row['completion_pondere'];
|
||||
// Compter les contributions par utilisateur
|
||||
$contributions = [];
|
||||
foreach ($places as $place) {
|
||||
$user = $place->getOsmUser();
|
||||
if ($user) {
|
||||
if (!isset($contributions[$user])) {
|
||||
$contributions[$user] = 0;
|
||||
}
|
||||
$contributions[$user]++;
|
||||
}
|
||||
}
|
||||
unset($row);
|
||||
|
||||
// Normalisation des scores pondérés entre 0 et 100
|
||||
foreach ($podium as &$row) {
|
||||
if ($maxPondere > 0 && $row['completion_pondere'] !== null) {
|
||||
$row['completion_pondere_normalisee'] = round($row['completion_pondere'] / $maxPondere * 100, 1);
|
||||
} else {
|
||||
$row['completion_pondere_normalisee'] = null;
|
||||
}
|
||||
}
|
||||
unset($row);
|
||||
// Trier par nombre de contributions décroissant
|
||||
arsort($contributions);
|
||||
|
||||
// Tri décroissant sur le score normalisé
|
||||
usort($podium, function ($a, $b) {
|
||||
return ($b['completion_pondere_normalisee'] ?? 0) <=> ($a['completion_pondere_normalisee'] ?? 0);
|
||||
});
|
||||
// Prendre les 10 premiers
|
||||
$topContributors = array_slice($contributions, 0, 10, true);
|
||||
|
||||
return $this->render('admin/podium_contributeurs_osm.html.twig', [
|
||||
'podium' => $podium
|
||||
'contributors' => $topContributors
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/admin/import-stats', name: 'app_admin_import_stats', methods: ['GET', 'POST'])]
|
||||
public function importStats(Request $request): Response
|
||||
{
|
||||
$this->actionLogger->log('admin/import_stats', []);
|
||||
|
||||
if ($request->isMethod('POST')) {
|
||||
$uploadedFile = $request->files->get('json_file');
|
||||
|
||||
if (!$uploadedFile) {
|
||||
$this->addFlash('error', 'Aucun fichier JSON n\'a été fourni.');
|
||||
return $this->redirectToRoute('app_admin_import_stats');
|
||||
}
|
||||
|
||||
// Vérifier le type de fichier
|
||||
if ($uploadedFile->getClientMimeType() !== 'application/json' &&
|
||||
$uploadedFile->getClientOriginalExtension() !== 'json') {
|
||||
$this->addFlash('error', 'Le fichier doit être au format JSON.');
|
||||
return $this->redirectToRoute('app_admin_import_stats');
|
||||
}
|
||||
|
||||
try {
|
||||
// Lire le contenu du fichier
|
||||
$jsonContent = file_get_contents($uploadedFile->getPathname());
|
||||
$data = json_decode($jsonContent, true);
|
||||
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
throw new \Exception('Erreur lors du décodage JSON: ' . json_last_error_msg());
|
||||
}
|
||||
|
||||
if (!is_array($data)) {
|
||||
throw new \Exception('Le fichier JSON doit contenir un tableau d\'objets Stats.');
|
||||
}
|
||||
|
||||
$createdCount = 0;
|
||||
$skippedCount = 0;
|
||||
$errors = [];
|
||||
|
||||
foreach ($data as $index => $statData) {
|
||||
try {
|
||||
// Vérifier que les champs requis sont présents
|
||||
if (!isset($statData['zone']) || !isset($statData['name'])) {
|
||||
$errors[] = "Ligne " . ($index + 1) . ": Champs 'zone' et 'name' requis";
|
||||
continue;
|
||||
}
|
||||
|
||||
$zone = $statData['zone'];
|
||||
$name = $statData['name'];
|
||||
|
||||
// Vérifier si l'objet Stats existe déjà
|
||||
$existingStats = $this->entityManager->getRepository(Stats::class)->findOneBy(['zone' => $zone]);
|
||||
|
||||
if ($existingStats) {
|
||||
$skippedCount++;
|
||||
continue; // Ignorer les objets existants
|
||||
}
|
||||
|
||||
// Créer un nouvel objet Stats
|
||||
$stats = new Stats();
|
||||
$stats->setZone($zone)
|
||||
->setName($name)
|
||||
->setDateCreated(new \DateTime())
|
||||
->setDateModified(new \DateTime());
|
||||
|
||||
// Remplir les champs optionnels
|
||||
if (isset($statData['population'])) {
|
||||
$stats->setPopulation($statData['population']);
|
||||
}
|
||||
if (isset($statData['budgetAnnuel'])) {
|
||||
$stats->setBudgetAnnuel($statData['budgetAnnuel']);
|
||||
}
|
||||
if (isset($statData['siren'])) {
|
||||
$stats->setSiren($statData['siren']);
|
||||
}
|
||||
if (isset($statData['codeEpci'])) {
|
||||
$stats->setCodeEpci($statData['codeEpci']);
|
||||
}
|
||||
if (isset($statData['codesPostaux'])) {
|
||||
$stats->setCodesPostaux($statData['codesPostaux']);
|
||||
}
|
||||
|
||||
// Remplir les décomptes si disponibles
|
||||
if (isset($statData['decomptes'])) {
|
||||
$decomptes = $statData['decomptes'];
|
||||
if (isset($decomptes['placesCount'])) {
|
||||
$stats->setPlacesCount($decomptes['placesCount']);
|
||||
}
|
||||
if (isset($decomptes['avecHoraires'])) {
|
||||
$stats->setAvecHoraires($decomptes['avecHoraires']);
|
||||
}
|
||||
if (isset($decomptes['avecAdresse'])) {
|
||||
$stats->setAvecAdresse($decomptes['avecAdresse']);
|
||||
}
|
||||
if (isset($decomptes['avecSite'])) {
|
||||
$stats->setAvecSite($decomptes['avecSite']);
|
||||
}
|
||||
if (isset($decomptes['avecAccessibilite'])) {
|
||||
$stats->setAvecAccessibilite($decomptes['avecAccessibilite']);
|
||||
}
|
||||
if (isset($decomptes['avecNote'])) {
|
||||
$stats->setAvecNote($decomptes['avecNote']);
|
||||
}
|
||||
if (isset($decomptes['completionPercent'])) {
|
||||
$stats->setCompletionPercent($decomptes['completionPercent']);
|
||||
}
|
||||
}
|
||||
|
||||
$this->entityManager->persist($stats);
|
||||
$createdCount++;
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$errors[] = "Ligne " . ($index + 1) . ": " . $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
// Sauvegarder les changements
|
||||
$this->entityManager->flush();
|
||||
|
||||
// Afficher les résultats
|
||||
$message = "Import terminé : $createdCount objet(s) créé(s), $skippedCount objet(s) ignoré(s) (déjà existants).";
|
||||
if (!empty($errors)) {
|
||||
$message .= " Erreurs : " . count($errors);
|
||||
foreach ($errors as $error) {
|
||||
$this->addFlash('warning', $error);
|
||||
}
|
||||
}
|
||||
|
||||
$this->addFlash('success', $message);
|
||||
$this->actionLogger->log('admin/import_stats_success', [
|
||||
'created' => $createdCount,
|
||||
'skipped' => $skippedCount,
|
||||
'errors' => count($errors)
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->addFlash('error', 'Erreur lors de l\'import : ' . $e->getMessage());
|
||||
$this->actionLogger->log('admin/import_stats_error', ['error' => $e->getMessage()]);
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('app_admin_import_stats');
|
||||
}
|
||||
|
||||
return $this->render('admin/import_stats.html.twig');
|
||||
}
|
||||
|
||||
#[Route('/admin/export-overpass-csv/{insee_code}', name: 'app_admin_export_overpass_csv')]
|
||||
public function exportOverpassCsv($insee_code): Response
|
||||
{
|
||||
$stats = $this->entityManager->getRepository(Stats::class)->findOneBy(['zone' => $insee_code]);
|
||||
|
||||
if (!$stats) {
|
||||
throw $this->createNotFoundException('Stats non trouvées pour ce code INSEE');
|
||||
}
|
||||
|
||||
// Construire la requête Overpass
|
||||
$overpassQuery = '[out:csv(::id,::type,name,amenity,shop,office,craft,leisure,healthcare,emergency,man_made,power,highway,railway,public_transport,landuse,historic,barrier,tourism,sport,place,waterway,natural,geological,route,military,traffic_sign,traffic_calming,seamark,route_master,water,airway,aerialway,building,other;true;false;false)]' . "\n";
|
||||
$overpassQuery .= 'area["ref:INSEE"="' . $insee_code . '"]->.searchArea;' . "\n";
|
||||
$overpassQuery .= 'nwr["amenity"]["name"](area.searchArea);' . "\n";
|
||||
$overpassQuery .= 'nwr["shop"]["name"](area.searchArea);' . "\n";
|
||||
$overpassQuery .= 'nwr["office"]["name"](area.searchArea);' . "\n";
|
||||
$overpassQuery .= 'nwr["craft"]["name"](area.searchArea);' . "\n";
|
||||
$overpassQuery .= 'nwr["leisure"]["name"](area.searchArea);' . "\n";
|
||||
$overpassQuery .= 'nwr["healthcare"]["name"](area.searchArea);' . "\n";
|
||||
$overpassQuery .= 'nwr["emergency"]["name"](area.searchArea);' . "\n";
|
||||
$overpassQuery .= 'nwr["man_made"]["name"](area.searchArea);' . "\n";
|
||||
$overpassQuery .= 'nwr["power"]["name"](area.searchArea);' . "\n";
|
||||
$overpassQuery .= 'nwr["highway"]["name"](area.searchArea);' . "\n";
|
||||
$overpassQuery .= 'nwr["railway"]["name"](area.searchArea);' . "\n";
|
||||
$overpassQuery .= 'nwr["public_transport"]["name"](area.searchArea);' . "\n";
|
||||
$overpassQuery .= 'nwr["landuse"]["name"](area.searchArea);' . "\n";
|
||||
$overpassQuery .= 'nwr["historic"]["name"](area.searchArea);' . "\n";
|
||||
$overpassQuery .= 'nwr["barrier"]["name"](area.searchArea);' . "\n";
|
||||
$overpassQuery .= 'nwr["tourism"]["name"](area.searchArea);' . "\n";
|
||||
$overpassQuery .= 'nwr["sport"]["name"](area.searchArea);' . "\n";
|
||||
$overpassQuery .= 'nwr["place"]["name"](area.searchArea);' . "\n";
|
||||
$overpassQuery .= 'nwr["waterway"]["name"](area.searchArea);' . "\n";
|
||||
$overpassQuery .= 'nwr["natural"]["name"](area.searchArea);' . "\n";
|
||||
$overpassQuery .= 'nwr["geological"]["name"](area.searchArea);' . "\n";
|
||||
$overpassQuery .= 'nwr["route"]["name"](area.searchArea);' . "\n";
|
||||
$overpassQuery .= 'nwr["military"]["name"](area.searchArea);' . "\n";
|
||||
$overpassQuery .= 'nwr["traffic_sign"]["name"](area.searchArea);' . "\n";
|
||||
$overpassQuery .= 'nwr["traffic_calming"]["name"](area.searchArea);' . "\n";
|
||||
$overpassQuery .= 'nwr["seamark"]["name"](area.searchArea);' . "\n";
|
||||
$overpassQuery .= 'nwr["route_master"]["name"](area.searchArea);' . "\n";
|
||||
$overpassQuery .= 'nwr["water"]["name"](area.searchArea);' . "\n";
|
||||
$overpassQuery .= 'nwr["airway"]["name"](area.searchArea);' . "\n";
|
||||
$overpassQuery .= 'nwr["aerialway"]["name"](area.searchArea);' . "\n";
|
||||
$overpassQuery .= 'nwr["building"]["name"](area.searchArea);' . "\n";
|
||||
$overpassQuery .= 'nwr["other"]["name"](area.searchArea);' . "\n";
|
||||
|
||||
$url = 'https://overpass-api.de/api/interpreter?data=' . urlencode($overpassQuery);
|
||||
|
||||
// Rediriger vers l'API Overpass
|
||||
return $this->redirect($url);
|
||||
}
|
||||
|
||||
#[Route('/admin/export-table-csv/{insee_code}', name: 'app_admin_export_table_csv')]
|
||||
public function exportTableCsv($insee_code): Response
|
||||
{
|
||||
$stats = $this->entityManager->getRepository(Stats::class)->findOneBy(['zone' => $insee_code]);
|
||||
|
||||
if (!$stats) {
|
||||
throw $this->createNotFoundException('Stats non trouvées pour ce code INSEE');
|
||||
}
|
||||
|
||||
$response = new Response();
|
||||
$response->headers->set('Content-Type', 'text/csv; charset=utf-8');
|
||||
$response->headers->set('Content-Disposition', 'attachment; filename="lieux_' . $insee_code . '_' . date('Y-m-d') . '.csv"');
|
||||
|
||||
$output = fopen('php://temp', 'r+');
|
||||
|
||||
// En-têtes CSV
|
||||
fputcsv($output, [
|
||||
'Nom',
|
||||
'Email',
|
||||
'Contenu email',
|
||||
'Completion %',
|
||||
'Type',
|
||||
'Adresse',
|
||||
'Numéro',
|
||||
'Rue',
|
||||
'Site web',
|
||||
'Accès PMR',
|
||||
'Note',
|
||||
'Texte de la note',
|
||||
'SIRET',
|
||||
'SIRET clos',
|
||||
'Dernière modif. OSM',
|
||||
'Utilisateur OSM',
|
||||
'Lien OSM'
|
||||
], ';');
|
||||
|
||||
// Données
|
||||
foreach ($stats->getPlaces() as $place) {
|
||||
$osmKind = $place->getOsmKind();
|
||||
$osmId = $place->getOsmId();
|
||||
$osmLink = ($osmKind && $osmId) ? 'https://www.openstreetmap.org/' . $osmKind . '/' . $osmId : '';
|
||||
|
||||
// Construire l'adresse complète
|
||||
$address = '';
|
||||
if ($place->getHousenumber() && $place->getStreet()) {
|
||||
$address = $place->getHousenumber() . ' ' . $place->getStreet();
|
||||
} elseif ($place->getStreet()) {
|
||||
$address = $place->getStreet();
|
||||
}
|
||||
|
||||
fputcsv($output, [
|
||||
$place->getName() ?: '(sans nom)',
|
||||
$place->getEmail() ?: '',
|
||||
$place->getEmailContent() ?: '',
|
||||
$place->getCompletionPercentage(),
|
||||
$place->getMainTag() ?: '',
|
||||
$address,
|
||||
$place->getHousenumber() ?: '',
|
||||
$place->getStreet() ?: '',
|
||||
$place->hasWebsite() ? 'Oui' : 'Non',
|
||||
$place->hasWheelchair() ? 'Oui' : 'Non',
|
||||
$place->getNote() ? 'Oui' : 'Non',
|
||||
$place->getNoteContent() ?: '',
|
||||
$place->getSiret() ?: '',
|
||||
'', // SIRET clos - à implémenter si nécessaire
|
||||
$place->getOsmDataDate() ? $place->getOsmDataDate()->format('Y-m-d H:i') : '',
|
||||
$place->getOsmUser() ?: '',
|
||||
$osmLink
|
||||
], ';');
|
||||
}
|
||||
|
||||
rewind($output);
|
||||
$csv = stream_get_contents($output);
|
||||
fclose($output);
|
||||
|
||||
$response->setContent($csv);
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
|
|
115
templates/admin/import_stats.html.twig
Normal file
115
templates/admin/import_stats.html.twig
Normal file
|
@ -0,0 +1,115 @@
|
|||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}Import d'objets Stats{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="container mt-4">
|
||||
<div class="row">
|
||||
<div class="col-md-8 offset-md-2">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h1><i class="bi bi-upload"></i> Import d'objets Stats</h1>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="alert alert-info">
|
||||
<h5><i class="bi bi-info-circle"></i> Instructions</h5>
|
||||
<p>Cette page permet d'importer des objets Stats à partir d'un fichier JSON.</p>
|
||||
<ul>
|
||||
<li>Seuls les objets Stats qui n'existent pas encore seront créés</li>
|
||||
<li>Les objets existants (même code INSEE) seront ignorés</li>
|
||||
<li>Le fichier JSON doit contenir un tableau d'objets avec les champs requis</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<form method="POST" enctype="multipart/form-data">
|
||||
<div class="mb-3">
|
||||
<label for="json_file" class="form-label">Fichier JSON</label>
|
||||
<input type="file"
|
||||
class="form-control"
|
||||
id="json_file"
|
||||
name="json_file"
|
||||
accept=".json,application/json"
|
||||
required>
|
||||
<div class="form-text">
|
||||
Sélectionnez un fichier JSON contenant un tableau d'objets Stats.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-upload"></i> Importer
|
||||
</button>
|
||||
<a href="{{ path('app_admin') }}" class="btn btn-secondary">
|
||||
<i class="bi bi-arrow-left"></i> Retour
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<hr>
|
||||
|
||||
<h5><i class="bi bi-file-text"></i> Format attendu</h5>
|
||||
<p>Le fichier JSON doit contenir un tableau d'objets avec la structure suivante :</p>
|
||||
|
||||
<div class="bg-light p-3 rounded">
|
||||
<pre><code>[
|
||||
{
|
||||
"zone": "75056",
|
||||
"name": "Paris",
|
||||
"population": 2161000,
|
||||
"budgetAnnuel": "8500000000",
|
||||
"siren": 200054781,
|
||||
"codeEpci": 200054781,
|
||||
"codesPostaux": "75001;75002;75003",
|
||||
"decomptes": {
|
||||
"placesCount": 1250,
|
||||
"avecHoraires": 980,
|
||||
"avecAdresse": 1200,
|
||||
"avecSite": 850,
|
||||
"avecAccessibilite": 450,
|
||||
"avecNote": 320,
|
||||
"completionPercent": 75
|
||||
}
|
||||
}
|
||||
]</code></pre>
|
||||
</div>
|
||||
|
||||
<h6 class="mt-3">Champs requis :</h6>
|
||||
<ul>
|
||||
<li><strong>zone</strong> : Code INSEE de la zone</li>
|
||||
<li><strong>name</strong> : Nom de la ville/zone</li>
|
||||
</ul>
|
||||
|
||||
<h6>Champs optionnels :</h6>
|
||||
<ul>
|
||||
<li><strong>population</strong> : Population de la zone</li>
|
||||
<li><strong>budgetAnnuel</strong> : Budget annuel de la collectivité</li>
|
||||
<li><strong>siren</strong> : Code SIREN</li>
|
||||
<li><strong>codeEpci</strong> : Code EPCI</li>
|
||||
<li><strong>codesPostaux</strong> : Codes postaux (séparés par des points-virgules)</li>
|
||||
<li><strong>decomptes</strong> : Objet contenant les statistiques
|
||||
<ul>
|
||||
<li>placesCount : Nombre de lieux</li>
|
||||
<li>avecHoraires : Lieux avec horaires</li>
|
||||
<li>avecAdresse : Lieux avec adresse</li>
|
||||
<li>avecSite : Lieux avec site web</li>
|
||||
<li>avecAccessibilite : Lieux avec accessibilité</li>
|
||||
<li>avecNote : Lieux avec note</li>
|
||||
<li>completionPercent : Pourcentage de complétion</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="alert alert-warning mt-3">
|
||||
<h6><i class="bi bi-exclamation-triangle"></i> Important</h6>
|
||||
<ul class="mb-0">
|
||||
<li>Les objets Stats existants ne seront pas modifiés</li>
|
||||
<li>Seuls les nouveaux objets seront créés</li>
|
||||
<li>Assurez-vous que le fichier JSON est valide avant l'import</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -311,10 +311,16 @@
|
|||
<h1 class="card-title p-4">Tableau des {{ stats.places |length }} lieux</h1>
|
||||
</div>
|
||||
<div class="col-md-6 col-12">
|
||||
<a class="btn btn-primary pull-right mt-4" href="{{ path('app_admin_export_csv', {'insee_code': stats.zone}) }}" class="btn btn-primary">
|
||||
<div class="btn-group mt-4" role="group">
|
||||
<a href="{{ path('app_admin_export_overpass_csv', {'insee_code': stats.zone}) }}" class="btn btn-primary">
|
||||
<i class="bi bi-filetype-csv"></i>
|
||||
Exporter en CSV
|
||||
Export Overpass CSV
|
||||
</a>
|
||||
<a href="{{ path('app_admin_export_table_csv', {'insee_code': stats.zone}) }}" class="btn btn-success">
|
||||
<i class="bi bi-table"></i>
|
||||
Export Tableau CSV
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -53,6 +53,12 @@
|
|||
Suivi global OSM
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ path('app_admin_import_stats') }}">
|
||||
<i class="bi bi-upload"></i>
|
||||
Import Stats
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
|
56
test_import_stats.json
Normal file
56
test_import_stats.json
Normal file
|
@ -0,0 +1,56 @@
|
|||
[
|
||||
{
|
||||
"zone": "75056",
|
||||
"name": "Paris",
|
||||
"population": 2161000,
|
||||
"budgetAnnuel": "8500000000",
|
||||
"siren": 200054781,
|
||||
"codeEpci": 200054781,
|
||||
"codesPostaux": "75001;75002;75003;75004;75005;75006;75007;75008;75009;75010;75011;75012;75013;75014;75015;75016;75017;75018;75019;75020",
|
||||
"decomptes": {
|
||||
"placesCount": 1250,
|
||||
"avecHoraires": 980,
|
||||
"avecAdresse": 1200,
|
||||
"avecSite": 850,
|
||||
"avecAccessibilite": 450,
|
||||
"avecNote": 320,
|
||||
"completionPercent": 75
|
||||
}
|
||||
},
|
||||
{
|
||||
"zone": "69123",
|
||||
"name": "Lyon",
|
||||
"population": 513275,
|
||||
"budgetAnnuel": "1200000000",
|
||||
"siren": 200046977,
|
||||
"codeEpci": 200046977,
|
||||
"codesPostaux": "69001;69002;69003;69004;69005;69006;69007;69008;69009",
|
||||
"decomptes": {
|
||||
"placesCount": 850,
|
||||
"avecHoraires": 680,
|
||||
"avecAdresse": 820,
|
||||
"avecSite": 580,
|
||||
"avecAccessibilite": 320,
|
||||
"avecNote": 250,
|
||||
"completionPercent": 72
|
||||
}
|
||||
},
|
||||
{
|
||||
"zone": "13055",
|
||||
"name": "Marseille",
|
||||
"population": 861635,
|
||||
"budgetAnnuel": "1500000000",
|
||||
"siren": 200054807,
|
||||
"codeEpci": 200054807,
|
||||
"codesPostaux": "13001;13002;13003;13004;13005;13006;13007;13008;13009;13010;13011;13012;13013;13014;13015;13016",
|
||||
"decomptes": {
|
||||
"placesCount": 1100,
|
||||
"avecHoraires": 880,
|
||||
"avecAdresse": 1050,
|
||||
"avecSite": 720,
|
||||
"avecAccessibilite": 380,
|
||||
"avecNote": 290,
|
||||
"completionPercent": 70
|
||||
}
|
||||
}
|
||||
]
|
Loading…
Add table
Add a link
Reference in a new issue