diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..b4eee3758 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +node_modules +.git +.claude diff --git a/.env.dist b/.env.dist index d7426001e..ab52fe734 100644 --- a/.env.dist +++ b/.env.dist @@ -16,4 +16,5 @@ BASE_URL=http://dev-datan.fr API_KEY_NOBG= API_KEY_MAILJET= API_KEY_SECRETE_MAILJET= -COMPOSER_AUTOLOAD=application/config/autoload.php \ No newline at end of file +COMPOSER_AUTOLOAD=application/config/autoload.php +POLITIC_ANALYSIS_PUBLIC_URL= \ No newline at end of file diff --git a/.gitignore b/.gitignore index 27bc30d53..bb01c7e73 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,9 @@ application/logs/* #custom config application/config/custom_config.php +#claude code +.claude/ + #root folder node_modules /test diff --git a/application/config/config.php b/application/config/config.php index 34e7f3dc2..eb54b9763 100644 --- a/application/config/config.php +++ b/application/config/config.php @@ -455,7 +455,7 @@ $config['csrf_cookie_name'] = 'csrf_cookie_name'; $config['csrf_expire'] = 7200; $config['csrf_regenerate'] = TRUE; -$config['csrf_exclude_uris'] = array('upload/image', 'api/.*'); +$config['csrf_exclude_uris'] = array('upload/image', 'api/.*', 'admin/amendements/review'); /* |-------------------------------------------------------------------------- diff --git a/application/config/routes.php b/application/config/routes.php index e044a469e..da43ab21f 100644 --- a/application/config/routes.php +++ b/application/config/routes.php @@ -52,6 +52,8 @@ // ADMIN $route['admin'] = 'admin/index'; +$route['admin/amendements'] = 'admin/amendements'; +$route['admin/amendements/review'] = 'admin/amendements_review'; $route['admin/votes'] = 'admin/votes'; $route['admin/elections/modifications-mps'] = 'admin/election_modifications_mps'; $route['admin/elections/(:any)'] = 'admin/election_candidates/$1'; @@ -211,6 +213,8 @@ $route['api/non_decrypted_votes'] = 'api/non_decrypted_votes/index'; $route['api/non_decrypted_votes/meta'] = 'api/non_decrypted_votes/meta'; $route['api/non_decrypted_votes/(:any)'] = 'api/non_decrypted_votes/index/$1'; +// Résumés IA des amendements (écriture depuis PoliticAnalysis) +$route['api/amendements_ia'] = 'api/amendements_ia/index'; // Exposés des motifs $route['api/exposes'] = 'api/exposes/index'; $route['api/exposes/meta'] = 'api/exposes/meta'; diff --git a/application/controllers/Admin.php b/application/controllers/Admin.php index bdd488315..c25e01ae8 100644 --- a/application/controllers/Admin.php +++ b/application/controllers/Admin.php @@ -1,5 +1,5 @@ session->set_flashdata('success', 'Clé API révoquée avec succès'); redirect('admin/api-keys'); } + + public function amendements() + { + $data = $this->data; + + $sort = $this->input->get('sort') ?: 'date'; + $direction = $this->input->get('direction') ?: 'DESC'; + + $period = $this->input->get('period'); + $date_start = $this->input->get('date_start'); + $date_end = $this->input->get('date_end'); + $hide_reviewed = filter_var($this->input->get('hide_reviewed'), FILTER_VALIDATE_BOOLEAN); + + $allowed_periods = array('all', '7', '30', '90', '180', '365'); + if (!in_array($period, $allowed_periods, true)) { + $period = 'all'; + } + + $valid_date = function ($d) { + return $d && preg_match('/^\d{4}-\d{2}-\d{2}$/', $d); + }; + $date_start = $valid_date($date_start) ? $date_start : ''; + $date_end = $valid_date($date_end) ? $date_end : ''; + + $filters = array( + 'period' => $period, + 'date_start' => $date_start, + 'date_end' => $date_end, + 'hide_reviewed' => $hide_reviewed, + ); + + $data['amendements'] = $this->admin_model->get_amendements_list($sort, $direction, $filters); + $data['sort'] = $sort; + $data['direction'] = $direction; + $data['period'] = $period; + $data['date_start'] = $date_start; + $data['date_end'] = $date_end; + $data['hide_reviewed'] = $hide_reviewed; + $data['title'] = 'Liste des amendements'; + $data['pa_public_url'] = rtrim($_SERVER['POLITIC_ANALYSIS_PUBLIC_URL'] ?? '', '/'); + + $data['title_meta'] = 'Amendements - Dashboard | Datan'; + $data['breadcrumb'] = array( + array('name' => 'Dashboard', 'url' => base_url() . 'admin', 'active' => FALSE), + array('name' => 'Amendements', 'url' => base_url() . 'admin/amendements', 'active' => TRUE), + ); + + $this->load->view('dashboard/header', $data); + $this->load->view('dashboard-mp/amendements/index', $data); + $this->load->view('dashboard/footer'); + } + + public function amendements_review() + { + if ($this->input->method(TRUE) !== 'POST') { + show_404(); + } + + $legislature = (int) $this->input->post('legislature'); + $voteNumero = (string)$this->input->post('voteNumero'); + $reviewed = filter_var($this->input->post('reviewed'), FILTER_VALIDATE_BOOLEAN); + + if (!$legislature || !$voteNumero) { + $this->output->set_status_header(400)->set_content_type('application/json') + ->set_output(json_encode(['error' => 'legislature et voteNumero sont requis'])); + return; + } + + $ok = $this->admin_model->set_amendement_reviewed($legislature, $voteNumero, $reviewed); + + if (!$ok) { + $this->output->set_status_header(500)->set_content_type('application/json') + ->set_output(json_encode(['error' => "Échec de l'enregistrement (vérifier que la table amendements_ia et sa clé unique uk_leg_vote existent)"])); + return; + } + + $this->output->set_status_header(200)->set_content_type('application/json') + ->set_output(json_encode(['success' => true, 'reviewed' => $reviewed ? 1 : 0])); + } } ?> diff --git a/application/models/Admin_model.php b/application/models/Admin_model.php index 52c3bcedf..56ebc1dbe 100644 --- a/application/models/Admin_model.php +++ b/application/models/Admin_model.php @@ -242,4 +242,130 @@ public function table_changes($table, $toInsert){ ); $this->db->insert('table_changes', $data); } + + /** + * Liste les votes de type amendement de la dernière législature, + * avec résumé IA, score de simplicité et statut de décryptage. + * + * @param string $sort Colonne de tri : 'date'|'votants'|'disparite'|'simplicite'|'decrypte' + * @param string $direction 'ASC'|'DESC' + * @param array $filters ['period' => '7'|'30'|'90'|'180'|'365'|'all', + * 'date_start' => 'YYYY-MM-DD', + * 'date_end' => 'YYYY-MM-DD'] + */ + public function get_amendements_list($sort = 'date', $direction = 'DESC', $filters = array()) + { + $allowed_sorts = array( + 'date' => 'vi.dateScrutin', + 'votants' => 'vi.nombreVotants', + 'disparite' => 'disparite', + 'interet' => 'interet', + 'simplicite' => 'aia.simplicite_ia', + 'decrypte' => 'decrypte', + ); + + $order_col = isset($allowed_sorts[$sort]) ? $allowed_sorts[$sort] : 'vi.dateScrutin'; + $direction = strtoupper($direction) === 'ASC' ? 'ASC' : 'DESC'; + + // Dernière législature disponible + $last_leg = $this->db->query('SELECT MAX(legislature) AS leg FROM votes_info')->row_array(); + $legislature = $last_leg['leg'] ?? 17; + + // Filtres date + $where_date = ''; + $params = array($legislature); + + $period = isset($filters['period']) ? (string)$filters['period'] : 'all'; + $date_start = isset($filters['date_start']) ? (string)$filters['date_start'] : ''; + $date_end = isset($filters['date_end']) ? (string)$filters['date_end'] : ''; + + $is_valid_date = function ($d) { + return $d && preg_match('/^\d{4}-\d{2}-\d{2}$/', $d); + }; + + if ($is_valid_date($date_start) || $is_valid_date($date_end)) { + if ($is_valid_date($date_start)) { + $where_date .= ' AND vi.dateScrutin >= ?'; + $params[] = $date_start; + } + if ($is_valid_date($date_end)) { + $where_date .= ' AND vi.dateScrutin <= ?'; + $params[] = $date_end; + } + } elseif (in_array($period, array('7', '30', '90', '180', '365'), true)) { + $where_date = ' AND vi.dateScrutin >= DATE_SUB(CURDATE(), INTERVAL ? DAY)'; + $params[] = (int)$period; + } + + // Filtre "cacher les reviewed" + $where_reviewed = ''; + if (!empty($filters['hide_reviewed'])) { + $where_reviewed = ' AND COALESCE(aia.reviewed, 0) = 0'; + } + + $sql = " + SELECT + vi.voteNumero, + vi.legislature, + vi.dateScrutin, + date_format(vi.dateScrutin, '%d/%m/%Y') AS dateScrutinFR, + vi.nombreVotants, + vi.decomptePour AS pour, + vi.decompteContre AS contre, + vi.decompteAbs AS abstention, + CASE + WHEN vi.nombreVotants > 0 + THEN ROUND(ABS(vi.decomptePour - vi.decompteContre) * 100 / vi.nombreVotants, 1) + ELSE 0 + END AS disparite, + CASE + WHEN vi.nombreVotants > 0 + THEN ROUND( + LEAST(vi.nombreVotants / 250, 1) + * (1 - ABS(vi.decomptePour - vi.decompteContre) / vi.nombreVotants) + * 100, 1) + ELSE 0 + END AS interet, + aia.titre_ia, + aia.resume_ia, + aia.simplicite_ia, + COALESCE(aia.reviewed, 0) AS reviewed, + COALESCE(vd.title, vi.titre, vi.seanceRef) AS titre + FROM votes_info vi + LEFT JOIN amendements_ia aia + ON aia.voteNumero = vi.voteNumero AND aia.legislature = vi.legislature + LEFT JOIN votes_datan vd + ON vd.voteNumero = vi.voteNumero AND vd.legislature = vi.legislature + WHERE vi.voteType IN ('amendement', 'les amen') + AND vi.legislature = ? + AND vd.id IS NULL + $where_date + $where_reviewed + ORDER BY $order_col $direction + "; + + return $this->db->query($sql, $params)->result_array(); + } + + /** + * Marque un amendement comme reviewed (ou non). + * Crée la ligne dans amendements_ia si elle n'existe pas encore. + * + * @return bool true si la requête a réussi, false en cas d'erreur SQL + */ + public function set_amendement_reviewed($legislature, $voteNumero, $reviewed) + { + $legislature = (int) $legislature; + $voteNumero = (string) $voteNumero; + $reviewed = $reviewed ? 1 : 0; + + $ok = $this->db->query( + "INSERT INTO amendements_ia (legislature, voteNumero, reviewed) + VALUES (?, ?, ?) + ON DUPLICATE KEY UPDATE reviewed = VALUES(reviewed), updated_at = NOW()", + array($legislature, $voteNumero, $reviewed) + ); + + return $ok !== FALSE; + } } diff --git a/application/models/Api_key_model.php b/application/models/Api_key_model.php index 1512c588f..abe806c68 100644 --- a/application/models/Api_key_model.php +++ b/application/models/Api_key_model.php @@ -42,6 +42,10 @@ public function get_available_endpoints() '/api/non_decrypted_votes' => array( 'GET' => 'Lister les votes non décryptés' ), + // Résumés IA des amendements (écriture depuis PoliticAnalysis) + '/api/amendements_ia' => array( + 'POST' => "Créer/mettre à jour un résumé IA d'amendement" + ), // Exposés des motifs '/api/exposes' => array( 'GET' => 'Lister les exposés', diff --git a/application/models/DashboardMP_model.php b/application/models/DashboardMP_model.php index 3d702ea53..03b092c01 100644 --- a/application/models/DashboardMP_model.php +++ b/application/models/DashboardMP_model.php @@ -84,7 +84,6 @@ public function get_votes_explained($mpId, $published = NULL){ } public function get_vote_explained($mpId, $legislature, $voteNumero){ - echo $voteNumero; $sql = 'SELECT e.id, e.voteNumero, e.legislature, e.text AS explication, vd.title AS vote_titre, CASE WHEN e.state = 1 THEN "publié" ELSE "brouillon" END AS state, CASE diff --git a/application/views/dashboard-mp/amendements/index.php b/application/views/dashboard-mp/amendements/index.php new file mode 100644 index 000000000..9d60d4401 --- /dev/null +++ b/application/views/dashboard-mp/amendements/index.php @@ -0,0 +1,245 @@ + +
+
+
+
+
+ load->view('dashboard-mp/partials/breadcrumb.php', $breadcrumb) ?> +
+
+
+
+ +
+
+ + +
+ +
+

