-
Notifications
You must be signed in to change notification settings - Fork 8
/
leaderBoardApp.py
483 lines (411 loc) · 18.1 KB
/
leaderBoardApp.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
# -*- coding: utf-8 -*-
"""
DataScienceLeaderboardApp
~~~~~~~~
A Leaderboard App for Data Science/Modeling competitions.
:copyright: (c) 2016 by Josiah Olson ([email protected]).
:license: MIT, see LICENSE for more details.
"""
import os
import time
from contest.helperfxns import loadAndScore
from sqlite3 import dbapi2 as sqlite3
from hashlib import md5
from datetime import datetime
from flask import Flask, Markup, request, session, url_for, redirect, \
render_template, abort, g, flash, send_from_directory, _app_ctx_stack
from markdown import markdown
import werkzeug
from werkzeug.security import check_password_hash, generate_password_hash
# configuration
DATABASE = 'dsLeaderboard.db'
DEBUG = True
SECRET_KEY = 'superSecretKeyGoesHere'
# contest specific variables
globalTitle = 'Modeling Contest'
usedPages = ['description', 'evaluation', 'rules', 'data', 'discussion']
# discussion navbar link will link to this forum-wiki-like resource
externalDiscussionLink = 'https://www.reddit.com/r/MachineLearning/'
# consider changing this, uploads can take a lot of drive space
UPLOAD_FOLDER = 'contest/submissions/'
ALLOWED_EXTENSIONS = ['csv', 'txt', 'zip', 'gz']
# order the score function by asc or desc
orderBy = 'asc'
# set the max number of submissions a user is able to submit for final contest
# scoring against the private leaderboard, ie best of # selected submissions are considered
subNbr = 1
# max number of submissions a user is allowed to make in a rolling 24hr period
dailyLimit = 2
# set the contest deadline where users can no longer upload and private score is published
contestDeadline = time.mktime(datetime(2016, 10, 21, 0, 0).timetuple())
# debug variable that if True allows private leaderboard to be displayed before contest deadline
# normally should be False
showPrivate = False
# create app
app = Flask(__name__)
app.config.from_object(__name__)
app.config.from_envvar('LEADERBOARDAPP_SETTINGS', silent=True)
def get_db():
"""Opens a new database connection if there is none yet for the
current application context.
"""
top = _app_ctx_stack.top
if not hasattr(top, 'sqlite_db'):
top.sqlite_db = sqlite3.connect(app.config['DATABASE'])
top.sqlite_db.row_factory = sqlite3.Row
return top.sqlite_db
@app.teardown_appcontext
def close_database(exception):
"""Closes the database again at the end of the request."""
top = _app_ctx_stack.top
if hasattr(top, 'sqlite_db'):
top.sqlite_db.close()
def init_db():
"""Creates the database tables."""
with app.app_context():
db = get_db()
with app.open_resource('schema.sql', mode='r') as f:
db.cursor().executescript(f.read())
db.commit()
def query_db(query, args=(), one=False):
"""Queries the database and returns a list of dictionaries."""
cur = get_db().execute(query, args)
rv = cur.fetchall()
return (rv[0] if rv else None) if one else rv
def get_user_id(username):
"""Convenience method to look up the id for a username."""
rv = query_db('select user_id from user where username = ?',
[username], one=True)
return rv[0] if rv else None
def format_datetime(timestamp):
"""Format a timestamp for display."""
return datetime.utcfromtimestamp(timestamp).strftime('%Y-%m-%d @ %H:%M')
def allowed_file(filename):
# checks if extension in filename is allowed
return '.' in filename and \
filename.rsplit('.', 1)[1] in ALLOWED_EXTENSIONS
def contestEndBool():
#return boolean if contest is over to change 'post' methods behavior
return (contestDeadline - time.time()) < 0 or showPrivate
@app.before_request
def before_request():
g.usedPages = usedPages
g.globalTitle = globalTitle
g.user = None
if 'user_id' in session:
g.user = query_db('select * from user where user_id = ?',
[session['user_id']], one=True)
@app.route('/')
def defaultlanding():
"""Shows a users leaderboard for modeling contest.
If not logged in then forward user to contest description.
"""
#send user to description page if not logged in
if not g.user:
return redirect(url_for('description'))
#display leaderboard for competition if logged in
return redirect(url_for('leaderboard'))
@app.route('/leaderboard')
def leaderboard():
#query the db and render the table used to display the leaderboard to users
if contestEndBool():
#when evaluating for private score we take user selection table
#or most recent submission if user didn't specify what submission was final
board = query_db('''
select username, max(public_score) public_score,
max(private_score) private_score, max(sub_cnt) sub_cnt
from (
select username, public_score, private_score, sub_cnt
from submission sub
left join (
select user_id, max(submit_date) max_submit_date
from submission
where user_id not in (select distinct user_id from selection)
group by user_id
) max_sub
on sub.user_id = max_sub.user_id
inner join (
select user_id, count(*) sub_cnt
from submission
group by user_id
) cnt
on sub.user_id = cnt.user_id
inner join user
on sub.user_id = user.user_id
left join selection
on sub.submission_id = selection.submission_id
where
case when select_date is not null then 1 else
case when max_submit_date is not null then 1 else 0 end
end = 1
) temp
group by username
order by private_score %s''' % orderBy)
else:
#display the public leader board when contest hasn't ended yet
board = query_db('''
select username, public_score, '?' private_score, sub_cnt
from submission sub
inner join (
select user_id, max(submit_date) max_submit_date, count(*) sub_cnt
from submission
group by user_id
) max_sub
on sub.user_id = max_sub.user_id and
sub.submit_date = max_sub.max_submit_date
inner join user
on sub.user_id = user.user_id
order by public_score %s''' % orderBy)
#Debug: board = [{'public_score': 0.3276235370053617, 'username': 'test3', 'private_score': 0.32036252335937015}, {'public_score': 0.3276235370053617, 'username': 'test1', 'private_score': 0.32036252335937015}, {'public_score': 0.33944709256230005, 'username': 'test2', 'private_score': 0.32003513414185064}]
board = [dict(row) for row in board]
for rank, row in enumerate(board):
row['rank'] = rank + 1
colNames = ['Rank', 'Participant', 'Public Score', 'Private Score', 'Submission Count']
deadlineStr = str(datetime.fromtimestamp(contestDeadline))
hoursLeft = abs(round((contestDeadline - time.time()) / 3600, 2))
return render_template('leaderboard.html',
title='Leaderboard',
colNames=colNames,
leaderboard=board,
deadlineStr=deadlineStr,
hoursLeft=hoursLeft)
@app.route('/description')
def description():
"""Displays a markdown doc describing the predictive modeling contest.
Note ./content/contest/<url calling path>.md must be modified for contest.
"""
#rule = request.url_rule
#print(rule)
file = open('./contest/content/description.md', 'r')
rawText = file.read()
file.close()
content = Markup(markdown(rawText,
extensions=['markdown.extensions.fenced_code', 'markdown.extensions.tables']))
return render_template('markdowntemplate.html',
title='Description',
content=content)
@app.route('/evaluation')
def evaluation():
"""Displays a markdown doc describing the predictive modeling contest.
Note ./content/contest/<url calling path>.md must be modified for contest.
"""
file = open('./contest/content/evaluation.md', 'r')
rawText = file.read()
file.close()
content = Markup(markdown(rawText,
extensions=['markdown.extensions.fenced_code', 'markdown.extensions.tables']))
return render_template('markdowntemplate.html',
title='Evaluation',
content=content)
@app.route('/rules')
def rules():
"""Displays a markdown doc describing the predictive modeling contest.
Note ./content/contest/<url calling path>.md must be modified for contest.
"""
file = open('./contest/content/rules.md', 'r')
rawText = file.read()
file.close()
content = Markup(markdown(rawText,
extensions=['markdown.extensions.fenced_code', 'markdown.extensions.tables']))
return render_template('markdowntemplate.html',
title='Rules',
content=content)
@app.route('/contest/download/<path:path>')
def send_dir(path):
# this function is used to serve the train/test files linked to in data.md
return send_from_directory('contest/download', path)
@app.route('/data')
def data():
"""Displays a markdown doc describing the predictive modeling contest.
Note ./content/contest/<url calling path>.md must be modified for contest.
"""
file = open('./contest/content/data.md', 'r')
rawText = file.read()
file.close()
content = Markup(markdown(rawText,
extensions=['markdown.extensions.fenced_code', 'markdown.extensions.tables']))
return render_template('markdowntemplate.html',
title='Data',
content=content)
# in dev version of the app the prizes page isn't used due to overlap with description/rules
@app.route('/prizes')
def prizes():
"""Displays a markdown doc describing the predictive modeling contest.
Note ./content/contest/<url calling path>.md must be modified for contest.
"""
file = open('./contest/content/prizes.md', 'r')
rawText = file.read()
file.close()
content = Markup(markdown(rawText,
extensions=['markdown.extensions.fenced_code', 'markdown.extensions.tables']))
return render_template('markdowntemplate.html',
title='Prizes',
content=content)
@app.route('/discussion')
def discussion():
return redirect(externalDiscussionLink)
@app.route('/selectmodel', methods=['POST'])
def select_model():
"""Allow user to select the upload they'd like to use for submission
Default selection should be most recent submissions
"""
try:
#check if contest has ended
if contestEndBool():
flash("Error: contest has ended")
raise Exception("contest has ended")
input = request.form
print(str(input))
for count, x in enumerate(input): print(count, x)
if len(input) != subNbr:
flash("Error: Wrong number of submissions selected")
else:
db = get_db()
db.execute("delete from selection where user_id = '%s'" % session['user_id'])
db.commit()
#upload user defined selections to database
for count, submission_id in enumerate(input):
db = get_db()
db.execute('''insert into selection (user_id, select_nbr, submission_id,
select_date) values (?, ?, ?, ?)''',
(session['user_id'], count + 1, int(submission_id), int(time.time())))
db.commit()
flash("Selection successful!")
except:
flash("Error: Your selection was not recorded")
return redirect('/uploadsubmission')
@app.route('/uploadsubmission', methods=['GET', 'POST'])
def upload_file():
"""Allow users to upload submissions to modeling contest
Users must be logged in."""
#query the db and render the table used to display the leaderboard to users
userBoard = query_db('''
select submission_id, submit_date, public_score
from submission sub
where user_id = '%s'
order by public_score %s''' % (session['user_id'], orderBy))
userBoard = [dict(row) for row in userBoard]
for row in userBoard:
row['score'] = row['public_score']
row['str_time'] = str(datetime.fromtimestamp(row['submit_date']))
colNames = ['Submission Time', 'Public Score']
if request.method == 'POST':
try:
#check if contest has ended
if contestEndBool():
flash("Error: contest has ended")
raise Exception("contest has ended")
print("here")
#ensure user hasn't exceeded daily submission limit
dailyCnt = query_db('''select count(*) sub_cnt
from submission sub
where submit_date > %s
and user_id = %s
group by user_id''' % (time.time() - 60*60*24, session['user_id']))
if len(dailyCnt) == 0:
dailyCnt = 0
else:
dailyCnt = int(dict(dailyCnt[0])['sub_cnt'])
if dailyCnt > dailyLimit:
flash("Error: exceeded daily upload limit")
raise Exception('Upload limit exceeded')
file = request.files['file']
#throw error if extension is not allowed
if not allowed_file(file.filename):
raise Exception('Invalid file extension')
if file and allowed_file(file.filename):
filename = werkzeug.secure_filename(file.filename)
#append userid and date to file to avoid duplicates
filename = str(session['user_id']) + '_' + \
str(int(time.time())) + '_' + filename
fullPath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
file.save(fullPath)
model_score = loadAndScore(fullPath)
#cache the filename and submission to database
db = get_db()
db.execute('''insert into submission (user_id, filename, submit_date,
public_score, private_score, total_score)
values (?, ?, ?, ?, ?, ?)''',
(session['user_id'], filename, int(time.time()), *model_score))
db.commit()
#inform user upload was a success
flash('Your submission was recorded.')
return redirect(url_for('leaderboard'))
except:
#if exception is thrown in process then flash user
flash('File did not upload or score! Make sure the submission format is correct.')
return render_template('uploadsubmission.html',
title="Upload Submission",
userBoard=userBoard,
subNbr=subNbr)
@app.route('/public')
def public_timeline():
"""Displays the latest messages of all users."""
return render_template('timeline.html', messages=query_db('''
select message.*, user.* from message, user
where message.author_id = user.user_id
order by message.pub_date desc limit ?''', [PER_PAGE]))
@app.route('/login', methods=['GET', 'POST'])
def login():
"""Logs the user in."""
if g.user:
return redirect(url_for('leaderboard'))
error = None
if request.method == 'POST':
user = query_db('''select * from user where
username = ?''', [request.form['username']], one=True)
if user is None:
error = 'Invalid username'
elif not check_password_hash(user['pw_hash'],
request.form['password']):
error = 'Invalid password'
else:
flash('You were logged in')
session['user_id'] = user['user_id']
return redirect(url_for('leaderboard'))
return render_template('login.html', error=error)
@app.route('/register', methods=['GET', 'POST'])
def register():
"""Registers the user."""
if g.user:
return redirect(url_for('timeline'))
error = None
if request.method == 'POST':
if not request.form['username']:
error = 'You have to enter a username'
elif not request.form['email'] or \
'@' not in request.form['email']:
error = 'You have to enter a valid email address'
elif not request.form['password']:
error = 'You have to enter a password'
elif request.form['password'] != request.form['password2']:
error = 'The two passwords do not match'
elif get_user_id(request.form['username']) is not None:
error = 'The username is already taken'
else:
db = get_db()
db.execute('''insert into user (
username, email, pw_hash) values (?, ?, ?)''',
[request.form['username'], request.form['email'],
generate_password_hash(request.form['password'])])
db.commit()
flash('You were successfully registered and can login now')
return redirect(url_for('login'))
return render_template('register.html', error=error)
@app.route('/logout')
def logout():
"""Logs the user out."""
flash('You were logged out')
session.pop('user_id', None)
return redirect(url_for('leaderboard'))
@app.errorhandler(404)
def page_not_found(e):
return render_template('404.html'), 404
#launch application for dev purposes when leaderBoardApp.py script is run
if __name__ == '__main__':
#only re-run init_db() on initial launch if you want to truncate you're sql tables
if not os.path.isfile('dsLeaderboard.db'):
init_db()
if not os.path.exists(UPLOAD_FOLDER):
os.mkdir(UPLOAD_FOLDER)
app.run()