Commit 95a78cc8 by Nicolas Joyard

Initial commit

parents
.env
*.egg-info
*.pyc
*~
## Prérequis
* Python 3
* virtualenvwrapper
* PostgreSQL
## Installation
```sh
$ git clone https://git.regardscitoyens.org/regardscitoyens/irfm.git
$ cd irfm
$ mkvirtualenv --python=$(which python3) irfm
$ pip install -e .
$ psql -c "create user irfm with password 'irfm';"
$ psql -c "create database irfm with owner irfm;"
$ irfm db upgrade
```
## Mise à jour
```sh
$ cd /path/to/irfm
$ workon irfm
$ git pull
$ irfm db upgrade
```
## Développement
### Génération de migrations
Après avoir modifié les modèles Python :
```bash
$ irfm db migrate -m <description>
$ irfm db upgrade
```
### Création de migration vierge
```bash
$ irfm db revision -m <description>
```
# -*- coding: utf-8 -*-
from flask_migrate import Migrate, MigrateCommand
from flask_script import Manager
from .irfm import app
from .models import db
manager = Manager(app)
migrate = Migrate(app, db)
manager.add_command('db', MigrateCommand)
@manager.command
def runserver():
"""Exécute le serveur web flask intégré"""
app.run()
# -*- coding: utf-8 -*-
import os
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
class DefaultConfig(object):
"""
Default irfm config file for standard environment
"""
DEBUG = False
SQLALCHEMY_DATABASE_URI = \
'postgresql://irfm:irfm@localhost:5432/irfm'
SQLALCHEMY_TRACK_MODIFICATIONS = False
SQLALCHEMY_ECHO = False
DATA_DIR = os.path.join(BASE_DIR, 'data')
API_PAGE_SIZE = 10
SECRET_KEY = 'no-secret-key'
PIWIK_HOST = None
PIWIK_ID = None
class DebugConfig(DefaultConfig):
"""
Debug-enabled default config
"""
DEBUG = True
SQLALCHEMY_ECHO = True
class AutoSecretKeyConfig(DefaultConfig):
"""
Default config that automatically generates a secret key in DATA_DIR
"""
_secret_key = None
@property
def SECRET_KEY(self):
if not self._secret_key:
secret_file = os.path.join(self.DATA_DIR, 'secret.txt')
if not os.path.exists(secret_file):
chars = 'abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)'
from random import SystemRandom
rnd = SystemRandom()
key = ''.join([chars[rnd.randint(1, len(chars))-1]
for i in range(1, 50)])
with open(secret_file, 'w+') as f:
f.write(key)
with open(secret_file, 'r') as f:
self._secret_key = f.read()
return self._secret_key
class EnvironmentConfig(AutoSecretKeyConfig):
"""
Config for environment-based setup.
- IRFM_DEBUG: 'True' to enable
- IRFM_DEBUG_SQL: 'True' to enable
- IRFM_DB_URL: database connection URL
- IRFM_DATA_DIR: directory for data files
- IRFM_PIWIK_HOST: piwik hostname
- IRFM_PIWIK_ID: piwik site ID
"""
DEBUG = os.environ.get('IRFM_DEBUG', 'False') == 'True'
SQLALCHEMY_ECHO = os.environ.get('IRFM_DEBUG_SQL', 'False') == 'True'
SQLALCHEMY_DATABASE_URI = os.environ.get(
'IRFM_DB_URL', DefaultConfig.SQLALCHEMY_DATABASE_URI)
DATA_DIR = os.environ.get('IRFM_DATA_DIR', DefaultConfig.DATA_DIR)
PIWIK_HOST = os.environ.get('IRFM_PIWIK_HOST', DefaultConfig.PIWIK_HOST)
PIWIK_ID = os.environ.get('IRFM_PIWIK_ID', DefaultConfig.PIWIK_ID)
# -*- coding: utf-8 -*-
from .setup_app import setup_app
app = setup_app(__name__)
# -*- coding: utf-8 -*-
from .database import db
from .parlementaire import Parlementaire
# -*- coding: utf-8 -*-
#
# Lors de la modification de ces énumérations, penser à créer une migration DB
# pour mettre à jour les types ENUM correspondants en base de données.
#
CHAMBRES = {
'AN': 'Assemblée nationale',
'SEN': 'Sénat',
}
ETAPES = {
'NOUVEAU': 'Nouveau'
}
SEXES = {
'F': 'Femme',
'H': 'Homme',
}
# -*- coding: utf-8 -*-
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy(session_options={'autoflush': False, 'autocommit': False})
# -*- coding: utf-8 -*-
import enum
from .constants import CHAMBRES, ETAPES, SEXES
from .database import db
class Parlementaire(db.Model):
__tablename__ = 'parlementaires'
id = db.Column(db.Integer, primary_key=True)
nom = db.Column(db.Unicode)
prenom = db.Column(db.Unicode)
sexe = db.Column(db.Enum(*SEXES.keys(), name='sexes'))
adresse = db.Column(db.Unicode)
chambre = db.Column(db.Enum(*CHAMBRES.keys(), name='chambres'))
mandat_debut = db.Column(db.DateTime)
mandat_fin = db.Column(db.DateTime)
num_deptmt = db.Column(db.Integer)
nom_circo = db.Column(db.Unicode)
num_circo = db.Column(db.Integer)
groupe = db.Column(db.Unicode)
groupe_sigle = db.Column(db.Unicode)
url_photo = db.Column(db.Unicode)
url_rc = db.Column(db.Unicode)
url_off = db.Column(db.Unicode)
etat = db.Column(db.Enum(*ETAPES.keys(), name='etapes'))
# -*- coding: utf-8 -*-
from flask import render_template
from .models import Parlementaire
def setup_routes(app):
@app.context_processor
def inject_piwik():
piwik = None
if app.config['PIWIK_HOST']:
piwik = {
'host': app.config['PIWIK_HOST'],
'id': app.config['PIWIK_ID']
}
return {'piwik':piwik}
@app.context_processor
def inject_menu():
return {
'menu': [
{'url': '/', 'label': 'Accueil', 'endpoint': 'home' },
{'url': '/parlementaires', 'label': 'Liste des parlementaires',
'endpoint': 'parlementaires' },
]
}
@app.route('/', endpoint='home')
def home():
return render_template('index.html.j2')
@app.route('/parlementaires', endpoint='parlementaires')
def parlementaires():
return render_template(
'list.html.j2',
parlementaires=Parlementaire.query.all()
)
# -*- coding: utf-8 -*-
import os
from flask import Flask
from flaskext.markdown import Markdown
from .routes import setup_routes
def setup_app(name):
# Create app
app = Flask(name)
# Load config
config_obj = os.environ.get('IRFM_CONFIG',
'irfm.config.DefaultConfig')
app.config.from_object(config_obj)
# Setup DB
from .models import db
db.init_app(app)
# Enable Markdown
Markdown(app)
# Setup routes
setup_routes(app)
return app
<!doctype html>
<head>
<title>Transparence IRFM</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous" />
<!-- <link rel="icon" type="image/png" href="{{ url_for('static', filename='favicon.png') }}" /> -->
</head>
<body>
<section class="container-fluid">
<header class="col-md-12 page-header">
<h1>
Transparence IRFM
</h1>
<ul class="nav nav-pills">
{% for item in menu %}
<li role="presentation" {% if request.endpoint == item.endpoint %}class="active"{% endif %}><a href="{{ item.url }}">{{ item.label }}</a></li>
{% endfor %}
</ul>
</header>
<div class="col-md-12">
{% block content %}
{% endblock %}
</div>
<div class="col-md-12">
</div>
</section>
<footer class="well text-center">
<small>
<p>
Ce site est un <a href="https://git.regardscitoyens.org/regardscitoyens/irfm" target="_blank">logiciel libre</a>,
distribué sous <a href="https://www.gnu.org/licenses/agpl-3.0.html" target="_blank">
license <img src="https://www.gnu.org/graphics/agplv3-155x51.png" alt="AGPLv3" style="height: 1.5em;"></a>
&mdash;
Les données sont réutilisables sous license
<a href="http://creativecommons.org/licenses/by-sa/2.0/fr/" target="_blank">
<img src="https://i.creativecommons.org/l/by-sa/2.0/fr/80x15.png" alt="CC-BY-SA"></a>
et <a href="http://opendatacommons.org/licenses/odbl/">ODbL</a>
&mdash;
<a href="https://www.regardscitoyens.org/" target="_blank">
<img src="{{ url_for('static', filename='rc.png') }}" style="height: 1.5em; vertical-align: top;">
Regards Citoyens</a>
</p>
<p>
<i>Vous êtes libre de réutiliser, modifier et recouper les données dans la mesure où vous indiquez leur source et que vous republiez les données modifiées ayant servi lors d'une réutilisation publiée.</i>
</p>
</small>
</footer>
{% if piwik %}
<!-- Piwik -->
<script type="text/javascript">
var _paq = _paq || [];
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
(function() {
var u="//{{ piwik.host }}/";
_paq.push(['setTrackerUrl', u+'piwik.php']);
_paq.push(['setSiteId', {{ piwik.id }}]);
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
g.type='text/javascript'; g.async=true; g.defer=true; g.src=u+'piwik.js'; s.parentNode.insertBefore(g,s);
})();
</script>
<noscript><p><img src="//{{ piwik.host }}/piwik.php?idsite={{ piwik.id }}" style="border:0;" alt="" /></p></noscript>
<!-- End Piwik Code -->
{% endif %}
</body>
{% 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>
{% endblock %}
{% extends "_base.html.j2" %}
{% block content %}
<section class="panel panel-default">
<header class="panel-heading">
<b>Liste des parlementaires</b>
</header>
<table class="table table-striped">
{% for parl in parlementaires %}
{% if loop.first %}
<tr>
<th>Parlementaire</th>
<th>Etape</th>
</tr>
{% endif %}
<tr>
<th>{{ parl.nom }}</th>
<th>{{ parl.etat }}</th>
</tr>
{% else %}
<tr>
<td class="warning" colspan="2">
<em>Aucun parlementaire trouvé :(</em>
</td>
</tr>
{% endfor %}
</table>
</section>
{% endblock %}
### C'est quoi ?
L'IRFM.
### Pourquoi ?
Parce que.
# A generic, single database configuration.
[alembic]
# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S
from __future__ import with_statement
from alembic import context
from sqlalchemy import engine_from_config, pool
from logging.config import fileConfig
import logging
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
fileConfig(config.config_file_name)
logger = logging.getLogger('alembic.env')
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
from flask import current_app
config.set_main_option('sqlalchemy.url',
current_app.config.get('SQLALCHEMY_DATABASE_URI'))
target_metadata = current_app.extensions['migrate'].db.metadata
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
def run_migrations_offline():
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(url=url)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online():
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
# this callback is used to prevent an auto-migration from being generated
# when there are no changes to the schema
# reference: http://alembic.readthedocs.org/en/latest/cookbook.html
def process_revision_directives(context, revision, directives):
if getattr(config.cmd_opts, 'autogenerate', False):
script = directives[0]
if script.upgrade_ops.is_empty():
directives[:] = []
logger.info('No changes in schema detected.')
engine = engine_from_config(config.get_section(config.config_ini_section),
prefix='sqlalchemy.',
poolclass=pool.NullPool)
connection = engine.connect()
context.configure(connection=connection,
target_metadata=target_metadata,
process_revision_directives=process_revision_directives,
**current_app.extensions['migrate'].configure_args)
try:
with context.begin_transaction():
context.run_migrations()
finally:
connection.close()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)}
def upgrade():
${upgrades if upgrades else "pass"}
def downgrade():
${downgrades if downgrades else "pass"}
"""Initialisation
Revision ID: eef599a4fb63
Revises:
Create Date: 2017-05-05 21:23:01.220831
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'eef599a4fb63'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
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('adresse', sa.Unicode(), nullable=True),
sa.Column('chambre', sa.Enum('SEN', 'AN', 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.Integer(), 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('url_photo', sa.Unicode(), nullable=True),
sa.Column('url_rc', sa.Unicode(), nullable=True),
sa.Column('url_off', sa.Unicode(), nullable=True),
sa.Column('etat', sa.Enum('NOUVEAU', name='etapes'), nullable=True),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('parlementaires')
# ### end Alembic commands ###
import os
from setuptools import setup
BASE_DIR = os.path.dirname(__file__)
def read_text(fname):
with open(os.path.join(BASE_DIR, fname)) as f:
return f.read()
def read_requirements(fname):
with open(os.path.join(BASE_DIR, fname)) as f:
lines = f.read().splitlines()
return [line
for line in lines
if bool(line and not line.startswith('#'))]
setup(
name="irfm",
version="0.0.1",
author="Nicolas Joyard",
author_email="joyard.nicolas@gmail.com",
description="",
license="AGPLv3+",
keywords="",
url="https://git.regardscitoyens.org/regardscitoyens/irfm",
packages=['irfm'],
long_description=read_text('README.md'),
install_requires=read_requirements('requirements.txt'),
classifiers=[
"Development Status :: 3 - Alpha",
"Framework :: Flask",
"Intended Audience :: Developers",
"License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)",
],
entry_points='''
[console_scripts]
irfm=irfm.cli:manager.run
'''
)
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 sign in to comment