import os import io from datetime import datetime, timedelta import uuid from flask import flash, render_template, request, redirect, url_for, jsonify, send_from_directory, abort from flask_admin.contrib.sqla import ModelView from flask_admin.form.upload import FileUploadField from flask_admin import AdminIndexView import flask_security as security from flask_security.utils import encrypt_password import flask_login as login from flask_login import login_required from wtforms.fields import PasswordField from sqlalchemy import Date, cast import numpy as np from sklearn.metrics import roc_auc_score from scipy.stats import hmean from submission import app, security, db from submission.models import User, Competition, Submission @app.route('/') def home(): return render_template('index.html') @app.route('/submission', methods=['GET', 'POST']) @login_required def upload_file(): try: now = datetime.now() competitions = [c for c in Competition.query.all() if (not c.start_on or c.start_on <= now) and (not c.end_on or c.end_on >= now)] if request.method == 'POST': competition_id = request.form.get('competitions') if competition_id == None: flash('No competition selected') return redirect(request.url) user_id = login.current_user.id # check if the post request has the file part if 'file' not in request.files: flash('No file part') return redirect(request.url) file = request.files['file'] # if user does not select file, browser also # submit a empty part without filename if file.filename == '': flash('No selected file') return redirect(request.url) # check if the user has made submissions in the past 24h if Submission.query.filter_by(user_id=user_id).filter_by(competition_id=competition_id).filter(Submission.submitted_on>now-timedelta(hours=23)).count() > 0: flash("You already did a submission in the past 24h.") return redirect(request.url) if file: filename = str(uuid.uuid4()) + ".csv" filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename) file.save(filepath) # save submission submission = Submission() submission.user_id = login.current_user.id submission.competition_id = competition_id submission.filename = filename (submission.preview_score, submission.score) = get_scores(filepath, competition_id) submission.submitted_on = now.replace(microsecond=0) submission.comment = request.form.get("comment") db.session.add(submission) db.session.commit() return redirect(url_for('scores')) return render_template('submission.html', competitions=competitions) except ParsingError as e: flash(str(e)) return redirect(request.url) #@login_required def get_scores(filename, competition_id): "Returns (preview_score, score)" # parse files predictions = np.fromregex(filename, r'(.+),(\d+\.\d+|\d+)', [('id', 'U128'), ('v0', np.float32)]) groundtruth_filename = os.path.join(app.config['GROUNDTRUTH_FOLDER'], Competition.query.get(competition_id).groundtruth) groundtruth = np.fromregex(groundtruth_filename, r'(.+),(.+),(\d+\.\d+|\d+)', [('id', 'U128'),('datasetid', 'U128'),('v0', np.float32)]) # sort data predictions.sort(order='id') groundtruth.sort(order='id') if predictions['id'].size == 0 or not np.array_equal(predictions['id'], groundtruth['id']): raise ParsingError("Error parsing the submission file. Make sure it has the right format and contains the right ids.") # split according to datasetid predictions_poland = [] groundtruth_poland = [] predictions_warblr = [] groundtruth_warblr = [] predictions_chern = [] groundtruth_chern = [] for i, _ in enumerate(groundtruth): if groundtruth['datasetid'][i] == 'PolandNFC': predictions_poland.append(predictions['v0'][i]) groundtruth_poland.append(groundtruth['v0'][i]) elif groundtruth['datasetid'][i] == 'warblrb10k': predictions_warblr.append(predictions['v0'][i]) groundtruth_warblr.append(groundtruth['v0'][i]) else: predictions_chern.append(predictions['v0'][i]) groundtruth_chern.append(groundtruth['v0'][i]) # take 12% of Chern+warblr for preview, and the remaining 88% of Chern+warblr plus 100% of Poland for final. preview_ratio = 0.12 chern_split_point = int(preview_ratio * len(predictions_chern)) predictions_chern_p = predictions_chern[:chern_split_point] groundtruth_chern_p = groundtruth_chern[:chern_split_point] predictions_chern_f = predictions_chern[chern_split_point:] groundtruth_chern_f = groundtruth_chern[chern_split_point:] warblr_split_point = int(preview_ratio * len(predictions_warblr)) predictions_warblr_p = predictions_warblr[:warblr_split_point] groundtruth_warblr_p = groundtruth_warblr[:warblr_split_point] predictions_warblr_f = predictions_warblr[warblr_split_point:] groundtruth_warblr_f = groundtruth_warblr[warblr_split_point:] # compute scores for all datasets score_poland = roc_auc_score(groundtruth_poland, predictions_poland) score_warblr_p = roc_auc_score(groundtruth_warblr_p, predictions_warblr_p) score_warblr_f = roc_auc_score(groundtruth_warblr_f, predictions_warblr_f) score_chern_p = roc_auc_score(groundtruth_chern_p, predictions_chern_p) score_chern_f = roc_auc_score(groundtruth_chern_f, predictions_chern_f) # compute preview / final scores score_p = hmean([score_warblr_p, score_chern_p]) score_f = hmean([score_warblr_f, score_chern_f, score_poland]) return (score_p, score_f) @app.route('/scores', methods=['GET', 'POST']) @login_required def scores(): competitions = Competition.query.all() return render_template('scores.html', competitions=competitions) @app.route('/_get_submissions', methods=['POST']) @login_required def get_submissions(): if request.method == 'POST': competition_id = request.form.get('competitions') submissions = Submission.query.filter(Submission.competition_id==competition_id) # if not login.current_user.has_role('admin'): # submissions = submissions.filter_by(user_id=login.current_user.id) count = submissions.count() if count == 0: return jsonify({"count": 0}) response = {} # get all users user_ids = sorted(list({s.user_id for s in submissions})) dates = sorted(list({s.submitted_on.date() for s in submissions})) rows = "" for d in dates: row = '{{"c":[{{"v":"Date({0},{1},{2})"}}'.format(d.year, d.month - 1, d.day) for u in user_ids: s = submissions.filter(cast(Submission.submitted_on, Date)==d).filter(Submission.user_id==u) if s.count() > 0: score = s.first().preview_score * 100 row += ',{{"v":{:.2f}}}'.format(score) row += ',{{"v":"<div style=\\"padding:5px\\"><b>Date</b>: {}<br><b>Username</b>: {}<br><b>Score</b>: {:.2f}<br><b>Comment</b>: {}</div>"}}'.format( s.first().submitted_on.strftime("%b %d, %Y"), User.query.get(u).username, score, s.first().comment.replace('\n',' ').replace('\r',' ')) else: row += ',{"v":"null"}' row += ',{"v":"null"}' row += "]}," rows += row s = """ {{ "cols": [ {{"id":"","label":"Date","pattern":"","type":"datetime"}}, {0} ], "rows": [ {1} ] }}""".format( ','.join('{{"id":"","label":"{}","pattern":"","type":"number"}},{{"id":"","label":"Comment","pattern":"","type":"string","role":"tooltip","p":{{"html":true}}}}'.format(User.query.get(u).username) for u in user_ids), rows ) return jsonify({"count": count, "s": s}) ############### # Admin views # ############### class MyAdminIndexView(AdminIndexView): def is_accessible(self): return login.current_user.has_role('admin') class AdminModelView(ModelView): can_set_page_size = True can_export = True def is_accessible(self): return login.current_user.has_role('admin') # def inaccessible_callback(self, name, **kwargs): # # redirect to login page if user doesn't have access # return redirect(url_for('security.login', next=request.url)) class UserAdmin(ModelView): # Don't display the password on the list of Users column_exclude_list = ('password',) # Don't include the standard password field when creating or editing a User (but see below) form_excluded_columns = ('password',) # Automatically display human-readable names for the current and available Roles when creating or editing a User column_auto_select_related = True # Prevent administration of Users unless the currently logged-in user has the "admin" role def is_accessible(self): return login.current_user.has_role('admin') # On the form for creating or editing a User, don't display a field corresponding to the model's password field. # There are two reasons for this. First, we want to encrypt the password before storing in the database. Second, # we want to use a password field (with the input masked) rather than a regular text field. def scaffold_form(self): # Start with the standard form as provided by Flask-Admin. We've already told Flask-Admin to exclude the # password field from this form. form_class = super(UserAdmin, self).scaffold_form() # Add a password field, naming it "password2" and labeling it "New Password". form_class.password2 = PasswordField('New Password') return form_class # This callback executes when the user saves changes to a newly-created or edited User -- before the changes are # committed to the database. def on_model_change(self, form, model, is_created): # If the password field isn't blank... if len(model.password2): # ... then encrypt the new password prior to storing it in the database. If the password field is blank, # the existing password in the database will be retained. model.password = encrypt_password(model.password2) class CompetitionAdmin(AdminModelView): # Override form field to use Flask-Admin FileUploadField form_overrides = { 'groundtruth': FileUploadField } # Pass additional parameters to 'path' to FileUploadField constructor form_args = { 'groundtruth': { 'label': 'Ground truth', 'base_path': os.path.join(app.config['GROUNDTRUTH_FOLDER']), 'allow_overwrite': False } } @login_required @app.route('/groundtruth/<filename>') def get_groundtruth(filename): if Competition.query.filter_by(groundtruth=filename).count() == 0: abort(404) if login.current_user.has_role('admin'): return send_from_directory(app.config['GROUNDTRUTH_FOLDER'], filename) else: abort(403) @login_required @app.route('/submissions/<filename>') def get_submission(filename): submissions = Submission.query.filter_by(filename=filename) # make sure the current user is whether admin or the user who actually submitted the file if ( submissions.count() > 0 and (login.current_user.has_role('admin') or login.current_user.id == submissions.first().user_id)): return send_from_directory(app.config['UPLOAD_FOLDER'], filename) else: abort(403) class Error(Exception): pass class ParsingError(Error): pass