Commit 09e4b966 authored by Tangui's avatar Tangui

Merge branch 'master' of git.regardscitoyens.org:regardscitoyens/irfm

parents af14a744 e1a7073c
......@@ -8,6 +8,7 @@ from flask_migrate import Migrate, MigrateCommand
from flask_script import Manager
from .importers.adresses import AdressesImporter
from .importers.laposte import LaPosteImporter
from .importers.nosdeputes import NosDeputesImporter
from .irfm import app
......@@ -15,7 +16,8 @@ from .irfm import app
from .models import db
from .tools.files import generer_demandes as generer_demandes_
from .tools.mails import envoyer_emails as envoyer_emails_
from .tools.mails import (envoyer_emails as envoyer_emails_,
mailing_lists as mailing_lists_)
from .tools.procedure import fix_procedure as fix_procedure_
from .tools.text import hash_password
......@@ -58,6 +60,14 @@ def envoyer_emails(envoyer=False):
print('Aucun parlementaire sans adresse mail :)')
@manager.command
def mailing_lists():
"""Affiche les abonnés aux mailing lists"""
app.config.update(SQLALCHEMY_ECHO=False)
for nom, emails in mailing_lists_().items():
print('%s :\n\t%s' % (nom, '\n\t'.join(emails)))
@manager.command
def fix_procedure():
"""Génère les étapes manquantes pour tous les parlementaires"""
......@@ -93,6 +103,13 @@ def import_adresses():
AdressesImporter(app).run()
@manager.command
def import_laposte():
"""Importe l'état des courriers suivi depuis La Poste"""
app.config.update(SQLALCHEMY_ECHO=False)
LaPosteImporter(app).run()
@manager.command
def password():
"""Chiffre un mot de passe admin"""
......
# -*- coding: utf-8 -*-
from bs4 import BeautifulSoup
import requests
from .base import BaseImporter
from ..models import Action, Parlementaire, db
from ..models.constants import ETAPE_ENVOYE
class LaPosteImporter(BaseImporter):
URL = 'http://www.part.csuivi.courrier.laposte.fr/suivi/index?id={}'
cache = {}
def _next_el_sibling(self, soup):
cur = soup
while cur and cur.next_sibling and cur.next_sibling.name is None:
cur = cur.next_sibling
return cur.next_sibling
def _import_suivi(self, suivi):
self.info('Recherche suivi %s' % suivi)
url = self.URL.format(suivi)
try:
soup = BeautifulSoup(requests.get(url).content, 'html5lib')
except Exception as e:
self.error('Erreur sur %s: %s' % (url, e))
return None
ident = soup.select('td.identifiant_num')
if not len(ident):
return None
ident = ident[0]
if ident.text.strip().startswith('Aucun '):
return None
produit = self._next_el_sibling(ident)
date = self._next_el_sibling(produit)
localisation = self._next_el_sibling(date)
statut = self._next_el_sibling(localisation)
if date and statut:
return '%s (%s)' % (statut.text, date.text)
else:
return None
def import_suivi(self, suivi):
if suivi not in self.cache:
statut = self._import_suivi(suivi)
self.info('SUIVI %s => %s' % (suivi, statut))
self.cache[suivi] = statut
return self.cache[suivi]
def run(self):
self.info('Début import suivi depuis La Poste')
acts = Action.query.join(Action.parlementaire) \
.filter(Parlementaire.etape == ETAPE_ENVOYE) \
.filter(Action.etape == ETAPE_ENVOYE) \
.filter(~Action.suivi.like('%:Distribué%')) \
.order_by(Action.suivi) \
.all()
for act in acts:
if not act.suivi:
self.error('Pas de suivi: action %s' % act.id)
continue
suivi = act.suivi.split(':', 1)[0]
status = self.import_suivi(suivi)
if status:
act.suivi = '%s:%s' % (suivi, status)
db.session.commit()
self.info('Import suivi terminé')
......@@ -36,6 +36,7 @@ SEXES = {
# L'ordre est utilisé comme clé primaire lors de cet import.
#
ETAPE_DOCUMENT = -30
ETAPE_COM_PUBLIE = -21
ETAPE_COM_A_MODERER = -20
ETAPE_COURRIEL = -10
......@@ -48,6 +49,17 @@ ETAPE_REPONSE_POSITIVE = 40
ETAPE_REPONSE_NEGATIVE = 50
ETAPES = [
{
'ordre': ETAPE_DOCUMENT,
'label': 'Document',
'description': """
Un document nous a été transmis par le parlementaire.
""",
'couleur': '#88dd88',
'icone': 'paperclip',
'hidden': True,
'alerte': False,
},
{
'ordre': ETAPE_COM_PUBLIE,
'label': 'Commentaire',
......@@ -57,6 +69,7 @@ ETAPES = [
""",
'couleur': '#bbbbbb',
'icone': 'commenting',
'hidden': False,
'alerte': False,
},
{
......@@ -69,6 +82,7 @@ ETAPES = [
""",
'couleur': '#bb6666',
'icone': 'commenting',
'hidden': True,
'alerte': False,
},
{
......@@ -80,6 +94,7 @@ ETAPES = [
""",
'couleur': '#66aadd',
'icone': 'at',
'hidden': False,
'alerte': False,
},
{
......@@ -88,6 +103,7 @@ ETAPES = [
'description': '',
'couleur': '',
'icone': '',
'hidden': True,
'alerte': False,
},
{
......@@ -99,6 +115,7 @@ ETAPES = [
""",
'couleur': '#bbbbbb',
'icone': 'envelope-open',
'hidden': False,
'alerte': False,
},
{
......@@ -110,6 +127,7 @@ ETAPES = [
""",
'couleur': '#aaaaff',
'icone': 'clock-o',
'hidden': False,
'alerte': False,
},
{
......@@ -121,6 +139,7 @@ ETAPES = [
""",
'couleur': '#8888dd',
'icone': 'envelope',
'hidden': False,
'alerte': False,
},
{
......@@ -132,6 +151,7 @@ ETAPES = [
""",
'couleur': '#4444bb',
'icone': 'check',
'hidden': False,
'alerte': False,
},
{
......@@ -143,6 +163,7 @@ ETAPES = [
""",
'couleur': '#66bb66',
'icone': 'heart',
'hidden': False,
'alerte': True,
},
{
......@@ -155,6 +176,7 @@ ETAPES = [
""",
'couleur': '#bb6666',
'icone': 'thumbs-down',
'hidden': False,
'alerte': True,
},
]
......
......@@ -5,6 +5,7 @@ from .admin import setup_routes as setup_admin
from .context_processors import setup as setup_cp
from .files import setup_routes as setup_files
from .filters import setup as setup_filters
from .help import setup_routes as setup_help
from .home import setup_routes as setup_home
from .parlementaires import setup_routes as setup_parl
from .session import setup_routes as setup_session
......@@ -14,6 +15,7 @@ def setup_routes(app):
setup_cp(app)
setup_filters(app)
setup_home(app)
setup_help(app)
setup_session(app)
setup_parl(app)
setup_files(app)
......
......@@ -10,7 +10,7 @@ from sqlalchemy.orm import joinedload
from ..models import Action, Parlementaire, User, db
from ..models.constants import (ETAPE_A_CONFIRMER, ETAPE_A_ENVOYER,
ETAPES_BY_ORDRE, ETAPE_COM_A_MODERER,
ETAPE_COM_PUBLIE)
ETAPE_COM_PUBLIE, ETAPE_DOCUMENT)
from ..tools.files import EXTENSIONS, handle_upload
from ..tools.mails import envoyer_alerte
......@@ -20,6 +20,7 @@ from ..tools.text import slugify
def setup_routes(app):
uploads_root = os.path.join(app.config['DATA_DIR'], 'uploads')
@app.route('/admin/recent', endpoint='admin_recent')
@require_admin
......@@ -73,6 +74,11 @@ def setup_routes(app):
if action:
parl_id = action.parlementaire_id
if action.attachment:
path = os.path.join(uploads_root, action.attachment)
if os.path.exists(path):
os.unlink(path)
db.session.delete(action)
db.session.flush()
......@@ -141,17 +147,24 @@ def setup_routes(app):
return redirect_back(error=msg,
fallback=url_for('parlementaire', id=id_parl))
if etape == ETAPE_DOCUMENT:
prefix = 'document'
else:
prefix = 'etape-%s' % etape
try:
filename = handle_upload(
os.path.join(app.config['DATA_DIR'], 'uploads'),
'etape-%s-%s' % (etape, slugify(parl.nom_complet))
uploads_root,
'%s-%s' % (prefix, slugify(parl.nom_complet))
)
except Exception as e:
return redirect_back(error=str(e),
fallback=url_for('parlementaire', id=id_parl))
etape_data = ETAPES_BY_ORDRE[etape]
parl.etape = etape
if etape > 0:
parl.etape = etape
action = Action(
date=datetime.now(),
......
......@@ -8,8 +8,9 @@ from ..models import Action, Parlementaire
from ..models.constants import (CHAMBRES, ETAPES, ETAPES_BY_ORDRE,
ETAPE_AR_RECU, ETAPE_A_CONFIRMER,
ETAPE_A_ENVOYER, ETAPE_COM_A_MODERER,
ETAPE_COM_PUBLIE, ETAPE_ENVOYE, ETAPE_NA,
ETAPE_REPONSE_NEGATIVE, ETAPE_REPONSE_POSITIVE)
ETAPE_COM_PUBLIE, ETAPE_COURRIEL, ETAPE_ENVOYE,
ETAPE_NA, ETAPE_REPONSE_NEGATIVE,
ETAPE_REPONSE_POSITIVE)
def setup(app):
......@@ -68,12 +69,38 @@ def setup(app):
},
]
if session.get('user'):
acts = Action.query \
.filter(Parlementaire.id == Action.parlementaire_id) \
.filter(Action.etape == ETAPE_A_CONFIRMER) \
.filter(Action.user_id == session['user']['id']) \
.exists()
nb = Parlementaire.query.filter(acts) \
.filter(Parlementaire.etape ==
ETAPE_A_CONFIRMER) \
.count()
label = 'Mes actions'
if nb > 0:
title = '%s envoi%s à confirmer' % (nb, 's' if nb > 1 else '')
label += ' <span class="badge" data-toggle="tooltip" ' \
'title="%s">%s</span>' % (title, nb)
menu += [
{
'url': url_for('mes_actions'),
'label': '%s' % label,
'endpoint': 'mes_actions'
}
]
if session.get('user') and session.get('user')['admin']:
menu += [
{
'url': url_for('admin_recent'),
'label': '<span class="admin">Actions récentes</span>',
'endpoint': 'admin_recent"'
'endpoint': 'admin_recent'
}
]
......@@ -86,7 +113,7 @@ def setup(app):
'url': url_for('admin_en_attente'),
'label': '<span class="admin">À confirmer (%s)</span>'
% nb_aconfirmer,
'endpoint': 'admin_en_attente"'
'endpoint': 'admin_en_attente'
}
]
......@@ -112,13 +139,14 @@ def setup(app):
'etapes_by_ordre': ETAPES_BY_ORDRE,
'etapes': ETAPES,
'ordres': {
'ETAPE_COM_PUBLIE': ETAPE_COM_PUBLIE,
'ETAPE_COM_A_MODERER': ETAPE_COM_A_MODERER,
'ETAPE_COURRIEL': ETAPE_COURRIEL,
'ETAPE_NA': ETAPE_NA,
'ETAPE_A_ENVOYER': ETAPE_A_ENVOYER,
'ETAPE_A_CONFIRMER': ETAPE_A_CONFIRMER,
'ETAPE_ENVOYE': ETAPE_ENVOYE,
'ETAPE_AR_RECU': ETAPE_AR_RECU,
'ETAPE_COM_A_MODERER': ETAPE_COM_A_MODERER,
'ETAPE_COM_PUBLIE': ETAPE_COM_PUBLIE,
'ETAPE_REPONSE_POSITIVE': ETAPE_REPONSE_POSITIVE,
'ETAPE_REPONSE_NEGATIVE': ETAPE_REPONSE_NEGATIVE
},
......
......@@ -87,6 +87,25 @@ def setup(app):
return ('<span data-toggle="tooltip"><i class="fa fa-%(icone)s"></i> '
'%(label)s</span>') % etape
@app.template_filter('suivi_laposte')
def suivi_laposte(suivi):
if not suivi:
return ''
parts = suivi.split(':', 1)
link = 'http://www.part.csuivi.courrier.laposte.fr/suivi/index?id=%s' \
% parts[0]
html = 'Suivi&nbsp: <a href="%s" target="_blank">%s</a>' \
% (link, parts[0])
if len(parts) > 1:
html += ' <span class="anon">&ndash; %s</span>' % parts[1]
warning = 'Le suivi peut ne pas fonctionner avant 24 heures.'
html += '<br><small>%s</small>' % warning
return html
_paragraph_re = re.compile(r'(?:\r\n|\r|\n){2,}')
@app.template_filter('nl2br')
......@@ -97,3 +116,13 @@ def setup(app):
if eval_ctx.autoescape:
result = Markup(result)
return result
@app.template_filter('image_tuto')
def image_tuto(filename):
url = url_for('static', filename=filename)
return ('<div class="well tuto-image">'
'<a target="_blank" href="%s">'
'<img src="%s">'
'</a>'
'</div>') % (url, url)
# -*- coding: utf-8 -*-
from flask import render_template
def setup_routes(app):
@app.route('/faq', endpoint='faq')
def faq():
return render_template('markdown.html.j2',
title='Foire aux Questions',
file='text/FAQ.md')
@app.route('/historique', endpoint='historique')
def historique():
return render_template('markdown.html.j2',
title='Qu\'est-ce que l\'IRFM ?',
file='text/historique.md')
@app.route('/derives', endpoint='derives')
def derives():
return render_template('markdown.html.j2',
title='Les dérives de l\'IRFM',
file='text/derives.md')
@app.route('/aide/papier', endpoint='tuto_papier')
def tuto_papier():
return render_template('markdown.html.j2',
title='Envoyer un recommander papier',
file='text/tuto_papier.md')
@app.route('/aide/enligne', endpoint='tuto_enligne')
def tuto_enligne():
return render_template('markdown.html.j2',
title='Envoyer un recommander en ligne',
file='text/tuto_enligne.md')
......@@ -5,8 +5,8 @@ from flask import render_template
from sqlalchemy.sql.expression import case, func
from ..models import Parlementaire, db
from ..models.constants import (ETAPES, ETAPES_BY_ORDRE, ETAPE_A_ENVOYER,
ETAPE_ENVOYE, ETAPE_NA)
from ..models.constants import (ETAPES, ETAPES_BY_ORDRE, ETAPE_A_CONFIRMER,
ETAPE_A_ENVOYER, ETAPE_ENVOYE, ETAPE_NA)
def setup_routes(app):
......@@ -40,10 +40,12 @@ def setup_routes(app):
for e in ETAPES
])
# ...et qui sont dans une étape >= envoyé
# ...et qui sont dans une étape >= pris en charge
dept_qs = dept_qs.add_columns(
func.sum(case([(Parlementaire.etape >= ETAPE_ENVOYE, 1)], else_=0))
.label('nb_envoyes')
func.sum(case([(Parlementaire.etape >= ETAPE_A_CONFIRMER, 1)],
else_=0)).label('nb_prisencharge'),
func.sum(case([(Parlementaire.etape >= ETAPE_ENVOYE, 1)],
else_=0)).label('nb_envoyes')
)
dept_qs = dept_qs.group_by(Parlementaire.num_deptmt) \
......@@ -70,21 +72,3 @@ def setup_routes(app):
},
departements=dept_qs
)
@app.route('/faq', endpoint='faq')
def faq():
return render_template('markdown.html.j2',
title='Foire aux Questions',
file='text/FAQ.md')
@app.route('/historique', endpoint='historique')
def historique():
return render_template('markdown.html.j2',
title='Qu\'est-ce que l\'IRFM ?',
file='text/historique.md')
@app.route('/derives', endpoint='derives')
def derives():
return render_template('markdown.html.j2',
title='Les dérives de l\'IRFM',
file='text/derives.md')
......@@ -8,12 +8,12 @@ from flask import (flash, redirect, render_template, request, session, url_for)
from flask_mail import Mail, Message
from sqlalchemy.orm import joinedload
from sqlalchemy.sql.expression import case, func
from sqlalchemy.sql.expression import func
from ..models import Action, Parlementaire, User, db
from ..models.constants import (ETAPE_A_CONFIRMER, ETAPE_A_ENVOYER,
ETAPE_COM_A_MODERER, ETAPE_COM_PUBLIE,
ETAPE_ENVOYE)
ETAPE_ENVOYE, ETAPE_NA)
from ..tools.files import generer_demande, handle_upload
from ..tools.routing import not_found, redirect_back, remote_addr, require_user
from ..tools.text import check_suivi, slugify
......@@ -66,7 +66,8 @@ def setup_routes(app):
return render_template(
'list.html.j2',
parlementaires=qs
parlementaires=qs,
full_list=True
)
@app.route('/parlementaires/<id>', endpoint='parlementaire')
......
......@@ -4,8 +4,8 @@ from flask import flash, redirect, render_template, request, session, url_for
from sqlalchemy.orm import joinedload
from ..models import Action, User, db
from ..models.constants import ETAPE_ENVOYE
from ..models import Action, Parlementaire, User, db
from ..models.constants import ETAPE_A_CONFIRMER, ETAPE_ENVOYE
from ..tools.routing import not_found, redirect_back, require_user
from ..tools.text import check_email, check_password, sanitize_hard
......@@ -124,3 +124,30 @@ def setup_routes(app):
return redirect_back()
return render_template('profil.html.j2', user=user, envois=envois)
@app.route('/mes-actions', endpoint='mes_actions')
@require_user
def mes_actions():
user = User.query.filter(User.id == session['user']['id']) \
.options(joinedload(User.abonnements)) \
.first()
if not user:
return not_found()
acts = Action.query \
.filter(Parlementaire.id == Action.parlementaire_id) \
.filter(Action.etape.in_([ETAPE_ENVOYE,
ETAPE_A_CONFIRMER])) \
.filter(Action.user_id == session['user']['id']) \
.exists()
qs = Parlementaire.query.filter(acts) \
.options(joinedload(Parlementaire.groupe)) \
.order_by(Parlementaire.nom) \
.all()
return render_template(
'list.html.j2',
parlementaires=qs,
full_list=False
)
......@@ -69,6 +69,15 @@ img.rc-small {
font-weight: bold;
}
.tuto-image {
text-align: center;
margin: 1em;
}
.tuto-image img {
max-width: 100%;
}
.anon {
color: #ccc;
}
......@@ -148,20 +157,24 @@ th.col-nobreak {
margin-bottom: 1em;
}
#carte-legend-title {
#carte-legend-buttons {
flex-basis: 100%;
font-weight: bold;
margin-bottom: 1em;
}
#carte-legend-gradient {
border: 1px solid #aaa;
height: .7em;
flex-basis: 100%;
}
.carte-mode-prisencharge #carte-legend-gradient {
background: linear-gradient(to right, #fff, #4444bb);
}
#carte-legend small {
.carte-mode-envoye #carte-legend-gradient {
background: linear-gradient(to right, #fff, #8844bb);
}
#carte .land {
......
......@@ -145,6 +145,10 @@
Un projet <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>
&mdash;
<a href="mailto:{{ config.ADMIN_EMAIL }}">Nous contacter</a>
</p>
<p>
......
......@@ -42,7 +42,7 @@
</td>
<td>
{% if action.suivi and action.etape == ordres.ETAPE_ENVOYE %}
<a href="http://www.part.csuivi.courrier.laposte.fr/suivi/index?id={{ action.suivi }}" target="_blank">{{ action.suivi }}</a>
{{ action.suivi|suivi_laposte }}
{% elif action.suivi %}
{{ action.suivi|e }}
{% endif %}
......
......@@ -85,9 +85,12 @@
<header class="panel-heading">
<b>Avancement par département</b>
</header>
<article class="panel-body" id="carte-container">
<article class="panel-body carte-mode-prisencharge" id="carte-container">
<div id="carte-legend">
<div id="carte-legend-title"><small>Taux d'envoi des demandes de documents aux parlementaires</small></div>
<div id="carte-legend-buttons">
<button id="btn-mode-prisencharge" class="btn btn-xs btn-primary">Pris en charge</button>
<button id="btn-mode-envoye" class="btn btn-xs btn-default">Courriers envoyés</button>
</div>
<div id="carte-legend-gradient">
</div>
<small>Aucun</small>
......@@ -138,6 +141,20 @@
/* Carte */
var modesCarte = ['prisencharge', 'envoye'];
modesCarte.forEach(function(mode) {
$('#btn-mode-' + mode).click(function(e) {
$('#carte-container .btn').removeClass('btn-primary').addClass('btn-default');
$(this).removeClass('btn-default').addClass('btn-primary');
var ctn = $('#carte-container');
modesCarte.forEach(function(m) { ctn.removeClass('carte-mode-' + m); });
ctn.addClass('carte-mode-' + mode);
e.preventDefault();
});
});
{% for dep in departements %}
$('.departement{{ dep.num_deptmt }}')
.attr('data-content',
......
......@@ -6,19 +6,36 @@
{% block content %}
<section class="panel panel-default">
<header class="panel-heading">
<b>Liste des {{ parlementaires|length }} parlementaires</b>
</header>
<article class="panel-body">
<div class="input-group">
<span class="input-group-addon" id="search-header">Filtrer les parlementaires</span>
<input type="search" autocomplete="off" autofocus="autofocus" class="form-control" id="search" placeholder="Nom, groupe, circonscription..." value="{{ request.args.q|e }}" aria-describedby="search-header">
<span class="input-group-btn">
<button class="btn btn-primary" type="button" id="search-clear" style="display: none;">Effacer le filtre</button>
</span>
</div>
</article>
{% if full_list %}
<header class="panel-heading">
<b>Liste des {{ parlementaires|length }} parlementaires</b>
</header>
<article class="panel-body">
<div class="input-group">