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