+

Dernière législature · amendements

+
+
+ + + session->flashdata('flash')) : ?> + + + + +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + Réinitialiser +
+
+
+ > + +
+
+
+ + Les dates personnalisées (Du / Au) ont priorité sur la période sélectionnée. + +
+
+ + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Amendement + Titre IARésumé IAVotantsDisparitéIntérêtSimplicitéReviewed
Aucun amendement trouvé.
+
+ +
+ Leg. · n° · +
+ —' ?> + + —' ?> + + + + 0) : + $d = (float)$a['disparite']; + $cls = $d >= 60 ? 'danger' : ($d >= 30 ? 'warning' : 'success'); + ?> + % + + + + + 0) : + $i = (float)$a['interet']; + $cls = $i >= 60 ? 'success' : ($i >= 30 ? 'warning' : ($i >= 15 ? 'info' : 'secondary')); + ?> + + + + + + = 4 ? 'success' : ($a['simplicite_ia'] >= 3 ? 'warning' : 'danger'); + ?> + + + + + + > + + + + + Décrypter + + +
+
+
+
+ +
+
+
+ + diff --git a/application/views/dashboard/footer.php b/application/views/dashboard/footer.php index b96e7b463..6fded5006 100644 --- a/application/views/dashboard/footer.php +++ b/application/views/dashboard/footer.php @@ -68,12 +68,24 @@ $(document).ready(function() { - $('#table_votes_datan').dataTable({ - "order": [[0, "desc"]], - language: french - }); - - $('#table_votes_an').DataTable( { + if ($('#table_votes_datan').length && !$.fn.DataTable.isDataTable('#table_votes_datan')) { + $('#table_votes_datan').dataTable({ + "order": [[0, "desc"]], + language: french + }); + } + + if ($('#table-amendements').length && !$.fn.DataTable.isDataTable('#table-amendements')) { + $('#table-amendements').DataTable({ + fixedHeader: true, + paging: true, + order: [[ 5, "desc" ]], + language: french, + }); + } + + if ($('#table_votes_an').length && !$.fn.DataTable.isDataTable('#table_votes_an')) { + $('#table_votes_an').DataTable( { dom: 'Bfrtip', //"lengthMenu": [[10, 25, 50, -1], [10, 25, 50, "All"]], fixedHeader: true, @@ -98,11 +110,14 @@ show: ':hidden' } ] - } ); - - $('#table_analyses').DataTable({ - "order": [[0, "desc"]] - }); + } ); + } + + if ($('#table_analyses').length && !$.fn.DataTable.isDataTable('#table_analyses')) { + $('#table_analyses').DataTable({ + "order": [[0, "desc"]] + }); + } for(let link of $('.nav-treeview .nav-link')){ let searchParams = new URLSearchParams(window.location.search); diff --git a/application/views/dashboard/header.php b/application/views/dashboard/header.php index 1a4328691..fb4f29f63 100644 --- a/application/views/dashboard/header.php +++ b/application/views/dashboard/header.php @@ -152,6 +152,12 @@

Créer un vote

+ diff --git a/conf/000-default.conf b/conf/000-default.conf index c3675d663..a1cc86dfa 100644 --- a/conf/000-default.conf +++ b/conf/000-default.conf @@ -47,6 +47,7 @@ SetEnv API_KEY_MAILJET ${API_KEY_MAILJET} SetEnv API_KEY_SECRETE_MAILJET ${API_KEY_SECRETE_MAILJET} SetEnv COMPOSER_AUTOLOAD ${COMPOSER_AUTOLOAD} + SetEnv POLITIC_ANALYSIS_PUBLIC_URL ${POLITIC_ANALYSIS_PUBLIC_URL} diff --git a/package.json b/package.json index ad4f63338..ff68bb5a3 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "docker-download": "docker exec -it datan php scripts/download.php", "docker-daily": "docker exec -it datan php scripts/daily.php", "docker-weekly": "docker exec -it datan php scripts/weekly.php", + "docker-ia": "docker exec -it datan php scripts/generate_amendements_ia.php", "docker-municipales": "docker exec -it datan php scripts/get_electoral_results.php", "docker-municipales-candidate": "docker exec -it datan php scripts/get_electoral_candidates.php", "docker-profession": "docker exec -it datan php scripts/profession.php", diff --git a/sql/amendements_ia.sql b/sql/amendements_ia.sql new file mode 100644 index 000000000..28e0d47f2 --- /dev/null +++ b/sql/amendements_ia.sql @@ -0,0 +1,11 @@ +CREATE TABLE IF NOT EXISTS `amendements_ia` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `legislature` INT NOT NULL, + `voteNumero` VARCHAR(20) NOT NULL, + `titre_ia` VARCHAR(255) NULL, + `resume_ia` TEXT NULL, + `simplicite_ia` TINYINT UNSIGNED NULL, + `reviewed` TINYINT(1) NOT NULL DEFAULT 0, + `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + UNIQUE KEY `uk_leg_vote` (`legislature`, `voteNumero`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; diff --git a/tests/urls.txt b/tests/urls.txt index ce7376f70..e584f77c2 100644 --- a/tests/urls.txt +++ b/tests/urls.txt @@ -73,5 +73,4 @@ http://dev-datan.fr/elections/regionales-2021 http://dev-datan.fr/elections/departementales-2021 http://dev-datan.fr/parrainages-2022 http://dev-datan.fr/outils/coalition-simulateur -http://dev-datan.fr/blog/categorie/actualite-politique -http://dev-datan.fr/blog/datan/interview-la-division-du-travail-parlementaire-repose-sur-une-vision-stereotypee-de-la-femme \ No newline at end of file +http://dev-datan.fr/blog/categorie/actualite-politique \ No newline at end of file