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