Commit 5cf7bf3b authored by Nicolas Joyard's avatar Nicolas Joyard

Ajout étapes et graphique

parent 2c097a31
......@@ -17,6 +17,15 @@ $ psql -c "create database irfm with owner irfm;"
$ irfm db upgrade
```
## Import des données
```sh
$ cd /path/to/irfm
$ workon irfm
$ irfm import_etapes
$ irfm import_nd
```
## Mise à jour
```sh
......
......@@ -4,6 +4,8 @@ from flask_migrate import Migrate, MigrateCommand
from flask_script import Manager
from .importers.nosdeputes import NosDeputesImporter
from .importers.etapes import EtapesImporter
from .irfm import app
from .models import db
......@@ -21,6 +23,12 @@ def runserver():
app.run()
@manager.command
def import_etapes():
"""Crée ou met à jour la liste des étapes"""
app.config.update(SQLALCHEMY_ECHO=False)
EtapesImporter(app).run()
@manager.command
def import_nd():
"""Importe les députés depuis NosDéputés.fr"""
......
# -*- coding: utf-8 -*-
class BaseImporter(object):
def __init__(self, app):
self.app = app
def info(self, msg):
self.app.logger.info(u'<%s> %s' % (self.__class__.__name__, msg))
def error(self, msg):
self.app.logger.error(u'<%s> %s' % (self.__class__.__name__, msg))
def run(self):
raise NotImplemented()
# -*- coding: utf-8 -*-
from .base import BaseImporter
from ..models import db, Etape
from ..models.constants import ETAPES
class EtapesImporter(BaseImporter):
def import_etape(self, data):
created = False
updated = False
id_data = {'ordre': data['ordre']}
etape = Etape.query.filter_by(**id_data).first()
if not etape:
etape = Etape(**id_data)
db.session.add(etape)
created = True
for key, newvalue in data.items():
if key == 'ordre':
continue
curvalue = getattr(etape, key)
if curvalue != newvalue:
updated = True
setattr(etape, key, newvalue)
return created, updated
def run(self):
self.info('Début import étapes')
self.info('%s étapes trouvés' % len(ETAPES))
created = 0
updated = 0
for etape in ETAPES:
c, u = self.import_etape(etape)
if c:
created += 1
elif u:
updated += 1
db.session.commit()
db.session.flush()
self.info('Import étapes terminé: %s créées, %s mises à jour'
% (created, updated))
......@@ -6,7 +6,8 @@ import dateparser
import requests
from sqlalchemy.inspection import inspect
from ..models import db, Groupe, Parlementaire
from .base import BaseImporter
from ..models import db, Etape, Groupe, Parlementaire
def parse_date(date):
......@@ -34,24 +35,16 @@ def parse_couleur(couleur):
return '#' + ''.join(map(dechex, couleur.split(',')))
class NosDeputesImporter(object):
class NosDeputesImporter(BaseImporter):
URL_GROUPES = 'https://www.nosdeputes.fr/organismes/groupe/json'
URL_DEPUTES = 'https://www.nosdeputes.fr/deputes/json'
URL_PHOTO = '//www.nosdeputes.fr/depute/photo/%(slug)s'
DATE_DEBUT = parse_date('2017-01-01')
columns = None
groupes = {}
def __init__(self, app):
self.app = app
def info(self, msg):
self.app.logger.info(u'<%s> %s' % (self.__class__.__name__, msg))
def error(self, msg):
self.app.logger.error(u'<%s> %s' % (self.__class__.__name__, msg))
def import_depute(self, data):
if not self.columns:
mapper = inspect(Parlementaire)
......@@ -70,7 +63,7 @@ class NosDeputesImporter(object):
depute = Parlementaire.query.filter_by(**id_data).first()
if not depute:
id_data.update({'etape': 'NOUVEAU'})
id_data.update({'etape': self.etape_nv})
depute = Parlementaire(**id_data)
db.session.add(depute)
created = True
......@@ -91,6 +84,9 @@ class NosDeputesImporter(object):
'url_off': data['url_an'],
}
if fields['mandat_fin'] and fields['mandat_fin'] < self.DATE_DEBUT:
fields['etape'] = self.etape_na
for key, newvalue in fields.items():
curvalue = getattr(depute, key)
......@@ -105,6 +101,16 @@ class NosDeputesImporter(object):
return created, updated
def import_deputes(self):
self.etape_na = Etape.query.filter_by(label='N/A').first()
if not self.etape_na:
self.error('Etape N/A introuvable, exécuter import_etapes ?')
return
self.etape_nv = Etape.query.filter_by(label='À envoyer').first()
if not self.etape_nv:
self.error('Etape À envoyer introuvable, exécuter import_etapes ?')
return
try:
data = requests.get(self.URL_DEPUTES).json()
except Exception as e:
......@@ -190,4 +196,4 @@ class NosDeputesImporter(object):
def run(self):
self.info('Début import NosDéputés.fr')
self.import_groupes()
self.import_deputes()
\ No newline at end of file
self.import_deputes()
# -*- coding: utf-8 -*-
from .database import db
from .parlementaire import Groupe, Parlementaire
from .procedure import Etape
......@@ -10,11 +10,69 @@ CHAMBRES = {
'SEN': 'Sénat',
}
ETAPES = {
'NOUVEAU': 'Nouveau'
}
SEXES = {
'F': 'Femme',
'H': 'Homme',
}
#
# Lors de la modification de cette énumération, relancer l'import des étapes.
# L'ordre est utilisé comme clé primaire lors de cet import.
#
ETAPES = [
{
'ordre': 0,
'label': 'N/A',
'description': '',
'couleur': '',
},
{
'ordre': 10,
'label': 'À envoyer',
'description': '',
'couleur': '#cccccc',
},
{
'ordre': 20,
'label': 'Envoyé',
'description': '',
'couleur': '#88dddd',
},
{
'ordre': 30,
'label': 'AR reçu',
'description': '',
'couleur': '#8888dd',
},
{
'ordre': 40,
'label': 'Réponse positive',
'description': '',
'couleur': '#88dd88',
},
{
'ordre': 50,
'label': 'Réponse négative',
'description': '',
'couleur': '#dd8888',
},
{
'ordre': 60,
'label': 'Demande CADA',
'description': '',
'couleur': '#ddaa88',
},
{
'ordre': 70,
'label': 'Accord CADA',
'description': '',
'couleur': '#44aa44',
},
{
'ordre': 90,
'label': 'Refus CADA',
'description': '',
'couleur': '#aa4444',
},
]
# -*- coding: utf-8 -*-
import enum
from .constants import CHAMBRES, ETAPES, SEXES
from .constants import CHAMBRES, SEXES
from .database import db
......@@ -42,4 +40,5 @@ class Parlementaire(db.Model):
url_rc = db.Column(db.Unicode)
url_off = db.Column(db.Unicode)
etape = db.Column(db.Enum(*ETAPES.keys(), name='etapes'))
etape_id = db.Column(db.Integer, db.ForeignKey('etapes.id'))
etape = db.relationship('Etape', back_populates='parlementaires')
# -*- coding: utf-8 -*-
from .constants import ETAPES
from .database import db
class Etape(db.Model):
__tablename__ = 'etapes'
id = db.Column(db.Integer, primary_key=True)
ordre = db.Column(db.Integer)
label = db.Column(db.Unicode)
description = db.Column(db.Unicode)
couleur = db.Column(db.Unicode)
parlementaires = db.relationship('Parlementaire', back_populates='etape')
# -*- coding: utf-8 -*-
from flask import render_template
from sqlalchemy.orm import joinedload
from sqlalchemy.orm import joinedload, contains_eager
from sqlalchemy.sql.expression import func
from .models import Parlementaire
from .models import db, Etape, Parlementaire
def setup_routes(app):
......@@ -33,12 +34,38 @@ def setup_routes(app):
@app.route('/', endpoint='home')
def home():
return render_template('index.html.j2')
pqs = Parlementaire.query.options(joinedload(Parlementaire.etape)) \
.filter(Etape.label == 'À envoyer') \
.order_by(func.random())
eqs = db.session.query(Etape) \
.outerjoin(Etape.parlementaires) \
.add_columns(func.count(Parlementaire.id)
.label('nb')) \
.filter(Etape.ordre > 0) \
.group_by(Etape) \
.order_by(Etape.ordre) \
.all()
return render_template(
'index.html.j2',
parlementaire=pqs.first(),
etapes={
'labels': [e.Etape.label for e in eqs],
'couleurs': [e.Etape.couleur for e in eqs],
'counts': [e.nb for e in eqs]
}
)
@app.route('/parlementaires', endpoint='parlementaires')
def parlementaires():
qs = Parlementaire.query.options(joinedload('groupe')).all()
qs = Parlementaire.query.join(Parlementaire.etape) \
.options(joinedload(Parlementaire.groupe)) \
.options(contains_eager(Parlementaire.etape)) \
.filter(Etape.ordre > 0) \
.all()
return render_template(
'list.html.j2',
parlementaires=qs
......
......@@ -14,9 +14,9 @@
<body>
<header class="jumbotron">
<div class="container-fluid">
<h1>
Transparence IRFM
</h1>
{% filter markdown -%}
{% include "text/header.md" %}
{% endfilter %}
<ul class="nav nav-pills">
{% for item in menu %}
......@@ -27,10 +27,8 @@
</header>
<section class="container-fluid">
<div class="col-md-12">
{% block content %}
{% endblock %}
</div>
{% block content %}
{% endblock %}
<div class="col-md-12">
</div>
......
{% extends "_base.html.j2" %}
{% block content %}
<section class="panel panel-default">
<header class="panel-heading">
<b>Introduction</b>
</header>
<article class="panel-body">
{% filter markdown -%}
{% include "text/intro.md" %}
{% endfilter %}
</article>
</section>
<div class="col-md-6">
<section class="panel panel-default">
<article class="panel-body">
{% filter markdown -%}
{% include "text/explications.md" %}
{% endfilter %}
</article>
</section>
</div>
<div class="col-md-6">
<section class="panel panel-default">
<header class="panel-heading">
<b>Avancement du projet</b>
</header>
<article class="panel-body">
<canvas id="pie-canvas" height="200"></canvas>
</article>
</section>
<section class="panel panel-default">
<article class="panel-body">
{% filter markdown -%}
{% include "text/nousaider.md" %}
{% endfilter %}
</article>
</section>
<section class="panel panel-default">
<header class="panel-heading">
<b>Un parlementaire au hasard...</b>
</header>
<article class="panel-body">
<img src="{{ parlementaire.url_photo }}/120" align="left">
<b>{{ parlementaire.prenom }} {{ parlementaire.nom }}</b>
</article>
</section>
</div>
{% endblock %}
{% block scripts %}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js" integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4=" crossorigin="anonymous"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/Chart.js/2.5.0/Chart.min.js" integrity="sha256-GcknncGKzlKm69d+sp+k3A2NyQE+jnu43aBl6rrDN2I=" crossorigin="anonymous"></script>
<script>
var chart = new Chart($('#pie-canvas'), {
type: 'pie',
data: {
labels: {{ etapes.labels|tojson }},
datasets: [
{
data: {{ etapes.counts|tojson }},
backgroundColor: {{ etapes.couleurs|tojson }}
}
]
},
options: {
maintainAspectRatio: false
}
});
</script>
{% endblock %}
\ No newline at end of file
......@@ -33,7 +33,7 @@
<td data-value="{{ parl.nom }} {{ parl.prenom }}">{{ parl.prenom }} {{ parl.nom }}</td>
<td><span title="{{ parl.groupe.nom }}" class="label" style="background-color: {{ parl.groupe.couleur }};">{{ parl.groupe.sigle }}</span></td>
<td data-value="{{ parl.num_deptmt }} {{ parl.num_circo }}">{{ parl.nom_circo }} n°{{ parl.num_circo }}</td>
<td>{{ parl.etape }}</td>
<td><span class="label" style="background-color: {{ parl.etape.couleur }};">{{ parl.etape.label }}</span></td>
</tr>
{% else %}
......@@ -59,7 +59,7 @@
{% endblock %}
{% block scripts %}
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js" integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4=" crossorigin="anonymous"></script>
<script src="{{ url_for('static', filename='moment.min.js') }}"></script>
<script src="{{ url_for('static', filename='bootstrap-sortable.js') }}"></script>
......
# Transparence IRFM
Aidez-nous à améliorer la transparence sur l'Indemnité Représentative de Frais de Mandat !
### Comment nous aider ?
La première étape consiste à envoyer une demande de document à chaque parlementaire concerné par lettre recommandée avec avis de réception. Malheureusement, notre petite équipe bénévole ne peut pas le faire pour tous les députés. **Mais vous pouvez nous aider à le faire !**
C'est simple : choisissez un parlementaire pour lequel la demande de document n'a pas encore été envoyée depuis la [liste des parlementaires](/parlementaires), cliquez sur le bouton « *Envoyer la demande* » et laissez-vous guider. Si vous ne savez pas lequel choisir, un parlementaire au hasard est affiché ci-dessous.
Notez bien que les parlementaires sont des élus de la Nation, vous êtes donc tout à fait en droit d'envoyer une demande à un élu d'une circonscription autre que celle de votre domicile.
"""Suppression groupe
Revision ID: 9784b9baaf7d
Revises: 8d32ba048444
Create Date: 2017-05-06 00:06:25.565160
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '9784b9baaf7d'
down_revision = '8d32ba048444'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('parlementaires', 'groupe')
op.drop_column('parlementaires', 'groupe_sigle')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('parlementaires', sa.Column('groupe_sigle', sa.VARCHAR(), autoincrement=False, nullable=True))
op.add_column('parlementaires', sa.Column('groupe', sa.VARCHAR(), autoincrement=False, nullable=True))
# ### end Alembic commands ###
"""Ajout entité groupe + relation parl
Revision ID: beb25e23d267
Revises: 9784b9baaf7d
Create Date: 2017-05-06 00:07:59.939030
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects.postgresql import ENUM
# revision identifiers, used by Alembic.
revision = 'beb25e23d267'
down_revision = '9784b9baaf7d'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('groupes',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('sigle', sa.Unicode(), nullable=True),
sa.Column('nom', sa.Unicode(), nullable=True),
sa.Column('chambre', ENUM('SEN', 'AN', name='chambres', create_type=False), nullable=True),
sa.Column('couleur', sa.Unicode(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.add_column('parlementaires', sa.Column('groupe_id', sa.Integer(), nullable=True))
op.create_foreign_key(None, 'parlementaires', 'groupes', ['groupe_id'], ['id'])
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, 'parlementaires', type_='foreignkey')
op.drop_column('parlementaires', 'groupe_id')
op.drop_table('groupes')
# ### end Alembic commands ###
"""Initialisation
"""init
Revision ID: 8d32ba048444
Revision ID: f7b6ff041b6f
Revises:
Create Date: 2017-05-05 23:06:35.469060
Create Date: 2017-05-06 11:00:23.580421
"""
from alembic import op
......@@ -10,7 +10,7 @@ import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '8d32ba048444'
revision = 'f7b6ff041b6f'
down_revision = None
branch_labels = None
depends_on = None
......@@ -18,24 +18,41 @@ depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('etapes',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('ordre', sa.Integer(), nullable=True),
sa.Column('label', sa.Unicode(), nullable=True),
sa.Column('description', sa.Unicode(), nullable=True),
sa.Column('couleur', sa.Unicode(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('groupes',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('sigle', sa.Unicode(), nullable=True),
sa.Column('nom', sa.Unicode(), nullable=True),
sa.Column('chambre', sa.Enum('AN', 'SEN', name='chambres'), nullable=True),
sa.Column('couleur', sa.Unicode(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('parlementaires',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('nom', sa.Unicode(), nullable=True),
sa.Column('prenom', sa.Unicode(), nullable=True),
sa.Column('sexe', sa.Enum('H', 'F', name='sexes'), nullable=True),
sa.Column('sexe', sa.Enum('F', 'H', name='sexes'), nullable=True),
sa.Column('adresse', sa.Unicode(), nullable=True),
sa.Column('chambre', sa.Enum('SEN', 'AN', name='chambres'), nullable=True),
sa.Column('chambre', sa.Enum('AN', 'SEN', name='chambres'), nullable=True),
sa.Column('mandat_debut', sa.DateTime(), nullable=True),
sa.Column('mandat_fin', sa.DateTime(), nullable=True),
sa.Column('num_deptmt', sa.Unicode(), nullable=True),
sa.Column('nom_circo', sa.Unicode(), nullable=True),
sa.Column('num_circo', sa.Integer(), nullable=True),
sa.Column('groupe', sa.Unicode(), nullable=True),
sa.Column('groupe_sigle', sa.Unicode(), nullable=True),
sa.Column('groupe_id', sa.Integer(), nullable=True),
sa.Column('url_photo', sa.Unicode(), nullable=True),
sa.Column('url_rc', sa.Unicode(), nullable=True),
sa.Column('url_off', sa.Unicode(), nullable=True),
sa.Column('etape', sa.Enum('NOUVEAU', name='etapes'), nullable=True),
sa.Column('etape_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['etape_id'], ['etapes.id'], ),
sa.ForeignKeyConstraint(['groupe_id'], ['groupes.id'], ),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###
......@@ -44,4 +61,6 @@ def upgrade():
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('parlementaires')
op.drop_table('groupes')
op.drop_table('etapes')
# ### end Alembic commands ###
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment