From 5b6623c6439a1bfd09e3700439be12b5e4a73284 Mon Sep 17 00:00:00 2001 From: Nicolas Joyard Date: Thu, 28 Jun 2018 11:23:53 +0200 Subject: [PATCH 01/53] Ajout script utilitaire de download open data AN --- batch/common/opendata.py | 117 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 batch/common/opendata.py diff --git a/batch/common/opendata.py b/batch/common/opendata.py new file mode 100644 index 000000000..0619deb53 --- /dev/null +++ b/batch/common/opendata.py @@ -0,0 +1,117 @@ +# -*- coding: utf8 -*- +from __future__ import print_function, unicode_literals + +import os +import json +from zipfile import ZipFile + +from bs4 import BeautifulSoup +import requests + +AN_BASE_URL = 'http://data.assemblee-nationale.fr' +AN_ENTRYPOINTS = { + '14': { + 'scrutins': 'opendata-archives-xive/scrutins-xive-legislature' + }, + '15': { + 'scrutins': 'travaux-parlementaires/votes' + } +} + + +def log(str): + print(str) + + +def fetch_an_jsonzip(legislature, objet, cache_dir='./tmp'): + """ + Télécharge le zip du JSON depuis une page de l'open data AN, s'il a été + modifié depuis le dernier téléchargement. + + Renvoie le chemin local du fichier zip téléchargé (stocké dans le + répertoire de cache) + """ + + if str(legislature) not in AN_ENTRYPOINTS \ + or objet not in AN_ENTRYPOINTS[str(legislature)]: + raise Exception('Objet inconnu: %s (%s legislature)' % (objet, + legislature)) + + if not os.path.exists(cache_dir): + os.makedirs(cache_dir) + + localzip = os.path.join(cache_dir, "%s_%s.zip" % (legislature, objet)) + localzip_lastmod = '%s.last_modified' % localzip + + url = '%s/%s' % (AN_BASE_URL, AN_ENTRYPOINTS[str(legislature)][objet]) + log('Téléchargement %s' % url) + + try: + soup = BeautifulSoup(requests.get(url).content, 'html5lib') + except Exception: + raise Exception('Téléchargement %s impossible' % url) + + def match_link(a): + return a['href'].endswith('.json.zip') or \ + a['href'].endswith('.json.zip ') + + try: + link = [a for a in soup.select('a[href]') if match_link(a)][0] + except Exception: + raise Exception('Lien vers dump .json.zip introuvable') + + jsonzip_url = link['href'].replace('.json.zip ', '.json.zip') + if jsonzip_url.startswith('/'): + jsonzip_url = '%s%s' % (AN_BASE_URL, jsonzip_url) + + log('URL JSON zippé : %s' % jsonzip_url) + + try: + lastmod = requests.head(jsonzip_url).headers['Last-Modified'] + except Exception: + raise Exception('Date du dump .json.zip introuvable') + + log('Date modification dump .json.zip: %s' % lastmod) + do_download = True + + if os.path.exists(localzip) and os.path.exists(localzip_lastmod): + with open(localzip_lastmod, 'r') as f: + known_lastmod = f.read() + + log('Date modification dernier telechargement: %s' % known_lastmod) + if known_lastmod == lastmod: + do_download = False + + if do_download: + log('Téléchargement .json.zip') + + try: + with open(localzip, 'wb') as out: + r = requests.get(jsonzip_url, stream=True) + for block in r.iter_content(1024): + out.write(block) + with open(localzip_lastmod, 'w') as f: + f.write(lastmod) + except Exception: + raise Exception('Téléchargement .json.zip impossible') + else: + log('Téléchargement skippé, fichier non mis à jour') + + return localzip + + +def fetch_an_json(page): + """ + Télécharge le zip du JSON depuis une page de l'open data AN, s'il a été + modifié depuis le dernier téléchargement. + + page: URL relative de la page, par exemple "travaux-parlementaires/votes" + + Renvoie les données JSON du fichier zip téléchargé. + """ + + with ZipFile(fetch_an_jsonzip(page), 'r') as z: + for f in [f for f in z.namelist() if f.endswith('.json')]: + log('JSON extrait : %s' % f) + with z.open(f) as zf: + return json.load(zf) From 86fd1cb074f3d103d0dd6630e8c56a5eb83b0ca8 Mon Sep 17 00:00:00 2001 From: Nicolas Joyard Date: Thu, 28 Jun 2018 11:52:52 +0200 Subject: [PATCH 02/53] Mise en forme + dependances --- .../cpc.install/templates/web_Dockerfile.j2 | 5 +- batch/common/opendata.py | 76 +++++++++---------- doc/install.md | 7 +- 3 files changed, 45 insertions(+), 43 deletions(-) diff --git a/ansible/roles/cpc.install/templates/web_Dockerfile.j2 b/ansible/roles/cpc.install/templates/web_Dockerfile.j2 index 28d640eb1..416faf3d3 100644 --- a/ansible/roles/cpc.install/templates/web_Dockerfile.j2 +++ b/ansible/roles/cpc.install/templates/web_Dockerfile.j2 @@ -9,7 +9,10 @@ RUN apt-get update && apt-get -my install --no-install-recommends \ libjpeg62-turbo-dev \ libmagickwand-dev \ libpng{{ use_stretch | ternary('', '12') }}-dev \ - libwww-mechanize-perl + libwww-mechanize-perl \ + python-bs4 \ + python-html5lib \ + python-requests RUN rm -rf /var/lib/apt/lists/* diff --git a/batch/common/opendata.py b/batch/common/opendata.py index 0619deb53..1a8ef924d 100644 --- a/batch/common/opendata.py +++ b/batch/common/opendata.py @@ -8,14 +8,10 @@ from bs4 import BeautifulSoup import requests -AN_BASE_URL = 'http://data.assemblee-nationale.fr' +AN_BASE_URL = "http://data.assemblee-nationale.fr" AN_ENTRYPOINTS = { - '14': { - 'scrutins': 'opendata-archives-xive/scrutins-xive-legislature' - }, - '15': { - 'scrutins': 'travaux-parlementaires/votes' - } + "14": {"scrutins": "opendata-archives-xive/scrutins-xive-legislature"}, + "15": {"scrutins": "travaux-parlementaires/votes"}, } @@ -23,7 +19,7 @@ def log(str): print(str) -def fetch_an_jsonzip(legislature, objet, cache_dir='./tmp'): +def fetch_an_jsonzip(legislature, objet, cache_dir="./tmp"): """ Télécharge le zip du JSON depuis une page de l'open data AN, s'il a été modifié depuis le dernier téléchargement. @@ -32,70 +28,74 @@ def fetch_an_jsonzip(legislature, objet, cache_dir='./tmp'): répertoire de cache) """ - if str(legislature) not in AN_ENTRYPOINTS \ - or objet not in AN_ENTRYPOINTS[str(legislature)]: - raise Exception('Objet inconnu: %s (%s legislature)' % (objet, - legislature)) + if ( + str(legislature) not in AN_ENTRYPOINTS + or objet not in AN_ENTRYPOINTS[str(legislature)] + ): + raise Exception( + "Objet inconnu: %s (%s legislature)" % (objet, legislature) + ) if not os.path.exists(cache_dir): os.makedirs(cache_dir) localzip = os.path.join(cache_dir, "%s_%s.zip" % (legislature, objet)) - localzip_lastmod = '%s.last_modified' % localzip + localzip_lastmod = "%s.last_modified" % localzip - url = '%s/%s' % (AN_BASE_URL, AN_ENTRYPOINTS[str(legislature)][objet]) - log('Téléchargement %s' % url) + url = "%s/%s" % (AN_BASE_URL, AN_ENTRYPOINTS[str(legislature)][objet]) + log("Téléchargement %s" % url) try: - soup = BeautifulSoup(requests.get(url).content, 'html5lib') + soup = BeautifulSoup(requests.get(url).content, "html5lib") except Exception: - raise Exception('Téléchargement %s impossible' % url) + raise Exception("Téléchargement %s impossible" % url) def match_link(a): - return a['href'].endswith('.json.zip') or \ - a['href'].endswith('.json.zip ') + return a["href"].endswith(".json.zip") or a["href"].endswith( + ".json.zip " + ) try: - link = [a for a in soup.select('a[href]') if match_link(a)][0] + link = [a for a in soup.select("a[href]") if match_link(a)][0] except Exception: - raise Exception('Lien vers dump .json.zip introuvable') + raise Exception("Lien vers dump .json.zip introuvable") - jsonzip_url = link['href'].replace('.json.zip ', '.json.zip') - if jsonzip_url.startswith('/'): - jsonzip_url = '%s%s' % (AN_BASE_URL, jsonzip_url) + jsonzip_url = link["href"].replace(".json.zip ", ".json.zip") + if jsonzip_url.startswith("/"): + jsonzip_url = "%s%s" % (AN_BASE_URL, jsonzip_url) - log('URL JSON zippé : %s' % jsonzip_url) + log("URL JSON zippé : %s" % jsonzip_url) try: - lastmod = requests.head(jsonzip_url).headers['Last-Modified'] + lastmod = requests.head(jsonzip_url).headers["Last-Modified"] except Exception: - raise Exception('Date du dump .json.zip introuvable') + raise Exception("Date du dump .json.zip introuvable") - log('Date modification dump .json.zip: %s' % lastmod) + log("Date modification dump .json.zip: %s" % lastmod) do_download = True if os.path.exists(localzip) and os.path.exists(localzip_lastmod): - with open(localzip_lastmod, 'r') as f: + with open(localzip_lastmod, "r") as f: known_lastmod = f.read() - log('Date modification dernier telechargement: %s' % known_lastmod) + log("Date modification dernier telechargement: %s" % known_lastmod) if known_lastmod == lastmod: do_download = False if do_download: - log('Téléchargement .json.zip') + log("Téléchargement .json.zip") try: - with open(localzip, 'wb') as out: + with open(localzip, "wb") as out: r = requests.get(jsonzip_url, stream=True) for block in r.iter_content(1024): out.write(block) - with open(localzip_lastmod, 'w') as f: + with open(localzip_lastmod, "w") as f: f.write(lastmod) except Exception: - raise Exception('Téléchargement .json.zip impossible') + raise Exception("Téléchargement .json.zip impossible") else: - log('Téléchargement skippé, fichier non mis à jour') + log("Téléchargement skippé, fichier non mis à jour") return localzip @@ -110,8 +110,8 @@ def fetch_an_json(page): Renvoie les données JSON du fichier zip téléchargé. """ - with ZipFile(fetch_an_jsonzip(page), 'r') as z: - for f in [f for f in z.namelist() if f.endswith('.json')]: - log('JSON extrait : %s' % f) + with ZipFile(fetch_an_jsonzip(page), "r") as z: + for f in [f for f in z.namelist() if f.endswith(".json")]: + log("JSON extrait : %s" % f) with z.open(f) as zf: return json.load(zf) diff --git a/doc/install.md b/doc/install.md index d39f17aa9..849a26040 100644 --- a/doc/install.md +++ b/doc/install.md @@ -17,7 +17,7 @@ Pour le parsing : ```bash sudo aptitude install libwww-mechanize-perl libfile-path-perl sudo aptitude install libxml2-devel libxslt-devel python-devel -sudo pip install bs4 lxml +sudo pip install bs4 lxml html5lib requests ``` ## Installation @@ -132,7 +132,7 @@ sudo pip install bs4 lxml sudo a2enmod rewrite ``` - * Pour accéder en local à votre instance de développement sur my.cpc.regardscitoyens.org : + * Pour accéder en local à votre instance de développement sur my.cpc.regardscitoyens.org : Ajouter cette ligne au fichier `/etc/hosts` (sudo) : @@ -160,7 +160,7 @@ L'utilisation de la page `frontend_dev.php` vous permet de naviguer sur le site ### Problèmes connus -Si à l'affichage de frontend_dev.php dans le navigateur, PHP dit qu'il n'a pas pu allouer assez de mémoire, augmenter la taille maximale de mémoire autorisée : +Si à l'affichage de frontend_dev.php dans le navigateur, PHP dit qu'il n'a pas pu allouer assez de mémoire, augmenter la taille maximale de mémoire autorisée : ```bash sudo nano /etc/php5/cli/php.ini @@ -280,4 +280,3 @@ Certains services de mail, ralentissent voire bloquent les envois de mails massi spool_arguments: Swift_FileSpool: %SF_ROOT_DIR%/data/mails ``` - From 170d5664ef7e17425bd5da1f5a932b5a11790aed Mon Sep 17 00:00:00 2001 From: Nicolas Joyard Date: Thu, 28 Jun 2018 14:21:47 +0200 Subject: [PATCH 03/53] Extraction referentiels open data + script presences scrutins --- batch/common/__init__.py | 0 batch/common/opendata.py | 162 ++++++++++++++++++++++++++-- batch/scrutin/presences_scrutins.py | 73 +++++++++++++ 3 files changed, 224 insertions(+), 11 deletions(-) create mode 100644 batch/common/__init__.py create mode 100755 batch/scrutin/presences_scrutins.py diff --git a/batch/common/__init__.py b/batch/common/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/batch/common/opendata.py b/batch/common/opendata.py index 1a8ef924d..806f9eb5c 100644 --- a/batch/common/opendata.py +++ b/batch/common/opendata.py @@ -1,31 +1,43 @@ # -*- coding: utf8 -*- from __future__ import print_function, unicode_literals -import os import json +import os +import sys from zipfile import ZipFile from bs4 import BeautifulSoup import requests +CACHE_DIR = os.path.abspath( + os.path.join(os.path.dirname(__file__), "../../data/opendata") +) AN_BASE_URL = "http://data.assemblee-nationale.fr" AN_ENTRYPOINTS = { - "14": {"scrutins": "opendata-archives-xive/scrutins-xive-legislature"}, - "15": {"scrutins": "travaux-parlementaires/votes"}, + "14": { + "amo": "opendata-archives-xive/deputes-senateurs-et-ministres-xive-legislature", + "reunions": "opendata-archives-xive/agendas-xive-legislature", + "scrutins": "opendata-archives-xive/scrutins-xive-legislature", + }, + "15": { + "amo": "acteurs/deputes-en-exercice", + "reunions": "reunions/reunions", + "scrutins": "travaux-parlementaires/votes", + }, } def log(str): - print(str) + print(str, file=sys.stderr) -def fetch_an_jsonzip(legislature, objet, cache_dir="./tmp"): +def fetch_an_jsonzip(legislature, objet, cache_dir=CACHE_DIR): """ Télécharge le zip du JSON depuis une page de l'open data AN, s'il a été modifié depuis le dernier téléchargement. Renvoie le chemin local du fichier zip téléchargé (stocké dans le - répertoire de cache) + répertoire de cache) et un flag indiquant s'il a été modifié """ if ( @@ -97,21 +109,149 @@ def match_link(a): else: log("Téléchargement skippé, fichier non mis à jour") - return localzip + return localzip, do_download -def fetch_an_json(page): +def fetch_an_json(legislature, objet, cache_dir=CACHE_DIR): """ Télécharge le zip du JSON depuis une page de l'open data AN, s'il a été modifié depuis le dernier téléchargement. page: URL relative de la page, par exemple "travaux-parlementaires/votes" - Renvoie les données JSON du fichier zip téléchargé. + Renvoie les données JSON du fichier zip téléchargé et un flag indiquant si + le fichier a été modifié. """ - with ZipFile(fetch_an_jsonzip(page), "r") as z: + localzip, updated = fetch_an_jsonzip(legislature, objet, cache_dir) + with ZipFile(localzip, "r") as z: for f in [f for f in z.namelist() if f.endswith(".json")]: log("JSON extrait : %s" % f) with z.open(f) as zf: - return json.load(zf) + return json.load(zf), updated + + +def _cached_ref( + legislature, + objet, + id_mapping, + extract_list, + extract_id, + extract_mapped, + cache_dir=CACHE_DIR, +): + """ + Génère et renvoie un cache de mapping d'identifiants à partir d'un dump + open data json. + + legislature, objet: définit le dump à utiliser + id_mapping: identifiant unique du mapping, utilisé pour stocker en cache + extract_list: fonction qui extrait la liste des items du dump json + extract_id: fonction qui extrait l'identifiant à mapper d'un item + extract_mapped: fonction qui extrait les données mappées d'un item + """ + + data, updated = fetch_an_json(legislature, objet, cache_dir) + cached_file = os.path.join( + cache_dir, "mapping_%s_%s.json" % (legislature, id_mapping) + ) + + if updated or not os.path.exists(cached_file): + cache = {} + for item in extract_list(data): + id = extract_id(item) + cache[id] = extract_mapped(item) + + with open(cached_file, "w") as f: + json.dump(cache, f) + return cache + else: + with open(cached_file) as f: + return json.load(f) + + +def ref_deputes(legislature, cache_dir=CACHE_DIR): + """ + Renvoie un mapping des id opendata des députés vers "Prénom Nom" + """ + + def _extract_list(data): + return data["export"]["acteurs"]["acteur"] + + def _extract_id(acteur): + return ( + acteur["uid"]["#text"] + if isinstance(acteur["uid"], dict) + else acteur["uid"] + ) + + def _extract_mapped(acteur): + ident = acteur["etatCivil"]["ident"] + return "%s %s" % (ident["prenom"], ident["nom"]) + + return _cached_ref( + legislature, + "amo", + "deputes", + _extract_list, + _extract_id, + _extract_mapped, + cache_dir, + ) + + +def ref_groupes(legislature, cache_dir=CACHE_DIR): + """ + Renvoie un mapping des id opendata des groupes parlementaires vers leur + abbréviation + """ + + def _extract_list(data): + return filter( + lambda o: o["codeType"] == "GP", + data["export"]["organes"]["organe"], + ) + + def _extract_id(organe): + return organe["uid"] + + def _extract_mapped(organe): + return organe["libelleAbrev"] + + return _cached_ref( + legislature, + "amo", + "groupes", + _extract_list, + _extract_id, + _extract_mapped, + cache_dir, + ) + + +def ref_seances(legislature, cache_dir=CACHE_DIR): + """ + Renvoie un mapping des id opendata des séances vers leur ID + """ + + def _extract_list(data): + return filter( + lambda reunion: "IDS" in reunion["uid"], + data["reunions"]["reunion"], + ) + + def _extract_id(reunion): + return reunion["uid"] + + def _extract_mapped(reunion): + return reunion["identifiants"]["idJO"] + + return _cached_ref( + legislature, + "reunions", + "seances", + _extract_list, + _extract_id, + _extract_mapped, + cache_dir, + ) diff --git a/batch/scrutin/presences_scrutins.py b/batch/scrutin/presences_scrutins.py new file mode 100755 index 000000000..300c40238 --- /dev/null +++ b/batch/scrutin/presences_scrutins.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python +# -*- coding: utf8 -*- +from __future__ import print_function, unicode_literals, absolute_import + +import os +import sys + +# Faux module dans .. (batch/) +sys.path.append(os.path.join(os.path.dirname(__file__), "..")) + +from common.opendata import ( + fetch_an_json, + ref_deputes, + ref_groupes, + ref_seances, +) # noqa + + +def parse_scrutins(legislature, json): + deputes = ref_deputes(legislature) + groupes = ref_groupes(legislature) + seances = ref_seances(legislature) + + for scrutin in json["scrutins"]["scrutin"]: + parse_scrutin(scrutin, seances, deputes, groupes) + + +def parse_scrutin(scrutin, seances, deputes, groupes): + num_scrutin = scrutin["numero"] + vote_groupes = scrutin["ventilationVotes"]["organe"]["groupes"]["groupe"] + + seance_ref = seances[scrutin["seanceRef"]] + titre = scrutin["titre"] + + for vote_groupe in vote_groupes: + nom_groupe = groupes[vote_groupe["organeRef"]] + dn = vote_groupe["vote"]["decompteNominatif"] + for position in ("pour", "contre", "abstention", "nonVotant"): + votants_position = dn.get("%ss" % position, dn.get(position)) + if not votants_position: + continue + votants = votants_position["votant"] + + if not isinstance(votants, list): + votants = [votants] + + for votant in votants: + try: + nom_depute = deputes[votant["acteurRef"]] + except KeyError: + continue + + print( + "%s,%s,%s,%s,%s,%s" + % ( + num_scrutin, + titre, + seance_ref, + nom_depute, + nom_groupe, + position, + ) + ) + + +if __name__ == "__main__": + if len(sys.argv) < 2: + print("Utilisation: %s " % sys.argv[0]) + sys.exit(1) + + legislature = sys.argv[1] + json, updated = fetch_an_json(legislature, "scrutins") + parse_scrutins(legislature, json) From b41aeb2b5b14df17839ad0d2dc3782dde4f199eb Mon Sep 17 00:00:00 2001 From: Nicolas Joyard Date: Thu, 28 Jun 2018 14:22:04 +0200 Subject: [PATCH 04/53] Schema scrutins --- config/doctrine/schema.yml | 63 +++++++++++++++++++++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) diff --git a/config/doctrine/schema.yml b/config/doctrine/schema.yml index 0fac1c411..2e7560729 100644 --- a/config/doctrine/schema.yml +++ b/config/doctrine/schema.yml @@ -162,6 +162,67 @@ Seance: index_annee: fields: [annee] +Scrutin: + actAs: + Timestampable: + columns: + annee: integer + numero_semaine: integer + date: date + seance_id: integer + nombre_votants: integer + nombre_pours: integer + nombre_contres: integer + nombre_abstentions: integer + sort: + type: enum + values: ['adopté', 'rejeté'] + titre: text + texteloi_id: integer + amendement_id: integer + sujet: text + demandeur: text + demandeur_groupe_acronyme: string(16) + avis_gouvernement: string(16) + avis_rapporteur: string(16) + relations: + Seance: + foreignAlias: Scrutins + Texteloi: + foreignAlias: Scrutins + Amendement: + foreignAlias: Scrutins + +ParlementaireScrutin: + actAs: + Timestampable: + columns: + scrutin_id: integer + parlementaire_id: integer + parlementaire_groupe_acronyme: string(16) + position: + type: enum + values: ['pour', 'contre', 'abstention', 'non-votant'] + position_groupe: + type: enum + values: ['pour', 'contre', 'abstention', 'non-votant'] + par_delegation: boolean + delegataire_parlementaire_id: integer + mise_au_point_position: + type: enum + values: ['pour', 'contre', 'abstention', 'non-votant'] + relations: + Scrutin: + foreignAlias: Votes + Parlementaire: + foreignAlias: Votes + indexes: + uniq_index: + fields: [scrutin_id, parlementaire_id] + type: unique + index_parlementaire: + fields: [parlementaire_id] + Presence: actAs: Timestampable: @@ -184,7 +245,7 @@ PreuvePresence: presence_id: integer type: type: enum - values: ['jo', 'intervention', 'compte-rendu', 'autre'] + values: ['jo', 'intervention', 'compte-rendu', 'autre', 'scrutin'] source: string(200) relations: Presence: From ef54d097cbef870c70c7a1fc602a5c4c121dff85 Mon Sep 17 00:00:00 2001 From: Nicolas Joyard Date: Thu, 28 Jun 2018 17:05:50 +0200 Subject: [PATCH 05/53] =?UTF-8?q?Maj=20sch=C3=A9ma=20+=20parsing=20scrutin?= =?UTF-8?q?s=20complet?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 + batch/common/__init__.py | 4 + batch/common/opendata.py | 67 +++--------- batch/scrutin/parse_scrutin.py | 147 ------------------------- batch/scrutin/parse_scrutins.py | 159 ++++++++++++++++++++++++++++ batch/scrutin/presences_scrutins.py | 73 ------------- config/doctrine/schema.yml | 7 +- 7 files changed, 184 insertions(+), 275 deletions(-) delete mode 100644 batch/scrutin/parse_scrutin.py create mode 100755 batch/scrutin/parse_scrutins.py delete mode 100755 batch/scrutin/presences_scrutins.py diff --git a/.gitignore b/.gitignore index 63d1d478f..3691330e9 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +36,7 @@ batch/commission/tmp/ batch/commission/out/ batch/commission/loaded/ batch/commission/presents/ +batch/common/opendata batch/depute/html/ batch/depute/json/ batch/depute/collabs.csv @@ -65,6 +66,7 @@ batch/questions/test/ batch/questions/dernier_numero.txt batch/questions/liste_sans_reponse.txt batch/sanctions/ +batch/scrutin/scrutins lib/filter/doctrine/ lib/form/doctrine/ diff --git a/batch/common/__init__.py b/batch/common/__init__.py index e69de29bb..d3c299935 100644 --- a/batch/common/__init__.py +++ b/batch/common/__init__.py @@ -0,0 +1,4 @@ +import os + +COMMON_DIR = os.path.abspath(os.path.dirname(__file__)) +BATCH_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) diff --git a/batch/common/opendata.py b/batch/common/opendata.py index 806f9eb5c..d504e80a8 100644 --- a/batch/common/opendata.py +++ b/batch/common/opendata.py @@ -9,9 +9,10 @@ from bs4 import BeautifulSoup import requests -CACHE_DIR = os.path.abspath( - os.path.join(os.path.dirname(__file__), "../../data/opendata") -) +from . import COMMON_DIR + +CACHE_DIR = os.path.join(COMMON_DIR, "opendata") + AN_BASE_URL = "http://data.assemblee-nationale.fr" AN_ENTRYPOINTS = { "14": { @@ -31,7 +32,7 @@ def log(str): print(str, file=sys.stderr) -def fetch_an_jsonzip(legislature, objet, cache_dir=CACHE_DIR): +def fetch_an_jsonzip(legislature, objet): """ Télécharge le zip du JSON depuis une page de l'open data AN, s'il a été modifié depuis le dernier téléchargement. @@ -48,10 +49,10 @@ def fetch_an_jsonzip(legislature, objet, cache_dir=CACHE_DIR): "Objet inconnu: %s (%s legislature)" % (objet, legislature) ) - if not os.path.exists(cache_dir): - os.makedirs(cache_dir) + if not os.path.exists(CACHE_DIR): + os.makedirs(CACHE_DIR) - localzip = os.path.join(cache_dir, "%s_%s.zip" % (legislature, objet)) + localzip = os.path.join(CACHE_DIR, "%s_%s.zip" % (legislature, objet)) localzip_lastmod = "%s.last_modified" % localzip url = "%s/%s" % (AN_BASE_URL, AN_ENTRYPOINTS[str(legislature)][objet]) @@ -112,7 +113,7 @@ def match_link(a): return localzip, do_download -def fetch_an_json(legislature, objet, cache_dir=CACHE_DIR): +def fetch_an_json(legislature, objet): """ Télécharge le zip du JSON depuis une page de l'open data AN, s'il a été modifié depuis le dernier téléchargement. @@ -123,7 +124,7 @@ def fetch_an_json(legislature, objet, cache_dir=CACHE_DIR): le fichier a été modifié. """ - localzip, updated = fetch_an_jsonzip(legislature, objet, cache_dir) + localzip, updated = fetch_an_jsonzip(legislature, objet) with ZipFile(localzip, "r") as z: for f in [f for f in z.namelist() if f.endswith(".json")]: log("JSON extrait : %s" % f) @@ -132,13 +133,7 @@ def fetch_an_json(legislature, objet, cache_dir=CACHE_DIR): def _cached_ref( - legislature, - objet, - id_mapping, - extract_list, - extract_id, - extract_mapped, - cache_dir=CACHE_DIR, + legislature, objet, id_mapping, extract_list, extract_id, extract_mapped ): """ Génère et renvoie un cache de mapping d'identifiants à partir d'un dump @@ -151,9 +146,9 @@ def _cached_ref( extract_mapped: fonction qui extrait les données mappées d'un item """ - data, updated = fetch_an_json(legislature, objet, cache_dir) + data, updated = fetch_an_json(legislature, objet) cached_file = os.path.join( - cache_dir, "mapping_%s_%s.json" % (legislature, id_mapping) + CACHE_DIR, "mapping_%s_%s.json" % (legislature, id_mapping) ) if updated or not os.path.exists(cached_file): @@ -170,37 +165,7 @@ def _cached_ref( return json.load(f) -def ref_deputes(legislature, cache_dir=CACHE_DIR): - """ - Renvoie un mapping des id opendata des députés vers "Prénom Nom" - """ - - def _extract_list(data): - return data["export"]["acteurs"]["acteur"] - - def _extract_id(acteur): - return ( - acteur["uid"]["#text"] - if isinstance(acteur["uid"], dict) - else acteur["uid"] - ) - - def _extract_mapped(acteur): - ident = acteur["etatCivil"]["ident"] - return "%s %s" % (ident["prenom"], ident["nom"]) - - return _cached_ref( - legislature, - "amo", - "deputes", - _extract_list, - _extract_id, - _extract_mapped, - cache_dir, - ) - - -def ref_groupes(legislature, cache_dir=CACHE_DIR): +def ref_groupes(legislature): """ Renvoie un mapping des id opendata des groupes parlementaires vers leur abbréviation @@ -225,11 +190,10 @@ def _extract_mapped(organe): _extract_list, _extract_id, _extract_mapped, - cache_dir, ) -def ref_seances(legislature, cache_dir=CACHE_DIR): +def ref_seances(legislature): """ Renvoie un mapping des id opendata des séances vers leur ID """ @@ -253,5 +217,4 @@ def _extract_mapped(reunion): _extract_list, _extract_id, _extract_mapped, - cache_dir, ) diff --git a/batch/scrutin/parse_scrutin.py b/batch/scrutin/parse_scrutin.py deleted file mode 100644 index 7f0f653b0..000000000 --- a/batch/scrutin/parse_scrutin.py +++ /dev/null @@ -1,147 +0,0 @@ -#!/usr/bin/env python -# DEPENDANCES -# * python-2.5, -# * python-html5lib -# * python-urllib2 -# BUGS -# v0.2 -# * not working with 2006-2007, nor with 2007-2008 (values are shifted) -# v0.1 -# * encoding failures -# * "objet" text being cut erroneously -# * not working with 2006-2007, nor with 2007-2008 (values are shifted) - -from __future__ import with_statement - -import sys -import os -import re - -import urllib2 - -import html5lib -from html5lib import sanitizer -from html5lib import treebuilders - -BASE_URL = "http://www.assemblee-nationale.fr/" - -# XXX ne marche pas pour 2006-2007 ni pour 2007-2008... -# parce que les positions des td changent -url = "http://www.assemblee-nationale.fr/13/scrutins/table-2008-2009.asp" - -#page = urllib2.urlopen("http://www.assemblee-nationale.fr/13/scrutins/table-2007-2008.asp") - -# document.findAll("td", {"class":"denom"})[1].parent.findAll('td') -class ScrutinParser: - def __init__(self, url): - page = urllib2.urlopen(url) - parser = html5lib.HTMLParser(tokenizer=sanitizer.HTMLSanitizer, tree=treebuilders.getTreeBuilder("beautifulsoup")) - self.document = parser.parse(page, encoding="iso8859-15") - self.type_scrutin = 0 - - def parse_scrutin_objet(self, node): - links = {} - try: - links['dossier'] = BASE_URL + str(node.findChildren('a', text='dossier')[0].parent.extract().attrs[0][1]) - except: pass - try: - links['analyse'] = BASE_URL + str(node.findChildren('a', text=re.compile(".*analyse.*"))[0].parent.extract().attrs[0][1]) - except: pass - for occurence in node.findAll('br'): - occurence.extract() -# objet = node.contents[0].encode('utf-8').replace('\n','').rstrip('[') - objet = node.renderContents().replace('\n','').replace('[]', '') - return (objet, links) - - def parse_scrutin(self, raw): - scrutin = {} - # Get scrutin's id - l = raw.find('td', {'class':'denom'}) - if l.find('a'): - id = str(l.find('a').contents[0]) - scrutin['65-1'] = False - if '*' in id: - scrutin['id'] = int(id.rstrip('*')) - scrutin['65-1'] = True - else: - scrutin['id'] = int(id) - else: - try: - scrutin['id'] = int(str(l.contents[0]).lstrip('\n')) - except: - self.type_scrutin += 1 - return None - # scrutin's type - if self.type_scrutin == 1: - scrutin['type'] = 'salles voisines' - elif self.type_scrutin == 2: - scrutin['type'] = 'ordinaire' - # get session's date - l = l.findNextSibling('td') - scrutin['date'] = l.contents[0] - - # get vote's topic - l = l.findNextSibling('td') - (scrutin['objet'], - scrutin['liens']) = self.parse_scrutin_objet(l) - - scrutin['vote'] = {} - # get number of 'positive' votes - l = l.findNextSibling('td') - if l.contents[0] == '-': - scrutin['vote']['pour'] = -1 - else: - scrutin['vote']['pour'] = int(l.contents[0]) - # get number of 'negative' votes - l = l.findNextSibling('td') - if l.contents[0] == '-': - scrutin['vote']['contre'] = -1 - else: - scrutin['vote']['contre'] = int(l.contents[0]) - # get number of 'neutral' votes - l = l.findNextSibling('td') - if l.contents[0] == '-': - scrutin['vote']['abstention'] = -1 - else: - scrutin['vote']['abstention'] = int(l.contents[0]) - - return scrutin - - def parse_document(self): - scrutins = {} - for raw_scrutin in self.document.findAll("td", {"class":"denom"}): - scrutin = self.parse_scrutin(raw_scrutin.parent) - if scrutin != None: - scrutins[scrutin['id']] = scrutin - return scrutins - -class type_scrutin(dict): - def dump(self, key): - string = " scrutin_"+str(key)+"\n" - for kk, vv in self.iteritems(): - if type(vv) is dict: - string += (2+4)*' '+str(kk)+": \n" - for kkk, vvv in vv.iteritems(): - if type(vvv) in (int, bool): - string += (2+6)*' '+str(kkk)+": "+str(vvv)+"\n" - else: - string += (2+6)*' '+'- '+str(kkk)+": \""+str(vvv)+"\"\n" - else: - if type(vv) in (int, bool): - string += (2+4)*' '+str(kk)+": "+str(vv)+"\n" - else: - string += (2+4)*' '+str(kk)+": \""+str(vv)+"\"\n" - return string - -try: - os.mkdir('scrutin') -except: - pass - -print "Vote grabber v0.2" -print "Processed vote number : ", -for k, s in ScrutinParser(url).parse_document().iteritems(): - with open("scrutin/"+str(k)+".yml", "w") as f: - print k, " ", - f.write(type_scrutin(s).dump(k)) -print diff --git a/batch/scrutin/parse_scrutins.py b/batch/scrutin/parse_scrutins.py new file mode 100755 index 000000000..eb45bc869 --- /dev/null +++ b/batch/scrutin/parse_scrutins.py @@ -0,0 +1,159 @@ +#!/usr/bin/env python +# -*- coding: utf8 -*- + +""" +parse_scrutins.py - Analyse des scrutins depuis l'open data AN + +Utilisation: + parse_scrutins.py + +Répertoire de travail: + /data/opendata/scrutins + +Extrait pour chaque scrutin une représentation JSON conforme au modèle de +données, et un hash SHA1 de cette représentation. + +Si le fichier scrutin__.sha1 existe dans le répertoire de +travail et contient un SHA1 différent, ou s'il n'existe pas, la réprésentation +JSON est stockée dans le fichier scrutin__.json et le SHA1 +est mis à jour. +""" + +from __future__ import print_function, unicode_literals, absolute_import + +import hashlib +import json +import os +import sys + +# Faux module dans .. (batch/) +sys.path.append(os.path.join(os.path.dirname(__file__), "..")) + +from common import BATCH_DIR # noqa +from common.opendata import ( + fetch_an_json, + ref_groupes, + ref_seances, + log, +) # noqa + + +POSITIONS = ("pour", "contre", "abstention", "nonVotant") +POS_MAP = { + "pour": "pours", + "nonVotant": "nonVotants", + "nonVotantVolontaire": "nonVotantsVolontaires", + "contre": "contres", + "abstention": "abstentions", +} + +SCRUTINS_DIR = os.path.join(BATCH_DIR, "scrutin", "scrutins") + + +def parse_scrutins(legislature, data): + groupes = ref_groupes(legislature) + seances = ref_seances(legislature) + + if not os.path.exists(SCRUTINS_DIR): + os.makedirs(SCRUTINS_DIR) + + for item in data["scrutins"]["scrutin"]: + scrutin = parse_scrutin(item, seances, groupes) + + basename = "scrutin_%s_%s" % (legislature, scrutin["numero"]) + hash_file = os.path.join(SCRUTINS_DIR, "%s.sha1" % basename) + json_file = os.path.join(SCRUTINS_DIR, "%s.json" % basename) + + json_data = json.dumps(scrutin, sort_keys=True) + + hash = hashlib.sha1() + hash.update(json_data) + sha1 = hash.hexdigest() + + updated = True + if os.path.exists(hash_file): + with open(hash_file) as f: + previous_sha1 = f.read() + if previous_sha1 == sha1: + updated = False + + if updated: + with open(hash_file, "w") as f: + f.write(sha1) + with open(json_file, "w") as f: + f.write(json_data) + log("Scrutin %s mis à jour" % scrutin["numero"]) + + +def parse_scrutin(data, seances, groupes): + synthese = data["syntheseVote"] + decompte = synthese["decompte"] + scrutin = { + "numero": int(data["numero"]), + "seance": seances[data["seanceRef"]], + "titre": data["titre"], + "nombre_votants": int(synthese["nombreVotants"]), + "nombre_pours": int(decompte["pour"]), + "nombre_contres": int(decompte["contre"]), + "nombre_abstentions": int(decompte["abstentions"]), + "sort": data["sort"]["code"], + "demandeur": data["demandeur"]["texte"], + "parlementaires": {}, + } + + vote_groupes = data["ventilationVotes"]["organe"]["groupes"]["groupe"] + for vote_groupe in vote_groupes: + acro_groupe = groupes[vote_groupe["organeRef"]] + dn = vote_groupe["vote"]["decompteNominatif"] + position_groupe = vote_groupe["vote"]["positionMajoritaire"] + + for position in POSITIONS: + votants_position = dn.get("%ss" % position, dn.get(position)) + if not votants_position: + continue + votants = votants_position["votant"] + + if not isinstance(votants, list): + votants = [votants] + + for votant in votants: + par_delegation = None + if "parDelegation" in votant: + par_delegation = votant["parDelegation"] == "true" + + scrutin["parlementaires"][votant["acteurRef"]] = { + "position": position, + "groupe": acro_groupe, + "position_groupe": position_groupe, + "par_delegation": par_delegation, + } + + vote_map = data["miseAuPoint"] + if vote_map: + for position, pluriel in POS_MAP.items(): + map_position = vote_map[pluriel] + if not isinstance(map_position, list): + map_position = [map_position] + for part in map_position: + if not part: + continue + votants = part["votant"] + if not isinstance(votants, list): + votants = [votants] + for votant in votants: + if votant["acteurRef"] not in scrutin["parlementaires"]: + scrutin["parlementaires"][votant["acteurRef"]] = {} + parl = scrutin["parlementaires"][votant["acteurRef"]] + parl["mise_au_point_position"] = position + + return scrutin + + +if __name__ == "__main__": + if len(sys.argv) < 2: + print("Utilisation: %s " % sys.argv[0]) + sys.exit(1) + + legislature = sys.argv[1] + data, updated = fetch_an_json(legislature, "scrutins") + parse_scrutins(legislature, data) diff --git a/batch/scrutin/presences_scrutins.py b/batch/scrutin/presences_scrutins.py deleted file mode 100755 index 300c40238..000000000 --- a/batch/scrutin/presences_scrutins.py +++ /dev/null @@ -1,73 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf8 -*- -from __future__ import print_function, unicode_literals, absolute_import - -import os -import sys - -# Faux module dans .. (batch/) -sys.path.append(os.path.join(os.path.dirname(__file__), "..")) - -from common.opendata import ( - fetch_an_json, - ref_deputes, - ref_groupes, - ref_seances, -) # noqa - - -def parse_scrutins(legislature, json): - deputes = ref_deputes(legislature) - groupes = ref_groupes(legislature) - seances = ref_seances(legislature) - - for scrutin in json["scrutins"]["scrutin"]: - parse_scrutin(scrutin, seances, deputes, groupes) - - -def parse_scrutin(scrutin, seances, deputes, groupes): - num_scrutin = scrutin["numero"] - vote_groupes = scrutin["ventilationVotes"]["organe"]["groupes"]["groupe"] - - seance_ref = seances[scrutin["seanceRef"]] - titre = scrutin["titre"] - - for vote_groupe in vote_groupes: - nom_groupe = groupes[vote_groupe["organeRef"]] - dn = vote_groupe["vote"]["decompteNominatif"] - for position in ("pour", "contre", "abstention", "nonVotant"): - votants_position = dn.get("%ss" % position, dn.get(position)) - if not votants_position: - continue - votants = votants_position["votant"] - - if not isinstance(votants, list): - votants = [votants] - - for votant in votants: - try: - nom_depute = deputes[votant["acteurRef"]] - except KeyError: - continue - - print( - "%s,%s,%s,%s,%s,%s" - % ( - num_scrutin, - titre, - seance_ref, - nom_depute, - nom_groupe, - position, - ) - ) - - -if __name__ == "__main__": - if len(sys.argv) < 2: - print("Utilisation: %s " % sys.argv[0]) - sys.exit(1) - - legislature = sys.argv[1] - json, updated = fetch_an_json(legislature, "scrutins") - parse_scrutins(legislature, json) diff --git a/config/doctrine/schema.yml b/config/doctrine/schema.yml index 2e7560729..76c933885 100644 --- a/config/doctrine/schema.yml +++ b/config/doctrine/schema.yml @@ -166,6 +166,7 @@ Scrutin: actAs: Timestampable: columns: + numero: integer annee: integer numero_semaine: integer date: date @@ -202,15 +203,15 @@ ParlementaireScrutin: parlementaire_groupe_acronyme: string(16) position: type: enum - values: ['pour', 'contre', 'abstention', 'non-votant'] + values: ['pour', 'contre', 'abstention', 'nonVotant'] position_groupe: type: enum - values: ['pour', 'contre', 'abstention', 'non-votant'] + values: ['pour', 'contre', 'abstention', 'nonVotant'] par_delegation: boolean delegataire_parlementaire_id: integer mise_au_point_position: type: enum - values: ['pour', 'contre', 'abstention', 'non-votant'] + values: ['pour', 'contre', 'abstention', 'nonVotant', 'nonVotantVolontaire'] relations: Scrutin: foreignAlias: Votes From f54d5ccbce50ebb78f0c043bd2de672614742ead Mon Sep 17 00:00:00 2001 From: Nicolas Joyard Date: Thu, 28 Jun 2018 17:33:59 +0200 Subject: [PATCH 06/53] =?UTF-8?q?G=C3=A9n=C3=A9ration=20modele=20doctrine?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/doctrine/schema.yml | 2 +- lib/model/doctrine/ParlementaireScrutin.class.php | 15 +++++++++++++++ .../doctrine/ParlementaireScrutinTable.class.php | 11 +++++++++++ lib/model/doctrine/Scrutin.class.php | 15 +++++++++++++++ lib/model/doctrine/ScrutinTable.class.php | 11 +++++++++++ 5 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 lib/model/doctrine/ParlementaireScrutin.class.php create mode 100644 lib/model/doctrine/ParlementaireScrutinTable.class.php create mode 100644 lib/model/doctrine/Scrutin.class.php create mode 100644 lib/model/doctrine/ScrutinTable.class.php diff --git a/config/doctrine/schema.yml b/config/doctrine/schema.yml index 76c933885..daffca36f 100644 --- a/config/doctrine/schema.yml +++ b/config/doctrine/schema.yml @@ -246,7 +246,7 @@ PreuvePresence: presence_id: integer type: type: enum - values: ['jo', 'intervention', 'compte-rendu', 'autre', 'scrutin'] + values: ['jo', 'intervention', 'compte-rendu', 'scrutin', 'autre'] source: string(200) relations: Presence: diff --git a/lib/model/doctrine/ParlementaireScrutin.class.php b/lib/model/doctrine/ParlementaireScrutin.class.php new file mode 100644 index 000000000..844550fc9 --- /dev/null +++ b/lib/model/doctrine/ParlementaireScrutin.class.php @@ -0,0 +1,15 @@ + Date: Fri, 29 Jun 2018 11:40:07 +0200 Subject: [PATCH 07/53] Ajout task load scrutins --- batch/scrutin/parse_scrutins.py | 3 + bin/load_scrutins | 9 ++ config/doctrine/schema.yml | 7 ++ .../doctrine/ParlementaireScrutin.class.php | 34 +++++--- .../ParlementaireScrutinTable.class.php | 26 +++++- lib/model/doctrine/Presence.class.php | 14 +++ lib/model/doctrine/Scrutin.class.php | 87 ++++++++++++++++--- lib/model/doctrine/ScrutinTable.class.php | 4 +- lib/model/doctrine/Seance.class.php | 29 +++++++ lib/task/loadScrutinsTask.class.php | 66 ++++++++++++++ 10 files changed, 253 insertions(+), 26 deletions(-) create mode 100644 bin/load_scrutins create mode 100644 lib/task/loadScrutinsTask.class.php diff --git a/batch/scrutin/parse_scrutins.py b/batch/scrutin/parse_scrutins.py index eb45bc869..88916c47d 100755 --- a/batch/scrutin/parse_scrutins.py +++ b/batch/scrutin/parse_scrutins.py @@ -49,6 +49,8 @@ SCRUTINS_DIR = os.path.join(BATCH_DIR, "scrutin", "scrutins") +TYPES = {"SPS": "solennel", "SPO": "ordinaire"} + def parse_scrutins(legislature, data): groupes = ref_groupes(legislature) @@ -92,6 +94,7 @@ def parse_scrutin(data, seances, groupes): "numero": int(data["numero"]), "seance": seances[data["seanceRef"]], "titre": data["titre"], + "type": TYPES[data["typeVote"]["codeTypeVote"]], "nombre_votants": int(synthese["nombreVotants"]), "nombre_pours": int(decompte["pour"]), "nombre_contres": int(decompte["contre"]), diff --git a/bin/load_scrutins b/bin/load_scrutins new file mode 100644 index 000000000..c517c0fe1 --- /dev/null +++ b/bin/load_scrutins @@ -0,0 +1,9 @@ +#!/bin/bash + +. $(echo $0 | sed 's/[^\/]*$//')db.inc +cd $PATH_APP + +source bin/db.inc + +batch/scrutins/parse_scrutins.py $LEGISLATURE +php symfony load:Scrutins diff --git a/config/doctrine/schema.yml b/config/doctrine/schema.yml index daffca36f..008fb470a 100644 --- a/config/doctrine/schema.yml +++ b/config/doctrine/schema.yml @@ -175,6 +175,9 @@ Scrutin: nombre_pours: integer nombre_contres: integer nombre_abstentions: integer + type: + type: enum + values: ['ordinaire', 'solennel'] sort: type: enum values: ['adopté', 'rejeté'] @@ -193,6 +196,10 @@ Scrutin: foreignAlias: Scrutins Amendement: foreignAlias: Scrutins + indexes: + uniq_index: + fields: [numero] + type: unique ParlementaireScrutin: actAs: diff --git a/lib/model/doctrine/ParlementaireScrutin.class.php b/lib/model/doctrine/ParlementaireScrutin.class.php index 844550fc9..88f9597d2 100644 --- a/lib/model/doctrine/ParlementaireScrutin.class.php +++ b/lib/model/doctrine/ParlementaireScrutin.class.php @@ -1,15 +1,29 @@ findOneByIdAn($id_an); + if (!$parl) { + throw new Exception("Aucun parlementaire trouvé avec l'ID AN $id_an"); + } + + return $this->_set('parlementaire_id', $parl->id); + } + + public function setScrutin($scrutin) { + return $this->_set('scrutin_id', $scrutin->id); + } + + public function updatePresence() + $this->Scrutin->Seance->setUnsetPresenceLight( + $this->parlementaire_id, + $this->groupe_acronyme, + 'scrutin', + $this->Scrutin->getLinkSource(), + $this->position != 'nonVotant' && $this->mise_au_point_position == NULL + ); + } + } diff --git a/lib/model/doctrine/ParlementaireScrutinTable.class.php b/lib/model/doctrine/ParlementaireScrutinTable.class.php index 67c8de0a5..55d99928e 100644 --- a/lib/model/doctrine/ParlementaireScrutinTable.class.php +++ b/lib/model/doctrine/ParlementaireScrutinTable.class.php @@ -3,9 +3,27 @@ class ParlementaireScrutinTable extends Doctrine_Table { + + public static function getInstance() + { + return Doctrine_Core::getTable('ParlementaireScrutin'); + } + + public function findOneByScrutinIDAN($scrutin_id, $id_an) { + $parl = Doctrine::getTable('Parlementaire')->findOneByIdAn($id_an); - public static function getInstance() - { - return Doctrine_Core::getTable('ParlementaireScrutin'); + if (!$parl) { + throw new Exception("Aucun parlementaire trouvé avec l'ID AN $id_an"); } -} \ No newline at end of file + + $query = $this->createQuery('ps') + ->where('ps.scrutin_id = ?', $scrutin_id) + ->andWhere('ps.parlementaire_id = ?', $parl->id); + $res = $query->fetchOne(); + $query->free(); + $parl->free(); + + return $res; + } + +} diff --git a/lib/model/doctrine/Presence.class.php b/lib/model/doctrine/Presence.class.php index 6ff415dd3..3e8cead31 100644 --- a/lib/model/doctrine/Presence.class.php +++ b/lib/model/doctrine/Presence.class.php @@ -22,6 +22,20 @@ public function addPreuve($type, $source) { return $res; } + public function delPreuve($type, $source) { + $q = Doctrine::getTable('PreuvePresence')->createQuery('p'); + $preuve = $q->where('presence_id = ?', $this->id)->andWhere('type = ?', $type)->fetchOne(); + $q->free(); + + if ($preuve) { + $preuve->delete(); + $preuve->free(); + + $this->nb_preuves--; + $this->save(); + } + } + public function getGroupeAcronyme() { return myTools::getObjectGroupeAcronyme($this); } diff --git a/lib/model/doctrine/Scrutin.class.php b/lib/model/doctrine/Scrutin.class.php index c5df40920..1a1490909 100644 --- a/lib/model/doctrine/Scrutin.class.php +++ b/lib/model/doctrine/Scrutin.class.php @@ -1,15 +1,82 @@ numero; + } + + public function setNumero($numero) { + return $this->_set('numero', $numero); + } + + public function setSeance($idseance) { + $seance = Doctrine::getTable('Seance')->findOneByTODO($idseance); + if (!$seance) { + throw new Exception("Aucune séance trouvée avec l'id $idseance"); + } + + $ret = $this->_set('seance_id', $seance->id) + && $this->_set('date', $seance->date) + && $this->_set('numero_semaine', $seance->numero_semaine) + && $this->_set('annee', $seance->annee); + + $seance->free(); + return $ret; + } + + public function setDemandeur($demandeur) { + // TODO? clean demandeur, set demandeur_groupe_acronyme + return $this->_set('demandeur', $demandeur); + } + + public function setTitre($titre) { + return $this->_set('titre', $titre); + } + + public function setType($type) { + return $this->_set('type', $type); + } + + public function setStats($sort, $nb_votants, $nb_pours, $nb_contres, $nb_abst) { + return $this->_set('sort', $sort) + && $this->_set('nombre_votants', $nb_votants) + && $this->_set('nombre_pours', $nb_pours) + && $this->_set('nombre_contres', $nb_contres) + && $this->_set('nombre_abstentions', $nb_abst); + } + + public function setVotes($parlementaires) { + foreach ($parlementaires as $id_an => $data) { + try { + $parlscrutin = Doctrine::getTable('ParlementaireScrutin') + ->findOneByScrutinIDAN($this->id, $id_an); + + if (!$parlscrutin) { + $parlscrutin = new ParlementaireScrutin(); + if (!$parlscrutin->setParlementaire($id_an) || + || !$parlscrutin->setScrutin($this)) { + return FALSE; + } + } + + if (!$parlscrutin->_set('parlementaire_groupe_acronyme', $data->groupe) + || !$parlscrutin->_set('position', $data->position) + || !$parlscrutin->_set('position_groupe', $data->position_groupe) + || !$parlscrutin->_set('par_delegation', $data->par_delegation) + || !$parlscrutin->_set('mise_au_point_position', $data->mise_au_point_position or NULL)) { + return FALSE; + } + + $parlscrutin->updatePresence(); + $parlscrutin->save(); + $parlscrutin->free(); + } catch (Exception $e) { + echo "ERREUR scrutin {$this->id}, parl $id_an: {$e->getMessage()}\n"; + } + } + } } diff --git a/lib/model/doctrine/ScrutinTable.class.php b/lib/model/doctrine/ScrutinTable.class.php index 098db4880..6517a29fd 100644 --- a/lib/model/doctrine/ScrutinTable.class.php +++ b/lib/model/doctrine/ScrutinTable.class.php @@ -3,9 +3,9 @@ class ScrutinTable extends Doctrine_Table { - + public static function getInstance() { return Doctrine_Core::getTable('Scrutin'); } -} \ No newline at end of file +} diff --git a/lib/model/doctrine/Seance.class.php b/lib/model/doctrine/Seance.class.php index 6b9f00726..ffec60dab 100644 --- a/lib/model/doctrine/Seance.class.php +++ b/lib/model/doctrine/Seance.class.php @@ -68,6 +68,35 @@ public function addPresenceLight($parlementaire_id, $groupe_acronyme, $type, $so return $res; } + public function setUnsetPresenceLight($parlementaire_id, $groupe_acronyme, $type, $source, $present = TRUE) { + $q = Doctrine::getTable('Presence')->createQuery('p'); + $q->where('parlementaire_id = ?', $parlementaire_id)->andWhere('seance_id = ?', $this->id); + $presence = $q->execute()->getFirst(); + $q->free(); + unset($q); + + if (!$presence && $present) { + $presence = new Presence(); + $presence->_set('parlementaire_id', $parlementaire_id); + $presence->_set('parlementaire_groupe_acronyme', $groupe_acronyme); + $presence->Seance = $this; + $presence->date = $this->date; + $presence->save(); + } + + if ($present) { + $res = $presence->addPreuve($type, $source); + } else if ($presence) { + $presence->delPreuve($type, $source); + if ($presence->nbPreuves == 0) { + $presence->delete(); + } + } + + if ($presence) { + $presence->free(); + } + } public static function convertMoment($moment) { if (strlen($moment) > 25) diff --git a/lib/task/loadScrutinsTask.class.php b/lib/task/loadScrutinsTask.class.php new file mode 100644 index 000000000..0527b1eda --- /dev/null +++ b/lib/task/loadScrutinsTask.class.php @@ -0,0 +1,66 @@ +namespace = 'load'; + $this->name = 'Scrutins'; + $this->briefDescription = 'Load Scrutin data'; + $this->addOption('env', null, sfCommandOption::PARAMETER_OPTIONAL, 'Changes the environment this task is run in', 'test'); + $this->addOption('app', null, sfCommandOption::PARAMETER_OPTIONAL, 'Changes the environment this task is run in', 'frontend'); + } + + protected function execute($arguments = array(), $options = array()) + { + // your code here + $dir = dirname(__FILE__).'/../../batch/scrutin/scrutin/'; + $backupdir = dirname(__FILE__).'/../../batch/scrutin/loaded/'; + $manager = new sfDatabaseManager($this->configuration); + + if (is_dir($dir)) { + foreach (scandir($dir) as $file) { + if (!preg_match('/\.json$/', $file)) { + continue; + } + + echo "$dir$file\n"; + $json = file_get_contents($dir . $file); + $data = json_decode($json); + + if (!$data) { + echo "ERROR json : $data\n"; + continue; + } + + $scrutin = Doctrine::getTable('Scrutin')->findOneByNumero($data->numero); + if (!$scrutin) { + $scrutin = new Scrutin(); + $scrutin->setNumero($data->numero); + } + + try { + $scrutin->setSeance($data->seance); + $scrutin->setDemandeur($data->demandeur); + $scrutin->setTitre($data->titre); + $scrutin->setType($data->type); + $scrutin->setStats($data->sort, + $data->nombre_votants, + $data->nombre_pours, + $data->nombre_contres, + $data->nombre_abstentions); + + } catch(Exception $e) { + echo "ERREUR $file (scrutin) : {$e->getMessage()}\n"; + continue; + } + + $scrutin->save(); + $scrutin->setVotes($data->parlementaires); + $scrutin->free(); + + rename($dir . $file, $backupdir . $file); + } + } + } +} From fd4063196f821d6f81aad1c6201863fd23679a33 Mon Sep 17 00:00:00 2001 From: Nicolas Joyard Date: Fri, 29 Jun 2018 12:11:33 +0200 Subject: [PATCH 08/53] Ajout script update db --- bin/updateDB8.sh | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100755 bin/updateDB8.sh diff --git a/bin/updateDB8.sh b/bin/updateDB8.sh new file mode 100755 index 000000000..f1ba13432 --- /dev/null +++ b/bin/updateDB8.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +# echo "OBSOLETE" +# exit + +source ./bin/db.inc + +php symfony cc +php symfony doctrine:build-model +php symfony doctrine:build-form +php symfony doctrine:build-filters +php symfony doctrine:build-sql + +echo "CREATE TABLE parlementaire_scrutin (id BIGINT AUTO_INCREMENT, scrutin_id BIGINT, parlementaire_id BIGINT, parlementaire_groupe_acronyme VARCHAR(16), position VARCHAR(255), position_groupe VARCHAR(255), par_delegation TINYINT(1), delegataire_parlementaire_id BIGINT, mise_au_point_position VARCHAR(255), created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, UNIQUE INDEX uniq_index_idx (scrutin_id, parlementaire_id), INDEX index_parlementaire_idx (parlementaire_id), INDEX scrutin_id_idx (scrutin_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = MyISAM" | mysql $MYSQLID $DBNAME +echo "CREATE TABLE scrutin (id BIGINT AUTO_INCREMENT, numero BIGINT, annee BIGINT, numero_semaine BIGINT, date DATE, seance_id BIGINT, nombre_votants BIGINT, nombre_pours BIGINT, nombre_contres BIGINT, nombre_abstentions BIGINT, sort VARCHAR(255), titre text, texteloi_id BIGINT, amendement_id BIGINT, sujet text, demandeur text, demandeur_groupe_acronyme VARCHAR(16), avis_gouvernement VARCHAR(16), avis_rapporteur VARCHAR(16), created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, INDEX seance_id_idx (seance_id), INDEX texteloi_id_idx (texteloi_id), INDEX amendement_id_idx (amendement_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = MyISAM" | mysql $MYSQLID $DBNAME +echo "ALTER TABLE parlementaire_scrutin ADD CONSTRAINT parlementaire_scrutin_scrutin_id_scrutin_id FOREIGN KEY (scrutin_id) REFERENCES scrutin(id)" | mysql $MYSQLID $DBNAME +echo "ALTER TABLE parlementaire_scrutin ADD CONSTRAINT parlementaire_scrutin_parlementaire_id_parlementaire_id FOREIGN KEY (parlementaire_id) REFERENCES parlementaire(id)" | mysql $MYSQLID $DBNAME +echo "ALTER TABLE scrutin ADD CONSTRAINT scrutin_texteloi_id_texteloi_id FOREIGN KEY (texteloi_id) REFERENCES texteloi(id)" | mysql $MYSQLID $DBNAME +echo "ALTER TABLE scrutin ADD CONSTRAINT scrutin_seance_id_seance_id FOREIGN KEY (seance_id) REFERENCES seance(id)" | mysql $MYSQLID $DBNAME +echo "ALTER TABLE scrutin ADD CONSTRAINT scrutin_amendement_id_amendement_id FOREIGN KEY (amendement_id) REFERENCES amendement(id)" | mysql $MYSQLID $DBNAME From 6bcb38f76d3c939ad0f54bb3faee9723cf29e597 Mon Sep 17 00:00:00 2001 From: Nicolas Joyard Date: Fri, 29 Jun 2018 14:59:38 +0200 Subject: [PATCH 09/53] =?UTF-8?q?Recherche=20s=C3=A9ance=20+=20tag=20inter?= =?UTF-8?q?ventions=20r=C3=A9sultat=20scrutins?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- batch/scrutin/parse_scrutins.py | 3 +- lib/model/doctrine/Scrutin.class.php | 50 +++++++++++++++++++++--- lib/model/doctrine/SeanceTable.class.php | 13 ++++++ lib/task/loadScrutinsTask.class.php | 22 +++++++++++ 4 files changed, 81 insertions(+), 7 deletions(-) diff --git a/batch/scrutin/parse_scrutins.py b/batch/scrutin/parse_scrutins.py index 88916c47d..06c316196 100755 --- a/batch/scrutin/parse_scrutins.py +++ b/batch/scrutin/parse_scrutins.py @@ -62,7 +62,8 @@ def parse_scrutins(legislature, data): for item in data["scrutins"]["scrutin"]: scrutin = parse_scrutin(item, seances, groupes) - basename = "scrutin_%s_%s" % (legislature, scrutin["numero"]) + numero = ("00000%s" % scrutin["numero"])[-5:] + basename = "scrutin_%s_%s" % (legislature, numero) hash_file = os.path.join(SCRUTINS_DIR, "%s.sha1" % basename) json_file = os.path.join(SCRUTINS_DIR, "%s.json" % basename) diff --git a/lib/model/doctrine/Scrutin.class.php b/lib/model/doctrine/Scrutin.class.php index 1a1490909..4e4fe88bc 100644 --- a/lib/model/doctrine/Scrutin.class.php +++ b/lib/model/doctrine/Scrutin.class.php @@ -13,19 +13,57 @@ public function setNumero($numero) { return $this->_set('numero', $numero); } - public function setSeance($idseance) { - $seance = Doctrine::getTable('Seance')->findOneByTODO($idseance); + public function setSeance($id_jo) { + $seance = Doctrine::getTable('Seance')->findOneByIDJO($id_jo); if (!$seance) { - throw new Exception("Aucune séance trouvée avec l'id $idseance"); + throw new Exception("Aucune séance trouvée avec l'id JO $id_jo"); } + $seance_id = $seance->id; + $seance->free(); - $ret = $this->_set('seance_id', $seance->id) + $ret = $this->_set('seance_id', $seance_id) && $this->_set('date', $seance->date) && $this->_set('numero_semaine', $seance->numero_semaine) && $this->_set('annee', $seance->annee); - $seance->free(); - return $ret; + } + + public function tagInterventions() { + // Comptage des scrutins avec numéro inférieur dans la même séance + $avant = $this->createQuery('s') + ->select('count(1) as cnt') + ->where('s.seance_id = ?', $this->seance_id) + ->andWhere('s.numero < ?', $this->numero) + ->fetchOne()['cnt']; + + // Recherche de l'intervention avec un tableau de votants qui correspond + $inter = Doctrine::getTable('Intervention') + ->createQuery('i') + ->where('i.seance_id = ?', $this->seance_id) + ->andWhere("i.intervention LIKE '%table class=\"scrutin\"%'") + ->orderBy('i.timestamp') + ->offset($avant) + ->getFirst(); + + $found = FALSE; + if ($inter) { + $found = TRUE; + + // Vérification du nombre de pour/contre + $text = $inter->intervention; + if (preg_match('/pour[^<]*<\/td>(\d+)/', $text, &$match) == 0 + || intval($match[0]) != $this->nombre_pours + || preg_match('/contre[^<]*<\/td>(\d+)/', $text, &$match) == 0 + || intval($match[0]) != $this->nombre_contres) { + $found = FALSE; + } else { + $inter->addTag("scrutin:numero={$this->numero}"); + } + } + + if (!$found) { + throw new Exception("Scrutin {$this->numero} non trouvé dans les interventions de la séance $seance_id"); + } } public function setDemandeur($demandeur) { diff --git a/lib/model/doctrine/SeanceTable.class.php b/lib/model/doctrine/SeanceTable.class.php index 3cafd2cc1..9e7702ba7 100644 --- a/lib/model/doctrine/SeanceTable.class.php +++ b/lib/model/doctrine/SeanceTable.class.php @@ -39,6 +39,19 @@ public function findOneOrCreateIt($type, $date, $heure, $session) { return $s; } + public function findOneByIDJO($id_jo) { + $seance = Doctrine::getTable('Intervention')->createQuery('i') + ->select('i.seance_id') + ->where('i.source LIKE ?', "%/$id_jo.asp%") + ->fetchOne(); + + if ($seance) { + return $this->find($seance['seance_id']) + } + + return NULL; + } + public function getPager($request, $query = NULL) { $pager = new sfDoctrinePager('Seance',30); diff --git a/lib/task/loadScrutinsTask.class.php b/lib/task/loadScrutinsTask.class.php index 0527b1eda..c86cc5ce3 100644 --- a/lib/task/loadScrutinsTask.class.php +++ b/lib/task/loadScrutinsTask.class.php @@ -17,6 +17,11 @@ protected function execute($arguments = array(), $options = array()) $dir = dirname(__FILE__).'/../../batch/scrutin/scrutin/'; $backupdir = dirname(__FILE__).'/../../batch/scrutin/loaded/'; $manager = new sfDatabaseManager($this->configuration); + $seances_manquantes = 0; + + if (!is_dir($backupdir)) { + mkdir($backupdir, 0777, TRUE); + } if (is_dir($dir)) { foreach (scandir($dir) as $file) { @@ -41,6 +46,12 @@ protected function execute($arguments = array(), $options = array()) try { $scrutin->setSeance($data->seance); + } catch (Exception $e) { + $seances_manquantes++; + continue; + } + + try { $scrutin->setDemandeur($data->demandeur); $scrutin->setTitre($data->titre); $scrutin->setType($data->type); @@ -56,11 +67,22 @@ protected function execute($arguments = array(), $options = array()) } $scrutin->save(); + + try { + $scrutin->tagInterventions(); + } catch(Exception $e) { + echo "ERREUR $file (tag interventions) : {$e->getMessage()}\n"; + } + $scrutin->setVotes($data->parlementaires); $scrutin->free(); rename($dir . $file, $backupdir . $file); } + + if ($seances_manquantes > 0) { + echo "WARNING: $seances_manquantes séances non trouvées\n"; + } } } } From c72738d8f7b6c58342bee7cadacd0b3a3332894c Mon Sep 17 00:00:00 2001 From: Nicolas Joyard Date: Fri, 29 Jun 2018 15:45:46 +0200 Subject: [PATCH 10/53] =?UTF-8?q?D=C3=A9tection=20d=C3=A9l=C3=A9gations=20?= =?UTF-8?q?trop=20nombreuses?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- batch/scrutin/parse_scrutins.py | 7 +++++++ lib/task/loadScrutinsTask.class.php | 1 - 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/batch/scrutin/parse_scrutins.py b/batch/scrutin/parse_scrutins.py index 06c316196..d6e28ffa2 100755 --- a/batch/scrutin/parse_scrutins.py +++ b/batch/scrutin/parse_scrutins.py @@ -105,6 +105,8 @@ def parse_scrutin(data, seances, groupes): "parlementaires": {}, } + delegations = 0 + vote_groupes = data["ventilationVotes"]["organe"]["groupes"]["groupe"] for vote_groupe in vote_groupes: acro_groupe = groupes[vote_groupe["organeRef"]] @@ -124,6 +126,8 @@ def parse_scrutin(data, seances, groupes): par_delegation = None if "parDelegation" in votant: par_delegation = votant["parDelegation"] == "true" + if par_delegation: + delegations += 1 scrutin["parlementaires"][votant["acteurRef"]] = { "position": position, @@ -132,6 +136,9 @@ def parse_scrutin(data, seances, groupes): "par_delegation": par_delegation, } + if 2 * delegations > int(synthese["nombreVotants"]): + log("Scrutin %s: trop de délégations" % scrutin["numero"]) + vote_map = data["miseAuPoint"] if vote_map: for position, pluriel in POS_MAP.items(): diff --git a/lib/task/loadScrutinsTask.class.php b/lib/task/loadScrutinsTask.class.php index c86cc5ce3..9c02148aa 100644 --- a/lib/task/loadScrutinsTask.class.php +++ b/lib/task/loadScrutinsTask.class.php @@ -13,7 +13,6 @@ protected function configure() protected function execute($arguments = array(), $options = array()) { - // your code here $dir = dirname(__FILE__).'/../../batch/scrutin/scrutin/'; $backupdir = dirname(__FILE__).'/../../batch/scrutin/loaded/'; $manager = new sfDatabaseManager($this->configuration); From 742c237304d60dbd9f4b4653b2e6d6bced217063 Mon Sep 17 00:00:00 2001 From: Nicolas Joyard Date: Fri, 29 Jun 2018 15:54:25 +0200 Subject: [PATCH 11/53] Typos --- bin/load_scrutins | 2 +- .../doctrine/ParlementaireScrutin.class.php | 4 +- lib/model/doctrine/Scrutin.class.php | 38 ++++++++++++------- lib/model/doctrine/Seance.class.php | 2 +- lib/model/doctrine/SeanceTable.class.php | 6 +-- lib/task/loadScrutinsTask.class.php | 5 ++- 6 files changed, 36 insertions(+), 21 deletions(-) mode change 100644 => 100755 bin/load_scrutins diff --git a/bin/load_scrutins b/bin/load_scrutins old mode 100644 new mode 100755 index c517c0fe1..066368aab --- a/bin/load_scrutins +++ b/bin/load_scrutins @@ -5,5 +5,5 @@ cd $PATH_APP source bin/db.inc -batch/scrutins/parse_scrutins.py $LEGISLATURE +batch/scrutin/parse_scrutins.py $LEGISLATURE php symfony load:Scrutins diff --git a/lib/model/doctrine/ParlementaireScrutin.class.php b/lib/model/doctrine/ParlementaireScrutin.class.php index 88f9597d2..028d11e5e 100644 --- a/lib/model/doctrine/ParlementaireScrutin.class.php +++ b/lib/model/doctrine/ParlementaireScrutin.class.php @@ -16,10 +16,10 @@ public function setScrutin($scrutin) { return $this->_set('scrutin_id', $scrutin->id); } - public function updatePresence() + public function updatePresence() { $this->Scrutin->Seance->setUnsetPresenceLight( $this->parlementaire_id, - $this->groupe_acronyme, + $this->parlementaire_groupe_acronyme, 'scrutin', $this->Scrutin->getLinkSource(), $this->position != 'nonVotant' && $this->mise_au_point_position == NULL diff --git a/lib/model/doctrine/Scrutin.class.php b/lib/model/doctrine/Scrutin.class.php index 4e4fe88bc..3b7ce88ba 100644 --- a/lib/model/doctrine/Scrutin.class.php +++ b/lib/model/doctrine/Scrutin.class.php @@ -19,22 +19,24 @@ public function setSeance($id_jo) { throw new Exception("Aucune séance trouvée avec l'id JO $id_jo"); } $seance_id = $seance->id; - $seance->free(); $ret = $this->_set('seance_id', $seance_id) && $this->_set('date', $seance->date) && $this->_set('numero_semaine', $seance->numero_semaine) && $this->_set('annee', $seance->annee); + $seance->free(); + return $ret; } public function tagInterventions() { // Comptage des scrutins avec numéro inférieur dans la même séance - $avant = $this->createQuery('s') - ->select('count(1) as cnt') - ->where('s.seance_id = ?', $this->seance_id) - ->andWhere('s.numero < ?', $this->numero) - ->fetchOne()['cnt']; + $avant = Doctrine::getTable('Scrutin') + ->createQuery('s') + ->select('count(1) as cnt') + ->where('s.seance_id = ?', $this->seance_id) + ->andWhere('s.numero < ?', $this->numero) + ->fetchOne()['cnt']; // Recherche de l'intervention avec un tableau de votants qui correspond $inter = Doctrine::getTable('Intervention') @@ -43,26 +45,36 @@ public function tagInterventions() { ->andWhere("i.intervention LIKE '%table class=\"scrutin\"%'") ->orderBy('i.timestamp') ->offset($avant) - ->getFirst(); + ->fetchOne(); $found = FALSE; + $info = "intervention non trouvée"; if ($inter) { $found = TRUE; + $info = "intervention trouvée"; // Vérification du nombre de pour/contre $text = $inter->intervention; - if (preg_match('/pour[^<]*<\/td>(\d+)/', $text, &$match) == 0 - || intval($match[0]) != $this->nombre_pours - || preg_match('/contre[^<]*<\/td>(\d+)/', $text, &$match) == 0 - || intval($match[0]) != $this->nombre_contres) { + if (preg_match('/pour[^<]*<\/td>(\d+)/i', $text, $match_pour) == 0 + || intval($match_pour[1]) != $this->nombre_pours + || preg_match('/contre[^<]*<\/td>(\d+)/i', $text, $match_contre) == 0 + || intval($match_contre[1]) != $this->nombre_contres) { $found = FALSE; + $info .= " pour: {$match_pour[1]}!={$this->nombre_pours}"; + $info .= " contre: {$match_contre[1]}!={$this->nombre_contres}"; + $info .= " \n$text"; } else { $inter->addTag("scrutin:numero={$this->numero}"); } } if (!$found) { - throw new Exception("Scrutin {$this->numero} non trouvé dans les interventions de la séance $seance_id"); + $seance = $this->Seance; + throw new Exception( + "Scrutin {$this->numero} non trouvé dans les interventions " + . "de la séance {$seance->id} du {$seance->date} {$seance->moment}\n" + . "scrutins avant=$avant, $info" + ); } } @@ -95,7 +107,7 @@ public function setVotes($parlementaires) { if (!$parlscrutin) { $parlscrutin = new ParlementaireScrutin(); - if (!$parlscrutin->setParlementaire($id_an) || + if (!$parlscrutin->setParlementaire($id_an) || !$parlscrutin->setScrutin($this)) { return FALSE; } diff --git a/lib/model/doctrine/Seance.class.php b/lib/model/doctrine/Seance.class.php index ffec60dab..688d8ba08 100644 --- a/lib/model/doctrine/Seance.class.php +++ b/lib/model/doctrine/Seance.class.php @@ -88,7 +88,7 @@ public function setUnsetPresenceLight($parlementaire_id, $groupe_acronyme, $type $res = $presence->addPreuve($type, $source); } else if ($presence) { $presence->delPreuve($type, $source); - if ($presence->nbPreuves == 0) { + if ($presence->nb_preuves == 0) { $presence->delete(); } } diff --git a/lib/model/doctrine/SeanceTable.class.php b/lib/model/doctrine/SeanceTable.class.php index 9e7702ba7..68d791c72 100644 --- a/lib/model/doctrine/SeanceTable.class.php +++ b/lib/model/doctrine/SeanceTable.class.php @@ -8,7 +8,7 @@ public function getFromSeanceArgs($type, $date, $heure, $session, $commission = if ($type == 'commission') { $commission = Doctrine::getTable('Organisme')->findOneByNomOrCreateIt($commission, 'parlementaire'); return $commission->getSeanceByDateAndMoment($date, $heure, $session); - } + } return $this->findOneByTypeDateHeureSession('hemicycle', $date, $heure, $session); } @@ -16,7 +16,7 @@ public function getOrCreateItFromSeanceArgs($type, $date, $heure, $session, $com if ($type == 'commission') { $commission = Doctrine::getTable('Organisme')->findOneByNomOrCreateIt($commission, 'parlementaire'); return $commission->getSeanceByDateAndMomentOrCreateIt($date, $heure, $session); - } + } return $this->findOneOrCreateIt('hemicycle', $date, $heure, $session); } @@ -46,7 +46,7 @@ public function findOneByIDJO($id_jo) { ->fetchOne(); if ($seance) { - return $this->find($seance['seance_id']) + return $this->find($seance['seance_id']); } return NULL; diff --git a/lib/task/loadScrutinsTask.class.php b/lib/task/loadScrutinsTask.class.php index 9c02148aa..a8f303f75 100644 --- a/lib/task/loadScrutinsTask.class.php +++ b/lib/task/loadScrutinsTask.class.php @@ -13,7 +13,7 @@ protected function configure() protected function execute($arguments = array(), $options = array()) { - $dir = dirname(__FILE__).'/../../batch/scrutin/scrutin/'; + $dir = dirname(__FILE__).'/../../batch/scrutin/scrutins/'; $backupdir = dirname(__FILE__).'/../../batch/scrutin/loaded/'; $manager = new sfDatabaseManager($this->configuration); $seances_manquantes = 0; @@ -46,6 +46,8 @@ protected function execute($arguments = array(), $options = array()) try { $scrutin->setSeance($data->seance); } catch (Exception $e) { + // Commenté pour ne pas spammer les cron avec les séances pas encore publiées + // echo "ERREUR $file (seance) : {$e->getMessage()}\n"; $seances_manquantes++; continue; } @@ -71,6 +73,7 @@ protected function execute($arguments = array(), $options = array()) $scrutin->tagInterventions(); } catch(Exception $e) { echo "ERREUR $file (tag interventions) : {$e->getMessage()}\n"; + continue; } $scrutin->setVotes($data->parlementaires); From e9d15582b78814765ec836dab1a85eb47e5e6297 Mon Sep 17 00:00:00 2001 From: Nicolas Joyard Date: Mon, 2 Jul 2018 10:44:21 +0200 Subject: [PATCH 12/53] =?UTF-8?q?Ajout=20check=20nb=20scrutins/s=C3=A9ance?= =?UTF-8?q?,=20recherche=20scrutins=20par=20votes=20et=20plus=20par=20ordr?= =?UTF-8?q?e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/model/doctrine/Scrutin.class.php | 51 ++++++++++++---------------- lib/task/loadScrutinsTask.class.php | 25 ++++++++++++++ 2 files changed, 46 insertions(+), 30 deletions(-) diff --git a/lib/model/doctrine/Scrutin.class.php b/lib/model/doctrine/Scrutin.class.php index 3b7ce88ba..23ed6db60 100644 --- a/lib/model/doctrine/Scrutin.class.php +++ b/lib/model/doctrine/Scrutin.class.php @@ -30,41 +30,32 @@ public function setSeance($id_jo) { } public function tagInterventions() { - // Comptage des scrutins avec numéro inférieur dans la même séance - $avant = Doctrine::getTable('Scrutin') - ->createQuery('s') - ->select('count(1) as cnt') - ->where('s.seance_id = ?', $this->seance_id) - ->andWhere('s.numero < ?', $this->numero) - ->fetchOne()['cnt']; - // Recherche de l'intervention avec un tableau de votants qui correspond - $inter = Doctrine::getTable('Intervention') - ->createQuery('i') - ->where('i.seance_id = ?', $this->seance_id) - ->andWhere("i.intervention LIKE '%table class=\"scrutin\"%'") - ->orderBy('i.timestamp') - ->offset($avant) - ->fetchOne(); + $inters = Doctrine::getTable('Intervention') + ->createQuery('i') + ->where('i.seance_id = ?', $this->seance_id) + ->andWhere("i.intervention LIKE '%table class=\"scrutin\"%'") + ->orderBy('i.timestamp') + ->execute(); $found = FALSE; - $info = "intervention non trouvée"; - if ($inter) { - $found = TRUE; - $info = "intervention trouvée"; - - // Vérification du nombre de pour/contre + $info = "votants: {$this->nombre_votants}, pour: {$this->nombre_pours}, contre: {$this->nombre_contres}"; + foreach ($inters as $inter) { $text = $inter->intervention; - if (preg_match('/pour[^<]*<\/td>(\d+)/i', $text, $match_pour) == 0 - || intval($match_pour[1]) != $this->nombre_pours - || preg_match('/contre[^<]*<\/td>(\d+)/i', $text, $match_contre) == 0 - || intval($match_contre[1]) != $this->nombre_contres) { - $found = FALSE; - $info .= " pour: {$match_pour[1]}!={$this->nombre_pours}"; - $info .= " contre: {$match_contre[1]}!={$this->nombre_contres}"; - $info .= " \n$text"; + $mv = preg_match('/votants[^<]*<\/td>(\d+)/i', $text, $match_votant); + $mp = preg_match('/pour[^<]*<\/td>(\d+)/i', $text, $match_pour); + $mc = preg_match('/contre[^<]*<\/td>(\d+)/i', $text, $match_contre); + + if ($mv == 0 || $mp == 0 || $mc == 0) { + echo "WARNING: décomptes intervention {$inter->id} incomplets :\n$text\n"; + } elseif (intval($match_votant[1]) != $this->nombre_votants + || intval($match_pour[1]) != $this->nombre_pours + || intval($match_contre[1]) != $this->nombre_contres) { + $info .= "\n inter {$inter->id} différente (v:{$match_votant[1]}, p:{$match_pour[1]}), c:{$match_contre[1]})"; } else { + $found = TRUE; $inter->addTag("scrutin:numero={$this->numero}"); + break; } } @@ -73,7 +64,7 @@ public function tagInterventions() { throw new Exception( "Scrutin {$this->numero} non trouvé dans les interventions " . "de la séance {$seance->id} du {$seance->date} {$seance->moment}\n" - . "scrutins avant=$avant, $info" + . "$info" ); } } diff --git a/lib/task/loadScrutinsTask.class.php b/lib/task/loadScrutinsTask.class.php index a8f303f75..fd068c946 100644 --- a/lib/task/loadScrutinsTask.class.php +++ b/lib/task/loadScrutinsTask.class.php @@ -17,6 +17,7 @@ protected function execute($arguments = array(), $options = array()) $backupdir = dirname(__FILE__).'/../../batch/scrutin/loaded/'; $manager = new sfDatabaseManager($this->configuration); $seances_manquantes = 0; + $seance_ids = array(); if (!is_dir($backupdir)) { mkdir($backupdir, 0777, TRUE); @@ -52,6 +53,10 @@ protected function execute($arguments = array(), $options = array()) continue; } + if (!in_array($scrutin->seance_id, $seance_ids)) { + $seance_ids[] = $scrutin->seance_id; + } + try { $scrutin->setDemandeur($data->demandeur); $scrutin->setTitre($data->titre); @@ -82,6 +87,26 @@ protected function execute($arguments = array(), $options = array()) rename($dir . $file, $backupdir . $file); } + // Vérification des scrutins pour chaque séance modifiée + $seances = Doctrine::getTable("Seance") + ->createQuery("s") + ->whereIn("s.id", $seance_ids) + ->execute(); + + foreach ($seances as $seance) { + $scrutins = count($seance->Scrutins); + $tables = Doctrine::getTable("Intervention") + ->createQuery("i") + ->select("count(1) as cnt") + ->where("i.seance_id = ?", $seance->id) + ->andWhere("i.intervention LIKE '%fetchOne()['cnt']; + + if ($scrutins != $tables) { + echo "WARNING: séance {$seance->id} du {$seance->date} {$seance->moment} : {$scrutins} scrutins, {$tables} tableaux\n"; + } + } + if ($seances_manquantes > 0) { echo "WARNING: $seances_manquantes séances non trouvées\n"; } From 6917b486d83e625051051242fd9f7d4422e2c784 Mon Sep 17 00:00:00 2001 From: Nicolas Joyard Date: Mon, 2 Jul 2018 21:46:31 +0200 Subject: [PATCH 13/53] =?UTF-8?q?Prise=20en=20compte=20date=20d=C3=A9but?= =?UTF-8?q?=20d=C3=A9l=C3=A9gations=20+=20typo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/model/doctrine/Scrutin.class.php | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/lib/model/doctrine/Scrutin.class.php b/lib/model/doctrine/Scrutin.class.php index 23ed6db60..6ed5b686c 100644 --- a/lib/model/doctrine/Scrutin.class.php +++ b/lib/model/doctrine/Scrutin.class.php @@ -2,6 +2,10 @@ class Scrutin extends BaseScrutin { + // Date des premiers scrutins où les délégations ne sont pas toutes à FALSE + // On ne génère pas de preuve de présence à partir des votes avant cette date + const DEBUT_DELEGATIONS = '2018-03-20'; + public function getLinkSource() { return "http://www2.assemblee-nationale.fr/scrutins/detail/(legislature)/" . sfConfig::get('app_legislature', 13) @@ -29,8 +33,9 @@ public function setSeance($id_jo) { return $ret; } + // Recherche de l'intervention avec un tableau de votants qui correspond public function tagInterventions() { - // Recherche de l'intervention avec un tableau de votants qui correspond + // Listing des interventions avec un tableau de scrutin $inters = Doctrine::getTable('Intervention') ->createQuery('i') ->where('i.seance_id = ?', $this->seance_id) @@ -40,7 +45,9 @@ public function tagInterventions() { $found = FALSE; $info = "votants: {$this->nombre_votants}, pour: {$this->nombre_pours}, contre: {$this->nombre_contres}"; + foreach ($inters as $inter) { + // Extraction des votants/pours/contres $text = $inter->intervention; $mv = preg_match('/votants[^<]*<\/td>
(\d+)/i', $text, $match_votant); $mp = preg_match('/pour[^<]*<\/td>(\d+)/i', $text, $match_pour); @@ -51,7 +58,7 @@ public function tagInterventions() { } elseif (intval($match_votant[1]) != $this->nombre_votants || intval($match_pour[1]) != $this->nombre_pours || intval($match_contre[1]) != $this->nombre_contres) { - $info .= "\n inter {$inter->id} différente (v:{$match_votant[1]}, p:{$match_pour[1]}), c:{$match_contre[1]})"; + $info .= "\n inter {$inter->id} différente (v:{$match_votant[1]}, p:{$match_pour[1]}, c:{$match_contre[1]})"; } else { $found = TRUE; $inter->addTag("scrutin:numero={$this->numero}"); @@ -112,7 +119,10 @@ public function setVotes($parlementaires) { return FALSE; } - $parlscrutin->updatePresence(); + if ($this->Seance->date >= self::DEBUT_DELEGATIONS) { + $parlscrutin->updatePresence(); + } + $parlscrutin->save(); $parlscrutin->free(); } catch (Exception $e) { From b1413b04ab5fbd25f4f67d23b0806dde8a4de49e Mon Sep 17 00:00:00 2001 From: Nicolas Joyard Date: Tue, 3 Jul 2018 11:01:58 +0200 Subject: [PATCH 14/53] Ne plus utiliser les pour trouver les scrutins --- lib/model/doctrine/Scrutin.class.php | 9 +++++---- lib/task/loadScrutinsTask.class.php | 3 ++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/model/doctrine/Scrutin.class.php b/lib/model/doctrine/Scrutin.class.php index 6ed5b686c..7ffc64311 100644 --- a/lib/model/doctrine/Scrutin.class.php +++ b/lib/model/doctrine/Scrutin.class.php @@ -39,7 +39,8 @@ public function tagInterventions() { $inters = Doctrine::getTable('Intervention') ->createQuery('i') ->where('i.seance_id = ?', $this->seance_id) - ->andWhere("i.intervention LIKE '%table class=\"scrutin\"%'") + ->andWhere("i.intervention LIKE '%nombre de votants%suffrages exprimés%pour%contre%'") + // ->andWhere("i.intervention LIKE '%table class=\"scrutin\"%'") ->orderBy('i.timestamp') ->execute(); @@ -49,9 +50,9 @@ public function tagInterventions() { foreach ($inters as $inter) { // Extraction des votants/pours/contres $text = $inter->intervention; - $mv = preg_match('/votants[^<]*<\/td>
(\d+)/i', $text, $match_votant); - $mp = preg_match('/pour[^<]*<\/td>(\d+)/i', $text, $match_pour); - $mc = preg_match('/contre[^<]*<\/td>(\d+)/i', $text, $match_contre); + $mv = preg_match('/nombre de votants(?:<\/td>|\s*)(\d+)/i', $text, $match_votant); + $mp = preg_match('/pour l\'(?:adoption|approbation)(?:<\/td>|\s*)(\d+)/i', $text, $match_pour); + $mc = preg_match('/contre(?:<\/td>|\s*)(\d+)/i', $text, $match_contre); if ($mv == 0 || $mp == 0 || $mc == 0) { echo "WARNING: décomptes intervention {$inter->id} incomplets :\n$text\n"; diff --git a/lib/task/loadScrutinsTask.class.php b/lib/task/loadScrutinsTask.class.php index fd068c946..b0161c6a6 100644 --- a/lib/task/loadScrutinsTask.class.php +++ b/lib/task/loadScrutinsTask.class.php @@ -99,7 +99,8 @@ protected function execute($arguments = array(), $options = array()) ->createQuery("i") ->select("count(1) as cnt") ->where("i.seance_id = ?", $seance->id) - ->andWhere("i.intervention LIKE '%andWhere("i.intervention LIKE '%nombre de votants%suffrages exprimés%pour%contre%'") + // ->andWhere("i.intervention LIKE '%
fetchOne()['cnt']; if ($scrutins != $tables) { From e0b58ae46d5fa624babbd56bc223b32db4ad41ac Mon Sep 17 00:00:00 2001 From: Nicolas Joyard Date: Thu, 19 Jul 2018 10:08:25 +0200 Subject: [PATCH 15/53] Accepter les virgules dans les scrutins --- lib/model/doctrine/Scrutin.class.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/model/doctrine/Scrutin.class.php b/lib/model/doctrine/Scrutin.class.php index 7ffc64311..b2118db92 100644 --- a/lib/model/doctrine/Scrutin.class.php +++ b/lib/model/doctrine/Scrutin.class.php @@ -50,9 +50,9 @@ public function tagInterventions() { foreach ($inters as $inter) { // Extraction des votants/pours/contres $text = $inter->intervention; - $mv = preg_match('/nombre de votants(?:<\/td> - + @@ -140,7 +140,7 @@ class="jstitle phototitle c_ - + @@ -196,7 +196,7 @@ class="jstitle ">

Explications :

    -
  • Semaines d'activité : Nombre de semaines où le député a été relevé présent en commission ou a pris la parole (même brièvement) en hémicycle
  • +
  • Semaines d'activité : Nombre de semaines où le député a été relevé présent en commission, a pris la parole (même brièvement) dans l'hémicycle ou a participé physiquement à un scrutin public
  • Commission réunions : Nombre de réunions de commission où le député a été relevé présent
  • Commission interventions : Nombre d'interventions prononcées par le député en commissions
  • Hémicycle interventions longues : Nombre d'interventions de plus de 20 mots prononcées par le député en hémicycle
  • diff --git a/apps/frontend/modules/plot/templates/_plotParlementaire.php b/apps/frontend/modules/plot/templates/_plotParlementaire.php index 5683d888a..5efcd1dcc 100644 --- a/apps/frontend/modules/plot/templates/_plotParlementaire.php +++ b/apps/frontend/modules/plot/templates/_plotParlementaire.php @@ -52,7 +52,7 @@ if (!isset($widthrate) || $widthrate > 1/3) : ?>

    Date: Mon, 20 Aug 2018 01:13:24 +0200 Subject: [PATCH 46/53] refacto indicateurs titles and descr for shared use between synthese and MPs pages --- .../modules/parlementaire/templates/_top.php | 35 ++++++----- .../parlementaire/templates/topSuccess.php | 48 +++++---------- lib/model/doctrine/myTools.class.php | 59 +++++++++++++++++++ 3 files changed, 90 insertions(+), 52 deletions(-) diff --git a/apps/frontend/modules/parlementaire/templates/_top.php b/apps/frontend/modules/parlementaire/templates/_top.php index 28d9a6f20..d1b9f0b11 100644 --- a/apps/frontend/modules/parlementaire/templates/_top.php +++ b/apps/frontend/modules/parlementaire/templates/_top.php @@ -8,21 +8,19 @@ if (!isset($target)) $target = ''; -$titres = array( - 'semaines_presence' => 'Semaines d\'activité', - 'commission_presences' => 'Présences en commission', - 'commission_interventions' => 'Interventions en commission', - 'hemicycle_presences' => 'Présences en hémicycle : Information non publique', - 'hemicycle_interventions' => 'Interventions longues en hémicycle', -//'hemicycle_interventions_courtes' => 'Interventions courtes en hémicycle', - 'amendements_proposes' => 'Amendements proposés', -//'amendements_adoptes' => 'Amendements adoptés', -//'amendements_rejetes' => 'Amendements rejetés', - 'rapports' => 'Rapports écrits', - 'propositions_ecrites' => 'Propositions de loi écrites', - 'propositions_signees' => 'Propositions de loi signées', - 'questions_ecrites' => 'Questions écrites', - 'questions_orales' => 'Questions orales', +$indicateurs = myTools::$indicateurs; +$fields = array( + 'semaines_presence', + 'commission_presences', + 'commission_interventions', + 'hemicycle_presences', + 'hemicycle_interventions', + 'amendements_proposes', + 'rapports', + 'propositions_ecrites', + 'propositions_signees', + 'questions_ecrites', + 'questions_orales' ); $images = array( 'semaines_presence' => 'ico_sem_%s.png', @@ -97,13 +95,13 @@ $icosize = 16; if (isset($widthrate)) $icosize = floor($icosize*$widthrate); -foreach(array_keys($images) as $k) { +foreach($fields as $k) { if ($k === "hemicycle_presences") { - echo ''.$titres[$k].' : ??'; + echo ''.$indicateurs[$k]['titre'].' : ??'; } else { $value = (isset($top[$k]['value']) ? $top[$k]['value'] : 0); $couleur = 'gris'; - $titre = $value.' '.$titres[$k]; + $titre = $value.' '.$indicateurs[$k]['titre']; if ($value < 2) $titre = preg_replace('/s$/', '', str_replace('s ', ' ', $titre)); if ($rank && $top[$k]['rank'] <= 150 && $value) { @@ -114,6 +112,7 @@ $couleur = 'rouge'; $titre .= ' (fait partie des 150 moins actifs sur ce critère)'; } + $titre .= ' -- -- '.$indicateurs[$k]['desc']; echo ''; echo '<'.($rank ? 'a' : 'span').$target.' class="jstitle" title="'.$titre.'" href="'.url_for('@top_global_sorted?sort='.$sort[$k].'#'.$parlementaire->slug, $abs).'">'; echo '(mise-à-jour quotidienne, dernière en date le )

    Activité de tous les députés :

    "d'activité", 'commission_presences' => 'réunion', @@ -40,22 +41,9 @@ 'questions_ecrites' => 'qe', 'questions_orales' => 'qo' ); -$bulles = array( - "", - "Semaines d'activité -- Nombre de semaines où le député a été relevé présent en commission, -- a pris la parole (même brièvement) dans hémicycle -- ou a participé physiquement à un scrutin public", - "Réunions de Commission -- Nombre de réunions de commission où le député a été relevé présent", - "Interventions en Commission -- Nombre d'interventions prononcées par le député en commissions", - "Interventions longues en Hémicycle -- Nombre d'interventions de plus de 20 mots prononcées par le député en hémicycle", - "Interventions courtes en Hémicycle -- Nombre d'interventions de 20 mots et moins prononcées par le député en hémicycle", - "Amendements proposés -- Nombre d'amendements proposés par le député (indiqué « auteur » par l'Assemblée)", - "Amendements signés -- Nombre d'amendements proposés ou co-signés par le député", - "Amendements adoptés -- Nombre d'amendements signés par le député qui ont été adoptés en séance", - "Rapports écrits -- Nombre de rapports ou avis dont le député est l'auteur", - "Propositions écrites -- Nombre de propositions de loi ou de résolution dont le député est l'auteur", - "Propositions signées -- Nombre de propositions de loi ou de résolution dont le député est cosignataire", - "Questions écrites -- Nombre de questions écrites soumises par le député", - "Questions orales -- Nombre de questions orales posées par le député" -); +$bulles = array(""); +foreach (array_keys($title) as $k) + $bulles[] = $indicateurs[$k]['titre'].' -- -- '.$indicateurs[$k]['desc']; ?>
    @@ -63,11 +51,11 @@
- + - + @@ -136,15 +124,17 @@ class="jstitle phototitle c_

Activité moyenne d'un député de chaque groupe politique :

+
|\s*)(\d+)/i', $text, $match_votant); - $mp = preg_match('/pour l\'(?:adoption|approbation)(?:<\/td>|\s*)(\d+)/i', $text, $match_pour); - $mc = preg_match('/contre(?:<\/td>|\s*)(\d+)/i', $text, $match_contre); + $mv = preg_match('/nombre de votants(?:<\/td>|[,\s]*)(\d+)/i', $text, $match_votant); + $mp = preg_match('/pour l\'(?:adoption|approbation)(?:<\/td>|[,\s]*)(\d+)/i', $text, $match_pour); + $mc = preg_match('/contre(?:<\/td>|[,\s])(\d+)/i', $text, $match_contre); if ($mv == 0 || $mp == 0 || $mc == 0) { echo "WARNING: décomptes intervention {$inter->id} incomplets :\n$text\n"; From 32b36724d97e5a7d6d28c733c56a1b189bb00744 Mon Sep 17 00:00:00 2001 From: RouxRC Date: Thu, 16 Aug 2018 06:40:53 +0200 Subject: [PATCH 16/53] merge recent dep fix --- ansible/roles/cpc.install/templates/web_Dockerfile.j2 | 1 - 1 file changed, 1 deletion(-) diff --git a/ansible/roles/cpc.install/templates/web_Dockerfile.j2 b/ansible/roles/cpc.install/templates/web_Dockerfile.j2 index b4e276ce4..416faf3d3 100644 --- a/ansible/roles/cpc.install/templates/web_Dockerfile.j2 +++ b/ansible/roles/cpc.install/templates/web_Dockerfile.j2 @@ -4,7 +4,6 @@ RUN apt-get update && apt-get -my install --no-install-recommends \ wget \ vim \ mysql-client \ - python-bs4 \ libfile-path-perl \ libfreetype6-dev \ libjpeg62-turbo-dev \ From d7ef8c3a20788f2b772b4ca5de697fcd1a55dec8 Mon Sep 17 00:00:00 2001 From: RouxRC Date: Thu, 16 Aug 2018 06:42:05 +0200 Subject: [PATCH 17/53] cleanup, cf comments --- .../doctrine/ParlementaireScrutin.class.php | 6 +----- lib/model/doctrine/Scrutin.class.php | 20 ++++++------------- lib/task/loadScrutinsTask.class.php | 2 +- 3 files changed, 8 insertions(+), 20 deletions(-) diff --git a/lib/model/doctrine/ParlementaireScrutin.class.php b/lib/model/doctrine/ParlementaireScrutin.class.php index 028d11e5e..0b079e936 100644 --- a/lib/model/doctrine/ParlementaireScrutin.class.php +++ b/lib/model/doctrine/ParlementaireScrutin.class.php @@ -3,7 +3,7 @@ class ParlementaireScrutin extends BaseParlementaireScrutin { - public function setParlementaire($id_an) { + public function setParlementaireByIDAN($id_an) { $parl = Doctrine::getTable('Parlementaire')->findOneByIdAn($id_an); if (!$parl) { throw new Exception("Aucun parlementaire trouvé avec l'ID AN $id_an"); @@ -12,10 +12,6 @@ public function setParlementaire($id_an) { return $this->_set('parlementaire_id', $parl->id); } - public function setScrutin($scrutin) { - return $this->_set('scrutin_id', $scrutin->id); - } - public function updatePresence() { $this->Scrutin->Seance->setUnsetPresenceLight( $this->parlementaire_id, diff --git a/lib/model/doctrine/Scrutin.class.php b/lib/model/doctrine/Scrutin.class.php index b2118db92..5dcb8f853 100644 --- a/lib/model/doctrine/Scrutin.class.php +++ b/lib/model/doctrine/Scrutin.class.php @@ -13,18 +13,13 @@ public function getLinkSource() { . $this->numero; } - public function setNumero($numero) { - return $this->_set('numero', $numero); - } - public function setSeance($id_jo) { $seance = Doctrine::getTable('Seance')->findOneByIDJO($id_jo); if (!$seance) { throw new Exception("Aucune séance trouvée avec l'id JO $id_jo"); } - $seance_id = $seance->id; - $ret = $this->_set('seance_id', $seance_id) + $ret = $this->_set('seance_id', $seance->id) && $this->_set('date', $seance->date) && $this->_set('numero_semaine', $seance->numero_semaine) && $this->_set('annee', $seance->annee); @@ -34,7 +29,7 @@ public function setSeance($id_jo) { } // Recherche de l'intervention avec un tableau de votants qui correspond - public function tagInterventions() { + public function tagIntervention() { // Listing des interventions avec un tableau de scrutin $inters = Doctrine::getTable('Intervention') ->createQuery('i') @@ -83,13 +78,10 @@ public function setDemandeur($demandeur) { } public function setTitre($titre) { + // TODO? clean title return $this->_set('titre', $titre); } - public function setType($type) { - return $this->_set('type', $type); - } - public function setStats($sort, $nb_votants, $nb_pours, $nb_contres, $nb_abst) { return $this->_set('sort', $sort) && $this->_set('nombre_votants', $nb_votants) @@ -106,9 +98,9 @@ public function setVotes($parlementaires) { if (!$parlscrutin) { $parlscrutin = new ParlementaireScrutin(); - if (!$parlscrutin->setParlementaire($id_an) + if (!$parlscrutin->setParlementaireByIDAN($id_an) || !$parlscrutin->setScrutin($this)) { - return FALSE; + throw new Exception('Could not set ParlId/ScrutinId'); } } @@ -117,7 +109,7 @@ public function setVotes($parlementaires) { || !$parlscrutin->_set('position_groupe', $data->position_groupe) || !$parlscrutin->_set('par_delegation', $data->par_delegation) || !$parlscrutin->_set('mise_au_point_position', $data->mise_au_point_position or NULL)) { - return FALSE; + throw new Exception("Could not set vote metadata: {$data}"); } if ($this->Seance->date >= self::DEBUT_DELEGATIONS) { diff --git a/lib/task/loadScrutinsTask.class.php b/lib/task/loadScrutinsTask.class.php index b0161c6a6..f18e1a211 100644 --- a/lib/task/loadScrutinsTask.class.php +++ b/lib/task/loadScrutinsTask.class.php @@ -75,7 +75,7 @@ protected function execute($arguments = array(), $options = array()) $scrutin->save(); try { - $scrutin->tagInterventions(); + $scrutin->tagIntervention(); } catch(Exception $e) { echo "ERREUR $file (tag interventions) : {$e->getMessage()}\n"; continue; From fabdb2c12dee9d240fc384f67132e346a2059dbd Mon Sep 17 00:00:00 2001 From: RouxRC Date: Thu, 16 Aug 2018 06:42:40 +0200 Subject: [PATCH 18/53] setup presences depending on position + no delegation cf https://github.com/regardscitoyens/nosdeputes.fr/pull/115#pullrequestreview-146688706 --- lib/model/doctrine/ParlementaireScrutin.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/model/doctrine/ParlementaireScrutin.class.php b/lib/model/doctrine/ParlementaireScrutin.class.php index 0b079e936..37aa32636 100644 --- a/lib/model/doctrine/ParlementaireScrutin.class.php +++ b/lib/model/doctrine/ParlementaireScrutin.class.php @@ -18,7 +18,7 @@ public function updatePresence() { $this->parlementaire_groupe_acronyme, 'scrutin', $this->Scrutin->getLinkSource(), - $this->position != 'nonVotant' && $this->mise_au_point_position == NULL + $this->position && !$this->par_delegation ); } From 6a6528c6d9267277eed14a8ae50f16b199421bb3 Mon Sep 17 00:00:00 2001 From: RouxRC Date: Thu, 16 Aug 2018 06:44:08 +0200 Subject: [PATCH 19/53] =?UTF-8?q?handle=20one=20preuve=20pr=C3=A9sence=20b?= =?UTF-8?q?y=20source=20even=20for=20same=20type=20except=20for=20interven?= =?UTF-8?q?tion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit to avoid removing a presence corresponding to multiple proofs in the same seance (like 2 scrutins with only one rectified) (and now keep first intervention instead of last as reference then) cf https://github.com/regardscitoyens/nosdeputes.fr/pull/115#pullrequestreview-146694350 --- lib/model/doctrine/Presence.class.php | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/lib/model/doctrine/Presence.class.php b/lib/model/doctrine/Presence.class.php index 3e8cead31..05afa9f71 100644 --- a/lib/model/doctrine/Presence.class.php +++ b/lib/model/doctrine/Presence.class.php @@ -6,16 +6,23 @@ class Presence extends BasePresence { public function addPreuve($type, $source) { - $q = Doctrine::getTable('PreuvePresence')->createQuery('p'); - $preuve = $q->where('presence_id = ?', $this->id)->andWhere('type = ?', $type)->fetchOne(); + $q = Doctrine::getTable('PreuvePresence')->createQuery('p') + ->where('presence_id = ?', $this->id) + ->andWhere('type = ?', $type); + if ($type != 'intervention') + $q->andWhere('source = ?', $source); + $preuve = $q->fetchOne(); $q->free(); + if (!$preuve) { $preuve = new PreuvePresence(); $preuve->presence_id = $this->id; $preuve->type = $type; + $preuve->source = $source; + $this->nb_preuves++ ; } - $preuve->source = $source; + $res = $preuve->save(); $this->save(); $preuve->free(); @@ -23,8 +30,12 @@ public function addPreuve($type, $source) { } public function delPreuve($type, $source) { - $q = Doctrine::getTable('PreuvePresence')->createQuery('p'); - $preuve = $q->where('presence_id = ?', $this->id)->andWhere('type = ?', $type)->fetchOne(); + $q = Doctrine::getTable('PreuvePresence')->createQuery('p') + ->where('presence_id = ?', $this->id) + ->andWhere('type = ?', $type); + if ($type != 'intervention') + $q->andWhere('source = ?', $source); + $preuve = $q->fetchOne(); $q->free(); if ($preuve) { From 5bb0aa233ee2289c45de87730bc6b8a45f7b3378 Mon Sep 17 00:00:00 2001 From: RouxRC Date: Thu, 16 Aug 2018 07:01:04 +0200 Subject: [PATCH 20/53] lighter log for future crons --- batch/common/opendata.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/batch/common/opendata.py b/batch/common/opendata.py index d504e80a8..472bb8e80 100644 --- a/batch/common/opendata.py +++ b/batch/common/opendata.py @@ -27,9 +27,11 @@ }, } +DEBUG = "--debug" in sys.argv -def log(str): - print(str, file=sys.stderr) +def log(str, debug=False): + if not debug or DEBUG: + print(str, file=sys.stderr) def fetch_an_jsonzip(legislature, objet): @@ -56,7 +58,7 @@ def fetch_an_jsonzip(legislature, objet): localzip_lastmod = "%s.last_modified" % localzip url = "%s/%s" % (AN_BASE_URL, AN_ENTRYPOINTS[str(legislature)][objet]) - log("Téléchargement %s" % url) + log("Téléchargement %s" % url, debug=True) try: soup = BeautifulSoup(requests.get(url).content, "html5lib") @@ -77,26 +79,26 @@ def match_link(a): if jsonzip_url.startswith("/"): jsonzip_url = "%s%s" % (AN_BASE_URL, jsonzip_url) - log("URL JSON zippé : %s" % jsonzip_url) + log("URL JSON zippé : %s" % jsonzip_url, debug=True) try: lastmod = requests.head(jsonzip_url).headers["Last-Modified"] except Exception: raise Exception("Date du dump .json.zip introuvable") - log("Date modification dump .json.zip: %s" % lastmod) + log("Date modification dump .json.zip: %s" % lastmod, debug=True) do_download = True if os.path.exists(localzip) and os.path.exists(localzip_lastmod): with open(localzip_lastmod, "r") as f: known_lastmod = f.read() - log("Date modification dernier telechargement: %s" % known_lastmod) + log("Date modification dernier telechargement: %s" % known_lastmod, debug=True) if known_lastmod == lastmod: do_download = False if do_download: - log("Téléchargement .json.zip") + log("Téléchargement .json.zip", debug=True) try: with open(localzip, "wb") as out: @@ -108,7 +110,7 @@ def match_link(a): except Exception: raise Exception("Téléchargement .json.zip impossible") else: - log("Téléchargement skippé, fichier non mis à jour") + log("Téléchargement skippé, fichier non mis à jour", debug=True) return localzip, do_download @@ -127,7 +129,7 @@ def fetch_an_json(legislature, objet): localzip, updated = fetch_an_jsonzip(legislature, objet) with ZipFile(localzip, "r") as z: for f in [f for f in z.namelist() if f.endswith(".json")]: - log("JSON extrait : %s" % f) + log("JSON extrait : %s" % f, debug=True) with z.open(f) as zf: return json.load(zf), updated From 93469c6861abaf3c281a7c8e6f656aacb48b9ff9 Mon Sep 17 00:00:00 2001 From: RouxRC Date: Thu, 16 Aug 2018 07:02:06 +0200 Subject: [PATCH 21/53] include load_scrutins in daily loads --- bin/loadupdate | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bin/loadupdate b/bin/loadupdate index 39adb02e2..05e0ee120 100644 --- a/bin/loadupdate +++ b/bin/loadupdate @@ -164,6 +164,9 @@ done; # Postprocessings +echo == Load scrutins +echo ======================= +bash bin/load_scrutins echo == Tag séances echo ======================= bash bin/tag_seance From 01619dae8c4c0c9d36d349edbb2ed66f2c6b7a91 Mon Sep 17 00:00:00 2001 From: RouxRC Date: Thu, 16 Aug 2018 07:56:29 +0200 Subject: [PATCH 22/53] adjust groupes names from opendata an to nd ones --- batch/common/opendata.py | 10 +++++++++- batch/scrutin/parse_scrutins.py | 3 ++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/batch/common/opendata.py b/batch/common/opendata.py index 472bb8e80..63b61dfcd 100644 --- a/batch/common/opendata.py +++ b/batch/common/opendata.py @@ -167,12 +167,18 @@ def _cached_ref( return json.load(f) -def ref_groupes(legislature): +def ref_groupes(legislature, ND_names=False): """ Renvoie un mapping des id opendata des groupes parlementaires vers leur abbréviation """ + GROUPES_ND = { + "UDI-AGIR": "UAI", + "LAREM": "LREM", + "FI": "LFI" + } + def _extract_list(data): return filter( lambda o: o["codeType"] == "GP", @@ -183,6 +189,8 @@ def _extract_id(organe): return organe["uid"] def _extract_mapped(organe): + if ND_names and organe["libelleAbrev"] in GROUPES_ND: + return GROUPES_ND[organe["libelleAbrev"]] return organe["libelleAbrev"] return _cached_ref( diff --git a/batch/scrutin/parse_scrutins.py b/batch/scrutin/parse_scrutins.py index d6e28ffa2..3c803aec2 100755 --- a/batch/scrutin/parse_scrutins.py +++ b/batch/scrutin/parse_scrutins.py @@ -47,13 +47,14 @@ "abstention": "abstentions", } + SCRUTINS_DIR = os.path.join(BATCH_DIR, "scrutin", "scrutins") TYPES = {"SPS": "solennel", "SPO": "ordinaire"} def parse_scrutins(legislature, data): - groupes = ref_groupes(legislature) + groupes = ref_groupes(legislature, ND_names=True) seances = ref_seances(legislature) if not os.path.exists(SCRUTINS_DIR): From cf463a0b230b13c57c0976dd61df85c09f8bc6fd Mon Sep 17 00:00:00 2001 From: RouxRC Date: Thu, 16 Aug 2018 20:29:06 +0200 Subject: [PATCH 23/53] warn on missing scrutins in opendata AN --- bin/load_scrutins | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/bin/load_scrutins b/bin/load_scrutins index 066368aab..330ab0e29 100755 --- a/bin/load_scrutins +++ b/bin/load_scrutins @@ -6,4 +6,13 @@ cd $PATH_APP source bin/db.inc batch/scrutin/parse_scrutins.py $LEGISLATURE + +# Check missing scrutins +ls batch/scrutin/scrutins/scrutin_15_0*.sha1 | sed 's/^.*scrutin_15_0\+//' | sed 's/.sha1//' > /tmp/numscrutins +seq 1 `tail -1 /tmp/numscrutins` > /tmp/allscrutins +if diff /tmp/{num,all}scrutins | grep . > /dev/null; then + echo "Some scrutins are missing from OpenData:" + diff /tmp/{num,all}scrutins | grep '>' | sed 's|> |- http://www2.assemblee-nationale.fr//detail/(legislature)/15/(num)/|' +fi + php symfony load:Scrutins From e3621a1bfc191b4f2dd57c77c424460770e60f2b Mon Sep 17 00:00:00 2001 From: RouxRC Date: Thu, 16 Aug 2018 20:33:43 +0200 Subject: [PATCH 24/53] opendata is updated only daily, no need to run load scrutins trice a day --- bin/loadupdate | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/bin/loadupdate b/bin/loadupdate index 05e0ee120..6529484bb 100644 --- a/bin/loadupdate +++ b/bin/loadupdate @@ -164,9 +164,11 @@ done; # Postprocessings -echo == Load scrutins -echo ======================= -bash bin/load_scrutins +if [[ $jo -eq 1 ]]; then + echo == Load scrutins + echo ======================= + bash bin/load_scrutins +fi echo == Tag séances echo ======================= bash bin/tag_seance From 5a48a934aeae411ae654f8b9d5477f0696d27cfe Mon Sep 17 00:00:00 2001 From: RouxRC Date: Thu, 16 Aug 2018 21:03:06 +0200 Subject: [PATCH 25/53] Adjust first date of delegations present MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Il me semble qu'on avait discuté de ça au sprint mais je ne suis plus sûr. On a deux dates de première apparition de délégations dans les données OpenData: - en octobre 2017 pour deux scrutins solennels http://www2.assemblee-nationale.fr/scrutins/detail/(legislature)/15/(num)/183 - en mars 2018 pour une série de scrutins ordinaires http://www2.assemblee-nationale.fr/scrutins/detail/(legislature)/15/(num)/424 (ce n'est pas explicité sur la page web du scrutin mais dans les données Adrien Taquet est déclaré votant par délégation sur plusieurs autres scrutins ordinaires après celui là) La question est : à partir de quand on considère un vote comme une présence s'il n'a pas le champ par_delegation à True. J'ai l'impression qu'on peut considérer la première des deux dates comme la bonne. Mais le choix dans le code de la seconde me fait douter de ce qu'on s'était dit avec @njoyard. Peut-être est-ce aussi que les données auraient changé depuis et qu'ils ont fini par rétrorenseigner les données notamment pour faire leur application des sanctions, qui sait !? ;) Du coup avis bienvenus ! cc @njoyard @teymour @tarnefys @Massiliane PS: au final, on a dans l'OpenData : 14 scrutins solennels avec des délégations et... 22 scrutins ordinaires. Donc bien que ce soit exceptionnel, c'est bien toujours appliqué aussi sur ces scrutins contrairement à ce qu'on nous a toujours répondu à tort. ;) --- lib/model/doctrine/Scrutin.class.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/model/doctrine/Scrutin.class.php b/lib/model/doctrine/Scrutin.class.php index 5dcb8f853..4cce4de73 100644 --- a/lib/model/doctrine/Scrutin.class.php +++ b/lib/model/doctrine/Scrutin.class.php @@ -4,7 +4,8 @@ class Scrutin extends BaseScrutin { // Date des premiers scrutins où les délégations ne sont pas toutes à FALSE // On ne génère pas de preuve de présence à partir des votes avant cette date - const DEBUT_DELEGATIONS = '2018-03-20'; + const DEBUT_DELEGATIONS = '2017-10-24'; # date premier solennel avec délégations + #const DEBUT_DELEGATIONS = '2018-03-20'; # date premier ordinaire avec délégations public function getLinkSource() { return "http://www2.assemblee-nationale.fr/scrutins/detail/(legislature)/" From 3a827d6c2da68ec33157dae07a11b8ec7f3bb869 Mon Sep 17 00:00:00 2001 From: RouxRC Date: Thu, 16 Aug 2018 22:52:41 +0200 Subject: [PATCH 26/53] add checks on missing fields --- batch/scrutin/parse_scrutins.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/batch/scrutin/parse_scrutins.py b/batch/scrutin/parse_scrutins.py index 3c803aec2..5bedb60af 100755 --- a/batch/scrutin/parse_scrutins.py +++ b/batch/scrutin/parse_scrutins.py @@ -61,9 +61,9 @@ def parse_scrutins(legislature, data): os.makedirs(SCRUTINS_DIR) for item in data["scrutins"]["scrutin"]: - scrutin = parse_scrutin(item, seances, groupes) + scrutin, logs = parse_scrutin(item, seances, groupes) - numero = ("00000%s" % scrutin["numero"])[-5:] + numero = "%05d" % scrutin["numero"] basename = "scrutin_%s_%s" % (legislature, numero) hash_file = os.path.join(SCRUTINS_DIR, "%s.sha1" % basename) json_file = os.path.join(SCRUTINS_DIR, "%s.json" % basename) @@ -82,6 +82,7 @@ def parse_scrutins(legislature, data): updated = False if updated: + [log(l) for l in logs] with open(hash_file, "w") as f: f.write(sha1) with open(json_file, "w") as f: @@ -90,6 +91,7 @@ def parse_scrutins(legislature, data): def parse_scrutin(data, seances, groupes): + logs = [] synthese = data["syntheseVote"] decompte = synthese["decompte"] scrutin = { @@ -105,6 +107,10 @@ def parse_scrutin(data, seances, groupes): "demandeur": data["demandeur"]["texte"], "parlementaires": {}, } + if not scrutin["seance"]: + logs.append("WARNING: scrutin %s has no seance %s" % (data["numero"], data["seanceRef"])) + if not scrutin["demandeurs"]: + logs.append("WARNING: scrutin %s has no demandeurs %s" % (data["numero"], data["demandeur"])) delegations = 0 @@ -137,8 +143,8 @@ def parse_scrutin(data, seances, groupes): "par_delegation": par_delegation, } - if 2 * delegations > int(synthese["nombreVotants"]): - log("Scrutin %s: trop de délégations" % scrutin["numero"]) + if 2 * delegations > scrutin["nombre_votants"]: + logs.append("Scrutin %s: trop de délégations" % scrutin["numero"]) vote_map = data["miseAuPoint"] if vote_map: @@ -158,7 +164,7 @@ def parse_scrutin(data, seances, groupes): parl = scrutin["parlementaires"][votant["acteurRef"]] parl["mise_au_point_position"] = position - return scrutin + return scrutin, logs if __name__ == "__main__": From 2e36bec4dba64334f4475e1fe20fa910114c1a93 Mon Sep 17 00:00:00 2001 From: RouxRC Date: Thu, 16 Aug 2018 23:20:30 +0200 Subject: [PATCH 27/53] hardfix missing idJO on some Seances from OpenData AN cf https://github.com/regardscitoyens/Issues-Parlement/issues/42 --- batch/common/opendata.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/batch/common/opendata.py b/batch/common/opendata.py index 63b61dfcd..00f53128e 100644 --- a/batch/common/opendata.py +++ b/batch/common/opendata.py @@ -208,6 +208,11 @@ def ref_seances(legislature): Renvoie un mapping des id opendata des séances vers leur ID """ + MISSING = { + "RUANR5L15S2018IDS21117": "20180278", + "RUANR5L15S2018IDS21120": "20180280" + } + def _extract_list(data): return filter( lambda reunion: "IDS" in reunion["uid"], @@ -218,7 +223,7 @@ def _extract_id(reunion): return reunion["uid"] def _extract_mapped(reunion): - return reunion["identifiants"]["idJO"] + return reunion["identifiants"]["idJO"] or MISSING.get(reunion["uid"]) return _cached_ref( legislature, From cc47b6bd5e8d198b26f7217fece58831a75e79bd Mon Sep 17 00:00:00 2001 From: RouxRC Date: Thu, 16 Aug 2018 23:33:06 +0200 Subject: [PATCH 28/53] first cleaning of demandeurs --- batch/scrutin/parse_scrutins.py | 20 +++++++++++++++++++- lib/task/loadScrutinsTask.class.php | 2 +- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/batch/scrutin/parse_scrutins.py b/batch/scrutin/parse_scrutins.py index 5bedb60af..25dc1002d 100755 --- a/batch/scrutin/parse_scrutins.py +++ b/batch/scrutin/parse_scrutins.py @@ -24,6 +24,7 @@ import hashlib import json import os +import re import sys # Faux module dans .. (batch/) @@ -52,6 +53,23 @@ TYPES = {"SPS": "solennel", "SPO": "ordinaire"} +CLEAN_DEMANDEUR = [ + ("President", u"Président"), + ("Conference", u"Conférence"), + ('"', ''), + (r'\s+', ' '), +] + +def clean_demandeur(d): + for reg, rep in CLEAN_DEMANDEUR: + d = re.compile(reg).sub(rep, d) + return d.strip() + +def clean_demandeurs(demandeurs): + if not demandeurs: + return [] + demandeurs = [clean_demandeur(d) for d in demandeurs.split("\n")] + return [d for d in demandeurs if d] def parse_scrutins(legislature, data): groupes = ref_groupes(legislature, ND_names=True) @@ -104,7 +122,7 @@ def parse_scrutin(data, seances, groupes): "nombre_contres": int(decompte["contre"]), "nombre_abstentions": int(decompte["abstentions"]), "sort": data["sort"]["code"], - "demandeur": data["demandeur"]["texte"], + "demandeurs": clean_demandeurs(data["demandeur"]["texte"]), "parlementaires": {}, } if not scrutin["seance"]: diff --git a/lib/task/loadScrutinsTask.class.php b/lib/task/loadScrutinsTask.class.php index f18e1a211..576aeb14f 100644 --- a/lib/task/loadScrutinsTask.class.php +++ b/lib/task/loadScrutinsTask.class.php @@ -58,7 +58,7 @@ protected function execute($arguments = array(), $options = array()) } try { - $scrutin->setDemandeur($data->demandeur); + $scrutin->setDemandeur($data->demandeurs); $scrutin->setTitre($data->titre); $scrutin->setType($data->type); $scrutin->setStats($data->sort, From 1ad9ef58a09b48748809b6ca4b76ba901f049510 Mon Sep 17 00:00:00 2001 From: RouxRC Date: Thu, 16 Aug 2018 23:40:25 +0200 Subject: [PATCH 29/53] hardfix missing demandeurs cf https://github.com/regardscitoyens/Issues-Parlement/issues/43 --- batch/scrutin/parse_scrutins.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/batch/scrutin/parse_scrutins.py b/batch/scrutin/parse_scrutins.py index 25dc1002d..5de1d5efb 100755 --- a/batch/scrutin/parse_scrutins.py +++ b/batch/scrutin/parse_scrutins.py @@ -65,9 +65,14 @@ def clean_demandeur(d): d = re.compile(reg).sub(rep, d) return d.strip() -def clean_demandeurs(demandeurs): +MISSING_DEMANDEURS = { + "17": [u"Président du groupe Nouvelle Gauche"], + "43": [u"Président du groupe La France Insoumise"], + "153": [u"Président du groupe Les Républicains"] +} +def clean_demandeurs(demandeurs, numero): if not demandeurs: - return [] + return MISSING_DEMANDEURS.get(numero, []) demandeurs = [clean_demandeur(d) for d in demandeurs.split("\n")] return [d for d in demandeurs if d] @@ -122,7 +127,7 @@ def parse_scrutin(data, seances, groupes): "nombre_contres": int(decompte["contre"]), "nombre_abstentions": int(decompte["abstentions"]), "sort": data["sort"]["code"], - "demandeurs": clean_demandeurs(data["demandeur"]["texte"]), + "demandeurs": clean_demandeurs(data["demandeur"]["texte"], data["numero"]), "parlementaires": {}, } if not scrutin["seance"]: From 8dc025b24f8401cec0e9f871de2d359547fb46b4 Mon Sep 17 00:00:00 2001 From: RouxRC Date: Fri, 17 Aug 2018 00:25:53 +0200 Subject: [PATCH 30/53] fix warnings on missing fields --- batch/scrutin/parse_scrutins.py | 8 +++++++- lib/model/doctrine/Scrutin.class.php | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/batch/scrutin/parse_scrutins.py b/batch/scrutin/parse_scrutins.py index 5de1d5efb..aee0cd9c8 100755 --- a/batch/scrutin/parse_scrutins.py +++ b/batch/scrutin/parse_scrutins.py @@ -164,6 +164,7 @@ def parse_scrutin(data, seances, groupes): "groupe": acro_groupe, "position_groupe": position_groupe, "par_delegation": par_delegation, + "mise_au_point_position": None } if 2 * delegations > scrutin["nombre_votants"]: @@ -183,7 +184,12 @@ def parse_scrutin(data, seances, groupes): votants = [votants] for votant in votants: if votant["acteurRef"] not in scrutin["parlementaires"]: - scrutin["parlementaires"][votant["acteurRef"]] = {} + scrutin["parlementaires"][votant["acteurRef"]] = { + "position": None, + "groupe": None, #TODO + "position_groupe": None, #TODO + "par_delegation": None + } parl = scrutin["parlementaires"][votant["acteurRef"]] parl["mise_au_point_position"] = position diff --git a/lib/model/doctrine/Scrutin.class.php b/lib/model/doctrine/Scrutin.class.php index 4cce4de73..8d3acbd6a 100644 --- a/lib/model/doctrine/Scrutin.class.php +++ b/lib/model/doctrine/Scrutin.class.php @@ -109,7 +109,7 @@ public function setVotes($parlementaires) { || !$parlscrutin->_set('position', $data->position) || !$parlscrutin->_set('position_groupe', $data->position_groupe) || !$parlscrutin->_set('par_delegation', $data->par_delegation) - || !$parlscrutin->_set('mise_au_point_position', $data->mise_au_point_position or NULL)) { + || !$parlscrutin->_set('mise_au_point_position', $data->mise_au_point_position)) { throw new Exception("Could not set vote metadata: {$data}"); } From b3dc00957dfaee21479edb12ca46f992b888cf71 Mon Sep 17 00:00:00 2001 From: RouxRC Date: Fri, 17 Aug 2018 00:49:01 +0200 Subject: [PATCH 31/53] change demandeur to plural in model + parse groupes --- bin/updateDB8.sh | 2 +- config/doctrine/schema.yml | 4 ++-- lib/model/doctrine/Scrutin.class.php | 26 +++++++++++++++++++++++--- lib/model/doctrine/myTools.class.php | 9 +++++++++ lib/task/loadScrutinsTask.class.php | 2 +- 5 files changed, 36 insertions(+), 7 deletions(-) diff --git a/bin/updateDB8.sh b/bin/updateDB8.sh index f1ba13432..ed1480eda 100755 --- a/bin/updateDB8.sh +++ b/bin/updateDB8.sh @@ -12,7 +12,7 @@ php symfony doctrine:build-filters php symfony doctrine:build-sql echo "CREATE TABLE parlementaire_scrutin (id BIGINT AUTO_INCREMENT, scrutin_id BIGINT, parlementaire_id BIGINT, parlementaire_groupe_acronyme VARCHAR(16), position VARCHAR(255), position_groupe VARCHAR(255), par_delegation TINYINT(1), delegataire_parlementaire_id BIGINT, mise_au_point_position VARCHAR(255), created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, UNIQUE INDEX uniq_index_idx (scrutin_id, parlementaire_id), INDEX index_parlementaire_idx (parlementaire_id), INDEX scrutin_id_idx (scrutin_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = MyISAM" | mysql $MYSQLID $DBNAME -echo "CREATE TABLE scrutin (id BIGINT AUTO_INCREMENT, numero BIGINT, annee BIGINT, numero_semaine BIGINT, date DATE, seance_id BIGINT, nombre_votants BIGINT, nombre_pours BIGINT, nombre_contres BIGINT, nombre_abstentions BIGINT, sort VARCHAR(255), titre text, texteloi_id BIGINT, amendement_id BIGINT, sujet text, demandeur text, demandeur_groupe_acronyme VARCHAR(16), avis_gouvernement VARCHAR(16), avis_rapporteur VARCHAR(16), created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, INDEX seance_id_idx (seance_id), INDEX texteloi_id_idx (texteloi_id), INDEX amendement_id_idx (amendement_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = MyISAM" | mysql $MYSQLID $DBNAME +echo "CREATE TABLE scrutin (id BIGINT AUTO_INCREMENT, numero BIGINT, annee BIGINT, numero_semaine BIGINT, date DATE, seance_id BIGINT, nombre_votants BIGINT, nombre_pours BIGINT, nombre_contres BIGINT, nombre_abstentions BIGINT, type VARCHAR(255), sort VARCHAR(255), titre text, texteloi_id BIGINT, amendement_id BIGINT, sujet text, demandeurs text, demandeurs_groupes_acronymes VARCHAR(64), avis_gouvernement VARCHAR(16), avis_rapporteur VARCHAR(16), created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, UNIQUE INDEX uniq_index_idx (numero), INDEX seance_id_idx (seance_id), INDEX texteloi_id_idx (texteloi_id), INDEX amendement_id_idx (amendement_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = MyISAM" | mysql $MYSQLID $DBNAME echo "ALTER TABLE parlementaire_scrutin ADD CONSTRAINT parlementaire_scrutin_scrutin_id_scrutin_id FOREIGN KEY (scrutin_id) REFERENCES scrutin(id)" | mysql $MYSQLID $DBNAME echo "ALTER TABLE parlementaire_scrutin ADD CONSTRAINT parlementaire_scrutin_parlementaire_id_parlementaire_id FOREIGN KEY (parlementaire_id) REFERENCES parlementaire(id)" | mysql $MYSQLID $DBNAME echo "ALTER TABLE scrutin ADD CONSTRAINT scrutin_texteloi_id_texteloi_id FOREIGN KEY (texteloi_id) REFERENCES texteloi(id)" | mysql $MYSQLID $DBNAME diff --git a/config/doctrine/schema.yml b/config/doctrine/schema.yml index 008fb470a..efaa7b557 100644 --- a/config/doctrine/schema.yml +++ b/config/doctrine/schema.yml @@ -185,8 +185,8 @@ Scrutin: texteloi_id: integer amendement_id: integer sujet: text - demandeur: text - demandeur_groupe_acronyme: string(16) + demandeurs: text + demandeurs_groupes_acronymes: string(64) avis_gouvernement: string(16) avis_rapporteur: string(16) relations: diff --git a/lib/model/doctrine/Scrutin.class.php b/lib/model/doctrine/Scrutin.class.php index 8d3acbd6a..fa3913288 100644 --- a/lib/model/doctrine/Scrutin.class.php +++ b/lib/model/doctrine/Scrutin.class.php @@ -73,9 +73,29 @@ public function tagIntervention() { } } - public function setDemandeur($demandeur) { - // TODO? clean demandeur, set demandeur_groupe_acronyme - return $this->_set('demandeur', $demandeur); + public function setDemandeurs($demandeurs) { + $ret = $this->_set('demandeurs', join("|", $demandeurs)); + $gpes = array(); + foreach($demandeurs as $d) { + if (preg_match('/^Président\S* du groupe [^A-Z]*(.*)$/', $d, $match)) { + $gpe_acro = myTools::findGroupeAcronyme($match[1]); + if (!$gpe_acro) + print("WARNING: no groupe acronyme found for $d"); + else $gpes[] = $gpe_acro; + } + } + if ($gpes) { + $this->_set('demandeurs_groupes_acronymes', join("|", $gpes)); + } + return $ret; + } + + public function getDemandeurs() { + return explode("|", $this->_get('demandeurs')); + } + + public function getDemandeursGroupesAcronymes() { + return explode("|", $this->_get('demandeurs_groupes_acronymes')); } public function setTitre($titre) { diff --git a/lib/model/doctrine/myTools.class.php b/lib/model/doctrine/myTools.class.php index 0ffa70430..48d902bfd 100644 --- a/lib/model/doctrine/myTools.class.php +++ b/lib/model/doctrine/myTools.class.php @@ -215,6 +215,15 @@ public static function getCurrentGroupesInfos() { return $gpes; } + public static function findGroupeAcronyme($gpe) { + foreach (myTools::getGroupesInfos() as $g) { + if (preg_match('/('.$g[4].'|'.$g[1].')/i', $gpe)) { + return $g[1]; + } + } + return null; + } + public static function getCommissionsPermanentes() { return self::convertYamlToArray(sfConfig::get('app_commissions_permanentes', array())); } diff --git a/lib/task/loadScrutinsTask.class.php b/lib/task/loadScrutinsTask.class.php index 576aeb14f..5a11cc620 100644 --- a/lib/task/loadScrutinsTask.class.php +++ b/lib/task/loadScrutinsTask.class.php @@ -58,7 +58,7 @@ protected function execute($arguments = array(), $options = array()) } try { - $scrutin->setDemandeur($data->demandeurs); + $scrutin->setDemandeurs($data->demandeurs); $scrutin->setTitre($data->titre); $scrutin->setType($data->type); $scrutin->setStats($data->sort, From 9d8f02e177e7a2b98245f801165a0ebc111e7324 Mon Sep 17 00:00:00 2001 From: RouxRC Date: Fri, 17 Aug 2018 00:49:47 +0200 Subject: [PATCH 32/53] add loaded dir to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 3691330e9..57e068eb0 100644 --- a/.gitignore +++ b/.gitignore @@ -67,6 +67,7 @@ batch/questions/dernier_numero.txt batch/questions/liste_sans_reponse.txt batch/sanctions/ batch/scrutin/scrutins +batch/scrutin/loaded lib/filter/doctrine/ lib/form/doctrine/ From 44f726c0941b187512fc68121a61417e20c9037c Mon Sep 17 00:00:00 2001 From: RouxRC Date: Fri, 17 Aug 2018 01:12:11 +0200 Subject: [PATCH 33/53] catch potential error --- lib/model/doctrine/Scrutin.class.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/model/doctrine/Scrutin.class.php b/lib/model/doctrine/Scrutin.class.php index fa3913288..b8ed4ed8b 100644 --- a/lib/model/doctrine/Scrutin.class.php +++ b/lib/model/doctrine/Scrutin.class.php @@ -82,7 +82,8 @@ public function setDemandeurs($demandeurs) { if (!$gpe_acro) print("WARNING: no groupe acronyme found for $d"); else $gpes[] = $gpe_acro; - } + } else if ($d != "Conférence des Présidents" && $d != "Gouvernement") + print("WARNING: unidentified kind of demandeur: $d"); } if ($gpes) { $this->_set('demandeurs_groupes_acronymes', join("|", $gpes)); From bb48b496da177be88661fe6eea50468ec8e1dbc1 Mon Sep 17 00:00:00 2001 From: RouxRC Date: Fri, 17 Aug 2018 02:21:24 +0200 Subject: [PATCH 34/53] get back from opendata missing groupe of votant when only in mise au point --- batch/common/opendata.py | 44 ++++++++++++++++++++++++++++++--- batch/scrutin/parse_scrutins.py | 17 ++++++++++--- 2 files changed, 54 insertions(+), 7 deletions(-) diff --git a/batch/common/opendata.py b/batch/common/opendata.py index 00f53128e..01151e29a 100644 --- a/batch/common/opendata.py +++ b/batch/common/opendata.py @@ -21,7 +21,7 @@ "scrutins": "opendata-archives-xive/scrutins-xive-legislature", }, "15": { - "amo": "acteurs/deputes-en-exercice", + "amo": "acteurs/historique-des-deputes", "reunions": "reunions/reunions", "scrutins": "travaux-parlementaires/votes", }, @@ -157,7 +157,7 @@ def _cached_ref( cache = {} for item in extract_list(data): id = extract_id(item) - cache[id] = extract_mapped(item) + cache[id] = extract_mapped(item, data) with open(cached_file, "w") as f: json.dump(cache, f) @@ -188,7 +188,7 @@ def _extract_list(data): def _extract_id(organe): return organe["uid"] - def _extract_mapped(organe): + def _extract_mapped(organe, data): if ND_names and organe["libelleAbrev"] in GROUPES_ND: return GROUPES_ND[organe["libelleAbrev"]] return organe["libelleAbrev"] @@ -202,6 +202,42 @@ def _extract_mapped(organe): _extract_mapped, ) +def ref_histo_groupes(legislature, ND_names=False): + """ + Renvoie un mapping des id opendata des parlementaires vers une liste + ordonnée temporellement de leurs appartenances à un groupe politique + """ + + GROUPES = ref_groupes(legislature, ND_names=ND_names) + sort_periods = lambda x: "%s-%s" % (x["debut"], x["fin"]) + + def _extract_list(data): + return data["export"]["acteurs"]["acteur"] + + def _extract_id(acteur): + return acteur["uid"]["#text"] + + def _extract_mapped(acteur, data): + groupes = [{ + "sigle": GROUPES[m["organes"]["organeRef"]], + "debut": m["dateDebut"], + "fin": m["dateFin"] + } for m in acteur["mandats"]["mandat"] + if m["typeOrgane"] == "GP" + and m["legislature"] == legislature + and m["preseance"] != "1" + ] + groupes = sorted(groupes, key=sort_periods) + return groupes + + return _cached_ref( + legislature, + "amo", + "histo_groupes", + _extract_list, + _extract_id, + _extract_mapped, + ) def ref_seances(legislature): """ @@ -222,7 +258,7 @@ def _extract_list(data): def _extract_id(reunion): return reunion["uid"] - def _extract_mapped(reunion): + def _extract_mapped(reunion, data): return reunion["identifiants"]["idJO"] or MISSING.get(reunion["uid"]) return _cached_ref( diff --git a/batch/scrutin/parse_scrutins.py b/batch/scrutin/parse_scrutins.py index aee0cd9c8..e4211806a 100755 --- a/batch/scrutin/parse_scrutins.py +++ b/batch/scrutin/parse_scrutins.py @@ -34,6 +34,7 @@ from common.opendata import ( fetch_an_json, ref_groupes, + ref_histo_groupes, ref_seances, log, ) # noqa @@ -78,13 +79,14 @@ def clean_demandeurs(demandeurs, numero): def parse_scrutins(legislature, data): groupes = ref_groupes(legislature, ND_names=True) + histo_groupes = ref_histo_groupes(legislature, ND_names=True) seances = ref_seances(legislature) if not os.path.exists(SCRUTINS_DIR): os.makedirs(SCRUTINS_DIR) for item in data["scrutins"]["scrutin"]: - scrutin, logs = parse_scrutin(item, seances, groupes) + scrutin, logs = parse_scrutin(item, seances, groupes, histo_groupes) numero = "%05d" % scrutin["numero"] basename = "scrutin_%s_%s" % (legislature, numero) @@ -113,7 +115,7 @@ def parse_scrutins(legislature, data): log("Scrutin %s mis à jour" % scrutin["numero"]) -def parse_scrutin(data, seances, groupes): +def parse_scrutin(data, seances, groupes, histo_groupes): logs = [] synthese = data["syntheseVote"] decompte = synthese["decompte"] @@ -172,6 +174,7 @@ def parse_scrutin(data, seances, groupes): vote_map = data["miseAuPoint"] if vote_map: + dat = data["dateScrutin"] for position, pluriel in POS_MAP.items(): map_position = vote_map[pluriel] if not isinstance(map_position, list): @@ -184,9 +187,17 @@ def parse_scrutin(data, seances, groupes): votants = [votants] for votant in votants: if votant["acteurRef"] not in scrutin["parlementaires"]: + groupe = None + histo = histo_groupes[votant["acteurRef"]] + for h in histo or []: + if h["debut"] <= dat <= (h["fin"] or "9999-99-99"): + groupe = h["sigle"] + break + if not groupe: + logs.append("WARNING: no groupe historique found for parl %s for date %s: %s" % (votant["acteurRef"], dat, histo)) scrutin["parlementaires"][votant["acteurRef"]] = { "position": None, - "groupe": None, #TODO + "groupe": groupe, "position_groupe": None, #TODO "par_delegation": None } From e1f16052d1b834976577edfdb3d0a030dbe5edf3 Mon Sep 17 00:00:00 2001 From: RouxRC Date: Fri, 17 Aug 2018 02:43:01 +0200 Subject: [PATCH 35/53] get position_groupe from groupe for mises au point --- batch/scrutin/parse_scrutins.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/batch/scrutin/parse_scrutins.py b/batch/scrutin/parse_scrutins.py index e4211806a..9c3582a86 100755 --- a/batch/scrutin/parse_scrutins.py +++ b/batch/scrutin/parse_scrutins.py @@ -122,6 +122,7 @@ def parse_scrutin(data, seances, groupes, histo_groupes): scrutin = { "numero": int(data["numero"]), "seance": seances[data["seanceRef"]], + "date": data["dateScrutin"], "titre": data["titre"], "type": TYPES[data["typeVote"]["codeTypeVote"]], "nombre_votants": int(synthese["nombreVotants"]), @@ -139,11 +140,13 @@ def parse_scrutin(data, seances, groupes, histo_groupes): delegations = 0 + positions_groupes = {} vote_groupes = data["ventilationVotes"]["organe"]["groupes"]["groupe"] for vote_groupe in vote_groupes: acro_groupe = groupes[vote_groupe["organeRef"]] dn = vote_groupe["vote"]["decompteNominatif"] position_groupe = vote_groupe["vote"]["positionMajoritaire"] + positions_groupes[acro_groupe] = position_groupe for position in POSITIONS: votants_position = dn.get("%ss" % position, dn.get(position)) @@ -174,7 +177,6 @@ def parse_scrutin(data, seances, groupes, histo_groupes): vote_map = data["miseAuPoint"] if vote_map: - dat = data["dateScrutin"] for position, pluriel in POS_MAP.items(): map_position = vote_map[pluriel] if not isinstance(map_position, list): @@ -190,15 +192,15 @@ def parse_scrutin(data, seances, groupes, histo_groupes): groupe = None histo = histo_groupes[votant["acteurRef"]] for h in histo or []: - if h["debut"] <= dat <= (h["fin"] or "9999-99-99"): + if h["debut"] <= scrutin["date"] <= (h["fin"] or "9999-99-99"): groupe = h["sigle"] break if not groupe: - logs.append("WARNING: no groupe historique found for parl %s for date %s: %s" % (votant["acteurRef"], dat, histo)) + logs.append("WARNING: no groupe historique found for parl %s for date %s: %s" % (votant["acteurRef"], scrutin["date"], histo)) scrutin["parlementaires"][votant["acteurRef"]] = { "position": None, "groupe": groupe, - "position_groupe": None, #TODO + "position_groupe": positions_groupes.get(groupe), "par_delegation": None } parl = scrutin["parlementaires"][votant["acteurRef"]] From 91048c8b9211500018041a42152f3471d377a655 Mon Sep 17 00:00:00 2001 From: RouxRC Date: Fri, 17 Aug 2018 03:53:21 +0200 Subject: [PATCH 36/53] avoid saving a scrutin for which we can't find the intervention and reload only metadata + votes --- lib/task/loadScrutinsTask.class.php | 37 ++++++++++++++++------------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/lib/task/loadScrutinsTask.class.php b/lib/task/loadScrutinsTask.class.php index 5a11cc620..3f163cfe9 100644 --- a/lib/task/loadScrutinsTask.class.php +++ b/lib/task/loadScrutinsTask.class.php @@ -38,21 +38,25 @@ protected function execute($arguments = array(), $options = array()) continue; } + $new = false; $scrutin = Doctrine::getTable('Scrutin')->findOneByNumero($data->numero); if (!$scrutin) { $scrutin = new Scrutin(); $scrutin->setNumero($data->numero); - } + $scrutin->setType($data->type); - try { - $scrutin->setSeance($data->seance); - } catch (Exception $e) { - // Commenté pour ne pas spammer les cron avec les séances pas encore publiées - // echo "ERREUR $file (seance) : {$e->getMessage()}\n"; - $seances_manquantes++; - continue; + try { + $scrutin->setSeance($data->seance); + } catch (Exception $e) { + // Commenté pour ne pas spammer les cron avec les séances pas encore publiées + // echo "ERREUR $file (seance) : {$e->getMessage()}\n"; + $seances_manquantes++; + continue; + } + $new = true; } + if (!in_array($scrutin->seance_id, $seance_ids)) { $seance_ids[] = $scrutin->seance_id; } @@ -60,7 +64,6 @@ protected function execute($arguments = array(), $options = array()) try { $scrutin->setDemandeurs($data->demandeurs); $scrutin->setTitre($data->titre); - $scrutin->setType($data->type); $scrutin->setStats($data->sort, $data->nombre_votants, $data->nombre_pours, @@ -72,15 +75,17 @@ protected function execute($arguments = array(), $options = array()) continue; } - $scrutin->save(); - - try { - $scrutin->tagIntervention(); - } catch(Exception $e) { - echo "ERREUR $file (tag interventions) : {$e->getMessage()}\n"; - continue; + if ($new) { + try { + $scrutin->tagIntervention(); + } catch(Exception $e) { + echo "ERREUR $file (tag interventions) : {$e->getMessage()}\n"; + continue; + } } + $scrutin->save(); + $scrutin->setVotes($data->parlementaires); $scrutin->free(); From 6c4574243b9bcb1d974960ca4b75c50cb739a9e2 Mon Sep 17 00:00:00 2001 From: RouxRC Date: Fri, 17 Aug 2018 03:59:10 +0200 Subject: [PATCH 37/53] wrong link --- bin/load_scrutins | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/load_scrutins b/bin/load_scrutins index 330ab0e29..a11de54bf 100755 --- a/bin/load_scrutins +++ b/bin/load_scrutins @@ -12,7 +12,7 @@ ls batch/scrutin/scrutins/scrutin_15_0*.sha1 | sed 's/^.*scrutin_15_0\+//' | sed seq 1 `tail -1 /tmp/numscrutins` > /tmp/allscrutins if diff /tmp/{num,all}scrutins | grep . > /dev/null; then echo "Some scrutins are missing from OpenData:" - diff /tmp/{num,all}scrutins | grep '>' | sed 's|> |- http://www2.assemblee-nationale.fr//detail/(legislature)/15/(num)/|' + diff /tmp/{num,all}scrutins | grep '>' | sed 's|> |- http://www2.assemblee-nationale.fr/scrutins/detail/(legislature)/15/(num)/|' fi php symfony load:Scrutins From 646417521557935f32e2266a4d3a7f3c368baaf6 Mon Sep 17 00:00:00 2001 From: RouxRC Date: Sun, 19 Aug 2018 01:38:00 +0200 Subject: [PATCH 38/53] hardfix errors association seance from opendata AN cf https://github.com/regardscitoyens/Issues-Parlement/issues/44 --- batch/scrutin/parse_scrutins.py | 10 ++++++++++ lib/model/doctrine/Scrutin.class.php | 1 + 2 files changed, 11 insertions(+) diff --git a/batch/scrutin/parse_scrutins.py b/batch/scrutin/parse_scrutins.py index 9c3582a86..2a692dbc8 100755 --- a/batch/scrutin/parse_scrutins.py +++ b/batch/scrutin/parse_scrutins.py @@ -114,6 +114,14 @@ def parse_scrutins(legislature, data): f.write(json_data) log("Scrutin %s mis à jour" % scrutin["numero"]) +ERREURS_AN = { + "120": "20172002", + "121": "20172002", + "334": "20180086", + "335": "20180086", + "336": "20180086", + "337": "20180086", +} def parse_scrutin(data, seances, groupes, histo_groupes): logs = [] @@ -133,6 +141,8 @@ def parse_scrutin(data, seances, groupes, histo_groupes): "demandeurs": clean_demandeurs(data["demandeur"]["texte"], data["numero"]), "parlementaires": {}, } + if data["numero"] in ERREURS_AN: + scrutin["seance"] = ERREURS_AN[data["numero"]] if not scrutin["seance"]: logs.append("WARNING: scrutin %s has no seance %s" % (data["numero"], data["seanceRef"])) if not scrutin["demandeurs"]: diff --git a/lib/model/doctrine/Scrutin.class.php b/lib/model/doctrine/Scrutin.class.php index b8ed4ed8b..12457dc84 100644 --- a/lib/model/doctrine/Scrutin.class.php +++ b/lib/model/doctrine/Scrutin.class.php @@ -68,6 +68,7 @@ public function tagIntervention() { throw new Exception( "Scrutin {$this->numero} non trouvé dans les interventions " . "de la séance {$seance->id} du {$seance->date} {$seance->moment}\n" + . "{$inter->source}\n" . "$info" ); } From 91f13153d49d0553b5b253c14847ede34a54668e Mon Sep 17 00:00:00 2001 From: RouxRC Date: Sun, 19 Aug 2018 02:24:46 +0200 Subject: [PATCH 39/53] fix more cases of bad seances listed in opendata --- batch/scrutin/parse_scrutins.py | 2 ++ lib/model/doctrine/Scrutin.class.php | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/batch/scrutin/parse_scrutins.py b/batch/scrutin/parse_scrutins.py index 2a692dbc8..923d2e295 100755 --- a/batch/scrutin/parse_scrutins.py +++ b/batch/scrutin/parse_scrutins.py @@ -121,6 +121,8 @@ def parse_scrutins(legislature, data): "335": "20180086", "336": "20180086", "337": "20180086", + "361": "20180121", + "438": "20180174", } def parse_scrutin(data, seances, groupes, histo_groupes): diff --git a/lib/model/doctrine/Scrutin.class.php b/lib/model/doctrine/Scrutin.class.php index 12457dc84..823ff7112 100644 --- a/lib/model/doctrine/Scrutin.class.php +++ b/lib/model/doctrine/Scrutin.class.php @@ -42,10 +42,12 @@ public function tagIntervention() { $found = FALSE; $info = "votants: {$this->nombre_votants}, pour: {$this->nombre_pours}, contre: {$this->nombre_contres}"; + $source = ""; foreach ($inters as $inter) { // Extraction des votants/pours/contres $text = $inter->intervention; + if (!$source) $source = $inter->source; $mv = preg_match('/nombre de votants(?:<\/td>|[,\s]*)(\d+)/i', $text, $match_votant); $mp = preg_match('/pour l\'(?:adoption|approbation)(?:<\/td>|[,\s]*)(\d+)/i', $text, $match_pour); $mc = preg_match('/contre(?:<\/td>|[,\s])(\d+)/i', $text, $match_contre); @@ -68,7 +70,7 @@ public function tagIntervention() { throw new Exception( "Scrutin {$this->numero} non trouvé dans les interventions " . "de la séance {$seance->id} du {$seance->date} {$seance->moment}\n" - . "{$inter->source}\n" + . "{$source}\n" . "$info" ); } From f090b3e1a0f78f5e302b4758eabd9261693cac31 Mon Sep 17 00:00:00 2001 From: RouxRC Date: Sun, 19 Aug 2018 02:55:56 +0200 Subject: [PATCH 40/53] match latest table in an intervention notably to handle manual correction cases such as https://www.nosdeputes.fr/15/seance/219#table_486 --- lib/model/doctrine/Scrutin.class.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/model/doctrine/Scrutin.class.php b/lib/model/doctrine/Scrutin.class.php index 823ff7112..b23cfe641 100644 --- a/lib/model/doctrine/Scrutin.class.php +++ b/lib/model/doctrine/Scrutin.class.php @@ -48,16 +48,16 @@ public function tagIntervention() { // Extraction des votants/pours/contres $text = $inter->intervention; if (!$source) $source = $inter->source; - $mv = preg_match('/nombre de votants(?:<\/td>|[,\s]*)(\d+)/i', $text, $match_votant); - $mp = preg_match('/pour l\'(?:adoption|approbation)(?:<\/td>|[,\s]*)(\d+)/i', $text, $match_pour); - $mc = preg_match('/contre(?:<\/td>|[,\s])(\d+)/i', $text, $match_contre); + $mv = preg_match_all('/nombre de votants(?:<\/td>|[,\s]*)(\d+)/i', $text, $match_votant); + $mp = preg_match_all('/pour l\'(?:adoption|approbation)(?:<\/td>|[,\s]*)(\d+)/i', $text, $match_pour); + $mc = preg_match_all('/contre(?:<\/td>|[,\s])(\d+)/i', $text, $match_contre); if ($mv == 0 || $mp == 0 || $mc == 0) { echo "WARNING: décomptes intervention {$inter->id} incomplets :\n$text\n"; - } elseif (intval($match_votant[1]) != $this->nombre_votants - || intval($match_pour[1]) != $this->nombre_pours - || intval($match_contre[1]) != $this->nombre_contres) { - $info .= "\n inter {$inter->id} différente (v:{$match_votant[1]}, p:{$match_pour[1]}, c:{$match_contre[1]})"; + } elseif (intval(end($match_votant)[0]) != $this->nombre_votants + || intval(end($match_pour)[0]) != $this->nombre_pours + || intval(end($match_contre)[0]) != $this->nombre_contres) { + $info .= "\n inter {$inter->id} différente (v:".end($match_votant[0]).", p:".end($match_pour[0]).", c:".end($match_contre[0]).")"; } else { $found = TRUE; $inter->addTag("scrutin:numero={$this->numero}"); From 3acfa52788078d024a0223ef210756fb3f8dc6b3 Mon Sep 17 00:00:00 2001 From: RouxRC Date: Sun, 19 Aug 2018 03:08:40 +0200 Subject: [PATCH 41/53] oups wrong interpretation of preg_match_all results architecture --- lib/model/doctrine/Scrutin.class.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/model/doctrine/Scrutin.class.php b/lib/model/doctrine/Scrutin.class.php index b23cfe641..d021aa525 100644 --- a/lib/model/doctrine/Scrutin.class.php +++ b/lib/model/doctrine/Scrutin.class.php @@ -54,10 +54,11 @@ public function tagIntervention() { if ($mv == 0 || $mp == 0 || $mc == 0) { echo "WARNING: décomptes intervention {$inter->id} incomplets :\n$text\n"; - } elseif (intval(end($match_votant)[0]) != $this->nombre_votants - || intval(end($match_pour)[0]) != $this->nombre_pours - || intval(end($match_contre)[0]) != $this->nombre_contres) { - $info .= "\n inter {$inter->id} différente (v:".end($match_votant[0]).", p:".end($match_pour[0]).", c:".end($match_contre[0]).")"; + } elseif (intval(end($match_votant[1])) != $this->nombre_votants + || intval(end($match_pour[1])) != $this->nombre_pours + || intval(end($match_contre[1])) != $this->nombre_contres) { + print_r($match_votant); + $info .= "\n inter {$inter->id} différente (v:".end($match_votant[1]).", p:".end($match_pour[1]).", c:".end($match_contre[1]).")"; } else { $found = TRUE; $inter->addTag("scrutin:numero={$this->numero}"); From 82a2b5ba50d943eb5d0a9055eac3b70fef6ba7f4 Mon Sep 17 00:00:00 2001 From: RouxRC Date: Sun, 19 Aug 2018 03:09:17 +0200 Subject: [PATCH 42/53] oups debug --- lib/model/doctrine/Scrutin.class.php | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/model/doctrine/Scrutin.class.php b/lib/model/doctrine/Scrutin.class.php index d021aa525..37585b5eb 100644 --- a/lib/model/doctrine/Scrutin.class.php +++ b/lib/model/doctrine/Scrutin.class.php @@ -57,7 +57,6 @@ public function tagIntervention() { } elseif (intval(end($match_votant[1])) != $this->nombre_votants || intval(end($match_pour[1])) != $this->nombre_pours || intval(end($match_contre[1])) != $this->nombre_contres) { - print_r($match_votant); $info .= "\n inter {$inter->id} différente (v:".end($match_votant[1]).", p:".end($match_pour[1]).", c:".end($match_contre[1]).")"; } else { $found = TRUE; From 7510acfcdd944a11c74405ba7a9489123b1f2b01 Mon Sep 17 00:00:00 2001 From: RouxRC Date: Sun, 19 Aug 2018 04:25:11 +0200 Subject: [PATCH 43/53] no need to search for scrutins in committees --- lib/task/loadScrutinsTask.class.php | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/task/loadScrutinsTask.class.php b/lib/task/loadScrutinsTask.class.php index 3f163cfe9..ec719dd9e 100644 --- a/lib/task/loadScrutinsTask.class.php +++ b/lib/task/loadScrutinsTask.class.php @@ -96,6 +96,7 @@ protected function execute($arguments = array(), $options = array()) $seances = Doctrine::getTable("Seance") ->createQuery("s") ->whereIn("s.id", $seance_ids) + ->andWhere("s.type = 'hemicycle'") ->execute(); foreach ($seances as $seance) { From 936666b1d684825423c1481a8210cd88148b1633 Mon Sep 17 00:00:00 2001 From: RouxRC Date: Sun, 19 Aug 2018 15:54:48 +0200 Subject: [PATCH 44/53] =?UTF-8?q?don't=20account=20votes=20'NonVotant'=20a?= =?UTF-8?q?s=20pr=C3=A9sents?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cf https://github.com/regardscitoyens/nosdeputes.fr/pull/115#issuecomment-413740999 --- lib/model/doctrine/ParlementaireScrutin.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/model/doctrine/ParlementaireScrutin.class.php b/lib/model/doctrine/ParlementaireScrutin.class.php index 37aa32636..d36287f01 100644 --- a/lib/model/doctrine/ParlementaireScrutin.class.php +++ b/lib/model/doctrine/ParlementaireScrutin.class.php @@ -18,7 +18,7 @@ public function updatePresence() { $this->parlementaire_groupe_acronyme, 'scrutin', $this->Scrutin->getLinkSource(), - $this->position && !$this->par_delegation + $this->position && $this->position != "nonVotant" && !$this->par_delegation ); } From 70f17559df0d8318599183d0055e08e9de2fbad6 Mon Sep 17 00:00:00 2001 From: RouxRC Date: Sun, 19 Aug 2018 22:01:42 +0200 Subject: [PATCH 45/53] =?UTF-8?q?update=20legends=20of=20semaines=20d'acti?= =?UTF-8?q?vit=C3=A9=20to=20signify=20scrutins=20are=20accounted?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../modules/parlementaire/templates/topSuccess.php | 8 ++++---- .../modules/plot/templates/_plotParlementaire.php | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/frontend/modules/parlementaire/templates/topSuccess.php b/apps/frontend/modules/parlementaire/templates/topSuccess.php index ece5f0ddd..162a268db 100644 --- a/apps/frontend/modules/parlementaire/templates/topSuccess.php +++ b/apps/frontend/modules/parlementaire/templates/topSuccess.php @@ -42,7 +42,7 @@ ); $bulles = array( "", - "Semaines d'activité -- Nombre de semaines où le député a été relevé présent -- en commission ou a pris la parole (même brièvement) en hémicycle", + "Semaines d'activité -- Nombre de semaines où le député a été relevé présent en commission, -- a pris la parole (même brièvement) dans hémicycle -- ou a participé physiquement à un scrutin public", "Réunions de Commission -- Nombre de réunions de commission où le député a été relevé présent", "Interventions en Commission -- Nombre d'interventions prononcées par le député en commissions", "Interventions longues en Hémicycle -- Nombre d'interventions de plus de 20 mots prononcées par le député en hémicycle", @@ -63,7 +63,7 @@
Commission Hémicycle Amendements
 SemainesSemaines Commission Hémicycle Amendements
Commission Hémicycle Amendements Propositions Questions
- + - + @@ -154,7 +144,7 @@ class="jstitle phototitle c_ foreach($ktop as $key) : if ($key === "") continue; $i++; - $bulles[$i] = str_replace('Nombre', 'Nombre moyen', str_replace('le député', 'un député de ce groupe', $bulles[$i])); ?> + ?>
 SemainesSemaines Commission Hémicycle AmendementsRapportsRapports Propositions Questions

Explications :

    -
  • Semaines d'activité : Nombre de semaines où le député a été relevé présent en commission, a pris la parole (même brièvement) dans l'hémicycle ou a participé physiquement à un scrutin public
  • -
  • Commission réunions : Nombre de réunions de commission où le député a été relevé présent
  • -
  • Commission interventions : Nombre d'interventions prononcées par le député en commissions
  • -
  • Hémicycle interventions longues : Nombre d'interventions de plus de 20 mots prononcées par le député en hémicycle
  • -
  • Hémicycle interventions courtes : Nombre d'interventions de 20 mots et moins prononcées par le député en hémicycle
  • -
  • Amendements proposés : Nombre d'amendements proposés par le député (indiqué « auteur » par l'Assemblée)
  • -
  • Amendements signés : Nombre d'amendements proposés ou co-signés par le député
  • -
  • Amendements adoptés : Nombre d'amendements signés par le député qui ont été adoptés
  • -
  • Rapports écrits : Nombre de rapports ou avis dont le député est l'auteur
  • -
  • Propositions écrites : Nombre de propositions de loi ou de résolution dont le député est l'auteur
  • -
  • Propositions signées : Nombre de propositions de loi ou de résolution dont le député est cosignataire
  • -
  • Questions écrites : Nombre de questions au gouvernement écrites soumises par le député
  • -
  • Questions orales : Nombre de questions au gouvernement orales posées par le député
  • +'.$indicateurs[$k]['titre'].' : '.str_replace(' -- ', ' ', $indicateurs[$k]['desc']).''; +?>

Lire notre FAQ pour plus d'explications

diff --git a/lib/model/doctrine/myTools.class.php b/lib/model/doctrine/myTools.class.php index 48d902bfd..37ee8eb7f 100644 --- a/lib/model/doctrine/myTools.class.php +++ b/lib/model/doctrine/myTools.class.php @@ -228,6 +228,65 @@ public static function getCommissionsPermanentes() { return self::convertYamlToArray(sfConfig::get('app_commissions_permanentes', array())); } + public static $indicateurs = array( + 'semaines_presence' => array( + 'titre' => "Semaines d'activité", + 'desc' => "Nombre de semaines où le député a été relevé présent en commission, -- a pris la parole (même brièvement) dans l'hémicycle -- ou a participé physiquement à un scrutin public" + ), + 'commission_presences' => array( + 'titre' => "Présences en Commission", + 'desc' => "Nombre de réunions de commission où le député a été relevé présent" + ), + 'commission_interventions' => array( + 'titre' => "Interventions en Commission", + 'desc' => "Nombre d'interventions prononcées par le député dans les commissions" + ), + 'hemicycle_presences' => array( + 'titre' => "Présences en Hémicycle", + 'desc' => "Information non publique" + ), + 'hemicycle_interventions' => array( + 'titre' => "Interventions longues en Hémicycle", + 'desc' => "Nombre d'interventions de plus de 20 mots -- prononcées par le député en hémicycle" + ), + 'hemicycle_interventions_courtes' => array( + 'titre' => "Interventions courtes en Hémicycle", + 'desc' => "Nombre d'interventions de 20 mots et moins -- prononcées par le député en hémicycle" + ), + 'amendements_proposes' => array( + 'titre' => "Amendements proposés", + 'desc' => "Nombre d'amendements proposés par le député -- (indiqué « auteur » par l'Assemblée)" + ), + 'amendements_signes' => array( + 'titre' => "Amendements signés", + 'desc' => "Nombre d'amendements proposés ou co-signés par le député" + ), + 'amendements_adoptes' => array( + 'titre' => "Amendements adoptés", + 'desc' => "Nombre d'amendements signés par le député -- qui ont été adoptés en séance" + ), + 'rapports' => array( + 'titre' => "Rapports écrits", + 'desc' => "Nombre de rapports ou avis dont le député est l'auteur" + ), + 'propositions_ecrites' => array( + 'titre' => "Propositions de loi écrites", + 'desc' => "Nombre de propositions de loi ou de résolution -- dont le député est l'auteur" + ), + 'propositions_signees' => array( + 'titre' => "Propositions de loi signées", + 'desc' => "Nombre de propositions de loi ou de résolution -- dont le député est cosignataire" + ), + 'questions_ecrites' => array( + 'titre' => "Questions écrites", + 'desc' => "Nombre de questions écrites soumises -- par le député au gouvernement" + ), + 'questions_orales' => array( + 'titre' => "Questions orales", + 'desc' => "Nombre de questions orales posées -- par le député au gouvernement" + ) + ); + static $num_mois = array( "01" => "janvier", "02" => "février", From a6c6f204f444401b1cdb77a7aa3e9cf511b1a906 Mon Sep 17 00:00:00 2001 From: RouxRC Date: Sun, 16 Sep 2018 22:56:43 +0200 Subject: [PATCH 47/53] don't integrate scrutins with seances missing --- lib/task/loadScrutinsTask.class.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/task/loadScrutinsTask.class.php b/lib/task/loadScrutinsTask.class.php index ec719dd9e..41aaac2f5 100644 --- a/lib/task/loadScrutinsTask.class.php +++ b/lib/task/loadScrutinsTask.class.php @@ -41,6 +41,10 @@ protected function execute($arguments = array(), $options = array()) $new = false; $scrutin = Doctrine::getTable('Scrutin')->findOneByNumero($data->numero); if (!$scrutin) { + if (!$data->seance) { + $seances_manquantes++; + continue; + } $scrutin = new Scrutin(); $scrutin->setNumero($data->numero); $scrutin->setType($data->type); From 8f4942eb58ed70205bdc74d648f9d0dcdd3450e9 Mon Sep 17 00:00:00 2001 From: RouxRC Date: Mon, 24 Sep 2018 00:47:12 +0200 Subject: [PATCH 48/53] add warnings on position identical to mise_au_point --- batch/scrutin/parse_scrutins.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/batch/scrutin/parse_scrutins.py b/batch/scrutin/parse_scrutins.py index 923d2e295..19d6c417e 100755 --- a/batch/scrutin/parse_scrutins.py +++ b/batch/scrutin/parse_scrutins.py @@ -217,6 +217,8 @@ def parse_scrutin(data, seances, groupes, histo_groupes): } parl = scrutin["parlementaires"][votant["acteurRef"]] parl["mise_au_point_position"] = position + if position == parl["position"]: + logs.append("WARNING: position and mise_au_point are identical for parl %s on scrutin %s: %s" % (votant["acteurRef"], scrutin["numero"], position)) return scrutin, logs From ebfb47f223238ef1644048e2743aef6550e9d120 Mon Sep 17 00:00:00 2001 From: RouxRC Date: Mon, 24 Sep 2018 00:58:27 +0200 Subject: [PATCH 49/53] adjust period of accounting presences via scrutins depending on delegations cf https://github.com/regardscitoyens/nosdeputes.fr/pull/115#issuecomment-421844588 --- batch/scrutin/parse_scrutins.py | 6 ++++-- lib/model/doctrine/Scrutin.class.php | 13 +++++++------ lib/task/loadScrutinsTask.class.php | 2 +- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/batch/scrutin/parse_scrutins.py b/batch/scrutin/parse_scrutins.py index 19d6c417e..af6643ea2 100755 --- a/batch/scrutin/parse_scrutins.py +++ b/batch/scrutin/parse_scrutins.py @@ -184,8 +184,10 @@ def parse_scrutin(data, seances, groupes, histo_groupes): "mise_au_point_position": None } - if 2 * delegations > scrutin["nombre_votants"]: - logs.append("Scrutin %s: trop de délégations" % scrutin["numero"]) + if delegations: + scrutin["nb_delegations"] = delegations + if 2 * delegations > scrutin["nombre_votants"]: + logs.append("Scrutin %s: trop de délégations" % scrutin["numero"]) vote_map = data["miseAuPoint"] if vote_map: diff --git a/lib/model/doctrine/Scrutin.class.php b/lib/model/doctrine/Scrutin.class.php index 37585b5eb..3fb626c14 100644 --- a/lib/model/doctrine/Scrutin.class.php +++ b/lib/model/doctrine/Scrutin.class.php @@ -2,10 +2,11 @@ class Scrutin extends BaseScrutin { - // Date des premiers scrutins où les délégations ne sont pas toutes à FALSE - // On ne génère pas de preuve de présence à partir des votes avant cette date - const DEBUT_DELEGATIONS = '2017-10-24'; # date premier solennel avec délégations - #const DEBUT_DELEGATIONS = '2018-03-20'; # date premier ordinaire avec délégations + // Date début délégations (cf https://github.com/regardscitoyens/nosdeputes.fr/pull/115#issuecomment-421844588 ) + // On ne génère pas de preuve de présence à partir des votes avant cette date sauf si le scrutin a des délégations (3 cas particuliers de solennel) + const DEBUT_DELEGATIONS = '2018-02-13'; + // Anticipe potentiel recul de la transparence en matière de publicité des délégations + const FIN_DELEGATIONS = '9999-99-99'; public function getLinkSource() { return "http://www2.assemblee-nationale.fr/scrutins/detail/(legislature)/" @@ -115,7 +116,7 @@ public function setStats($sort, $nb_votants, $nb_pours, $nb_contres, $nb_abst) { && $this->_set('nombre_abstentions', $nb_abst); } - public function setVotes($parlementaires) { + public function setVotes($parlementaires, $has_delegations) { foreach ($parlementaires as $id_an => $data) { try { $parlscrutin = Doctrine::getTable('ParlementaireScrutin') @@ -137,7 +138,7 @@ public function setVotes($parlementaires) { throw new Exception("Could not set vote metadata: {$data}"); } - if ($this->Seance->date >= self::DEBUT_DELEGATIONS) { + if ($has_delegations || (self::DEBUT_DELEGATIONS <= $this->Seance->date && $this->Seance->date <= self::FIN_DELEGATIONS) { $parlscrutin->updatePresence(); } diff --git a/lib/task/loadScrutinsTask.class.php b/lib/task/loadScrutinsTask.class.php index 41aaac2f5..500da5834 100644 --- a/lib/task/loadScrutinsTask.class.php +++ b/lib/task/loadScrutinsTask.class.php @@ -90,7 +90,7 @@ protected function execute($arguments = array(), $options = array()) $scrutin->save(); - $scrutin->setVotes($data->parlementaires); + $scrutin->setVotes($data->parlementaires, $data->nb_delegations); $scrutin->free(); rename($dir . $file, $backupdir . $file); From fe7d5b40752d1722b87ec2d66785dfe677a5b93a Mon Sep 17 00:00:00 2001 From: RouxRC Date: Mon, 24 Sep 2018 01:12:22 +0200 Subject: [PATCH 50/53] adjust accounting presences for special cases of mise_au_point cf https://github.com/regardscitoyens/nosdeputes.fr/pull/115#issuecomment-418172211 --- lib/model/doctrine/ParlementaireScrutin.class.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/model/doctrine/ParlementaireScrutin.class.php b/lib/model/doctrine/ParlementaireScrutin.class.php index d36287f01..7c7251048 100644 --- a/lib/model/doctrine/ParlementaireScrutin.class.php +++ b/lib/model/doctrine/ParlementaireScrutin.class.php @@ -18,7 +18,13 @@ public function updatePresence() { $this->parlementaire_groupe_acronyme, 'scrutin', $this->Scrutin->getLinkSource(), - $this->position && $this->position != "nonVotant" && !$this->par_delegation + // on compte une présence quand toutes les conditions suivantes sont vérifiées : + // cf https://github.com/regardscitoyens/nosdeputes.fr/pull/115#issuecomment-418172211 + // - on a effectivement une position et pas seulement une mise au point + // - la position n'est pas nonVotant (ce qui correspond aux présidents de Séance + systématiquement au président de l'Assemblée même en son absence cf https://github.com/regardscitoyens/nosdeputes.fr/pull/115#issuecomment-413740999 ) + // - l'éventuelle mise au point n'indique pas non plus nonVotant + // - le vote n'était pas par délégation + $this->position && $this->position !== "nonVotant" && $this->mise_au_point_position !== "nonVotant" && !$this->par_delegation ); } From af19de5ca7afa9a59fc51a3898b3c808ffd9fa2e Mon Sep 17 00:00:00 2001 From: RouxRC Date: Mon, 24 Sep 2018 01:22:10 +0200 Subject: [PATCH 51/53] woups --- lib/model/doctrine/Scrutin.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/model/doctrine/Scrutin.class.php b/lib/model/doctrine/Scrutin.class.php index 3fb626c14..60503dee1 100644 --- a/lib/model/doctrine/Scrutin.class.php +++ b/lib/model/doctrine/Scrutin.class.php @@ -138,7 +138,7 @@ public function setVotes($parlementaires, $has_delegations) { throw new Exception("Could not set vote metadata: {$data}"); } - if ($has_delegations || (self::DEBUT_DELEGATIONS <= $this->Seance->date && $this->Seance->date <= self::FIN_DELEGATIONS) { + if ($has_delegations || (self::DEBUT_DELEGATIONS <= $this->Seance->date && $this->Seance->date <= self::FIN_DELEGATIONS)) { $parlscrutin->updatePresence(); } From ba6abfc04ed8ee7d81b250aded409d704429f1d3 Mon Sep 17 00:00:00 2001 From: RouxRC Date: Mon, 24 Sep 2018 01:27:10 +0200 Subject: [PATCH 52/53] set the field for all to avoid php warnings --- batch/scrutin/parse_scrutins.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/batch/scrutin/parse_scrutins.py b/batch/scrutin/parse_scrutins.py index af6643ea2..0f34676eb 100755 --- a/batch/scrutin/parse_scrutins.py +++ b/batch/scrutin/parse_scrutins.py @@ -184,10 +184,9 @@ def parse_scrutin(data, seances, groupes, histo_groupes): "mise_au_point_position": None } - if delegations: - scrutin["nb_delegations"] = delegations - if 2 * delegations > scrutin["nombre_votants"]: - logs.append("Scrutin %s: trop de délégations" % scrutin["numero"]) + scrutin["nb_delegations"] = delegations + if 2 * delegations > scrutin["nombre_votants"]: + logs.append("Scrutin %s: trop de délégations" % scrutin["numero"]) vote_map = data["miseAuPoint"] if vote_map: From 92a6c96a733d7c2e033ede89be6fa0a463662341 Mon Sep 17 00:00:00 2001 From: RouxRC Date: Tue, 25 Sep 2018 05:03:56 +0200 Subject: [PATCH 53/53] [WIP] Edit FAQ --- .../parlementaire/templates/faqSuccess.php | 133 +++++++++++++----- 1 file changed, 96 insertions(+), 37 deletions(-) diff --git a/apps/frontend/modules/parlementaire/templates/faqSuccess.php b/apps/frontend/modules/parlementaire/templates/faqSuccess.php index 7cbd32da8..1f38d55ca 100644 --- a/apps/frontend/modules/parlementaire/templates/faqSuccess.php +++ b/apps/frontend/modules/parlementaire/templates/faqSuccess.php @@ -1,20 +1,27 @@ +https://github.com/regardscitoyens/nosdeputes.fr/blob/master/apps/frontend/modules/parlementaire/templates/faqSuccess.php +

Questions fréquemment posées

+

NosDéputés.fr est-il un site officiel de l'Assemblée nationale ?

+

Non.

+

NosDéputés.fr est une initiative totalement indépendante des institutions publiques proposée par l'association Regards Citoyens.

+

Origine et nature des données publiées

D'où viennent les données publiées sur le site ?

-

Toutes les données relatives à l'activité parlementaire présentées sur ce site sont issues du site de l'Assemblée nationale et du Journal Officiel. Il s'agit donc d'informations intégralement publiques. Ces informations sont mises-à-jour toutes les 8 heures à partir des sites officiels. Les compte-rendus des débats peuvent parfois mettre plusieurs heures voire plusieurs jours avant d'être publiés par les services de l'Assemblée, les données des dernières semaines sont donc amenées à évoluer régulièrement et ne sauraient refléter la présence en temps réel des députés à l'Assemblée.

-

Les cartes utilisées sont librement adaptées de celles remises à disposition par Wikipedia, Toxicode et Jérôme Cukier. Vous pouvez les télécharger sur NosDonnées.fr.

+

Toutes les données relatives à l'activité parlementaire présentées sur ce site sont issues du site de l'Assemblée nationale et du Journal Officiel. Il s'agit donc d'informations intégralement publiques. +

Les cartes utilisées sont librement adaptées de celles remises à disposition par Wikipedia, Toxicode et Jérôme Cukier. Vous pouvez les télécharger depuis cette page.

Avez vous le droit de publier ces données ?

-

Oui, cela est précisé sur le site de l'Assemblée nationale "Les débats et les documents parlementaires [...] peuvent donc être reproduits librement [sauf] à des fins commerciales ou publicitaires".

+

Oui, cela est précisé sur le site de l'Assemblée nationale sous réserve de conditions parfaitement remplies par NosDéputés.fr :

+
Les documents « publics » ou « officiels » ne sont couverts par aucun droit d'auteur. Ils peuvent donc être reproduits librement. C'est le cas pour les débats et les documents parlementaires. Les informations utilisées ne doivent l'être qu'à des fins personnelles, associatives ou professionnelles, toute utilisation ou reproduction à des fins commerciales ou publicitaires étant interdite. La reproduction des documents |...] sous forme électronique est autorisée, sous réserve de la gratuité de leur diffusion, du respect de l’intégrité des documents reproduits, de la mention du nom de l’auteur, de la source, et d’un lien renvoyant vers le document original en ligne sur le site.
-

Dans les graphes des députés et la synthèse, quelle est la différence entre les "présences détectées", les "présences enregistrées" et la présence "médiane des députés" ?

-

Si les présences en commission sont enregistrées et diffusées publiquement à tous, les présences en hémicycle ne le sont pas. Pour détecter la présence d'un député en séance d'hémicycle, nous détectons sa participation orale au sein des compte-rendus de séance diffusés. Il est cependant rare que tous les députés présents parlent lors d'une séance et nous ne pouvons donc pas comptabiliser de manière exhaustive les présences des députés en hémicycle. C'est également pour cela que le graphe d'activité globale d'un député indique des présences détectées puisqu'il combine les données des commissions et de l'hémicycle.

Dans les graphes d'activité, la présence médiane affichée en liseré gris vise à apporter un élément de comparaison entre les différents graphes. Elle indique pour chaque semaine le nombre de réunions ou séances auxquelles a été détecté présent le député médian, c'est-à-dire qu'au moins la moitié des députés a été détecté présent autant de fois cette semaine là.

+

Vos données sont-elles à jour ?

+

Une mise à jour automatique de NosDéputés.fr est réalisée toutes les 8 heures à partir des sources officielles au Journal Officiel et sur le site de l'Assemblée.

+

Les compte-rendus des débats peuvent parfois mettre plusieurs heures voire plusieurs jours avant d'être publiés par les services de l'Assemblée et intégrés par nos soins après un travail de vérification humain préalable. De même, afin de pouvoir intégrer au mieux le fond des interventions des parlementaires, les sommaires des compte-rendus en vidéo ne sont pas pris en compte tant qu'un compte-rendu déclarant explicitement que la vidéo fait office de seul compte-rendu n'a pas été publié.

+

Les mises-à-jour sont donc fréquentes et au plus proche de la source, mais les données des dernières semaines sont pour autant toujours amenées à évoluer régulièrement et ne sauraient refléter l'activité en temps réel des députés à l'Assemblée.

-

Mais alors, pour les données en hémicycle, comment faites vous la différence entre une participation et une présence puisque vous détectez la présence grâce aux interventions ?

-

Un député est compté comme participant en hémicycle si au moins une de ses interventions pendant la séance étudiée comporte plus de 20 mots. Vous pourrez en effet constater comme nous que les très courtes interventions ne peuvent pas aborder le fond du débat.

+

À quoi correspondent les icones et les nombres de la barre « Activité » sur la page de chaque député ?

+

Il s'agit d'une série d'indicateurs permettant d'apprécier l'activité parlementaire dans sa diversité : travaux en commissions et en séance plénière, propositions d'amendements et de lois, rapports ou encore questions soumises au gouvernement. Vous pouvez survoler ces éléments et lire la suite de ces questions fréquentes pour avoir plus de précisions sur chacun.

+

Pour les députés dont le mandat est clos, ces nombres indiquent le total pour chaque indicateur au cours de l'intégralité de la durée du ou des mandats du parlementaire au cours de la législature.

+

Pour les députés en activité, ils mesurent la valeur des indicateurs sur les douze derniers mois seulement, comme explicité entre parenthèses. L'ensemble des activités passées des députés reste pour autant naturellement intégré à NosDéputés.fr et accessible à l'aide des multiples liens présentés plus bas vers les différentes activités.

-

Quelle est la différence entre une "intervention courte" et une "intervention longue" ?

-

Une intervention courte est une intervention de moins de 20 mots. Comme expliqué précédemment, il s'agit d'interventions qui n'abordent pas le fond du débat, qu'il s'agisse pour le président de la séance d'annonces de prises de parole ou de simples interjections.

+

J'ai vu baisser plusieurs des indicateurs d'activité d'un député, comment cela est-il possible ?

+

Si durant la première année de mandat d'un député les indicateurs synthétisent son activité totale depuis sa prise de fonction, ce n'est plus le cas une fois la première année de législature écoulée. Puisque les indicateurs portent sur les douze derniers mois des députés en activité, ces chiffres peuvent évoluer, positivement ou négativement, jour après jour en fonction de l'activité du député le même jour l'année passée : il s'agit d'une synthèse glissante, un instantané de l'année écoulée.

+

Il peut par ailleurs arriver que nous opérions à des corrections entraînant de légers ajustements de certains indicateurs, comme par exemple en fusionnant des éléments identiques intégrés plusieurs fois, notamment lorsque deux commissions se sont réunies conjointement et ont produit chacune un relevé de présence et un compte-rendu.

-

Pourquoi ne donnez-vous pas également accès aux votes des députés ?

-

Cela va venir. Nous n'avons pas encore eu le temps de nous pencher sur la question et leur intégration demande un travail approfondi car seules les données de certains votes sont publiques. En effet une grande majorité des votes se font à main levée et lorsqu'un scrutin public a lieu, seuls les votes solennels sont enregistrés avec l'intégralité des votes individuels.

Pour le reste de ces scrutins, seuls les députés ayant voté différemment de leur groupe politique sont signalés. Cela pourrait éventuellement permettra à terme d'évaluer par exemple la fidélité d'un député à son groupe. Nous vous recommandons cependant en attendant le site Mon-Depute.fr qui présente ces données.

+

Que signifient les couleurs rouge et vert sur les indicateurs d'activité des députés ?

+

Si un député a exercé son mandat durant au moins dix des douze derniers mois, il est intégré à la synthèse globale permettant de comparer l'activité des députés, indicateur par indicateur, sur l'année passée.

+

Si un indicateur du député se trouve parmi les 150 plus hautes ou plus basses valeurs de l'ensemble des députés de la synthèse, il apparaît respectivement en vert ou en rouge (gras ou italique pour les daltoniens).

+

Cette synthèse et ce code couleur ne s'appliquent pas aux députés dont le mandat est clos ou qui sont en exercice depuis moins de dix mois, comme par exemple les suppléants de députés nommés au gouvernement. Il serait en effet inadéquat de comparer des mesures d'activité sur des périodes incomparables.

-

Pourquoi ne prenez-vous pas en compte le travail des députés dans leur circonscription ?

-

Comme son sous-titre l'indique, NosDéputés.fr se veut un observatoire de l'activité parlementaire. Comme le définit la Constitution, les députés ne sont pas élus selon le principe du mandat impératif et tout député est donc un élu de la Nation avant d'être celui de sa circonscription. C'est pourquoi nous nous intéressons exclusivement au travail législatif et de contrôle du gouvernement propres à la fonction parlementaire.

Nous conseillons aux visiteurs de ce site s'intéressant à l'activité en circonscription de leurs élus de consulter la presse régionale ou les sites/blogs des députés qui peuvent être une bonne source d'information.

+

Proposez-vous un palmarès ou classement général des députés ?

+

Non.

+

Si la synthèse globale permet d'ordonner les députés indicateur par indicateur, il n'existe pas à notre sens de combinaison de coefficients parfaite qui permettrait d'établir un palmarès général des députés.

+

Si nous proposons des outils permettant aux citoyens de se faire leur propre évaluation du travail de chacun dans différents domaines, nous sommes conscients de la complexité et de la richesse du travail parlementaire et nous n'estimons pas possible ni souhaitable de réaliser un véritable classement.

+

De par la diversité de leurs circonscriptions d'origine, de leurs groupes politiques, de l'activité de leurs commissions permanentes, ou encore des fonctions qui leur incombent, la situation de chaque parlementaire est unique et porteuse de profils d'activité très différents. Il arrive également que des députés puissent être comme tout un chacun en arrêt maladie, en congé parental ou encore en mission pour le gouvernement. Certaines responsabilités extra-parlementaires peuvent également expliquer des activité plus éparses à l'Assemblée nationale.

+

L'ensemble de ces informations et responsabilités ainsi que le fond des activités dénombrées sont disponibles sur la page dédiée à chaque député. Nous invitons donc les visiteurs à consulter toutes ces pages avant de tirer des jugements hâtifs uniquement fondés sur la synthèse. Cette page ne peut être correctement appréhendée que comme une introduction à l'activité des députés.

+ +

Pourquoi proposez-vous un indicateur de présence pour les commissions mais pas pour l'hémicycle ?

+

La présence au sein des travaux des commissions parlementaires de tous types (permanentes, européenne, spéciales, d'enquête, délégations, offices...) est relevée par les services de l'Assemblée nationale de manière systématique pour chaque réunion (et ce bien avant la mise en place distincte en 2009 du système d'émargement limité aux réunions des commissions permanentes le mercredi matin). Ces relevés sont publiés chaque matin au Journal Officiel, et parfois, lorsque la réunion a donné lieu à un compte-rendu, à la fin de celui-ci. C'est grâce à ces relevés que NosDéputés.fr peut proposer avec précision un indicateur de présence aux réunions de commission, parfois complété par l'identification de présents oubliés par les services mais s'étant manifesté par une intervention consignée au compte-rendu ou au script de sa retranscription vidéo.

+

Il n'existe en revanche aucun relevé de la présence des députés dans l'hémicycle : quiconque a déjà assisté à un débat a pu constater le va et vient permanent de nombreux députés tout au long des séances plénières. C'est pourquoi NosDéputés.fr ne propose pas d'indicateur de présence en hémicycle et que les graphiques d'activité affichent non la présence dans l'hémicycle mais la participation effective, identifiable par les prises de parole ou la participation physique aux scrutins électroniques. Un certain nombre de séances ne donne cependant lieu à aucun scrutin électronique et il est relativement rare que tous les députés présents parlent lors d'une séance : NosDéputés.fr ne peut donc pas rendre compte de manière exhaustive de la présence des députés en hémicycle.

-

Sur la page synthèse, pourquoi ne proposez-vous pas un classement des députés ?

-

Si nous proposons des outils permettant aux citoyens de se faire leur propre évaluation du travail de chacun dans différents domaines, nous sommes conscients de la complexité et de la richesse du travail parlementaire et nous n'estimons pas possible ni souhaitable de réaliser un véritable classement. De plus si nous limitons notre page de synthèse aux seuls députés en activité depuis plus de 10 mois, il reste que certains bilans sont effectivement proposés sur des durées plus courtes pour les députés ayant commencé leur mandat en cours de législature.

Il y a également des députés malades ou ayant des responsabilités extra-parlementaires qui peuvent expliquer une activité plus réduite à l'Assemblée nationale. Ces responsabilités sont indiquées sur la page dédiée à chaque député. Nous invitons donc les visiteurs à consulter toutes ces pages avant de faire des jugements hâtifs uniquement fondés sur la synthèse. Cette page ne peut être correctement appréhendée que comme une introduction à l'activité des députés.

+parler du fait qu'on pourrait ajouter à l'avenir les applaudissements tirés des didascalies ? -

Que représentent les « vacances parlementaires » indiquées sur les graphiques ?

-

L'agenda parlementaire prévoit chaque année quelques semaines de repos pour les députés. Nous signalons ces périodes en gris sur les graphiques d'activité des parlementaires. Il arrive cependant que les députés se réunissent tout de même lors de ces périodes, notamment pour des réunions de commission. Seules les semaines au cours desquelles aucune réunion ne s'est effectivement déroulée au Palais Bourbon sont indiquées en gris comme « vacances parlementaires ».

Par ailleurs, au cours d'une législature, certains députés ne peuvent parfois plus siéger à l'Assemblée pour diverses raisons (nomination au gouvernement, mission gouvernementale prolongée, ...). Ils sont alors remplacés par leurs suppléants, puis reprennent parfois leur siège plus tard au cours de la législature. Ces périodes « hors-mandat » sont également grisées dans les graphiques d'activité du député, au même titre que les périodes dites de « vacances ».

+

Quelle est la différence entre une "intervention courte" et une "intervention longue" ?

+

Une intervention courte est une intervention de moins de 20 mots. En parcourant les compte-rendus des débats en hémicycle, vous pourrez en effet constater comme nous que les très courtes interventions n'abordent pas le fond du débat : il s'agit pour l'essentiel de simples interjections ou d'annonces de prises de parole par le président de séance.

+ +

Que représente l'indicateur « semaines d'activité » proposé pour chaque député ?

+

Les semaines d'activité sont mesurées à partir de tous les éléments disponibles au Journal Officiel et sur le site de l'Assemblée nationale permettant de détecter la participation effective des parlementaires aux travaux des commissions et de l'hémicycle :

+

 - la présence aux réunions des commissions (voir ci-dessus) ;

+

 - la participation aux débats en commission et en hémicycle (identifiées par les prises de parole, même brèves, consignées dans les compte-rendus du site de l'Assemblée ou les scripts des retranscriptions vidéos) ;

+

 - la participation, sans faire usage de délégation de vote, aux scrutins publics en hémicycle (uniquement depuis la mise en place de la publicité des délégations des votes le 13 février 2018).

+

Les semaines d'activité décomptent le nombre de semaines au cours desquelles un parlementaire a été détecté via ces sources d'information comme participant au moins une fois à ces travaux.

+

Nous sommes conscients qu'un certain nombre d'activités à l'Assemblée comme les réunions de groupe, les auditions par des rapporteurs ou les participations à divers travaux préparatoires ne sont pas visibles via ces sources d'informations, mais il est impossible d'en prendre compte sans source exhaustive. Nous encourageons régulièrement les services de l'Assemblée à publier des informations complémentaire les plus précises et complètes possibles, et nous utilisons toute nouvelle source d'information exploitable lorsqu'elle est rendue disponible de manière systématique et exhaustive pour l'ensemble des députés.

+ +

À quoi correspondent les différentes informations représentées sur les graphiques d'activité des députés ?

+

Les différents graphiques d'activité visualisent semaine par semaine l'activité d'un parlementaire au sein des commissions et/ou de l'hémicycle. Vous pouvez les survoler pour obtenir les détails relatifs à chaque semaine.

+

- Présences détectées : sur le graphique global et celui des seuls travaux en hémicycle, la présence décomptée ne saurait être exhaustive faute de relevés exhaustifs de la présence en hémicycle (voir ci-dessus), seules les présences "détectées" via notamment la participation orale aux débat y compris les invectives, ou la participation physique aux scrutins électroniques sont représentables.

+

- Présences enregistrées : pour le graphique des seuls travaux en commission, la présence précise des parlementaires étant relevée par les services, l'information présentée est dite "enregistrée" en opposition aux "présences détectées".

+

- Participations : un député est compté comme participant à une séance d'hémicycle s'il y a prononcé au moins une "intervention longue", c'est-à-dire comportant plus de 20 mots. Vous pourrez en effet constater comme nous que les très courtes interventions ne peuvent pas aborder le fond du débat.

+

- Médiane des députés : la présence médiane affichée en liseré gris vise à apporter un élément de comparaison entre les différents graphes. Elle indique pour chaque semaine le nombre de réunions ou séances auxquelles le député médian a été détecté présent, c'est-à-dire qu'au moins un député sur deux a été détecté présent autant de fois cette semaine là.

+

- Questions orales : sur le graphique global ainsi que sur le graphique de la seule activité en hémicycle, le nombre de questions posées par le député lors de séances de questions au gouvernement ou de questions orales dans débat apparaît sous la forme de barres bleues.

+

- Vacances parlementaires : l'agenda parlementaire prévoit chaque année quelques semaines de repos pour les députés. Nous signalons ces périodes en gris sur les graphiques d'activité des parlementaires. Il arrive cependant que les députés se réunissent tout de même lors de ces périodes, notamment pour des réunions de commission. Seules les semaines au cours desquelles aucune réunion ne s'est effectivement déroulée au Palais Bourbon sont indiquées en gris comme « vacances parlementaires ». Par ailleurs, au cours d'une législature, certains députés ne peuvent parfois plus siéger à l'Assemblée pour diverses raisons (nomination au gouvernement, mission gouvernementale prolongée...). Ils sont alors remplacés par leurs suppléants, puis reprennent parfois leur siège plus tard au cours de la législature. Ces périodes « hors-mandat » sont également grisées dans les graphiques d'activité du député, au même titre que les périodes dites de « vacances ».

+ +

Pourquoi ne donnez-vous pas également accès aux votes des députés ?

+

Cela arrive !

+

Nous avons récemment mis en œuvre les développements nécessaires à l'intégration des données relatives aux scrutins publics à partir de l'OpenData proposée par l'Assemblée nationale afin notamment d'en tirer des informations complémentaire sur la participation des députés aux travaux dans l'hémicycle. Nous n'avons cependant pas encore eu le temps de nous pencher sur la présentation des positions individuelles de chaque député lors de ces scrutins ou l'association de ces scrutins aux textes de lois et amendements correspondants mais tout cela est au programme. Cela pourra également permettre par exemple d'évaluer par exemple la fidélité d'un député à son groupe politique.

+

Il reste à noter qu'une immense majorité des votes se déroulant à main levée, l'analyse des votes au prisme des seuls scrutin publics enregistrés électroniquement ne peut permettre de rendre compte de l'ensemble des positions d'un député. -

Que représentent les « semaines d'activité » indiquées pour chaque député ?

-

Les semaines d'activité sont calculées à partir de tous les éléments mesurables disponibles au Journal Officiel et sur le site de l'Assemblée nationale qui permettent de détecter la présence physique d'un parlementaire au sein du Palais Bourbon :

 - la présence aux réunions de commissions (Journal Officiel),

 - les prises de parole en commission ou en hémicycle (site de l'Assemblée nationale).

Si un parlementaire a été détecté via ces sources d'information comme présent au moins une fois pour une semaine considérée, nos algorithmes enregistrent sa présence pour la semaine.

Nous sommes conscients que les réunions de groupe, ainsi que les participations à divers travaux préparatoires ou une présence dans les bureaux de l'Assemblée ne sont pas visibles via ces sources d'informations, mais il est impossible de les prendre en compte sans source exhaustive. De même, nous ne pouvons prendre en compte la présence en hémicycle à partir des informations relatives aux votes car les informations publiées sont malheureusement incomplètes (pas de mention des délégations de vote, ni de tous les votants pour les scrutins publics). Nous encourageons régulièrement les services de l'Assemblée à publier les informations les plus précises et complètes possibles en la matière et nous utiliserons ces nouvelles sources d'information dès qu'elles seront disponibles.

+TODO: COMPLETE

Prenez-vous en compte tous les travaux effectués à l'Assemblée nationale ?

Nous prenons compte de l'ensemble des éléments publics, publiés, et exploitables, disponibles sur le site de l'Assemblée nationale et au Journal Officiel.

Les travaux des commissions d'enquête, des missions d'information ou encore des offices et délégations sont donc bien intégrés dans la mesure de leur disponibilité.

Faute de compte-rendu public ou exploitable, les réunions des groupes politiques et de nombreuses délégations ou missions parlementaires internationales restent en revanche difficiles à prendre en compte. Bien qu'elles représentent une charge de travail importante limitant naturellement la participation aux travaux à l'Assemblée, les missions confiées individuellement à quelques députés par le gouvernement ou l'Elysée ne sont malheureusement pas documentées et ne peuvent donc être prises en compte en l'état.

Nous encourageons régulièrement l'Assemblée nationale à publier plus largement et précisément ces informations afin de renforcer l'exhaustivité de notre observatoire de l'activité parlementaire.

+

Nous sommes conscients que les réunions de groupe, ainsi que les participations à divers travaux préparatoires ou une présence dans les bureaux de l'Assemblée ne sont pas visibles via ces sources d'informations, mais il est impossible de les prendre en compte sans source exhaustive. -

Que représentent les nombres indiqués dans la barre « Activité » sur la page de chaque député ?

-

Pour les députés en activité, chaque nombre représente la valeur d'un indicateur (semaines d'activité, présences en commissions, interventions en commission, longues ou courtes en hémicycle, amendements, propositions, rapports et questions) mesuré sur les douze derniers mois. Ces chiffres peuvent donc évoluer, positivement ou négativement, jour après jour : il s'agit d'une synthèse glissante, un instantané de l'année écoulée. Lorsqu'un député exerce sa fonction depuis moins de 12 mois, les indicateurs synthétisent son activité depuis sa prise de fonction.

Si un député a exercé son mandat durant au moins dix des douze derniers mois, il est intégré à la synthèse globale permettant de comparer l'activité des députés sur l'année passée. Il serait en effet injuste et donc peu pertinent de comparer l'activité sur douze mois de la plupart des députés à l'activité de certains autres sur une durée réduite.

Si un indicateur du député se trouve parmi les 150 plus hauts ou plus bas chiffres de la synthèse globale, il apparaît respectivement en vert ou en rouge.

Lorsque le mandat d'un député est clos, celui-ci disparaît également de la synthèse globale. Les indicateurs sur sa page représentent alors les totaux sur l'ensemble de la durée de son ou ses mandats au cours de la législature. Il ne serait en effet pas pertinent de comparer les chiffres d'un député en activité à ceux d'un ancien député qui correspondraient à une période différente (dans le temps ou dans la longueur).

+

Pourquoi ne prenez-vous pas en compte le travail des députés dans leur circonscription ?

+

Comme son sous-titre l'indique, NosDéputés.fr se veut un observatoire de l'activité parlementaire. Comme le définit la Constitution, les députés ne sont pas élus selon le principe du mandat impératif et tout député est donc un élu de la Nation avant d'être celui de sa circonscription. C'est pourquoi nous nous intéressons exclusivement au travail législatif et de contrôle du gouvernement propres à la fonction parlementaire.

+

Nous conseillons aux visiteurs de ce site s'intéressant à l'activité en circonscription de leurs élus de consulter la presse régionale ou les sites/blogs des députés qui peuvent être de bonnes sources d'information.

Les responsables du site

+ ADD CONTACT ELUS

Comment peut-on vous contacter ?

Vous pouvez écrire à l'ensemble des responsables du collectif Regards Citoyens à notre adresse de contact : contact [at] regardscitoyens.org

Etes vous affiliés à un parti ou à une organisation politique ?

Les activités du collectif Regards Citoyens et du site NosDéputés.fr sont totalement indépendantes de tout parti politique.

+ADD LINK DECLARATIONS INTERETS -

Avez-vous été payés pour réaliser ce site ?

+ EDIT + h3 id="post_14">Avez-vous été payés pour réaliser ce site ?

Non, ce site a été entièrement réalisé par des bénévoles. Nous avons payé nous-mêmes l'ensemble de l'infrastructure technique et les quelques supports de communication nécessaires au lancement du site.

Quelles sont vos sources d'inspiration ?

@@ -98,26 +153,30 @@

Pourquoi dois-je créer un compte pour pouvoir déposer un commentaire ?

Nous avons choisi un système de modération à posteriori. N'étant pas responsables des propos tenus par nos utilisateurs dans ces commentaires, la loi nous oblige au vu de notre statut d'hébergeur à être capable d'identifier la ou les personnes responsables d'éventuels propos diffamatoires sur le site. C'est donc pour offir des conditions de débats optimales tout en se conformant à la loi que nous avons choisi ce système. Pour autant, vous pourrez noter que l'inscription est d'une très grande simplicité.

+

Quelles sont les règles d'encadrement des commentaires ?

+

Regards Citoyens a à coeur de permettre à tous de s'exprimer. C'est dans cet objectif que nous faisons le choix d'une modération a posteriori uniquement des commentaires publiés par les citoyens et qui en portent la responsabilité individuelle. Cependant, afin d'assurer un débat constructif, il convient de respecter certaines règles de bienséance :

- respecter la loi, et donc refuser tout propos raciste, incitation à la haine ou à la violence, ou qui puisse porter atteinte à l'intégrité ou diffamer autrui ;

- commenter l'objet des travaux lui-même et non sur tout autre sujet sans lien ;

- éviter à tout prix le copier/coller du même argumentaire d'un commentaire à l'autre.

En cas de manquement à ces règles, les commentaires pourront être supprimés et le compte de l'utilisateur suspendu de manière temporaire, voire définitive. En cas de conflit ou contestation, il est toujours possible de nous contacter à contact@regardscitoyens.org pour dialoguer.

+ +TODO: UPDATE

Comment puis-je relayer mon activité du site sur mes réseaux sociaux ?

Comme vous avez pu le constater, nous publions un certain nombre de flux RSS qui vous permettent de suivre en temps réel l'activité des députés que vous choisissez, ainsi que l'activité générale du site. Vous pouvez les utiliser pour relayer votre activité ou celle de votre député sur Twitter ou Identica via le site TwitterFeed. Facebook propose également ce type d'outils.

+TODO: UPDATE

Comment puis-je vous aider ?

La meilleure manière de nous aider est de vous exprimer dans les commentaires sur les travaux parlementaires et ainsi de contribuer aux débats qui ont lieu sur le site. Vous pouvez également enrichir la qualité des données en nous aidant à mettre en valeur les interventions utiles au débat en signalant les commentaires jugés non-constructifs. Si vous le souhaitez, vous pouvez aussi nous aider à populariser le site en en parlant autour de vous. Vous pouvez également nous soutenir financièrement, pour faire face aux coûts de mise en place de ce site. Enfin, si vous avez des idées d'amélioration ou des compétences particulières que vous estimez utiles à notre travail, n'hésitez à nous en faire part également par e-mail!

Proposez-vous un suivi par alertes e-mails ?

Oui. Depuis la page de chaque député, vous pouvez vous abonner pour recevoir par mail au rythme que vous souhaitez les informations relatives à l'activité de ce parlementaire.

Nous proposons également un système d'alerte par mots clés depuis le moteur de recherche. Pour ce faire, il suffit de cliquer sur l’icône représentant un courriel et d'indiquer votre adresse e-mail.

-

Quelles sont les rêgles d'encadrement des commentaires ?

-

Regards Citoyens a à coeur de permettre à tous de s'exprimer. C'est dans cet objectif que nous faisons le choix d'une modération a posteriori uniquement des commentaires publiés par les citoyens et qui en portent la responsabilité individuelle. Cependant, afin d'assurer un débat constructif, il convient de respecter certaines règles de bienséance :

- respecter la loi, et donc refuser tout propos raciste, incitation à la haine ou à la violence, ou qui puisse porter atteinte à l'intégrité ou diffamer autrui ;

- commenter l'objet des travaux lui-même et non sur tout autre sujet sans lien ;

- éviter à tout prix le copier/coller du même argumentaire d'un commentaire à l'autre.

En cas de manquement à ces règles, les commentaires pourront être supprimés et le compte de l'utilisateur suspendu de manière temporaire, voire définitive. En cas de conflit ou contestation, il est toujours possible de nous contacter à contact@regardscitoyens.org pour dialoguer.

-

Fonctionnalités techniques

-

Est ce que ce site est un logiciel libre ?

-

Oui ! Le code source du site et sa documentation technique sont disponibles sous licence AGPL à l'adresse suivante : https://github.com/regardscitoyens/nosdeputes.fr

Si vous avez des talents de développeur, n'hésitez donc pas à venir nous aider !

- +TODO: UPDATE

Proposez-vous une API ?

Une API a été développée. Elle est encore en version Beta. Vous trouverez sa documentation sur la page suivante : https://github.com/regardscitoyens/nosdeputes.fr/blob/master/doc/api.md

+

Est ce que ce site est un logiciel libre ?

+

Oui ! Le code source du site et sa documentation technique sont disponibles sous licence AGPL à l'adresse suivante : https://github.com/regardscitoyens/nosdeputes.fr

N'hésitez pas à [nous faire remonter](https://github.com/regardscitoyens/nosdeputes.fr/issues/new) tout bug que vous pourriez rencontrer ou nous proposer des suggestions d'amélioration !

Et si vous avez des talents de développeur, vous êtes les bienvenus pour nous aider !

+ +TODO: UPDATE

Quelles sont les technologies employées pour la création ce site ?

Le site a été construit et est hébergé entièrement avec des logiciels libres. Notre serveur est une machine GNU/Linux Debian utilisant les services Apache 2 et MySQL. Le site a été développé en PHP grâce à l'environnement de développement Symfony. Nous utilisons la librairie pChart et d3.js pour tracer les graphiques, gd pour le traitement d'image et jQuery et jQuery-ui pour la surcouche javascript (ainsi que les plugins mapHighlight et DynaTable). Nous nous efforçons de respecter les standards définis par le W3C. Si vous avez des problèmes d'accessibilité, n'hésitez pas à nous le signaler à contact [at] regardscitoyens.org et nous ferons le maximum pour les corriger rapidement.