Users can see waiting votes, votes can be terminated
[cavote.git] / main.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3
4 from flask import Flask, request, session, g, redirect, url_for, abort, \
5 render_template, flash
6 import sqlite3
7 from datetime import date, time, timedelta
8 import time
9 from contextlib import closing
10 import locale
11 locale.setlocale(locale.LC_ALL, '')
12 import os
13 import hashlib
14 import smtplib
15 import string
16
17 DATABASE = '/tmp/cavote.db'
18 SECRET_KEY = '{J@uRKO,xO-PK7B,jF?>iHbxLasF9s#zjOoy=+:'
19 DEBUG = True
20 TITLE = u"Cavote FFDN"
21 EMAIL = '"' + TITLE + '"' + ' <' + u"cavote@ffdn.org" + '>'
22 BASEURL = "http://localhost:5000"
23 VERSION = "cavote 0.0.1"
24 SMTP_SERVER = "10.33.33.30"
25
26 app = Flask(__name__)
27 app.config.from_object(__name__)
28
29 def connect_db():
30 return sqlite3.connect(app.config['DATABASE'])
31
32 @app.before_request
33 def before_request():
34 g.db = connect_db()
35
36 @app.teardown_request
37 def teardown_request(exception):
38 g.db.close()
39
40 @app.route('/')
41 def home():
42 return render_template('index.html', active_button="home")
43
44 def query_db(query, args=(), one=False):
45 cur = g.db.execute(query, args)
46 rv = [dict((cur.description[idx][0], value)
47 for idx, value in enumerate(row)) for row in cur.fetchall()]
48 return (rv[0] if rv else None) if one else rv
49
50 def init_db():
51 with closing(connect_db()) as db:
52 with app.open_resource('schema.sql') as f:
53 db.cursor().executescript(f.read())
54 db.commit()
55
56 #----------------
57 # Login / Logout
58
59 def valid_login(username, password):
60 return query_db('select * from users where email = ? and password = ?', [username, crypt(password)], one=True)
61
62 def connect_user(user):
63 session['user'] = user
64 del session['user']['password']
65 del session['user']['key']
66
67 def disconnect_user():
68 session.pop('user', None)
69
70 def crypt(passwd):
71 return hashlib.sha1(passwd).hexdigest()
72
73 def keygen():
74 return hashlib.sha1(os.urandom(24)).hexdigest()
75
76 def get_userid():
77 user = session.get('user')
78 if user is None:
79 return -1
80 elif user.get('id') < 0:
81 return -1
82 else:
83 return user.get('id')
84
85 @app.route('/login', methods=['GET', 'POST'])
86 def login():
87 if request.method == 'POST':
88 user = valid_login(request.form['username'], request.form['password'])
89 if user is None:
90 flash('Invalid username/password', 'error')
91 else:
92 connect_user(user)
93 flash('You were logged in', 'success')
94 return redirect(url_for('home'))
95 return render_template('login.html')
96
97 @app.route('/logout')
98 def logout():
99 disconnect_user()
100 flash('You were logged out', 'info')
101 return redirect(url_for('home'))
102
103 #-----------------
104 # Change password
105
106 @app.route('/password/lost', methods=['GET', 'POST'])
107 def password_lost():
108 info = None
109 if request.method == 'POST':
110 user = query_db('select * from users where email = ?', [request.form['email']], one=True)
111 if user is None:
112 flash('Cet utilisateur n\'existe pas !', 'error')
113 else:
114 key = keygen()
115 g.db.execute('update users set key = ? where id = ?', [key, user['id']])
116 g.db.commit()
117 link = BASEURL + url_for('login_key', userid=user['id'], key=key)
118 BODY = string.join((
119 "From: %s" % EMAIL,
120 "To: %s" % user['email'],
121 "Subject: [Cavote] Password lost",
122 "Date: %s" % time.strftime("%a, %d %b %Y %H:%M:%S +0000", time.gmtime()),
123 "X-Mailer: %s" % VERSION,
124 "",
125 "You have lost your password.",
126 "This link will log you without password.",
127 "Don't forget to define a new one as soon a possible!",
128 "This link will only work one time.",
129 "",
130 link,
131 "",
132 "If you think this mail is not for you, please ignore and delete it."
133 ), "\r\n")
134 server = smtplib.SMTP(SMTP_SERVER)
135 server.sendmail(EMAIL, [user['email']], BODY)
136 server.quit()
137 flash(u"Un mail a été envoyé à " + user['email'], 'info')
138 return render_template('password_lost.html')
139
140 @app.route('/login/<userid>/<key>')
141 def login_key(userid, key):
142 user = query_db('select * from users where id = ? and key = ?', [userid, key], one=True)
143 if user is None or user['key'] == "invalid":
144 abort(404)
145 else:
146 connect_user(user)
147 g.db.execute('update users set key = "invalid" where id = ?', [user['id']])
148 g.db.commit()
149 flash(u"Veuillez mettre à jour votre mot de passe", 'info')
150 return redirect(url_for('user_password', userid=user['id']))
151
152 #---------------
153 # User settings
154
155 @app.route('/user/<userid>')
156 def user(userid):
157 if int(userid) != get_userid():
158 abort(401)
159 groups = query_db('select * from groups join user_group on id=id_group where id_user = ?', userid)
160 return render_template('user.html', groups=groups)
161
162 @app.route('/user/settings/<userid>', methods=['GET', 'POST'])
163 def user_edit(userid):
164 if int(userid) != get_userid():
165 abort(401)
166 if request.method == 'POST':
167 if query_db('select * from users where email=? and id!=?', [request.form['email'], userid], one=True) is None:
168 if query_db('select * from users where name=? and id!=?', [request.form['name'], userid], one=True) is None:
169 g.db.execute('update users set email = ?, name = ?, organization = ? where id = ?',
170 [request.form['email'], request.form['name'], request.form['organization'], session['user']['id']])
171 g.db.commit()
172 disconnect_user()
173 user = query_db('select * from users where id=?', [userid], one=True)
174 if user is None:
175 flash(u'Une erreur s\'est produite.', 'error')
176 return redirect(url_for('login'))
177 connect_user(user)
178 flash(u'Votre profil a été mis à jour !', 'success')
179 else:
180 flash(u'Le nom ' + request.form['name'] + u' est déjà pris ! Veuillez en choisir un autre.', 'error')
181 else:
182 flash(u'Il existe déjà un compte pour cette adresse e-mail : ' + request.form['email'], 'error')
183 return render_template('user_edit.html')
184
185 @app.route('/user/password/<userid>', methods=['GET', 'POST'])
186 def user_password(userid):
187 if int(userid) != get_userid():
188 abort(401)
189 if request.method == 'POST':
190 if request.form['password'] == request.form['password2']:
191 g.db.execute('update users set password = ? where id = ?', [crypt(request.form['password']), session['user']['id']])
192 g.db.commit()
193 flash(u'Votre mot de passe a été mis à jour.', 'success')
194 else:
195 flash(u'Les mots de passe sont différents.', 'error')
196 return render_template('user_edit.html')
197
198 #------------
199 # User admin
200
201 @app.route('/admin/users')
202 def admin_users():
203 if not session.get('user').get('is_admin'):
204 abort(401)
205 tuples = query_db('select *, groups.name as groupname from (select *, id as userid, name as username from users join user_group on id=id_user order by id desc) join groups on id_group=groups.id')
206 users = dict()
207 for t in tuples:
208 if t['userid'] in users:
209 users[t['userid']]['groups'].append(t["groupname"])
210 else:
211 users[t['userid']] = dict()
212 users[t['userid']]['userid'] = t['userid']
213 users[t['userid']]['email'] = t['email']
214 users[t['userid']]['username'] = t['username']
215 users[t['userid']]['is_admin'] = t['is_admin']
216 users[t['userid']]['groups'] = [t['groupname']]
217
218 return render_template('admin_users.html', users=users.values())
219
220 @app.route('/admin/users/add', methods=['GET', 'POST'])
221 def admin_user_add():
222 if not session.get('user').get('is_admin'):
223 abort(401)
224 if request.method == 'POST':
225 if request.form['email']:
226 # :TODO:maethor:120528: Check fields
227 password = "toto" # :TODO:maethor:120528: Generate password
228 admin = 0
229 if 'admin' in request.form.keys():
230 admin = 1
231 g.db.execute('insert into users (email, name, organization, password, is_admin, key) values (?, ?, ?, ?, ?, "invalid")',
232 [request.form['email'], request.form['username'], request.form['organization'], password, admin])
233 g.db.commit()
234 user = query_db('select * from users where email = ?', [request.form["email"]], one=True)
235 if user:
236 for group in request.form.getlist('groups'):
237 if query_db('select id from groups where id = ?', group, one=True) is None:
238 abort(401)
239 g.db.execute('insert into user_group values (?, ?)', [user['id'], group])
240 g.db.commit()
241 # :TODO:maethor:120528: Send mail
242 flash(u'Le nouvel utilisateur a été créé avec succès', 'success')
243 return redirect(url_for('admin_users'))
244 else:
245 flash(u'Une erreur s\'est produite.', 'error')
246 else:
247 flash(u"Vous devez spécifier une adresse email.", 'error')
248 groups = query_db('select * from groups where system=0')
249 return render_template('admin_user_new.html', groups=groups)
250
251 #-------------
252 # Roles admin
253
254 @app.route('/admin/groups')
255 def admin_groups():
256 if not session.get('user').get('is_admin'):
257 abort(401)
258 groups = query_db('select * from groups')
259 return render_template('admin_groups.html', groups=groups)
260
261 @app.route('/admin/groups/add', methods=['POST'])
262 def admin_group_add():
263 if not session.get('user').get('is_admin'):
264 abort(401)
265 if request.method == 'POST':
266 if request.form['name']:
267 g.db.execute('insert into groups (name) values (?)', [request.form['name']])
268 g.db.commit()
269 else:
270 flash(u"Vous devez spécifier un nom.", "error")
271 return redirect(url_for('admin_groups'))
272
273 @app.route('/admin/groups/delete/<idgroup>')
274 def admin_group_del(idgroup):
275 if not session.get('user').get('is_admin'):
276 abort(401)
277 group = query_db('select * from groups where id = ?', [idgroup], one=True)
278 if group is None:
279 abort(404)
280 if group['system']:
281 abort(401)
282 g.db.execute('delete from groups where id = ?', [idgroup])
283 g.db.commit()
284 return redirect(url_for('admin_groups'))
285
286 #------------
287 # Votes list
288
289 @app.route('/votes/<votes>')
290 def votes(votes):
291 today = date.today()
292 active_button = votes
293 max_votes ='select id_group, count(*) as max_votes from user_group group by id_group'
294 basequery = 'select votes.*, max_votes from votes join (' + max_votes + ') as max_votes on votes.id_group = max_votes.id_group'
295 nb_votes = 'select id_vote, count(*) as nb_votes from (select id_user, id_vote from user_choice join choices on id_choice = choices.id group by id_user, id_vote) group by id_vote'
296 basequery = 'select * from (' + basequery + ') left join (' + nb_votes + ') on id = id_vote'
297 basequery = 'select *, votes.id as voteid, groups.name as groupname from (' + basequery + ') as votes join groups on groups.id = id_group where is_open=1'
298 if votes == 'all':
299 votes = query_db(basequery + ' order by id desc')
300 elif votes == 'archive':
301 votes = query_db(basequery + ' and is_terminated=1 order by id desc')
302 elif votes == 'current':
303 votes = query_db(basequery + ' and is_terminated=0 order by id desc')
304 elif votes == 'waiting':
305 basequery = 'select votes.* from user_group join (' + basequery + ') as votes on votes.id_group = user_group.id_group where user_group.id_user = ?'
306 already_voted = 'select id_vote from user_choice join choices on user_choice.id_choice = choices.id where id_user = ?'
307 votes = query_db(basequery + ' and votes.id not in (' + already_voted + ') and is_terminated=0', [get_userid(), get_userid()])
308 else:
309 abort(404)
310 for vote in votes:
311 if not vote.get('nb_votes'):
312 vote['nb_votes'] = 0
313 vote['percent'] = int((float(vote['nb_votes']) / float(vote['max_votes'])) * 100)
314 return render_template('votes.html', votes=votes, active_button=active_button)
315
316 #------
317 # Vote
318
319 def can_see_vote(idvote, iduser=-1):
320 vote = query_db('select * from votes where id=?', [idvote], one=True)
321 if vote is None:
322 return False
323 if not vote['is_public']:
324 user = query_db('select * from users where id=?', [iduser], one=True)
325 if query_db('select * from user_group where id_user = ? and id_group = ?', [iduser, vote['id']], one=True) is None:
326 return False
327 return True
328
329 def can_vote(idvote, iduser=-1):
330 vote = query_db('select * from votes where id=?', [idvote], one=True)
331 if vote is None:
332 return False
333 if not vote['is_finished']:
334 if iduser > 0:
335 if can_see_vote(idvote, iduser):
336 if not has_voted(idvote, iduser):
337 if query_db('select * from user_group where id_user = ? and id_group = ?', [iduser, vote['id']], one=True):
338 return True
339 return False
340
341 def has_voted(idvote, iduser=-1):
342 vote = query_db('select * from user_choice join choices on id_choice=choices.id where id_vote = ? and id_user = ?', [idvote, iduser], one=True)
343 return (vote is not None)
344
345 @app.route('/vote/<idvote>', methods=['GET', 'POST'])
346 def vote(idvote):
347 vote = query_db('select votes.*, groups.name as groupname from votes join groups on groups.id=votes.id_group where votes.id=?', [idvote], one=True)
348 if vote is None:
349 abort(404)
350 if can_see_vote(idvote, get_userid()):
351 if request.method == 'POST':
352 if can_vote(idvote, get_userid()):
353 choices = query_db('select name, id from choices where id_vote=?', [idvote])
354 for choice in choices:
355 if str(choice['id']) in request.form.keys():
356 g.db.execute('insert into user_choice (id_user, id_choice) values (?, ?)',
357 [session.get('user').get('id'), choice['id']])
358 g.db.commit()
359 if vote['is_multiplechoice'] == 0:
360 break
361 else:
362 abort(401)
363 tuples = query_db('select choiceid, choicename, users.id as userid, users.name as username from (select choices.id as choiceid, choices.name as choicename, id_user as userid from choices join user_choice on choices.id = user_choice.id_choice where id_vote = ?) join users on userid = users.id', [idvote])
364 users = dict()
365 for t in tuples:
366 if t['userid'] in users:
367 users[t['userid']]['choices'].append(t['choiceid'])
368 else:
369 users[t['userid']] = dict()
370 users[t['userid']]['userid'] = t['userid']
371 users[t['userid']]['username'] = t['username']
372 users[t['userid']]['choices'] = [t['choiceid']]
373 choices = query_db('select choices.name, choices.id, choices.name, choices.id_vote, count(id_choice) as nb from choices left join user_choice on id_choice = choices.id where id_vote = ? group by id_choice, name, id_vote order by id', [idvote])
374 attachments = query_db('select * from attachments where id_vote=?', [idvote])
375 tmp = query_db('select id_group, count(*) as nb from user_group where id_group = ? group by id_group', [vote['id_group']], one=True)
376 if tmp is None:
377 vote['percent'] = 0
378 else:
379 vote['max_votes'] = tmp['nb']
380 tmp = query_db('select id_vote, count(*) as nb from (select id_user, id_vote from user_choice join choices on id_choice = choices.id group by id_user, id_vote) where id_vote = ? group by id_vote', [idvote], one=True)
381 if tmp is None:
382 vote['percent'] = 0
383 else:
384 vote['nb_votes'] = tmp['nb']
385 vote['percent'] = int((float(vote['nb_votes']) / float(vote['max_votes'])) * 100)
386 return render_template('vote.html', vote=vote, attachments=attachments, choices=choices, users=users.values(), can_vote=can_vote(idvote, get_userid()))
387 flash(u'Vous n\'avez pas le droit de voir ce vote, désolé.')
388 return redirect(url_for('home'))
389
390 @app.route('/vote/deletechoices/<idvote>/<iduser>')
391 def vote_deletechoices(idvote, iduser):
392 if int(iduser) != get_userid():
393 abort(401)
394 g.db.execute('delete from user_choice where id_user = ? and id_choice in (select id from choices where id_vote = ?)',
395 [iduser, idvote])
396 g.db.commit()
397 return redirect(url_for('vote', idvote=idvote))
398
399 #-------------
400 # Votes admin
401
402 @app.route('/admin/votes/list')
403 def admin_votes():
404 if not session.get('user').get('is_admin'):
405 abort(401)
406 votes = query_db('select *, votes.id as voteid, groups.name as groupname from votes join groups on groups.id=votes.id_group order by id desc')
407 return render_template('admin_votes.html', votes=votes)
408
409 @app.route('/admin/votes/add', methods=['GET', 'POST'])
410 def admin_vote_add():
411 if not session.get('user').get('is_admin'):
412 abort(401)
413 if request.method == 'POST':
414 if request.form['title']:
415 if query_db('select * from votes where title = ?', [request.form['title']], one=True) is None:
416 date_begin = date.today()
417 date_end = date.today() + timedelta(days=int(request.form['days']))
418 transparent = 0
419 public = 0
420 multiplechoice = 0
421 if 'transparent' in request.form.keys():
422 transparent = 1
423 if 'public' in request.form.keys():
424 public = 1
425 if 'multiplechoice' in request.form.keys():
426 multiplechoice = 1
427 group = query_db('select id from groups where name = ?', [request.form['group']], one=True)
428 if group is None:
429 group[id] = 1
430 g.db.execute('insert into votes (title, description, category, date_begin, date_end, is_transparent, is_public, is_multiplechoice, id_group, id_author) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
431 [request.form['title'], request.form['description'], request.form['category'], date_begin, date_end, transparent, public, multiplechoice, group['id'], session['user']['id']])
432 g.db.commit()
433 vote = query_db('select * from votes where title = ? and date_begin = ? order by id desc',
434 [request.form['title'], date_begin], one=True)
435 if vote is None:
436 flash(u'Une erreur est survenue !', 'error')
437 return redirect(url_for('home'))
438 else:
439 flash(u"Le vote a été créé", 'info')
440 return redirect(url_for('admin_vote_edit', voteid=vote['id']))
441 else:
442 flash(u'Le titre que vous avez choisi est déjà pris.', 'error')
443 else:
444 flash(u'Vous devez spécifier un titre.', 'error')
445 groups = query_db('select * from groups')
446 return render_template('admin_vote_new.html', groups=groups)
447
448 @app.route('/admin/votes/edit/<voteid>', methods=['GET', 'POST'])
449 def admin_vote_edit(voteid):
450 if not session.get('user').get('is_admin'):
451 abort(401)
452 vote = query_db('select * from votes where id = ?', [voteid], one=True)
453 if vote is None:
454 abort(404)
455 if request.method == 'POST':
456 if request.form['title']:
457 # :TODO:maethor:120529: Calculer date_begin pour pouvoir y ajouter duration et obtenir date_end
458 transparent = 0
459 public = 0
460 if 'transparent' in request.form.keys():
461 transparent = 1
462 if 'public' in request.form.keys():
463 public = 1
464 isopen = 0
465 isterminated = 0
466 if request.form['status'] == 'Ouvert':
467 choices = query_db('select id_vote, count(*) as nb from choices where id_vote = ? group by id_vote', [voteid], one=True)
468 if choices is not None and choices['nb'] >= 2:
469 isopen = 1
470 else:
471 flash(u'Vous devez proposer au moins deux choix pour ouvrir le vote.', 'error')
472 elif request.form['status'] == u'Terminé':
473 isterminated = 1
474 if vote['is_open']:
475 isopen = 1
476 g.db.execute('update votes set title = ?, description = ?, category = ?, is_transparent = ?, is_public = ?, is_open = ?, is_terminated = ? where id = ?',
477 [request.form['title'], request.form['description'], request.form['category'], transparent, public, isopen, isterminated, voteid])
478 g.db.commit()
479 vote = query_db('select * from votes where id = ?', [voteid], one=True)
480 flash(u"Le vote a bien été mis à jour.", "success")
481 else:
482 flash(u'Vous devez spécifier un titre.', 'error')
483
484 # :TODO:maethor:120529: Calculer la durée du vote (différence date_end - date_begin)
485 vote['duration'] = 15
486 group = query_db('select name from groups where id = ?', [vote['id_group']], one=True)
487 choices = query_db('select * from choices where id_vote = ?', [voteid])
488 attachments = query_db('select * from attachments where id_vote = ?', [voteid])
489 return render_template('admin_vote_edit.html', vote=vote, group=group, choices=choices, attachments=attachments)
490
491 @app.route('/admin/votes/addchoice/<voteid>', methods=['POST'])
492 def admin_vote_addchoice(voteid):
493 if not session.get('user').get('is_admin'):
494 abort(401)
495 vote = query_db('select * from votes where id = ?', [voteid], one=True)
496 if vote is None:
497 abort(404)
498 g.db.execute('insert into choices (name, id_vote) values (?, ?)', [request.form['title'], voteid])
499 g.db.commit()
500 return redirect(url_for('admin_vote_edit', voteid=voteid))
501
502 @app.route('/admin/votes/editchoice/<voteid>/<choiceid>', methods=['POST', 'DELETE'])
503 def admin_vote_editchoice(voteid, choiceid):
504 if not session.get('user').get('is_admin'):
505 abort(401)
506 choice = query_db('select * from choices where id = ? and id_vote = ?', [choiceid, voteid], one=True)
507 if choice is None:
508 abort(404)
509 if request.method == 'POST':
510 g.db.execute('update choices set name=? where id = ? and id_vote = ?', [request.form['title'], choiceid, voteid])
511 g.db.commit()
512 return redirect(url_for('admin_vote_edit', voteid=voteid))
513
514 @app.route('/admin/votes/deletechoice/<voteid>/<choiceid>')
515 def admin_vote_deletechoice(voteid, choiceid):
516 if not session.get('user').get('is_admin'):
517 abort(401)
518 choice = query_db('select * from choices where id = ? and id_vote = ?', [choiceid, voteid], one=True)
519 if choice is None:
520 abort(404)
521 g.db.execute('delete from choices where id = ? and id_vote = ?', [choiceid, voteid])
522 g.db.commit()
523 choices = query_db('select id_vote, count(*) as nb from choices where id_vote = ? group by id_vote', [voteid], one=True)
524 if choices is None or choices['nb'] < 2:
525 g.db.execute('update votes set is_open=0 where id = ?', [voteid])
526 g.db.commit()
527 flash(u'Attention ! Il y a moins de deux choix. Le vote a été fermé.', 'error')
528 return redirect(url_for('admin_vote_edit', voteid=voteid))
529
530 @app.route('/admin/votes/addattachment/<voteid>', methods=['POST'])
531 def admin_vote_addattachment(voteid):
532 if not session.get('user').get('is_admin'):
533 abort(401)
534 vote = query_db('select * from votes where id = ?', [voteid], one=True)
535 if vote is None:
536 abort(404)
537 g.db.execute('insert into attachments (url, id_vote) values (?, ?)', [request.form['url'], voteid])
538 g.db.commit()
539 return redirect(url_for('admin_vote_edit', voteid=voteid))
540
541 @app.route('/admin/votes/deleteattachment/<voteid>/<attachmentid>')
542 def admin_vote_deleteattachment(voteid, attachmentid):
543 if not session.get('user').get('is_admin'):
544 abort(401)
545 attachment = query_db('select * from attachments where id = ? and id_vote = ?', [attachmentid, voteid], one=True)
546 if attachment is None:
547 abort(404)
548 g.db.execute('delete from attachments where id = ? and id_vote = ?', [attachmentid, voteid])
549 g.db.commit()
550 return redirect(url_for('admin_vote_edit', voteid=voteid))
551
552 #------
553 # Main
554
555 if __name__ == '__main__':
556 app.run()
557