├── .gitignore ├── README.md ├── TODO.md ├── app.py ├── bin ├── backup ├── pull └── weekly_email ├── bp ├── __init__.py └── admin.py ├── deploy.yaml ├── deployment ├── nginx.conf └── web-supervisor.conf ├── dev-config ├── ADMIN_EMAILS ├── EMAIL_FROM ├── FLASK_SECRET_KEY ├── ITSD_KEY ├── OBSERVER_EMAILS ├── PSQL_CONNECTION_STRING ├── PYCON_API_HOST ├── PYCON_API_KEY ├── PYCON_API_SECRET ├── ROOMS ├── ROOM_SCHEDULES ├── SENDGRID_API_KEY ├── SLACK_TOKEN └── WEB_HOST ├── fill_db_with_fakes.py ├── hosts ├── logic.py ├── logic_test.py ├── pull_updates.py ├── requirements.pip ├── schedule_export.py ├── screening_export.py ├── send_acceptances.py ├── send_email.py ├── setup_db.sql ├── static ├── css │ ├── bootstrap-theme.min.css │ ├── bootstrap.min.css │ └── style.css ├── favicons │ ├── android-chrome-144x144.png │ ├── android-chrome-192x192.png │ ├── android-chrome-36x36.png │ ├── android-chrome-48x48.png │ ├── android-chrome-72x72.png │ ├── android-chrome-96x96.png │ ├── apple-touch-icon-114x114.png │ ├── apple-touch-icon-120x120.png │ ├── apple-touch-icon-144x144.png │ ├── apple-touch-icon-152x152.png │ ├── apple-touch-icon-180x180.png │ ├── apple-touch-icon-57x57.png │ ├── apple-touch-icon-60x60.png │ ├── apple-touch-icon-72x72.png │ ├── apple-touch-icon-76x76.png │ ├── apple-touch-icon-precomposed.png │ ├── apple-touch-icon.png │ ├── browserconfig.xml │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon-96x96.png │ ├── favicon.ico │ ├── manifest.json │ ├── mstile-144x144.png │ ├── mstile-150x150.png │ ├── mstile-310x150.png │ ├── mstile-310x310.png │ ├── mstile-70x70.png │ └── test.html ├── fonts │ ├── glyphicons-halflings-regular.eot │ ├── glyphicons-halflings-regular.svg │ ├── glyphicons-halflings-regular.ttf │ ├── glyphicons-halflings-regular.woff │ └── glyphicons-halflings-regular.woff2 ├── js │ ├── app.js │ ├── bootstrap.3.3.5.min.js │ ├── jquery-2.1.4.min.js │ └── underscore-1.8.3.min.js └── robots.txt ├── stats.py ├── tables.sql └── templates ├── activity_button_fragment.html ├── admin ├── admin_page.html ├── batchgroups.html ├── rough_scores.html ├── schedule.html ├── standards.html └── user_list.html ├── author_feedback.html ├── bad_feedback_key.html ├── base.html ├── batch ├── batch.html ├── batch_discussion_snippet.html ├── batch_render.html ├── batchgroup.html ├── full_list.html ├── my_pycon.html └── single_proposal.html ├── confirmation.html ├── discussion_snippet.html ├── email ├── accept.txt ├── decline.txt ├── feedback_notice.txt ├── login_email.txt ├── new_user_pending.txt ├── weekly_email.txt └── welcome_user.txt ├── my_votes.html ├── progress_render.html ├── proposal_render.html ├── proposal_snippet.html ├── reconsider.html ├── screening_proposal.html ├── screening_stats.html ├── splash.html ├── unread.html ├── user ├── login.html ├── new_user.html ├── request_reset.html └── reset_password.html ├── user_vote_snippet.html └── vote_display.html /.gitignore: -------------------------------------------------------------------------------- 1 | private-config/ 2 | deploy-config/ 3 | dev-config/THIS_IS_BATCH 4 | dev-config/CUTOFF_FEEDBACK 5 | *.pyc 6 | certs 7 | deploy.retry 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | PyCon Program Committee Web App 2 | ------------------------------- 3 | 4 | The goal of this app is to provide a useful tool for asynchronous review of 5 | PyCon talk submissions. The program committee's job is extensive and daunting, 6 | and I'm trying to knock together a simple web app to allow the work to proceed 7 | as efficiently and effectively as I can. Requiring large groupings of 8 | busy professionals to come together at the same time to chat on IRC is hard, 9 | and doesn't feel very scalable. This is my first step toward understanding how 10 | to scale the whole thing. 11 | 12 | 13 | 14 | Configuring the Applications 15 | ------------------------ 16 | 17 | The application picks up configuration from environment variables. I like to 18 | use the envdir tool, but you can set them however you like. A complete set of 19 | configuration values, reasonable for testing, are available in `dev-config/`. 20 | In production, you should add a SENTRY_DSN. 21 | 22 | You can install envdir via `brew install daemontools` on OS X, and `apt-get 23 | install daemontools` on Ubuntu and Debian. 24 | 25 | As configured by the values in `dev-config`, the application connects to a local 26 | postgresql database, with username, password, and database name 'test'. 27 | 28 | Configuring the Database 29 | --------------------- 30 | 31 | The application uses a Postgresql database. If you're not familiar with setting 32 | up Postgresql, I've included `setup_db.sql` for you. Getting to the point where 33 | you're able to execute those commands is going to depend on your system. If 34 | you're on a Ubuntu-like system, and you've installed postgresql via something 35 | like `apt-get install postgresql`, you can probably run the `psql` command via 36 | something like `sudo -u postgres psql`. On OSX, if you've installed postgresql 37 | via brew, with something like `brew install postgresql`, you can probably just 38 | type `psql`. 39 | 40 | You can create the test database and test user via 41 | `psql template1 < setup_db.sql`. 42 | 43 | The unit tests will create the tables for you, or you can do something like 44 | `psql -U test test < tables.sql` to create empty tables from scratch. 45 | 46 | 47 | 48 | 49 | Running the Application 50 | ----------------- 51 | Make a virtualenv, `pip install -r requirements.pip`. Run the application 52 | locally via `envdir dev-config ./app.py`, run the tests via 53 | `envdir dev-config py.test`. 54 | 55 | You can fill the database up with lots of lorem ipsum nonsense by running the 56 | script `envdir dev-config ./fill_db_with_fakes.py`. You can then log in with 57 | an email from the sequence `user{0-24}@example.com`, and a password of `abc123`. 58 | `user0@example.com` is an administrator. 59 | 60 | 61 | 62 | Deployment 63 | ---------- 64 | 65 | You'll need deploy-config in your root directory, which should have all the 66 | appropriate secrets. From the application's root directory, you can run 67 | `ansible-playbook -i hosts deploy.yaml`. 68 | 69 | 70 | 71 | Understanding The PyCon Talk Review Process 72 | ------------ 73 | The process runs in two rounds; the first is called "screening", and is 74 | basically about winnowing out talks. Talks which aren't relevant for 75 | PyCon, have poorly prepared proposals, or otherwise won't make the cut, get 76 | eliminated from consideration early. Talks aren't compared to one another; a 77 | low-ish bar is set, and talks that don't make it over the bar are removed. 78 | 79 | The second part of the process is "batch". In batch, talks are 80 | moved into groups, and those groups are then reviewed one at a time, with 81 | a winner or two picked from every group. Some groups feel weak enough that no 82 | winners are picked. 83 | 84 | To turn on Batch, `echo 1 > dev-config/THIS_IS_BATCH`. 85 | 86 | To disable feedback in screening, `echo 1 > dev-config/CUTOFF_FEEDBACK`. 87 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | - [x] Unread 2 | - [x] Feedback from proposal authors 3 | - [x] Votes should reload 4 | - [x] batch Logic 5 | - [x] Disallow viewing own proposal 6 | - [x] Display talk data as markdown 7 | - [x] batch voting UI 8 | - [x] Handle withdrawn talks 9 | - [x] Markdown links should target blank 10 | - [x] Change voting style for screening 11 | - [x] Tweak voting on batch 12 | - [x] Deployment 13 | - [x] Fix names 14 | - [x] Flag as special, screening stage 15 | - [x] Add nominate to vote displays 16 | - [x] Sentry on live 17 | - [x] Link to edit URL on us.pycon.org 18 | - [x] Link login and new users stuff to mailing list 19 | - [x] Anonymize replies on filter 20 | - [x] batch chat 21 | - [x] unread tools on batch chat 22 | - [x] Clean up the menu bar in batch 23 | - [x] batch hide your groups from yourself 24 | - [x] Turn emailing back on 25 | - [x] Backups 26 | - [x] Integrate with us.pycon.org 27 | - [x] cron us.pycon.org pull 28 | - [x] What's up with the talks not being pulled? 29 | - [x] Reply when a user is approved! 30 | - [x] Weekly status email 31 | - [x] Better display of talks with votes before an update 32 | - [ ] Email reminders/updates to committee members 33 | - [ ] Handle withdrawn talks 34 | - [ ] Need an inbetween mode; post proposals closing, before batch opens 35 | - [ ] ~~Analysis export~~ (Just SQL query it) 36 | - [ ] ~~Remove random chat from talks~~ (Leave chat on talks) 37 | - [ ] ~~batch group tweaking UI~~ (Do it before import) 38 | 39 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import json 4 | import random 5 | import time 6 | from collections import defaultdict 7 | 8 | from flask import (Flask, render_template, request, session, url_for, redirect, 9 | flash, abort, jsonify) 10 | from jinja2 import Markup 11 | import bleach 12 | import markdown2 as markdown 13 | import dateutil.parser 14 | from raven.contrib.flask import Sentry 15 | 16 | 17 | import logic as l 18 | 19 | from bp.admin import bp as bp_admin 20 | 21 | app = Flask(__name__) 22 | app.secret_key = os.environ['FLASK_SECRET_KEY'] 23 | 24 | app.register_blueprint(bp_admin, url_prefix='/admin') 25 | 26 | if 'SENTRY_DSN' in os.environ: 27 | sentry = Sentry(app) 28 | print 'Sentry' 29 | 30 | THIS_IS_BATCH = 'THIS_IS_BATCH' in os.environ 31 | app.config.THIS_IS_BATCH = THIS_IS_BATCH 32 | 33 | CUTOFF_FEEDBACK = 'CUTOFF_FEEDBACK' in os.environ 34 | app.config.CUTOFF_FEEDBACK = CUTOFF_FEEDBACK 35 | 36 | _ADMIN_EMAILS = set(json.loads(os.environ['ADMIN_EMAILS'])) 37 | app.config.ADMIN_EMAILS = _ADMIN_EMAILS 38 | 39 | _OBSERVER_EMAILS = set(json.loads(os.environ['OBSERVER_EMAILS'])) 40 | app.config.OBSERVER_EMAILS = _OBSERVER_EMAILS 41 | 42 | if THIS_IS_BATCH: 43 | print 'THIS IS BATCH' 44 | else: 45 | print 'This is Screening!' 46 | 47 | @app.template_filter('date') 48 | def date_filter(d): 49 | if not d: 50 | return '' 51 | if isinstance(d, (str, unicode)): 52 | d = dateutil.parser.parse(d) 53 | return d.strftime('%B %-d, %-I:%M %p') 54 | 55 | @app.template_filter('minutes') 56 | def time_to_minutes(d): 57 | return d.hour*60+d.minute 58 | 59 | def set_nofollow(attrs, new=False): 60 | attrs['target'] = '_blank' 61 | return attrs 62 | 63 | 64 | __ALLOWED_TAGS =['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'br', 'hr', 'pre'] 65 | @app.template_filter('markdown') 66 | def markdown_filter(s): 67 | raw = bleach.clean(markdown.markdown('' if not s else s), 68 | tags=bleach.ALLOWED_TAGS+__ALLOWED_TAGS) 69 | raw = bleach.linkify(raw, callbacks=[set_nofollow]) 70 | return Markup(raw) 71 | 72 | """ 73 | Account Silliness 74 | """ 75 | @app.before_request 76 | def security_check(): 77 | request.user = l.get_user(session.get('userid')) 78 | 79 | if request.user and not request.user.approved: 80 | session.clear() 81 | return redirect(url_for('login')) 82 | 83 | path = request.path 84 | if (request.user and path.startswith('/admin') 85 | and request.user.email not in _ADMIN_EMAILS): 86 | abort(403) 87 | 88 | if path.startswith('/screening') and THIS_IS_BATCH: 89 | abort(403) 90 | 91 | if path.startswith('/batch') and not THIS_IS_BATCH: 92 | if request.user and request.user.email not in _ADMIN_EMAILS: 93 | abort(403) 94 | 95 | if request.user: 96 | return 97 | 98 | safe_prefixes = ('/static', '/user', '/feedback', '/confirmation') 99 | for prefix in safe_prefixes: 100 | if path.startswith(prefix): 101 | return 102 | 103 | return redirect(url_for('login')) 104 | 105 | @app.route('/user/login/') 106 | def login(): 107 | return render_template('user/login.html') 108 | 109 | @app.route('/user/login/', methods=['POST']) 110 | def login_post(): 111 | uid = l.check_pw(request.values.get('email'), 112 | request.values.get('pw')) 113 | if not uid: 114 | flash('Bad email or password.') 115 | return redirect(url_for('login')) 116 | user = l.get_user(uid) 117 | if not user.approved: 118 | flash('You have not yet been approved.') 119 | return redirect(url_for('login')) 120 | session['userid'] = uid 121 | return redirect('/') 122 | 123 | @app.route('/user/new/') 124 | def new_user(): 125 | return render_template('user/new_user.html') 126 | 127 | @app.route('/user/new/', methods=['POST']) 128 | def new_user_post(): 129 | email = request.values.get('email') 130 | name = request.values.get('name') 131 | pw = request.values.get('pw') 132 | if not pw or not pw.strip(): 133 | flash('No empty passwords, please!') 134 | return redirect(url_for('new_user')) 135 | uid = l.add_user(email, name, pw) 136 | if uid == -1: 137 | flash('An account with that email address already exists') 138 | return redirect(url_for('login')) 139 | l.email_new_user_pending(email, name) 140 | flash('You will be able to log in after your account is approved!') 141 | return redirect(url_for('login')) 142 | 143 | @app.route('/user/logout/') 144 | def logout(): 145 | session.clear() 146 | return redirect(url_for('login')) 147 | 148 | @app.route('/user/email_login/') 149 | def request_reset(): 150 | return render_template('user/request_reset.html') 151 | 152 | @app.route('/user/email_login/', methods=['POST']) 153 | def request_reset_post(): 154 | if l.send_login_email(request.values.get('email')): 155 | flash('Reset sent') 156 | else: 157 | flash('Reset failed; perhaps a bad email address?') 158 | return redirect(url_for('request_reset')) 159 | 160 | @app.route('/user/login//') 161 | def view_reset_key(key): 162 | uid = l.test_login_string(key) 163 | if not uid: 164 | flash('Bad key; has it expired?') 165 | return redirect(url_for('request_reset')) 166 | session['userid'] = uid 167 | flash('Logged in!') 168 | return redirect(url_for('reset_password')) 169 | 170 | @app.route('/me/change_password/') 171 | def reset_password(): 172 | return render_template('user/reset_password.html') 173 | 174 | 175 | @app.route('/me/change_password/', methods=['POST']) 176 | def reset_password_post(): 177 | l.change_pw(request.user.id, request.values.get('pw')) 178 | flash('Password changed') 179 | return redirect('/') 180 | 181 | """ 182 | User State 183 | """ 184 | @app.route('/votes/') 185 | def show_votes(): 186 | votes = l.get_my_votes(request.user.id) 187 | votes = [x._replace(updated_on=l._js_time(x.updated_on)) for x in votes] 188 | percent = l.get_vote_percentage(request.user.email, request.user.id) 189 | return render_template('my_votes.html', votes=votes, percent=percent, 190 | standards = l.get_standards()) 191 | 192 | @app.route('/unread/') 193 | def show_unread(): 194 | return render_template('unread.html', unread=l.get_unread(request.user.id)) 195 | 196 | """ 197 | Batch Actions 198 | """ 199 | @app.route('/batch/') 200 | def batch_splash_page(): 201 | groups = [x._asdict() for x in l.list_groups(request.user.id)] 202 | unread = l.get_unread_batches(request.user.id) 203 | stats = l.get_batch_stats() 204 | for group in groups: 205 | group['unread'] = group['id'] in unread 206 | group.update(stats[group['id']]) 207 | percent = int( 100.0*sum(1.0 for x in groups if x['voted']) / len(groups)) 208 | return render_template('batch/batch.html', groups=groups, percent=percent) 209 | 210 | @app.route('/batch/full//') 211 | def view_single_proposals(id): 212 | proposal = l.get_proposal(id) 213 | if request.user.email in [x.email for x in proposal.authors]: 214 | abort(404) 215 | return render_template('batch/single_proposal.html', proposal=proposal, 216 | discussion=l.get_discussion(id)) 217 | 218 | @app.route('/batch/full/') 219 | def full_list(): 220 | return render_template('batch/full_list.html', 221 | proposals=l.full_proposal_list(request.user.email)) 222 | 223 | @app.route('/batch//') 224 | def batch_view(id): 225 | l.l('batch_view', uid=request.user.id, gid=id) 226 | group = l.get_group(id) 227 | if not group: 228 | abort(404) 229 | if request.user.email in group.author_emails: 230 | abort(404) 231 | raw_proposals = l.get_group_proposals(id) 232 | votes = l.get_group_votes(id) 233 | proposals = [] 234 | for rp in raw_proposals: 235 | clean_prop = {'proposal': rp, 'discussion': l.get_discussion(rp.id)} 236 | voters = [v.display_name for v in votes if rp.id in v.accept] 237 | clean_prop['voters'] = ', '.join(voters) 238 | clean_prop['voters_count'] = len(voters) 239 | proposals.append(clean_prop) 240 | proposal_map = {x.id:x for x in raw_proposals} 241 | if group.locked: 242 | proposals.sort(key=lambda x:-x['voters_count']) 243 | else: 244 | random.shuffle(proposals) 245 | basics = {x['proposal'].id:x['proposal'].data['title'] for x in proposals} 246 | vote = l.get_batch_vote(id, request.user.id) 247 | msgs = l.get_batch_messages(id) 248 | l.mark_batch_read(id, request.user.id) 249 | return render_template('batch/batchgroup.html', group=group, 250 | proposals=proposals, proposal_map=proposal_map, 251 | basics=basics, msgs=msgs, 252 | all_votes=votes, 253 | vote = vote._asdict() if vote else None) 254 | 255 | @app.route('/batch//vote/', methods=['POST']) 256 | def batch_vote(id): 257 | group = l.get_group(id) 258 | if request.user.email in group.author_emails or group.locked: 259 | abort(404) 260 | 261 | accept = request.values.getlist('accept', int) 262 | 263 | l.vote_group(id, request.user.id, accept) 264 | return redirect(url_for('batch_view', id=id)) 265 | 266 | @app.route('/batch//comment/', methods=['POST']) 267 | def batch_discussion(id): 268 | group = l.get_group(id) 269 | if request.user.email in group.author_emails or group.locked: 270 | abort(404) 271 | txt = request.values.get('comment','').strip() 272 | if txt: 273 | l.add_batch_message(request.user.id, id, txt) 274 | return render_template('batch/batch_discussion_snippet.html', 275 | msgs=l.get_batch_messages(id)) 276 | 277 | @app.route('/batch/nominations/') 278 | def my_nominations(): 279 | return render_template('batch/my_pycon.html', 280 | proposals=l.get_my_pycon(request.user.id)) 281 | 282 | """ 283 | Screening Actions 284 | """ 285 | @app.route('/activity_buttons/') 286 | def activity_buttons(): 287 | return render_template('activity_button_fragment.html') 288 | 289 | 290 | @app.route('/screening/stats/') 291 | def screening_stats(): 292 | users = [x for x in l.list_users() if x.votes] 293 | users.sort(key=lambda x:-x.votes) 294 | progress = l.screening_progress() 295 | votes_when = l.get_votes_by_day() 296 | coverage_by_age = l.coverage_by_age() 297 | active_discussions = l.active_discussions() 298 | nomination_density = l.nomination_density() 299 | return render_template('screening_stats.html', 300 | users=users, progress=progress, 301 | nomination_density=nomination_density, 302 | coverage_by_age=coverage_by_age, 303 | total_votes=sum(u.votes for u in users), 304 | total_proposals=sum(p.quantity for p in progress), 305 | active_discussions=active_discussions, 306 | votes_when=votes_when) 307 | 308 | @app.route('/screening//') 309 | def screening(id): 310 | l.l('screening_view', uid=request.user.id, id=id) 311 | proposal = l.get_proposal(id) 312 | if not proposal or proposal.withdrawn: 313 | abort(404) 314 | 315 | if request.user.email in (x.email.lower() for x in proposal.authors): 316 | abort(404) 317 | 318 | unread = l.is_unread(request.user.id, id) 319 | discussion = l.get_discussion(id) 320 | 321 | standards = l.get_standards() 322 | 323 | existing_vote = l.get_user_vote(request.user.id, id) 324 | votes = l.get_votes(id) 325 | 326 | my_votes = l.get_my_votes(request.user.id) 327 | percent = l.get_vote_percentage(request.user.email, request.user.id) 328 | 329 | return render_template('screening_proposal.html', proposal=proposal, 330 | votes=votes, discussion=discussion, 331 | standards=standards, 332 | existing_vote=existing_vote, 333 | unread=unread, 334 | percent=percent) 335 | 336 | @app.route('/screening//vote/', methods=['POST']) 337 | def vote(id): 338 | standards = l.get_standards() 339 | scores = {} 340 | for s in standards: 341 | scores[s.id] = int(request.values['standard-{}'.format(s.id)]) 342 | nominate = request.values.get('nominate', '0') == '1' 343 | l.vote(request.user.id, id, scores, nominate) 344 | return render_template('user_vote_snippet.html', 345 | standards=l.get_standards(), 346 | votes = l.get_votes(id), 347 | existing_vote=l.get_user_vote(request.user.id, id)) 348 | 349 | @app.route('/screening//comment/', methods=['POST']) 350 | def comment(id): 351 | comment = request.values.get('comment').strip() 352 | if comment: 353 | l.add_to_discussion(request.user.id, id, comment, feedback=False) 354 | return render_template('discussion_snippet.html', 355 | unread = l.is_unread(request.user.id, id), 356 | discussion = l.get_discussion(id)) 357 | 358 | @app.route('/screening//feedback/', methods=['POST']) 359 | def feedback(id): 360 | if CUTOFF_FEEDBACK: 361 | abort(404) 362 | comment = request.values.get('feedback').strip() 363 | if comment: 364 | l.add_to_discussion(request.user.id, id, comment, feedback=True) 365 | return render_template('discussion_snippet.html', 366 | unread = l.is_unread(request.user.id, id), 367 | discussion = l.get_discussion(id)) 368 | 369 | @app.route('/screening//mark_read/', methods=['POST']) 370 | def mark_read(id): 371 | l.mark_read(request.user.id, id) 372 | return render_template('discussion_snippet.html', 373 | unread = l.is_unread(request.user.id, id), 374 | discussion = l.get_discussion(id)) 375 | 376 | @app.route('/screening//mark_read/next/', methods=['POST']) 377 | def mark_read_read_next(id): 378 | l.mark_read(request.user.id, id) 379 | unread = l.get_unread(request.user.id) 380 | if not unread: 381 | flash('All unread messages marked as read') 382 | return redirect(url_for('screening_stats')) 383 | target = random.choice(unread) 384 | return redirect(url_for('screening', id=random.choice(unread).id)) 385 | 386 | """ 387 | Author Feedback 388 | """ 389 | 390 | @app.route('/feedback/') 391 | def author_feedback(key): 392 | name, id = l.check_author_key(key) 393 | if not name: 394 | return render_template('bad_feedback_key.html') 395 | proposal = l.get_proposal(id) 396 | return render_template('author_feedback.html', name=name, 397 | proposal=proposal, messages=l.get_discussion(id)) 398 | 399 | 400 | @app.route('/feedback/', methods=['POST']) 401 | def author_post_feedback(key): 402 | if CUTOFF_FEEDBACK: 403 | abort(404) 404 | name, id = l.check_author_key(key) 405 | if not name: 406 | return render_template('bad_feedback_key.html') 407 | message = request.values.get('message', '').strip() 408 | redir = redirect(url_for('author_feedback', key=key)) 409 | if not message: 410 | flash('Empty message') 411 | return redir 412 | l.add_to_discussion(None, id, request.values.get('message'), name=name) 413 | flash('Your message has been saved!') 414 | return redir 415 | """ 416 | Observer View 417 | """ 418 | 419 | @app.route('/schedule/') 420 | def view_schedule(): 421 | return render_template('admin/schedule.html', schedule=l.get_schedule(), 422 | talks=l.get_accepted(), read_only=True) 423 | 424 | """ 425 | Confirmation 426 | """ 427 | @app.route('/confirmation//') 428 | def confirmation(key): 429 | id = l.acknowledge_confirmation(key) 430 | if not id: 431 | return render_template('bad_feedback_key.html') 432 | return render_template('confirmation.html', proposal=l.get_proposal(id)) 433 | 434 | 435 | """ 436 | Default Action 437 | """ 438 | @app.route('/') 439 | def pick(): 440 | if THIS_IS_BATCH: 441 | return redirect(url_for('batch_splash_page')) 442 | 443 | 444 | if request.user.revisit: 445 | data = [x for x in l.get_my_votes(request.user.id) if x.updated] 446 | if data: 447 | msg = """This proposal has been updated since your last vote. 448 | Please reconsider and save your vote!""" 449 | flash(msg) 450 | return redirect(url_for('screening', id=data[0].proposal)) 451 | 452 | id = l.needs_votes(request.user.email, request.user.id) 453 | if not id: 454 | flash("You have voted on every proposal!") 455 | return redirect(url_for('screening_stats')) 456 | return redirect(url_for('screening', id=id)) 457 | 458 | if __name__ == '__main__': 459 | app.run(port=4000, debug=True) 460 | -------------------------------------------------------------------------------- /bin/backup: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | sudo -u postgres /usr/bin/pg_dump progcom > /opt/progcom-backup/db/progcom-db-`date +%Y-%m-%d_%H-%M-%S`.sql 4 | 5 | /opt/tarsnap/bin/tarsnap -c --keyfile /root/tarsnap.key --cachedir /opt/progcom-backup-cache -f progcom-`date +%Y-%m-%d_%H-%M-%S` /opt/progcom-backup && curl https://nosnch.in/c5fce6f393 > /dev/null 6 | -------------------------------------------------------------------------------- /bin/pull: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | /usr/bin/envdir /opt/progcom-envdir/ /opt/progcom-venv/bin/python /opt/progcom/pull_updates.py && curl https://nosnch.in/e71a3f17a8 > /dev/null 4 | -------------------------------------------------------------------------------- /bin/weekly_email: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | /usr/bin/envdir /opt/progcom-envdir/ /opt/progcom-venv/bin/python /opt/progcom/send_email.py 4 | -------------------------------------------------------------------------------- /bp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/njl/progcom/13d43927e2399073b2a092917d1d5dca5a8b33e9/bp/__init__.py -------------------------------------------------------------------------------- /bp/admin.py: -------------------------------------------------------------------------------- 1 | from flask import (Blueprint, render_template, jsonify, request, 2 | redirect, url_for, flash) 3 | import requests, os 4 | import logic as l 5 | 6 | bp = Blueprint('admin', __name__) 7 | 8 | @bp.route('/') 9 | def admin_menu(): 10 | return render_template('admin/admin_page.html') 11 | 12 | @bp.route('/batchgroups//lock/', methods=['POST']) 13 | def lock_batch_group(id): 14 | lock = request.values.get('lock', None) == 't' 15 | l.l('lock_batch_group', user=request.user.id, lock=lock, id=id) 16 | l.toggle_lock_batch(id, lock) 17 | return jsonify(result='ok', status=lock) 18 | 19 | @bp.route('/batchgroups/') 20 | def list_batchgroups(): 21 | l.l('list_batchgroups', user=request.user.id) 22 | return render_template('admin/batchgroups.html', 23 | groups=l.raw_list_groups()) 24 | 25 | @bp.route('/batchgroups/', methods=['POST']) 26 | def add_batchgroup(): 27 | name = request.values.get('name') 28 | id = l.create_group(request.values.get('name'), None) 29 | l.l('add_batchgroup', uid=request.user.id, name = name, id=id) 30 | if request.is_xhr: 31 | return jsonify(groups=l.raw_list_groups()) 32 | return redirect(url_for('admin.list_batchgroups')) 33 | 34 | @bp.route('/batchgroups//', methods=['POST']) 35 | def rename_batch_group(id): 36 | name = request.values.get('name') 37 | l.l('rename_batch_group', uid=request.user.id, name=name, gid=id) 38 | l.rename_batch_group(id,name) 39 | if request.is_xhr: 40 | return jsonify(groups=l.raw_list_groups()) 41 | return redirect(url_for('admin.list_batchgroups')) 42 | 43 | @bp.route('/assign/', methods=['POST']) 44 | def assign_proposal(): 45 | gid = request.values.get('gid', None) 46 | pid = request.values.get('pid') 47 | l.l('assign_proposal', uid=request.user.id, gid=gid, pid=pid) 48 | l.assign_proposal(gid, pid) 49 | return jsonify(status='ok') 50 | 51 | @bp.route('/users/') 52 | def list_users(): 53 | return render_template('admin/user_list.html', users=l.list_users()) 54 | 55 | @bp.route('/users//approve/', methods=['POST']) 56 | def approve_user(uid): 57 | l.approve_user(uid) 58 | user = l.get_user(uid) 59 | flash('Approved user {}'.format(user.email)) 60 | l.email_approved(uid) 61 | requests.post('https://slack.com/api/users.admin.invite', 62 | data = dict(token=os.environ['SLACK_TOKEN'], email=user.email)) 63 | return redirect(url_for('admin.list_users')) 64 | 65 | @bp.route('/standards/') 66 | def list_standards(): 67 | return render_template('admin/standards.html', standards=l.get_standards()) 68 | 69 | @bp.route('/standards/', methods=['POST']) 70 | def add_reason(): 71 | text = request.values.get('text') 72 | l.add_standard(text) 73 | flash('Added standard "{}"'.format(text)) 74 | return redirect(url_for('admin.list_standards')) 75 | 76 | @bp.route('/rough_scores/auto_grouping/') 77 | def get_auto_grouping(): 78 | return jsonify(data=l.get_proposals_auto_grouped()) 79 | 80 | @bp.route('/rough_scores/') 81 | def rough_scores(): 82 | proposals = l.scored_proposals() 83 | consensus = l.get_batch_coverage() 84 | for x in proposals: 85 | x['auto_group'] = '' 86 | if x['batchgroup']: 87 | x['consensus'] = consensus[x['batch_id']][x['id']] 88 | else: 89 | x['consensus'] = -1 90 | return render_template('admin/rough_scores.html', 91 | proposals=proposals, 92 | groups=l.raw_list_groups()) 93 | 94 | @bp.route('/talk//status/', methods=['POST']) 95 | def set_status(id): 96 | accepted = request.values.get('accepted', None) 97 | if accepted != None: 98 | accepted = (accepted == 'true') 99 | l.l('set_accepted_status', id=id, uid=request.user.id, accepted=accepted) 100 | l.change_acceptance(id, accepted) 101 | return jsonify(accepted=accepted) 102 | 103 | @bp.route('/schedule/') 104 | def view_schedule(): 105 | return render_template('admin/schedule.html', schedule=l.get_schedule(), 106 | talks=l.get_accepted(), read_only=False) 107 | 108 | @bp.route('/schedule/', methods=['POST']) 109 | def adjust_schedule(): 110 | l.set_schedule(request.values.get('proposal'), 111 | request.values.get('slot')) 112 | return jsonify(ok='ok') 113 | -------------------------------------------------------------------------------- /deploy.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: all 3 | become: yes 4 | tasks: 5 | - apt: upgrade=dist update_cache=yes 6 | - apt: pkg={{item}} state=latest 7 | with_items: 8 | - nginx 9 | - supervisor 10 | - git 11 | - python-dev 12 | - htop 13 | - build-essential 14 | - python-pip 15 | - python-virtualenv 16 | - postgresql 17 | - daemontools 18 | - ufw 19 | - libpq-dev 20 | - libffi-dev 21 | - libssl-dev 22 | - python-psycopg2 23 | - e2fslibs-dev 24 | - anacron 25 | - libblas-dev 26 | - liblapack-dev 27 | - gfortran 28 | - cython 29 | 30 | 31 | #Basic service 32 | - service: name=postgresql state=started 33 | - service: name=supervisor state=started 34 | - template: src=certs/progcom.njl.us.crt dest=/etc/nginx/progcom.njl.us.crt 35 | - template: src=certs/progcom.njl.us.key dest=/etc/nginx/progcom.njl.us.key 36 | - template: src=deployment/nginx.conf dest=/etc/nginx/sites-enabled/default 37 | notify: restart nginx 38 | - lineinfile: dest=/etc/postgresql/9.5/main/pg_hba.conf 39 | line="local all all md5" 40 | regexp="local\s+all\s+all\s+peer" 41 | state=present backrefs=yes 42 | notify: restart postgresql 43 | 44 | #Repo 45 | - file: path=/opt/progcom state=directory owner=n group=n 46 | - git: repo=git@github.com:njl/progcom.git 47 | dest=/opt/progcom accept_hostkey=True force=True 48 | become: no 49 | notify: restart progcom 50 | 51 | #Virtualenv 52 | - pip: virtualenv=/opt/progcom-venv 53 | requirements=/opt/progcom/requirements.pip 54 | notify: restart progcom 55 | 56 | #Envdir 57 | - file: path=/opt/progcom-envdir state=directory owner=n group=n 58 | - synchronize: src=deploy-config/ dest=/opt/progcom-envdir/ 59 | delete=yes 60 | become: no 61 | notify: restart progcom 62 | 63 | #Backup (Installed tarsnap by hand with `./configure #--prefix=/opt/tarsnap`) 64 | - file: path=/opt/progcom-backup/db state=directory 65 | - file: path=/opt/progcom-backup/logs state=directory 66 | - file: path=/opt/progcom-backup-cache state=directory 67 | - cron: name="backup" minute="0" hour="1" job="/opt/progcom/bin/backup >/dev/null 2>&1" 68 | 69 | #Supervisor 70 | - template: src=deployment/web-supervisor.conf dest=/etc/supervisor/conf.d/progcom.conf 71 | - supervisorctl: name=progcom state=started 72 | 73 | 74 | #us.pycon.org pull 75 | - cron: name="pycon pull" minute="0,15,30,45" job="/opt/progcom/bin/pull >/dev/null 2>&1" state=absent 76 | - cron: name="pycon update" minute="20" hour="7" weekday="1" job="/opt/progcom/bin/weekly_email >/dev/null 2>&1" state=absent 77 | 78 | - name: Notify in slack channel 79 | become: no 80 | local_action: 81 | module: slack 82 | token: T0HUP6ZL6/B0JC6MH0E/hwrx6XAuyLSEnaq9zkUzMjag 83 | msg: "Web app update deployed" 84 | 85 | 86 | handlers: 87 | - name: restart nginx 88 | service: name=nginx state=restarted 89 | - name: restart postgresql 90 | service: name=postgresql state=restarted 91 | - name: restart progcom 92 | supervisorctl: name=progcom state=restarted 93 | -------------------------------------------------------------------------------- /deployment/nginx.conf: -------------------------------------------------------------------------------- 1 | upstream website { 2 | server 127.0.0.1:4000 fail_timeout=0; 3 | } 4 | 5 | server { 6 | root /usr/share/nginx/www; 7 | index index.html index.htm; 8 | 9 | 10 | # Make site accessible from http://localhost/ 11 | server_name staging.cs.qu.to; 12 | 13 | listen 80 default_server; 14 | listen 443 ssl; 15 | 16 | server_name progcom.njl.us; 17 | ssl_certificate progcom.njl.us.crt; 18 | ssl_certificate_key progcom.njl.us.key; 19 | 20 | 21 | 22 | location /static/ { 23 | 24 | alias /opt/progcom/static/; 25 | 26 | gzip on; 27 | gzip_min_length 1000; 28 | gzip_proxied expired no-cache no-store private auth; 29 | gzip_types text/plain application/json text/css application/x-javascript; 30 | } 31 | 32 | location / { 33 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 34 | proxy_set_header Host $http_host; 35 | proxy_redirect off; 36 | proxy_pass http://website; 37 | 38 | gzip on; 39 | gzip_min_length 1000; 40 | gzip_proxied expired no-cache no-store private auth; 41 | gzip_types text/plain application/json; 42 | } 43 | 44 | location /robots.txt { 45 | alias /opt/progcom/static/robots.txt; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /deployment/web-supervisor.conf: -------------------------------------------------------------------------------- 1 | [program:progcom] 2 | command=/usr/bin/envdir /opt/progcom-envdir /opt/progcom-venv/bin/gunicorn app:app --worker-class gevent -b 127.0.0.1:4000 --log-level=info 3 | directory=/opt/progcom 4 | user=nobody 5 | autostart=True 6 | autorestart=True 7 | stdout_logfile=/opt/progcom-backup/logs/stdout.log 8 | stderr_logfile=/opt/progcom-backup/logs/stderr.log 9 | stderr_logfile_backups=100 10 | stdout_logfile_backups=100 11 | -------------------------------------------------------------------------------- /dev-config/ADMIN_EMAILS: -------------------------------------------------------------------------------- 1 | ["user0@example.com"] 2 | -------------------------------------------------------------------------------- /dev-config/EMAIL_FROM: -------------------------------------------------------------------------------- 1 | noreply@njl.us 2 | -------------------------------------------------------------------------------- /dev-config/FLASK_SECRET_KEY: -------------------------------------------------------------------------------- 1 | TEST KEY 2 | -------------------------------------------------------------------------------- /dev-config/ITSD_KEY: -------------------------------------------------------------------------------- 1 | Test Key 2 | -------------------------------------------------------------------------------- /dev-config/OBSERVER_EMAILS: -------------------------------------------------------------------------------- 1 | ["user8@example.com"] 2 | -------------------------------------------------------------------------------- /dev-config/PSQL_CONNECTION_STRING: -------------------------------------------------------------------------------- 1 | postgresql+psycopg2://progcom:progcom@localhost:5432/progcom 2 | -------------------------------------------------------------------------------- /dev-config/PYCON_API_HOST: -------------------------------------------------------------------------------- 1 | us.pycon.org 2 | -------------------------------------------------------------------------------- /dev-config/PYCON_API_KEY: -------------------------------------------------------------------------------- 1 | BLAHBLAH 2 | 3 | INVALID 4 | -------------------------------------------------------------------------------- /dev-config/PYCON_API_SECRET: -------------------------------------------------------------------------------- 1 | BLAHBLAH 2 | 3 | INVALID 4 | -------------------------------------------------------------------------------- /dev-config/ROOMS: -------------------------------------------------------------------------------- 1 | {"A": 0, "C": 0, "B": 1, "E": 0, "D": 1} 2 | -------------------------------------------------------------------------------- /dev-config/ROOM_SCHEDULES: -------------------------------------------------------------------------------- 1 | {"0": [{"11:30": 30, "10:50": 30, "15:15": 45, "16:30": 30, "13:55": 30, "14:35": 30, "12:10": 45, "17:10": 30}, {"11:30": 30, "10:50": 30, "15:15": 45, "16:30": 30, "13:55": 30, "14:35": 30, "12:10": 45, "17:10": 30}, {"13:10": 30, "13:50": 30, "14:30": 30}], "1": [{"11:30": 30, "13:40": 45, "14:35": 30, "15:15": 30, "17:10": 30, "10:50": 30, "16:15": 45, "12:10": 30}, {"11:30": 30, "13:40": 45, "14:35": 30, "15:15": 30, "17:10": 30, "10:50": 30, "16:15": 45, "12:10": 30}, {"13:10": 30, "13:50": 30, "14:30": 30}]} 2 | -------------------------------------------------------------------------------- /dev-config/SENDGRID_API_KEY: -------------------------------------------------------------------------------- 1 | BLAHBLAH 2 | 3 | INVALID 4 | -------------------------------------------------------------------------------- /dev-config/SLACK_TOKEN: -------------------------------------------------------------------------------- 1 | BLAHBLAH 2 | 3 | INVALID 4 | -------------------------------------------------------------------------------- /dev-config/WEB_HOST: -------------------------------------------------------------------------------- 1 | localhost:4000 2 | -------------------------------------------------------------------------------- /fill_db_with_fakes.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from jinja2.utils import generate_lorem_ipsum 3 | import logic as l 4 | import logic_test as lt 5 | import random 6 | 7 | def words(mn, mx): 8 | return generate_lorem_ipsum(n=1, min=mn, max=mx, html=False)[:-1] 9 | 10 | def ipsum(n, **kwargs): 11 | kwargs['html'] = False 12 | return generate_lorem_ipsum(n=n, **kwargs) 13 | 14 | def main(): 15 | lt.transact() 16 | emails = ['user{}@example.com'.format(n) for n in range(50)] 17 | for e in emails[:25]: 18 | uid = l.add_user(e, '{} Person'.format(e.split('@')[0]), 'abc123') 19 | l.approve_user(uid) 20 | 21 | 22 | for n in range(6): 23 | l.add_standard(words(3, 10)[:50]) 24 | 25 | user_ids = [x.id for x in l.list_users()] 26 | standards = [x.id for x in l.get_standards()] 27 | 28 | proposal_ids = [] 29 | for n in range(200): 30 | prop_id = n*2 31 | data = {'id': prop_id, 'authors': [{'email': random.choice(emails), 32 | 'name': 'Speaker Name Here'}], 33 | 'title': words(3,8).title(), 34 | 'category': words(1,2), 35 | 'duration': '30 minutes', 36 | 'description': ipsum(4), 37 | 'audience': ipsum(1), 38 | 'audience_level': 'Novice', 39 | 'notes': ipsum(2), 40 | 'objective': ipsum(1), 41 | 'recording_release': bool(random.random() > 0.05), 42 | 'abstract': ipsum(1), 43 | 'outline': ipsum(5)+"\n[test](http://www.google.com/)\n", 44 | 'additional_notes': ipsum(1), 45 | 'additional_requirements': ipsum(1)} 46 | l.add_proposal(data) 47 | proposal_ids.append(prop_id) 48 | 49 | if random.randint(0, 3) == 0: 50 | for n in range(random.randint(1, 10)): 51 | l.add_to_discussion(random.choice(user_ids), prop_id, ipsum(1)) 52 | 53 | if random.randint(0, 2) == 0: 54 | for n in range(random.randint(1, 5)): 55 | vote = {k:random.randint(0, 2) for k in standards} 56 | l.vote(random.choice(user_ids), prop_id, vote) 57 | 58 | if random.randint(0, 3) == 0: 59 | data['description'] = 'UPDATED ' + ipsum(4) 60 | l.add_proposal(data) 61 | 62 | 63 | random.shuffle(proposal_ids) 64 | 65 | proposal_ids = proposal_ids[:70] 66 | for n in range(0, len(proposal_ids), 5): 67 | l.create_group(words(2,4).title(), 68 | proposal_ids[n:n+5]) 69 | 70 | 71 | if __name__ == '__main__': 72 | main() 73 | -------------------------------------------------------------------------------- /hosts: -------------------------------------------------------------------------------- 1 | progcom.njl.us 2 | 3 | -------------------------------------------------------------------------------- /logic_test.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | import random 3 | 4 | import pytest 5 | import mock 6 | 7 | import logic as l 8 | 9 | l._S_OLD = l._SENDGRID 10 | l._SENDGRID = mock.Mock() 11 | 12 | 13 | @pytest.fixture(autouse=True) 14 | def transact(): 15 | """Since the whole point of bcrypt is to be slow, it helps to dial the knob 16 | down while testing.""" 17 | l._SALT_ROUNDS=4 18 | e = l._e 19 | q = "SELECT tablename FROM pg_tables WHERE schemaname='public'" 20 | for table in e.execute(q).fetchall(): 21 | table = table[0] 22 | e.execute('DROP TABLE IF EXISTS %s CASCADE' % table) 23 | sql = open(os.path.join(os.path.dirname(__file__), 'tables.sql')).read() 24 | e.execute(sql) 25 | 26 | def test_pw(): 27 | pw = [u'blah blah blah', 'blah blah blah', 28 | u"\u2603", 'Blah Blah Blah'] 29 | for p in pw: 30 | s = l._mangle_pw(p) 31 | assert s 32 | assert s == l._mangle_pw(p, s) 33 | 34 | def test_user_basics(): 35 | for n in range(20): 36 | assert l.add_user(u'{}@example.com'.format(n), u'Name {}'.format(n), 37 | u'pw{}'.format(n)) 38 | 39 | 40 | email = u'ned@example.com' 41 | name = u'Ned Jackson Lovely' 42 | pw = u'password' 43 | 44 | assert not l.check_pw(email, pw) 45 | 46 | uid = l.add_user(email, name, pw) 47 | 48 | assert len(l.list_users()) == 21 49 | 50 | for user in l.list_users(): 51 | assert not user.approved_on 52 | 53 | l.approve_user(uid) 54 | 55 | for user in l.list_users(): 56 | assert not user.approved_on if user.id != uid else user.approved_on 57 | 58 | 59 | assert l.get_user(uid).display_name == name 60 | 61 | assert l.check_pw(email, pw) 62 | assert l.check_pw(email.upper(), pw) 63 | assert not l.check_pw(email, pw.upper()) 64 | 65 | pw2 = u'\u2603' 66 | l.change_pw(uid, pw2) 67 | 68 | assert not l.check_pw(email, pw) 69 | assert l.check_pw(email, pw2) 70 | 71 | 72 | data = {'id': 123, 'title': 'Title Here', 'category': 'Python', 73 | 'duration': '30', 'description':'the description goes here.', 74 | 'audience': 'People who want to learn about python', 75 | 'audience_level': 'Intermediate', 'objective': 'Talk about Python', 76 | 'abstract':'This is an abstract\n#This is a headline\nThis is not.', 77 | 'outline':"First I'll talk about one thing, then another", 78 | 'notes': 'Additional stuff', 'recording_release': True, 79 | 'additional_requirements':'I need a fishtank', 80 | 'authors': [{'name':'Person Personson','email':'person@example.com'}]} 81 | 82 | def test_proposal_basics(): 83 | assert l.add_proposal(data) 84 | assert not l.add_proposal(data) 85 | assert l.get_proposal(data['id']).data['outline'] == data['outline'] 86 | 87 | changed = data.copy() 88 | changed['abstract'] = 'This is a longer abstract.' 89 | 90 | assert l.add_proposal(changed) 91 | 92 | def test_voting_basics(): 93 | l.add_proposal(data) 94 | standards = [l.add_standard("About Pythong"), l.add_standard("Awesome")] 95 | uid = l.add_user('bob@example.com', 'Bob', 'bob') 96 | assert not l.get_votes(123) 97 | assert not l.vote(uid, 123, {k:3 for k in standards}) 98 | assert not l.get_votes(123) 99 | 100 | assert l.get_proposal(123).vote_count == 0 101 | 102 | l.approve_user(uid) 103 | 104 | assert not l.vote(uid, 123, {}) 105 | assert not l.get_votes(123) 106 | 107 | assert l.vote(uid, 123, {k:2 for k in standards}) 108 | assert l.get_votes(123)[0].scores == {k:2 for k in standards} 109 | assert l.get_proposal(123).vote_count == 1 110 | 111 | assert not l.vote(uid, 123, {k:7 for k in standards}) 112 | assert l.get_votes(123)[0].scores == {k:2 for k in standards} 113 | 114 | assert l.vote(uid, 123, {k:0 for k in standards}) 115 | assert len(l.get_votes(123)) == 1 116 | assert l.get_votes(123)[0].scores == {k:0 for k in standards} 117 | assert l.get_proposal(123).vote_count == 1 118 | 119 | 120 | def test_needs_votes(): 121 | proposals = [] 122 | users = {} 123 | standards = [l.add_standard("About Pythong"), l.add_standard("Awesome")] 124 | sample_vote = {k:2 for k in standards} 125 | for n in range(1,10): 126 | prop = data.copy() 127 | prop['id'] = n*2 128 | prop['abstract'] = 'Proposal {}'.format(n) 129 | email = '{}@example.com'.format(n) 130 | uid = l.add_user(email, email, email) 131 | l.approve_user(uid) 132 | users[email] = uid 133 | prop['authors'] = [{'email':email, 'name':'foo'}] 134 | l.add_proposal(prop) 135 | proposals.append(n*2) 136 | 137 | non_author_email = 'none@example.com' 138 | non_author_id = l.add_user(non_author_email, non_author_email, non_author_email) 139 | l.approve_user(non_author_id) 140 | 141 | random.seed(0) 142 | seen_ids = set() 143 | for n in range(100): 144 | seen_ids.add(l.needs_votes(non_author_email, non_author_id)) 145 | assert seen_ids == set(proposals) 146 | 147 | seen_ids = set() 148 | for n in range(100): 149 | seen_ids.add(l.needs_votes('2@example.com', users['2@example.com'])) 150 | not_2_proposals = set(proposals) 151 | not_2_proposals.remove(4) 152 | assert seen_ids == not_2_proposals 153 | 154 | for n in range(1, 9): 155 | l.vote(users['8@example.com'], n*2, sample_vote) 156 | 157 | seen_ids = set() 158 | for n in range(100): 159 | seen_ids.add(l.needs_votes(non_author_email, non_author_id)) 160 | assert seen_ids == set([18]) 161 | 162 | l.vote(users['8@example.com'], 18, sample_vote) 163 | 164 | seen_ids = set() 165 | for n in range(100): 166 | seen_ids.add(l.needs_votes(non_author_email, non_author_id)) 167 | assert seen_ids == set(proposals) 168 | 169 | def test_standards(): 170 | assert l.get_standards() == [] 171 | l.add_standard('Bob') 172 | assert l.get_standards()[0].description == 'Bob' 173 | 174 | 175 | def test_discussion(): 176 | l.add_proposal(data) 177 | proposal = data['id'] 178 | 179 | users = [] 180 | for n in range(10): 181 | uid = l.add_user('{}@example.com'.format(n), 'name {}'.format(n), 'blah') 182 | l.approve_user(uid) 183 | users.append(uid) 184 | 185 | l.add_to_discussion(users[0], proposal, 'Lorem ipsum') 186 | 187 | for u in users: 188 | assert len(l.get_unread(u)) == 0 189 | 190 | assert len(l.get_discussion(proposal)) == 1 191 | assert l.get_discussion(proposal)[0].body == 'Lorem ipsum' 192 | 193 | l.add_to_discussion(users[-1], proposal, 'dolor sit') 194 | assert [x.id for x in l.get_unread(users[0])] == [proposal] 195 | l.add_to_discussion(users[-1], proposal, 'amet, consectetur') 196 | assert [x.id for x in l.get_unread(users[0])] == [proposal] 197 | 198 | l.mark_read(users[0], proposal) 199 | for u in users: 200 | assert len(l.get_unread(u)) == 0 201 | 202 | l.add_to_discussion(users[0], proposal, 'LOREM IPSUM') 203 | assert l.get_discussion(proposal)[-1].body == 'LOREM IPSUM' 204 | assert l.get_discussion(proposal)[0].body == 'Lorem ipsum' 205 | 206 | def test_batch(): 207 | 208 | user = l.add_user('example@example.com', 'Voter', '123') 209 | l.approve_user(user) 210 | 211 | submitter = l.add_user('bob@example.com', 'Submitted', '123') 212 | l.approve_user(submitter) 213 | 214 | proposals = [] 215 | for n in range(1,50): 216 | prop = data.copy() 217 | prop['id'] = n 218 | if n == 6: 219 | prop['authors'] = [{'name':'Blah', 'email':'bob@example.com'}] 220 | proposals.append(l.add_proposal(prop)) 221 | 222 | group_one = l.create_group('Group One', proposals[4:10]) 223 | group_two = l.create_group('Group Two', proposals[16:27]) 224 | 225 | assert l.get_group(group_one).name == 'Group One' 226 | 227 | group_one_proposals = l.get_group_proposals(group_one) 228 | assert set(x.id for x in group_one_proposals) == set(proposals[4:10]) 229 | 230 | all_groups = l.list_groups(user) 231 | assert set([group_one, group_two]) == set(x.id for x in all_groups) 232 | assert not any(x.voted for x in all_groups) 233 | 234 | votes1 = list(reversed(proposals[5:6])) 235 | votes2 = proposals[4:5] 236 | 237 | l.vote_group(group_one, user, votes1) 238 | 239 | all_groups = {x.id:x.voted for x in l.list_groups(user)} 240 | assert all_groups[group_one] 241 | assert not all_groups[group_two] 242 | 243 | assert l.get_batch_vote(group_one, user).accept == votes1 244 | 245 | l.vote_group(group_one, user, votes2) 246 | 247 | assert l.get_batch_vote(group_one, user).accept == votes2 248 | 249 | assert len(l.list_groups(submitter)) == 1 250 | -------------------------------------------------------------------------------- /pull_updates.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | from hashlib import sha1 4 | from calendar import timegm 5 | from datetime import datetime 6 | import sys 7 | 8 | import pytz 9 | import requests 10 | from raven import Client 11 | from simplejson import JSONDecodeError 12 | from requests.exceptions import RequestException 13 | 14 | import logic as l 15 | 16 | API_KEY = os.environ['PYCON_API_KEY'] 17 | API_SECRET = os.environ['PYCON_API_SECRET'] 18 | API_HOST = os.environ['PYCON_API_HOST'] 19 | 20 | def api_call(uri): 21 | method = 'GET' 22 | body = '' 23 | 24 | timestamp = timegm(datetime.now(tz=pytz.UTC).timetuple()) 25 | base_string = unicode(''.join(( 26 | API_SECRET, 27 | unicode(timestamp), 28 | method.upper(), 29 | uri, 30 | body, 31 | ))) 32 | 33 | headers = { 34 | 'X-API-Key': str(API_KEY), 35 | 'X-API-Signature': str(sha1(base_string.encode('utf-8')).hexdigest()), 36 | 'X-API-Timestamp': str(timestamp), 37 | } 38 | url = 'http://{}{}'.format(API_HOST, uri) 39 | try: 40 | return requests.get(url, headers=headers).json() 41 | except JSONDecodeError, RequestException: 42 | sys.exit(1) 43 | 44 | """ 45 | TALK_IDS_FORCE = [1553, 1554, 1555, 1556, 1557, 1559, 1560, 1561, 1562, 1565, 46 | 1566, 1568, 1569, 1570, 1571, 1572, 1573, 1576, 1577, 1579, 1580, 47 | 1581, 1582, 1583, 1584, 1585, 1586, 1587, 1590, 2057, 2134, 48 | 2135, 2136, 2143, 2144, 2145, 2210] 49 | """ 50 | TALK_IDS_FORCE = [] 51 | 52 | def fetch_ids(): 53 | raw = api_call('/2017/pycon_api/proposals/?type=talk&limit=5000&status=undecided') 54 | #print len(raw['data']) 55 | rv = [x['id'] for x in raw['data']] 56 | return list(set(TALK_IDS_FORCE + rv + l.get_all_proposal_ids())) 57 | 58 | def fetch_talk(id): 59 | rv = api_call('/2017/pycon_api/proposals/{}/'.format(id)) 60 | if not rv or 'data' not in rv: 61 | return {} 62 | rv = rv['data'] 63 | rv['authors'] = rv['speakers'] 64 | del rv['speakers'] 65 | rv.update(rv['details']) 66 | del rv['details'] 67 | return rv 68 | 69 | def main(): 70 | for id in fetch_ids(): 71 | #print 'FETCHING {}'.format(id) 72 | proposal = fetch_talk(id) 73 | if proposal: 74 | l.add_proposal(proposal) 75 | 76 | 77 | raven_client = Client(os.environ['SENTRY_DSN']) 78 | if __name__ == '__main__': 79 | try: 80 | main() 81 | except: 82 | raven_client.captureException() 83 | sys.exit(1) 84 | 85 | -------------------------------------------------------------------------------- /requirements.pip: -------------------------------------------------------------------------------- 1 | flask 2 | sqlalchemy 3 | bcrypt 4 | psycopg2 5 | itsdangerous 6 | sendgrid 7 | bleach 8 | markdown2 9 | simplejson 10 | pandas 11 | gensim 12 | python-dateutil 13 | 14 | requests[security] 15 | pytz 16 | 17 | gevent 18 | 19 | pytest 20 | mock 21 | pytest-cov 22 | 23 | gunicorn 24 | raven[flask] 25 | -------------------------------------------------------------------------------- /schedule_export.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import logic as l 4 | import csv 5 | import sys 6 | 7 | def main(out): 8 | q = "SELECT schedules.*, proposals.data->>'title' as title from schedules JOIN proposals ON proposals.id=schedules.proposal" 9 | keys = ('proposal', 'day', 'room', 'time', 'duration', 'title') 10 | with open(out, 'wb') as csvfile: 11 | writer = csv.writer(csvfile) 12 | writer.writerow(keys) 13 | for row in l.fetchall(q): 14 | writer.writerow(list(unicode(getattr(row, k)).encode('utf-8') for k in keys)) 15 | 16 | if __name__ == "__main__": 17 | main(sys.argv[1]) 18 | -------------------------------------------------------------------------------- /screening_export.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import sys 3 | import json 4 | 5 | import logic as l 6 | 7 | 8 | 9 | def main(target): 10 | data = [x._asdict() 11 | for x in l.fetchall('SELECT yea, proposal, reason FROM votes')] 12 | with open(target, 'w') as out: 13 | json.dump(data, out) 14 | 15 | if __name__ == '__main__': 16 | main(sys.argv[1]) 17 | -------------------------------------------------------------------------------- /send_acceptances.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import logic 3 | 4 | logic.send_emails() 5 | -------------------------------------------------------------------------------- /send_email.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import logic 3 | logic.send_weekly_update() 4 | -------------------------------------------------------------------------------- /setup_db.sql: -------------------------------------------------------------------------------- 1 | DROP DATABASE IF EXISTS test; 2 | DROP USER IF EXISTS test; 3 | 4 | CREATE USER test WITH UNENCRYPTED PASSWORD 'test'; 5 | CREATE DATABASE test WITH OWNER = test; 6 | -------------------------------------------------------------------------------- /static/css/bootstrap-theme.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.3.5 (http://getbootstrap.com) 3 | * Copyright 2011-2015 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */.btn-danger,.btn-default,.btn-info,.btn-primary,.btn-success,.btn-warning{text-shadow:0 -1px 0 rgba(0,0,0,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 1px rgba(0,0,0,.075)}.btn-danger.active,.btn-danger:active,.btn-default.active,.btn-default:active,.btn-info.active,.btn-info:active,.btn-primary.active,.btn-primary:active,.btn-success.active,.btn-success:active,.btn-warning.active,.btn-warning:active{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-danger.disabled,.btn-danger[disabled],.btn-default.disabled,.btn-default[disabled],.btn-info.disabled,.btn-info[disabled],.btn-primary.disabled,.btn-primary[disabled],.btn-success.disabled,.btn-success[disabled],.btn-warning.disabled,.btn-warning[disabled],fieldset[disabled] .btn-danger,fieldset[disabled] .btn-default,fieldset[disabled] .btn-info,fieldset[disabled] .btn-primary,fieldset[disabled] .btn-success,fieldset[disabled] .btn-warning{-webkit-box-shadow:none;box-shadow:none}.btn-danger .badge,.btn-default .badge,.btn-info .badge,.btn-primary .badge,.btn-success .badge,.btn-warning .badge{text-shadow:none}.btn.active,.btn:active{background-image:none}.btn-default{text-shadow:0 1px 0 #fff;background-image:-webkit-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:-o-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#e0e0e0));background-image:linear-gradient(to bottom,#fff 0,#e0e0e0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#dbdbdb;border-color:#ccc}.btn-default:focus,.btn-default:hover{background-color:#e0e0e0;background-position:0 -15px}.btn-default.active,.btn-default:active{background-color:#e0e0e0;border-color:#dbdbdb}.btn-default.disabled,.btn-default.disabled.active,.btn-default.disabled.focus,.btn-default.disabled:active,.btn-default.disabled:focus,.btn-default.disabled:hover,.btn-default[disabled],.btn-default[disabled].active,.btn-default[disabled].focus,.btn-default[disabled]:active,.btn-default[disabled]:focus,.btn-default[disabled]:hover,fieldset[disabled] .btn-default,fieldset[disabled] .btn-default.active,fieldset[disabled] .btn-default.focus,fieldset[disabled] .btn-default:active,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:hover{background-color:#e0e0e0;background-image:none}.btn-primary{background-image:-webkit-linear-gradient(top,#337ab7 0,#265a88 100%);background-image:-o-linear-gradient(top,#337ab7 0,#265a88 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#265a88));background-image:linear-gradient(to bottom,#337ab7 0,#265a88 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff265a88', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#245580}.btn-primary:focus,.btn-primary:hover{background-color:#265a88;background-position:0 -15px}.btn-primary.active,.btn-primary:active{background-color:#265a88;border-color:#245580}.btn-primary.disabled,.btn-primary.disabled.active,.btn-primary.disabled.focus,.btn-primary.disabled:active,.btn-primary.disabled:focus,.btn-primary.disabled:hover,.btn-primary[disabled],.btn-primary[disabled].active,.btn-primary[disabled].focus,.btn-primary[disabled]:active,.btn-primary[disabled]:focus,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary,fieldset[disabled] .btn-primary.active,fieldset[disabled] .btn-primary.focus,fieldset[disabled] .btn-primary:active,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:hover{background-color:#265a88;background-image:none}.btn-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:-o-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5cb85c),to(#419641));background-image:linear-gradient(to bottom,#5cb85c 0,#419641 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#3e8f3e}.btn-success:focus,.btn-success:hover{background-color:#419641;background-position:0 -15px}.btn-success.active,.btn-success:active{background-color:#419641;border-color:#3e8f3e}.btn-success.disabled,.btn-success.disabled.active,.btn-success.disabled.focus,.btn-success.disabled:active,.btn-success.disabled:focus,.btn-success.disabled:hover,.btn-success[disabled],.btn-success[disabled].active,.btn-success[disabled].focus,.btn-success[disabled]:active,.btn-success[disabled]:focus,.btn-success[disabled]:hover,fieldset[disabled] .btn-success,fieldset[disabled] .btn-success.active,fieldset[disabled] .btn-success.focus,fieldset[disabled] .btn-success:active,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:hover{background-color:#419641;background-image:none}.btn-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:-o-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5bc0de),to(#2aabd2));background-image:linear-gradient(to bottom,#5bc0de 0,#2aabd2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#28a4c9}.btn-info:focus,.btn-info:hover{background-color:#2aabd2;background-position:0 -15px}.btn-info.active,.btn-info:active{background-color:#2aabd2;border-color:#28a4c9}.btn-info.disabled,.btn-info.disabled.active,.btn-info.disabled.focus,.btn-info.disabled:active,.btn-info.disabled:focus,.btn-info.disabled:hover,.btn-info[disabled],.btn-info[disabled].active,.btn-info[disabled].focus,.btn-info[disabled]:active,.btn-info[disabled]:focus,.btn-info[disabled]:hover,fieldset[disabled] .btn-info,fieldset[disabled] .btn-info.active,fieldset[disabled] .btn-info.focus,fieldset[disabled] .btn-info:active,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:hover{background-color:#2aabd2;background-image:none}.btn-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:-o-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f0ad4e),to(#eb9316));background-image:linear-gradient(to bottom,#f0ad4e 0,#eb9316 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#e38d13}.btn-warning:focus,.btn-warning:hover{background-color:#eb9316;background-position:0 -15px}.btn-warning.active,.btn-warning:active{background-color:#eb9316;border-color:#e38d13}.btn-warning.disabled,.btn-warning.disabled.active,.btn-warning.disabled.focus,.btn-warning.disabled:active,.btn-warning.disabled:focus,.btn-warning.disabled:hover,.btn-warning[disabled],.btn-warning[disabled].active,.btn-warning[disabled].focus,.btn-warning[disabled]:active,.btn-warning[disabled]:focus,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning,fieldset[disabled] .btn-warning.active,fieldset[disabled] .btn-warning.focus,fieldset[disabled] .btn-warning:active,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:hover{background-color:#eb9316;background-image:none}.btn-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:-o-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9534f),to(#c12e2a));background-image:linear-gradient(to bottom,#d9534f 0,#c12e2a 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#b92c28}.btn-danger:focus,.btn-danger:hover{background-color:#c12e2a;background-position:0 -15px}.btn-danger.active,.btn-danger:active{background-color:#c12e2a;border-color:#b92c28}.btn-danger.disabled,.btn-danger.disabled.active,.btn-danger.disabled.focus,.btn-danger.disabled:active,.btn-danger.disabled:focus,.btn-danger.disabled:hover,.btn-danger[disabled],.btn-danger[disabled].active,.btn-danger[disabled].focus,.btn-danger[disabled]:active,.btn-danger[disabled]:focus,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger,fieldset[disabled] .btn-danger.active,fieldset[disabled] .btn-danger.focus,fieldset[disabled] .btn-danger:active,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:hover{background-color:#c12e2a;background-image:none}.img-thumbnail,.thumbnail{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.075);box-shadow:0 1px 2px rgba(0,0,0,.075)}.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover{background-color:#e8e8e8;background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#e8e8e8));background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-repeat:repeat-x}.dropdown-menu>.active>a,.dropdown-menu>.active>a:focus,.dropdown-menu>.active>a:hover{background-color:#2e6da4;background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}.navbar-default{background-image:-webkit-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:-o-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#f8f8f8));background-image:linear-gradient(to bottom,#fff 0,#f8f8f8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-radius:4px;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075);box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075)}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.open>a{background-image:-webkit-linear-gradient(top,#dbdbdb 0,#e2e2e2 100%);background-image:-o-linear-gradient(top,#dbdbdb 0,#e2e2e2 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dbdbdb),to(#e2e2e2));background-image:linear-gradient(to bottom,#dbdbdb 0,#e2e2e2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdbdbdb', endColorstr='#ffe2e2e2', GradientType=0);background-repeat:repeat-x;-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,.075);box-shadow:inset 0 3px 9px rgba(0,0,0,.075)}.navbar-brand,.navbar-nav>li>a{text-shadow:0 1px 0 rgba(255,255,255,.25)}.navbar-inverse{background-image:-webkit-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:-o-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#3c3c3c),to(#222));background-image:linear-gradient(to bottom,#3c3c3c 0,#222 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-radius:4px}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.open>a{background-image:-webkit-linear-gradient(top,#080808 0,#0f0f0f 100%);background-image:-o-linear-gradient(top,#080808 0,#0f0f0f 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#080808),to(#0f0f0f));background-image:linear-gradient(to bottom,#080808 0,#0f0f0f 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff080808', endColorstr='#ff0f0f0f', GradientType=0);background-repeat:repeat-x;-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,.25);box-shadow:inset 0 3px 9px rgba(0,0,0,.25)}.navbar-inverse .navbar-brand,.navbar-inverse .navbar-nav>li>a{text-shadow:0 -1px 0 rgba(0,0,0,.25)}.navbar-fixed-bottom,.navbar-fixed-top,.navbar-static-top{border-radius:0}@media (max-width:767px){.navbar .navbar-nav .open .dropdown-menu>.active>a,.navbar .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar .navbar-nav .open .dropdown-menu>.active>a:hover{color:#fff;background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}}.alert{text-shadow:0 1px 0 rgba(255,255,255,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 2px rgba(0,0,0,.05);box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 2px rgba(0,0,0,.05)}.alert-success{background-image:-webkit-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:-o-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dff0d8),to(#c8e5bc));background-image:linear-gradient(to bottom,#dff0d8 0,#c8e5bc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);background-repeat:repeat-x;border-color:#b2dba1}.alert-info{background-image:-webkit-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:-o-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9edf7),to(#b9def0));background-image:linear-gradient(to bottom,#d9edf7 0,#b9def0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);background-repeat:repeat-x;border-color:#9acfea}.alert-warning{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:-o-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fcf8e3),to(#f8efc0));background-image:linear-gradient(to bottom,#fcf8e3 0,#f8efc0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);background-repeat:repeat-x;border-color:#f5e79e}.alert-danger{background-image:-webkit-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:-o-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f2dede),to(#e7c3c3));background-image:linear-gradient(to bottom,#f2dede 0,#e7c3c3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);background-repeat:repeat-x;border-color:#dca7a7}.progress{background-image:-webkit-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#ebebeb),to(#f5f5f5));background-image:linear-gradient(to bottom,#ebebeb 0,#f5f5f5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0);background-repeat:repeat-x}.progress-bar{background-image:-webkit-linear-gradient(top,#337ab7 0,#286090 100%);background-image:-o-linear-gradient(top,#337ab7 0,#286090 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#286090));background-image:linear-gradient(to bottom,#337ab7 0,#286090 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff286090', GradientType=0);background-repeat:repeat-x}.progress-bar-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:-o-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5cb85c),to(#449d44));background-image:linear-gradient(to bottom,#5cb85c 0,#449d44 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0);background-repeat:repeat-x}.progress-bar-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:-o-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5bc0de),to(#31b0d5));background-image:linear-gradient(to bottom,#5bc0de 0,#31b0d5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0);background-repeat:repeat-x}.progress-bar-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:-o-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f0ad4e),to(#ec971f));background-image:linear-gradient(to bottom,#f0ad4e 0,#ec971f 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0);background-repeat:repeat-x}.progress-bar-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:-o-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9534f),to(#c9302c));background-image:linear-gradient(to bottom,#d9534f 0,#c9302c 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0);background-repeat:repeat-x}.progress-bar-striped{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.list-group{border-radius:4px;-webkit-box-shadow:0 1px 2px rgba(0,0,0,.075);box-shadow:0 1px 2px rgba(0,0,0,.075)}.list-group-item.active,.list-group-item.active:focus,.list-group-item.active:hover{text-shadow:0 -1px 0 #286090;background-image:-webkit-linear-gradient(top,#337ab7 0,#2b669a 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2b669a 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2b669a));background-image:linear-gradient(to bottom,#337ab7 0,#2b669a 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2b669a', GradientType=0);background-repeat:repeat-x;border-color:#2b669a}.list-group-item.active .badge,.list-group-item.active:focus .badge,.list-group-item.active:hover .badge{text-shadow:none}.panel{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.05);box-shadow:0 1px 2px rgba(0,0,0,.05)}.panel-default>.panel-heading{background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#e8e8e8));background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-repeat:repeat-x}.panel-primary>.panel-heading{background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}.panel-success>.panel-heading{background-image:-webkit-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:-o-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dff0d8),to(#d0e9c6));background-image:linear-gradient(to bottom,#dff0d8 0,#d0e9c6 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0);background-repeat:repeat-x}.panel-info>.panel-heading{background-image:-webkit-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:-o-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9edf7),to(#c4e3f3));background-image:linear-gradient(to bottom,#d9edf7 0,#c4e3f3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0);background-repeat:repeat-x}.panel-warning>.panel-heading{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:-o-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fcf8e3),to(#faf2cc));background-image:linear-gradient(to bottom,#fcf8e3 0,#faf2cc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0);background-repeat:repeat-x}.panel-danger>.panel-heading{background-image:-webkit-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:-o-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f2dede),to(#ebcccc));background-image:linear-gradient(to bottom,#f2dede 0,#ebcccc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0);background-repeat:repeat-x}.well{background-image:-webkit-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#e8e8e8),to(#f5f5f5));background-image:linear-gradient(to bottom,#e8e8e8 0,#f5f5f5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);background-repeat:repeat-x;border-color:#dcdcdc;-webkit-box-shadow:inset 0 1px 3px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 3px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1)} -------------------------------------------------------------------------------- /static/css/style.css: -------------------------------------------------------------------------------- 1 | .panel table { 2 | margin-bottom: 0; 3 | } 4 | 5 | 6 | #accept li, #unranked li { 7 | cursor: pointer; 8 | } 9 | #unranked li:hover { 10 | background-color: #f6f6f6; 11 | } 12 | 13 | #accept li { 14 | background-color: #f0fff0; 15 | } 16 | #accept li:hover { 17 | background-color: #e0ffe0; 18 | } 19 | 20 | .schedule-block { 21 | border: 1px solid #bbb; 22 | position: absolute; 23 | width:95%; 24 | margin-right:5%; 25 | background:rgba(200,200,200,0.3); 26 | } 27 | 28 | .block-header { 29 | display:flex; 30 | justify-content: space-between; 31 | width:100%; 32 | padding: 3px 5%; 33 | background-color: #BBB; 34 | font-weight: bold; 35 | } 36 | 37 | .block-body { 38 | width:100%; 39 | padding 1px 5%; 40 | font-size: 80%; 41 | } 42 | 43 | #available { 44 | position: fixed; 45 | right:0; 46 | width:25%; 47 | height: 50%; 48 | top: 49%; 49 | overflow-y: scroll; 50 | opacity: 0.9; 51 | } 52 | -------------------------------------------------------------------------------- /static/favicons/android-chrome-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/njl/progcom/13d43927e2399073b2a092917d1d5dca5a8b33e9/static/favicons/android-chrome-144x144.png -------------------------------------------------------------------------------- /static/favicons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/njl/progcom/13d43927e2399073b2a092917d1d5dca5a8b33e9/static/favicons/android-chrome-192x192.png -------------------------------------------------------------------------------- /static/favicons/android-chrome-36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/njl/progcom/13d43927e2399073b2a092917d1d5dca5a8b33e9/static/favicons/android-chrome-36x36.png -------------------------------------------------------------------------------- /static/favicons/android-chrome-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/njl/progcom/13d43927e2399073b2a092917d1d5dca5a8b33e9/static/favicons/android-chrome-48x48.png -------------------------------------------------------------------------------- /static/favicons/android-chrome-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/njl/progcom/13d43927e2399073b2a092917d1d5dca5a8b33e9/static/favicons/android-chrome-72x72.png -------------------------------------------------------------------------------- /static/favicons/android-chrome-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/njl/progcom/13d43927e2399073b2a092917d1d5dca5a8b33e9/static/favicons/android-chrome-96x96.png -------------------------------------------------------------------------------- /static/favicons/apple-touch-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/njl/progcom/13d43927e2399073b2a092917d1d5dca5a8b33e9/static/favicons/apple-touch-icon-114x114.png -------------------------------------------------------------------------------- /static/favicons/apple-touch-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/njl/progcom/13d43927e2399073b2a092917d1d5dca5a8b33e9/static/favicons/apple-touch-icon-120x120.png -------------------------------------------------------------------------------- /static/favicons/apple-touch-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/njl/progcom/13d43927e2399073b2a092917d1d5dca5a8b33e9/static/favicons/apple-touch-icon-144x144.png -------------------------------------------------------------------------------- /static/favicons/apple-touch-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/njl/progcom/13d43927e2399073b2a092917d1d5dca5a8b33e9/static/favicons/apple-touch-icon-152x152.png -------------------------------------------------------------------------------- /static/favicons/apple-touch-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/njl/progcom/13d43927e2399073b2a092917d1d5dca5a8b33e9/static/favicons/apple-touch-icon-180x180.png -------------------------------------------------------------------------------- /static/favicons/apple-touch-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/njl/progcom/13d43927e2399073b2a092917d1d5dca5a8b33e9/static/favicons/apple-touch-icon-57x57.png -------------------------------------------------------------------------------- /static/favicons/apple-touch-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/njl/progcom/13d43927e2399073b2a092917d1d5dca5a8b33e9/static/favicons/apple-touch-icon-60x60.png -------------------------------------------------------------------------------- /static/favicons/apple-touch-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/njl/progcom/13d43927e2399073b2a092917d1d5dca5a8b33e9/static/favicons/apple-touch-icon-72x72.png -------------------------------------------------------------------------------- /static/favicons/apple-touch-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/njl/progcom/13d43927e2399073b2a092917d1d5dca5a8b33e9/static/favicons/apple-touch-icon-76x76.png -------------------------------------------------------------------------------- /static/favicons/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/njl/progcom/13d43927e2399073b2a092917d1d5dca5a8b33e9/static/favicons/apple-touch-icon-precomposed.png -------------------------------------------------------------------------------- /static/favicons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/njl/progcom/13d43927e2399073b2a092917d1d5dca5a8b33e9/static/favicons/apple-touch-icon.png -------------------------------------------------------------------------------- /static/favicons/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | #b91d47 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /static/favicons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/njl/progcom/13d43927e2399073b2a092917d1d5dca5a8b33e9/static/favicons/favicon-16x16.png -------------------------------------------------------------------------------- /static/favicons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/njl/progcom/13d43927e2399073b2a092917d1d5dca5a8b33e9/static/favicons/favicon-32x32.png -------------------------------------------------------------------------------- /static/favicons/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/njl/progcom/13d43927e2399073b2a092917d1d5dca5a8b33e9/static/favicons/favicon-96x96.png -------------------------------------------------------------------------------- /static/favicons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/njl/progcom/13d43927e2399073b2a092917d1d5dca5a8b33e9/static/favicons/favicon.ico -------------------------------------------------------------------------------- /static/favicons/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PyCon Talk Review", 3 | "icons": [ 4 | { 5 | "src": "\/android-chrome-36x36.png", 6 | "sizes": "36x36", 7 | "type": "image\/png", 8 | "density": "0.75" 9 | }, 10 | { 11 | "src": "\/android-chrome-48x48.png", 12 | "sizes": "48x48", 13 | "type": "image\/png", 14 | "density": "1.0" 15 | }, 16 | { 17 | "src": "\/android-chrome-72x72.png", 18 | "sizes": "72x72", 19 | "type": "image\/png", 20 | "density": "1.5" 21 | }, 22 | { 23 | "src": "\/android-chrome-96x96.png", 24 | "sizes": "96x96", 25 | "type": "image\/png", 26 | "density": "2.0" 27 | }, 28 | { 29 | "src": "\/android-chrome-144x144.png", 30 | "sizes": "144x144", 31 | "type": "image\/png", 32 | "density": "3.0" 33 | }, 34 | { 35 | "src": "\/android-chrome-192x192.png", 36 | "sizes": "192x192", 37 | "type": "image\/png", 38 | "density": "4.0" 39 | } 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /static/favicons/mstile-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/njl/progcom/13d43927e2399073b2a092917d1d5dca5a8b33e9/static/favicons/mstile-144x144.png -------------------------------------------------------------------------------- /static/favicons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/njl/progcom/13d43927e2399073b2a092917d1d5dca5a8b33e9/static/favicons/mstile-150x150.png -------------------------------------------------------------------------------- /static/favicons/mstile-310x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/njl/progcom/13d43927e2399073b2a092917d1d5dca5a8b33e9/static/favicons/mstile-310x150.png -------------------------------------------------------------------------------- /static/favicons/mstile-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/njl/progcom/13d43927e2399073b2a092917d1d5dca5a8b33e9/static/favicons/mstile-310x310.png -------------------------------------------------------------------------------- /static/favicons/mstile-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/njl/progcom/13d43927e2399073b2a092917d1d5dca5a8b33e9/static/favicons/mstile-70x70.png -------------------------------------------------------------------------------- /static/favicons/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PyCon Talk Review 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |

Hello world.

26 | 27 | -------------------------------------------------------------------------------- /static/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/njl/progcom/13d43927e2399073b2a092917d1d5dca5a8b33e9/static/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /static/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/njl/progcom/13d43927e2399073b2a092917d1d5dca5a8b33e9/static/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /static/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/njl/progcom/13d43927e2399073b2a092917d1d5dca5a8b33e9/static/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /static/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/njl/progcom/13d43927e2399073b2a092917d1d5dca5a8b33e9/static/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /static/js/app.js: -------------------------------------------------------------------------------- 1 | function show_proposal_tabs(ev){ 2 | ev.preventDefault(); 3 | $(this).tab('show'); 4 | } 5 | 6 | function batch_add(ev){ 7 | var $accept = $('#accept'); 8 | if ($accept.find('li').length >= 2){ 9 | return; 10 | } 11 | var $this = $(this); 12 | var id = $this.data().id; 13 | $this.hide(); 14 | $('#notalks').hide(); 15 | $accept.append(TEMPLATES.ordered_row({id:id, title:TALKS[id]})); 16 | } 17 | 18 | function batch_rem(ev){ 19 | var $accept = $('#accept'), 20 | $this=$(this), 21 | id = $this.find('input').val(); 22 | $('#unranked-prop-'+id).show(); 23 | $this.remove(); 24 | if ($accept.find('li').length <= 0){ 25 | $('#notalks').show(); 26 | } 27 | } 28 | 29 | function nominate_status(){ 30 | var enabled = false; 31 | $('#vote-form .btn-group input[type=hidden]').each(function(){ 32 | var val = $(this).val(); 33 | if(val == "1" || val == "0"){ 34 | enabled = true; 35 | } 36 | }); 37 | $("#nominate").attr("disabled", !enabled); 38 | if(!enabled && $('input[name=nominate]').val() == '1'){ 39 | $('#nominate').click(); 40 | } 41 | } 42 | 43 | function vote_click(){ 44 | var $this = $(this); 45 | $this.siblings('input').val($this.data().val); 46 | $this.siblings().addClass('btn-default').removeClass('btn-success btn-warning btn-danger'); 47 | $this.addClass({'0':'btn-danger', '1':'btn-warning', '2': 'btn-success'}[$this.data().val]) 48 | $this.removeClass('btn-default'); 49 | $('#save').attr('disabled', $('#vote-form input[value=-1]').length > 0); 50 | nominate_status() 51 | } 52 | 53 | function nominate_click(){ 54 | var $this=$(this), 55 | $inp = $(this).siblings('input'); 56 | if($inp.val() == 0){ 57 | $this.text("Nominated for Special Consideration!") 58 | $inp.val(1); 59 | $this.addClass("btn-success").removeClass("btn-default"); 60 | }else{ 61 | $this.text("Nominate for Special Consideration"); 62 | $inp.val(0); 63 | $this.addClass("btn-default").removeClass("btn-success") 64 | } 65 | } 66 | 67 | function save_vote(ev){ 68 | ev.preventDefault(); 69 | $.post('vote/', $('#vote-form').serialize(), null, 'html').then(function(data){ 70 | $('#existing-votes-block').remove(); 71 | $('#user-vote-block').replaceWith(data); 72 | update_activity_buttons(); 73 | }); 74 | } 75 | 76 | function mark_read(ev){ 77 | ev.preventDefault(); 78 | $.post('mark_read/').then(function(text){ 79 | $('#discussion-panel').replaceWith(text) 80 | update_activity_buttons(); 81 | }); 82 | } 83 | 84 | function give_feedback(ev){ 85 | ev.preventDefault(); 86 | $.post('feedback/', $('#feedback-form').serialize()).then(function(text){ 87 | $('#discussion-panel').replaceWith(text); 88 | $('#feedback-form textarea').val(''); 89 | }); 90 | } 91 | 92 | function leave_comment(ev){ 93 | ev.preventDefault(); 94 | $.post('comment/', $('#comment-form').serialize()).then(function(text){ 95 | $('#discussion-panel').replaceWith(text); 96 | }); 97 | } 98 | 99 | function batch_add_comment(ev){ 100 | ev.preventDefault(); 101 | $.post('comment/', $('#add-comment').serialize()).then(function(text){ 102 | $('#batch-messages').replaceWith(text); 103 | }); 104 | } 105 | 106 | function table_sorter($table, data_src, row_template, extra_column_functions){ 107 | var data = data_src, 108 | $body = $table.find('tbody'), 109 | extra_column_functions = extra_column_functions?extra_column_functions:{}; 110 | 111 | function handle_click(ev){ 112 | ev.preventDefault(); 113 | var $this = $(this); 114 | if($this.hasClass('warning')){ 115 | data = data.reverse(); 116 | }else{ 117 | $this.siblings().removeClass('warning'); 118 | $this.addClass('warning'); 119 | var column = $this.data().column; 120 | var value_function = function(x){ 121 | return x[column]; 122 | } 123 | if(extra_column_functions[column]){ 124 | value_function = extra_column_functions[column]($this); 125 | } 126 | if (column){ 127 | data = _.sortBy(data, value_function); 128 | if($this.data().reverse){ 129 | data = data.reverse(); 130 | } 131 | } 132 | } 133 | render(); 134 | } 135 | 136 | function render(){ 137 | var result = ''; 138 | for(var i=0; i < data.length; ++i){ 139 | result += row_template({e:data[i], index:i}); 140 | } 141 | $body.html(result); 142 | } 143 | 144 | $table.find('thead th').on('click', handle_click); 145 | $table.on('rerender', render); 146 | render(); 147 | } 148 | 149 | TEMPLATES = {}; 150 | 151 | function update_activity_buttons(){ 152 | $('#activity-buttons').load('/activity_buttons/'); 153 | } 154 | 155 | $(document).ready(function(){ 156 | $('script[type="underscore/template"]').each(function(){ 157 | var $this = $(this); 158 | TEMPLATES[$this.attr("id")] = _.template($this.text()); 159 | }); 160 | 161 | //Batch 162 | $('#proposal-tabs a').click(show_proposal_tabs); 163 | $('#unranked li').on('click', batch_add); 164 | $('#accept').on('click', 'li', batch_rem); 165 | $('#proposal-tabs a').first().tab("show"); 166 | $('#batch-right-column').on('submit', '#add-comment', batch_add_comment); 167 | 168 | //Screening 169 | $('#right-column').on('click', '.voting-stripe button', vote_click); 170 | $('#right-column').on('click', '#nominate', nominate_click); 171 | $('#right-column').on('click', '#save', save_vote); 172 | $('#right-column').on('click', '#mark-read', mark_read); 173 | $('#right-column').on('submit', '#feedback-form', give_feedback); 174 | $('#right-column').on('submit', '#comment-form', leave_comment); 175 | 176 | $('.tab-button').on('click', function(ev){ 177 | console.log('click', $(this)); 178 | ev.preventDefault();$(this).tab('show') 179 | }); 180 | 181 | if($("#vote-form").length > 0){ 182 | nominate_status(); 183 | } 184 | }); 185 | -------------------------------------------------------------------------------- /static/js/underscore-1.8.3.min.js: -------------------------------------------------------------------------------- 1 | // Underscore.js 1.8.3 2 | // http://underscorejs.org 3 | // (c) 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors 4 | // Underscore may be freely distributed under the MIT license. 5 | (function(){function n(n){function t(t,r,e,u,i,o){for(;i>=0&&o>i;i+=n){var a=u?u[i]:i;e=r(e,t[a],a,t)}return e}return function(r,e,u,i){e=b(e,i,4);var o=!k(r)&&m.keys(r),a=(o||r).length,c=n>0?0:a-1;return arguments.length<3&&(u=r[o?o[c]:c],c+=n),t(r,e,u,o,c,a)}}function t(n){return function(t,r,e){r=x(r,e);for(var u=O(t),i=n>0?0:u-1;i>=0&&u>i;i+=n)if(r(t[i],i,t))return i;return-1}}function r(n,t,r){return function(e,u,i){var o=0,a=O(e);if("number"==typeof i)n>0?o=i>=0?i:Math.max(i+a,o):a=i>=0?Math.min(i+1,a):i+a+1;else if(r&&i&&a)return i=r(e,u),e[i]===u?i:-1;if(u!==u)return i=t(l.call(e,o,a),m.isNaN),i>=0?i+o:-1;for(i=n>0?o:a-1;i>=0&&a>i;i+=n)if(e[i]===u)return i;return-1}}function e(n,t){var r=I.length,e=n.constructor,u=m.isFunction(e)&&e.prototype||a,i="constructor";for(m.has(n,i)&&!m.contains(t,i)&&t.push(i);r--;)i=I[r],i in n&&n[i]!==u[i]&&!m.contains(t,i)&&t.push(i)}var u=this,i=u._,o=Array.prototype,a=Object.prototype,c=Function.prototype,f=o.push,l=o.slice,s=a.toString,p=a.hasOwnProperty,h=Array.isArray,v=Object.keys,g=c.bind,y=Object.create,d=function(){},m=function(n){return n instanceof m?n:this instanceof m?void(this._wrapped=n):new m(n)};"undefined"!=typeof exports?("undefined"!=typeof module&&module.exports&&(exports=module.exports=m),exports._=m):u._=m,m.VERSION="1.8.3";var b=function(n,t,r){if(t===void 0)return n;switch(null==r?3:r){case 1:return function(r){return n.call(t,r)};case 2:return function(r,e){return n.call(t,r,e)};case 3:return function(r,e,u){return n.call(t,r,e,u)};case 4:return function(r,e,u,i){return n.call(t,r,e,u,i)}}return function(){return n.apply(t,arguments)}},x=function(n,t,r){return null==n?m.identity:m.isFunction(n)?b(n,t,r):m.isObject(n)?m.matcher(n):m.property(n)};m.iteratee=function(n,t){return x(n,t,1/0)};var _=function(n,t){return function(r){var e=arguments.length;if(2>e||null==r)return r;for(var u=1;e>u;u++)for(var i=arguments[u],o=n(i),a=o.length,c=0;a>c;c++){var f=o[c];t&&r[f]!==void 0||(r[f]=i[f])}return r}},j=function(n){if(!m.isObject(n))return{};if(y)return y(n);d.prototype=n;var t=new d;return d.prototype=null,t},w=function(n){return function(t){return null==t?void 0:t[n]}},A=Math.pow(2,53)-1,O=w("length"),k=function(n){var t=O(n);return"number"==typeof t&&t>=0&&A>=t};m.each=m.forEach=function(n,t,r){t=b(t,r);var e,u;if(k(n))for(e=0,u=n.length;u>e;e++)t(n[e],e,n);else{var i=m.keys(n);for(e=0,u=i.length;u>e;e++)t(n[i[e]],i[e],n)}return n},m.map=m.collect=function(n,t,r){t=x(t,r);for(var e=!k(n)&&m.keys(n),u=(e||n).length,i=Array(u),o=0;u>o;o++){var a=e?e[o]:o;i[o]=t(n[a],a,n)}return i},m.reduce=m.foldl=m.inject=n(1),m.reduceRight=m.foldr=n(-1),m.find=m.detect=function(n,t,r){var e;return e=k(n)?m.findIndex(n,t,r):m.findKey(n,t,r),e!==void 0&&e!==-1?n[e]:void 0},m.filter=m.select=function(n,t,r){var e=[];return t=x(t,r),m.each(n,function(n,r,u){t(n,r,u)&&e.push(n)}),e},m.reject=function(n,t,r){return m.filter(n,m.negate(x(t)),r)},m.every=m.all=function(n,t,r){t=x(t,r);for(var e=!k(n)&&m.keys(n),u=(e||n).length,i=0;u>i;i++){var o=e?e[i]:i;if(!t(n[o],o,n))return!1}return!0},m.some=m.any=function(n,t,r){t=x(t,r);for(var e=!k(n)&&m.keys(n),u=(e||n).length,i=0;u>i;i++){var o=e?e[i]:i;if(t(n[o],o,n))return!0}return!1},m.contains=m.includes=m.include=function(n,t,r,e){return k(n)||(n=m.values(n)),("number"!=typeof r||e)&&(r=0),m.indexOf(n,t,r)>=0},m.invoke=function(n,t){var r=l.call(arguments,2),e=m.isFunction(t);return m.map(n,function(n){var u=e?t:n[t];return null==u?u:u.apply(n,r)})},m.pluck=function(n,t){return m.map(n,m.property(t))},m.where=function(n,t){return m.filter(n,m.matcher(t))},m.findWhere=function(n,t){return m.find(n,m.matcher(t))},m.max=function(n,t,r){var e,u,i=-1/0,o=-1/0;if(null==t&&null!=n){n=k(n)?n:m.values(n);for(var a=0,c=n.length;c>a;a++)e=n[a],e>i&&(i=e)}else t=x(t,r),m.each(n,function(n,r,e){u=t(n,r,e),(u>o||u===-1/0&&i===-1/0)&&(i=n,o=u)});return i},m.min=function(n,t,r){var e,u,i=1/0,o=1/0;if(null==t&&null!=n){n=k(n)?n:m.values(n);for(var a=0,c=n.length;c>a;a++)e=n[a],i>e&&(i=e)}else t=x(t,r),m.each(n,function(n,r,e){u=t(n,r,e),(o>u||1/0===u&&1/0===i)&&(i=n,o=u)});return i},m.shuffle=function(n){for(var t,r=k(n)?n:m.values(n),e=r.length,u=Array(e),i=0;e>i;i++)t=m.random(0,i),t!==i&&(u[i]=u[t]),u[t]=r[i];return u},m.sample=function(n,t,r){return null==t||r?(k(n)||(n=m.values(n)),n[m.random(n.length-1)]):m.shuffle(n).slice(0,Math.max(0,t))},m.sortBy=function(n,t,r){return t=x(t,r),m.pluck(m.map(n,function(n,r,e){return{value:n,index:r,criteria:t(n,r,e)}}).sort(function(n,t){var r=n.criteria,e=t.criteria;if(r!==e){if(r>e||r===void 0)return 1;if(e>r||e===void 0)return-1}return n.index-t.index}),"value")};var F=function(n){return function(t,r,e){var u={};return r=x(r,e),m.each(t,function(e,i){var o=r(e,i,t);n(u,e,o)}),u}};m.groupBy=F(function(n,t,r){m.has(n,r)?n[r].push(t):n[r]=[t]}),m.indexBy=F(function(n,t,r){n[r]=t}),m.countBy=F(function(n,t,r){m.has(n,r)?n[r]++:n[r]=1}),m.toArray=function(n){return n?m.isArray(n)?l.call(n):k(n)?m.map(n,m.identity):m.values(n):[]},m.size=function(n){return null==n?0:k(n)?n.length:m.keys(n).length},m.partition=function(n,t,r){t=x(t,r);var e=[],u=[];return m.each(n,function(n,r,i){(t(n,r,i)?e:u).push(n)}),[e,u]},m.first=m.head=m.take=function(n,t,r){return null==n?void 0:null==t||r?n[0]:m.initial(n,n.length-t)},m.initial=function(n,t,r){return l.call(n,0,Math.max(0,n.length-(null==t||r?1:t)))},m.last=function(n,t,r){return null==n?void 0:null==t||r?n[n.length-1]:m.rest(n,Math.max(0,n.length-t))},m.rest=m.tail=m.drop=function(n,t,r){return l.call(n,null==t||r?1:t)},m.compact=function(n){return m.filter(n,m.identity)};var S=function(n,t,r,e){for(var u=[],i=0,o=e||0,a=O(n);a>o;o++){var c=n[o];if(k(c)&&(m.isArray(c)||m.isArguments(c))){t||(c=S(c,t,r));var f=0,l=c.length;for(u.length+=l;l>f;)u[i++]=c[f++]}else r||(u[i++]=c)}return u};m.flatten=function(n,t){return S(n,t,!1)},m.without=function(n){return m.difference(n,l.call(arguments,1))},m.uniq=m.unique=function(n,t,r,e){m.isBoolean(t)||(e=r,r=t,t=!1),null!=r&&(r=x(r,e));for(var u=[],i=[],o=0,a=O(n);a>o;o++){var c=n[o],f=r?r(c,o,n):c;t?(o&&i===f||u.push(c),i=f):r?m.contains(i,f)||(i.push(f),u.push(c)):m.contains(u,c)||u.push(c)}return u},m.union=function(){return m.uniq(S(arguments,!0,!0))},m.intersection=function(n){for(var t=[],r=arguments.length,e=0,u=O(n);u>e;e++){var i=n[e];if(!m.contains(t,i)){for(var o=1;r>o&&m.contains(arguments[o],i);o++);o===r&&t.push(i)}}return t},m.difference=function(n){var t=S(arguments,!0,!0,1);return m.filter(n,function(n){return!m.contains(t,n)})},m.zip=function(){return m.unzip(arguments)},m.unzip=function(n){for(var t=n&&m.max(n,O).length||0,r=Array(t),e=0;t>e;e++)r[e]=m.pluck(n,e);return r},m.object=function(n,t){for(var r={},e=0,u=O(n);u>e;e++)t?r[n[e]]=t[e]:r[n[e][0]]=n[e][1];return r},m.findIndex=t(1),m.findLastIndex=t(-1),m.sortedIndex=function(n,t,r,e){r=x(r,e,1);for(var u=r(t),i=0,o=O(n);o>i;){var a=Math.floor((i+o)/2);r(n[a])i;i++,n+=r)u[i]=n;return u};var E=function(n,t,r,e,u){if(!(e instanceof t))return n.apply(r,u);var i=j(n.prototype),o=n.apply(i,u);return m.isObject(o)?o:i};m.bind=function(n,t){if(g&&n.bind===g)return g.apply(n,l.call(arguments,1));if(!m.isFunction(n))throw new TypeError("Bind must be called on a function");var r=l.call(arguments,2),e=function(){return E(n,e,t,this,r.concat(l.call(arguments)))};return e},m.partial=function(n){var t=l.call(arguments,1),r=function(){for(var e=0,u=t.length,i=Array(u),o=0;u>o;o++)i[o]=t[o]===m?arguments[e++]:t[o];for(;e=e)throw new Error("bindAll must be passed function names");for(t=1;e>t;t++)r=arguments[t],n[r]=m.bind(n[r],n);return n},m.memoize=function(n,t){var r=function(e){var u=r.cache,i=""+(t?t.apply(this,arguments):e);return m.has(u,i)||(u[i]=n.apply(this,arguments)),u[i]};return r.cache={},r},m.delay=function(n,t){var r=l.call(arguments,2);return setTimeout(function(){return n.apply(null,r)},t)},m.defer=m.partial(m.delay,m,1),m.throttle=function(n,t,r){var e,u,i,o=null,a=0;r||(r={});var c=function(){a=r.leading===!1?0:m.now(),o=null,i=n.apply(e,u),o||(e=u=null)};return function(){var f=m.now();a||r.leading!==!1||(a=f);var l=t-(f-a);return e=this,u=arguments,0>=l||l>t?(o&&(clearTimeout(o),o=null),a=f,i=n.apply(e,u),o||(e=u=null)):o||r.trailing===!1||(o=setTimeout(c,l)),i}},m.debounce=function(n,t,r){var e,u,i,o,a,c=function(){var f=m.now()-o;t>f&&f>=0?e=setTimeout(c,t-f):(e=null,r||(a=n.apply(i,u),e||(i=u=null)))};return function(){i=this,u=arguments,o=m.now();var f=r&&!e;return e||(e=setTimeout(c,t)),f&&(a=n.apply(i,u),i=u=null),a}},m.wrap=function(n,t){return m.partial(t,n)},m.negate=function(n){return function(){return!n.apply(this,arguments)}},m.compose=function(){var n=arguments,t=n.length-1;return function(){for(var r=t,e=n[t].apply(this,arguments);r--;)e=n[r].call(this,e);return e}},m.after=function(n,t){return function(){return--n<1?t.apply(this,arguments):void 0}},m.before=function(n,t){var r;return function(){return--n>0&&(r=t.apply(this,arguments)),1>=n&&(t=null),r}},m.once=m.partial(m.before,2);var M=!{toString:null}.propertyIsEnumerable("toString"),I=["valueOf","isPrototypeOf","toString","propertyIsEnumerable","hasOwnProperty","toLocaleString"];m.keys=function(n){if(!m.isObject(n))return[];if(v)return v(n);var t=[];for(var r in n)m.has(n,r)&&t.push(r);return M&&e(n,t),t},m.allKeys=function(n){if(!m.isObject(n))return[];var t=[];for(var r in n)t.push(r);return M&&e(n,t),t},m.values=function(n){for(var t=m.keys(n),r=t.length,e=Array(r),u=0;r>u;u++)e[u]=n[t[u]];return e},m.mapObject=function(n,t,r){t=x(t,r);for(var e,u=m.keys(n),i=u.length,o={},a=0;i>a;a++)e=u[a],o[e]=t(n[e],e,n);return o},m.pairs=function(n){for(var t=m.keys(n),r=t.length,e=Array(r),u=0;r>u;u++)e[u]=[t[u],n[t[u]]];return e},m.invert=function(n){for(var t={},r=m.keys(n),e=0,u=r.length;u>e;e++)t[n[r[e]]]=r[e];return t},m.functions=m.methods=function(n){var t=[];for(var r in n)m.isFunction(n[r])&&t.push(r);return t.sort()},m.extend=_(m.allKeys),m.extendOwn=m.assign=_(m.keys),m.findKey=function(n,t,r){t=x(t,r);for(var e,u=m.keys(n),i=0,o=u.length;o>i;i++)if(e=u[i],t(n[e],e,n))return e},m.pick=function(n,t,r){var e,u,i={},o=n;if(null==o)return i;m.isFunction(t)?(u=m.allKeys(o),e=b(t,r)):(u=S(arguments,!1,!1,1),e=function(n,t,r){return t in r},o=Object(o));for(var a=0,c=u.length;c>a;a++){var f=u[a],l=o[f];e(l,f,o)&&(i[f]=l)}return i},m.omit=function(n,t,r){if(m.isFunction(t))t=m.negate(t);else{var e=m.map(S(arguments,!1,!1,1),String);t=function(n,t){return!m.contains(e,t)}}return m.pick(n,t,r)},m.defaults=_(m.allKeys,!0),m.create=function(n,t){var r=j(n);return t&&m.extendOwn(r,t),r},m.clone=function(n){return m.isObject(n)?m.isArray(n)?n.slice():m.extend({},n):n},m.tap=function(n,t){return t(n),n},m.isMatch=function(n,t){var r=m.keys(t),e=r.length;if(null==n)return!e;for(var u=Object(n),i=0;e>i;i++){var o=r[i];if(t[o]!==u[o]||!(o in u))return!1}return!0};var N=function(n,t,r,e){if(n===t)return 0!==n||1/n===1/t;if(null==n||null==t)return n===t;n instanceof m&&(n=n._wrapped),t instanceof m&&(t=t._wrapped);var u=s.call(n);if(u!==s.call(t))return!1;switch(u){case"[object RegExp]":case"[object String]":return""+n==""+t;case"[object Number]":return+n!==+n?+t!==+t:0===+n?1/+n===1/t:+n===+t;case"[object Date]":case"[object Boolean]":return+n===+t}var i="[object Array]"===u;if(!i){if("object"!=typeof n||"object"!=typeof t)return!1;var o=n.constructor,a=t.constructor;if(o!==a&&!(m.isFunction(o)&&o instanceof o&&m.isFunction(a)&&a instanceof a)&&"constructor"in n&&"constructor"in t)return!1}r=r||[],e=e||[];for(var c=r.length;c--;)if(r[c]===n)return e[c]===t;if(r.push(n),e.push(t),i){if(c=n.length,c!==t.length)return!1;for(;c--;)if(!N(n[c],t[c],r,e))return!1}else{var f,l=m.keys(n);if(c=l.length,m.keys(t).length!==c)return!1;for(;c--;)if(f=l[c],!m.has(t,f)||!N(n[f],t[f],r,e))return!1}return r.pop(),e.pop(),!0};m.isEqual=function(n,t){return N(n,t)},m.isEmpty=function(n){return null==n?!0:k(n)&&(m.isArray(n)||m.isString(n)||m.isArguments(n))?0===n.length:0===m.keys(n).length},m.isElement=function(n){return!(!n||1!==n.nodeType)},m.isArray=h||function(n){return"[object Array]"===s.call(n)},m.isObject=function(n){var t=typeof n;return"function"===t||"object"===t&&!!n},m.each(["Arguments","Function","String","Number","Date","RegExp","Error"],function(n){m["is"+n]=function(t){return s.call(t)==="[object "+n+"]"}}),m.isArguments(arguments)||(m.isArguments=function(n){return m.has(n,"callee")}),"function"!=typeof/./&&"object"!=typeof Int8Array&&(m.isFunction=function(n){return"function"==typeof n||!1}),m.isFinite=function(n){return isFinite(n)&&!isNaN(parseFloat(n))},m.isNaN=function(n){return m.isNumber(n)&&n!==+n},m.isBoolean=function(n){return n===!0||n===!1||"[object Boolean]"===s.call(n)},m.isNull=function(n){return null===n},m.isUndefined=function(n){return n===void 0},m.has=function(n,t){return null!=n&&p.call(n,t)},m.noConflict=function(){return u._=i,this},m.identity=function(n){return n},m.constant=function(n){return function(){return n}},m.noop=function(){},m.property=w,m.propertyOf=function(n){return null==n?function(){}:function(t){return n[t]}},m.matcher=m.matches=function(n){return n=m.extendOwn({},n),function(t){return m.isMatch(t,n)}},m.times=function(n,t,r){var e=Array(Math.max(0,n));t=b(t,r,1);for(var u=0;n>u;u++)e[u]=t(u);return e},m.random=function(n,t){return null==t&&(t=n,n=0),n+Math.floor(Math.random()*(t-n+1))},m.now=Date.now||function(){return(new Date).getTime()};var B={"&":"&","<":"<",">":">",'"':""","'":"'","`":"`"},T=m.invert(B),R=function(n){var t=function(t){return n[t]},r="(?:"+m.keys(n).join("|")+")",e=RegExp(r),u=RegExp(r,"g");return function(n){return n=null==n?"":""+n,e.test(n)?n.replace(u,t):n}};m.escape=R(B),m.unescape=R(T),m.result=function(n,t,r){var e=null==n?void 0:n[t];return e===void 0&&(e=r),m.isFunction(e)?e.call(n):e};var q=0;m.uniqueId=function(n){var t=++q+"";return n?n+t:t},m.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var K=/(.)^/,z={"'":"'","\\":"\\","\r":"r","\n":"n","\u2028":"u2028","\u2029":"u2029"},D=/\\|'|\r|\n|\u2028|\u2029/g,L=function(n){return"\\"+z[n]};m.template=function(n,t,r){!t&&r&&(t=r),t=m.defaults({},t,m.templateSettings);var e=RegExp([(t.escape||K).source,(t.interpolate||K).source,(t.evaluate||K).source].join("|")+"|$","g"),u=0,i="__p+='";n.replace(e,function(t,r,e,o,a){return i+=n.slice(u,a).replace(D,L),u=a+t.length,r?i+="'+\n((__t=("+r+"))==null?'':_.escape(__t))+\n'":e?i+="'+\n((__t=("+e+"))==null?'':__t)+\n'":o&&(i+="';\n"+o+"\n__p+='"),t}),i+="';\n",t.variable||(i="with(obj||{}){\n"+i+"}\n"),i="var __t,__p='',__j=Array.prototype.join,"+"print=function(){__p+=__j.call(arguments,'');};\n"+i+"return __p;\n";try{var o=new Function(t.variable||"obj","_",i)}catch(a){throw a.source=i,a}var c=function(n){return o.call(this,n,m)},f=t.variable||"obj";return c.source="function("+f+"){\n"+i+"}",c},m.chain=function(n){var t=m(n);return t._chain=!0,t};var P=function(n,t){return n._chain?m(t).chain():t};m.mixin=function(n){m.each(m.functions(n),function(t){var r=m[t]=n[t];m.prototype[t]=function(){var n=[this._wrapped];return f.apply(n,arguments),P(this,r.apply(m,n))}})},m.mixin(m),m.each(["pop","push","reverse","shift","sort","splice","unshift"],function(n){var t=o[n];m.prototype[n]=function(){var r=this._wrapped;return t.apply(r,arguments),"shift"!==n&&"splice"!==n||0!==r.length||delete r[0],P(this,r)}}),m.each(["concat","join","slice"],function(n){var t=o[n];m.prototype[n]=function(){return P(this,t.apply(this._wrapped,arguments))}}),m.prototype.value=function(){return this._wrapped},m.prototype.valueOf=m.prototype.toJSON=m.prototype.value,m.prototype.toString=function(){return""+this._wrapped},"function"==typeof define&&define.amd&&define("underscore",[],function(){return m})}).call(this); 6 | //# sourceMappingURL=underscore-min.map -------------------------------------------------------------------------------- /static/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: / 3 | 4 | -------------------------------------------------------------------------------- /stats.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import sys 3 | import json 4 | import collections 5 | import logic as l 6 | 7 | def parse_log(f): 8 | 9 | prefix = 'INFO:logic:' 10 | seen = collections.Counter() 11 | for row in open(f): 12 | if not row.startswith(prefix): 13 | continue 14 | seen[json.loads(row[len(prefix):])['key']] += 1 15 | return seen 16 | 17 | def avg(v): 18 | v = list(v) 19 | return sum(float(x) for x in v)/len(v) 20 | 21 | def main(): 22 | actions = parse_log(sys.argv[1]) 23 | 24 | print '\nScreening' 25 | 26 | q = 'SELECT count(*) FROM proposals' 27 | proposal_count = l.scalar(q) 28 | print 'There were {} proposals.'.format(proposal_count) 29 | 30 | q = 'SELECT count(*), voter FROM votes group by voter' 31 | print '{} voters did '.format(len(l.fetchall(q))) 32 | 33 | q = 'SELECT count(*) FROM votes' 34 | print '{:,} reviews'.format(actions['vote']) 35 | 36 | q = 'SELECT COUNT(*) FROM votes WHERE nominate' 37 | print 'and gave {} nominations.'.format(l.scalar(q)) 38 | 39 | q = 'SELECT id from discussion' 40 | print '{:,} messages were written,'.format(len(l.fetchall(q))) 41 | 42 | q = 'SELECT id from discussion WHERE feedback' 43 | print '{:,} of them feedback to proposal authors.'.format(len(l.fetchall(q))) 44 | 45 | q = 'SELECT count(*), proposal from VOTES group by proposal order by count' 46 | votes = l.fetchall(q) 47 | print 'Every proposal received at least {} reviews,'.format(votes[0].count) 48 | 49 | full_coverage = sum(1 for x in l.list_users() 50 | if (x.votes + x.proposals_made) == proposal_count) 51 | print 'and {} voters performed the incredible task of reviewing all of the proposals.'.format(full_coverage) 52 | 53 | 54 | 55 | print '\n\n' 56 | 57 | q = 'SELECT COUNT(*) FROM proposals WHERE batchgroup is not null' 58 | print '{} talks made it into the second round,'.format(l.scalar(q)) 59 | 60 | q = '''SELECT COUNT(*), batchgroup FROM proposals WHERE batchgroup is not null 61 | group by batchgroup''' 62 | print 'where they were grouped into {} batches.'.format(len(l.fetchall(q))) 63 | 64 | q = 'SELECT count(*) FROM batchvotes' 65 | batch_vote_count = l.scalar(q) 66 | print '{:,} reviews happened,'.format(actions['vote_group'], 67 | batch_vote_count) 68 | 69 | q= 'SELECT id from batchmessages' 70 | print '{} more messages were sent, and'.format(len(l.fetchall(q))) 71 | 72 | q = 'SELECT count(*), voter from batchvotes group by voter' 73 | print '{} voters participated.'.format(len(l.fetchall(q))) 74 | 75 | 76 | q = '''SELECT count(*), batchgroup from batchvotes group by batchgroup 77 | order by count''' 78 | batch_count = l.fetchall(q) 79 | print 'Every batch got at least {} reviews'.format(batch_count[0].count) 80 | 81 | print 'to arrive at our final 95 accepted talks!' 82 | 83 | 84 | if __name__ == '__main__': 85 | main() 86 | -------------------------------------------------------------------------------- /tables.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE users ( 2 | id BIGSERIAL PRIMARY KEY, 3 | email VARCHAR(254) NOT NULL, 4 | display_name VARCHAR(80), 5 | pw VARCHAR(80), 6 | created_on TIMESTAMP WITH TIME ZONE DEFAULT now(), 7 | approved_on TIMESTAMP WITH TIME ZONE DEFAULT NULL 8 | ); 9 | CREATE UNIQUE INDEX idx_users_email 10 | ON users (lower(email)); 11 | 12 | CREATE TABLE batchgroups ( 13 | id BIGSERIAL PRIMARY KEY, 14 | name VARCHAR(254), 15 | author_emails VARCHAR(254)[], 16 | locked BOOLEAN DEFAULT FALSE 17 | ); 18 | 19 | 20 | CREATE TABLE proposals ( 21 | id BIGINT PRIMARY KEY, 22 | updated TIMESTAMP WITH TIME ZONE DEFAULT now(), 23 | added_on TIMESTAMP WITH TIME ZONE DEFAULT now(), 24 | vote_count INT DEFAULT 0, --Total # of votes 25 | voters BIGINT[] DEFAULT '{}', 26 | batchgroup BIGINT REFERENCES batchgroups DEFAULT NULL, 27 | 28 | withdrawn BOOLEAN DEFAULT FALSE, 29 | 30 | author_emails VARCHAR(254)[], 31 | author_names VARCHAR(254)[], 32 | 33 | data JSONB, 34 | data_history JSONB, 35 | 36 | accepted BOOLEAN DEFAULT NULL 37 | ); 38 | 39 | CREATE TABLE schedules ( 40 | id BIGSERIAL PRIMARY KEY, 41 | proposal BIGINT REFERENCES proposals UNIQUE DEFAULT NULL, 42 | day INT, 43 | room VARCHAR(32), 44 | time TIME, 45 | duration INT 46 | ); 47 | 48 | CREATE UNIQUE INDEX idx_schedules 49 | ON schedules (day, room, time); 50 | 51 | DROP AGGREGATE IF EXISTS email_aggregate(VARCHAR(254)[]); 52 | CREATE AGGREGATE email_aggregate (basetype = VARCHAR(254)[], 53 | sfunc = array_cat, 54 | stype = VARCHAR(254)[], initcond = '{}'); 55 | 56 | CREATE OR REPLACE FUNCTION batch_change() RETURNS trigger AS 57 | $$ 58 | BEGIN 59 | UPDATE batchgroups SET 60 | author_emails=(SELECT email_aggregate(author_emails) 61 | FROM proposals WHERE batchgroup = NEW.batchgroup) 62 | WHERE id = NEW.batchgroup; 63 | RETURN NEW; 64 | END; 65 | $$ LANGUAGE 'plpgsql'; 66 | 67 | CREATE TRIGGER batch_change_trigger AFTER INSERT OR UPDATE 68 | ON proposals FOR EACH ROW EXECUTE PROCEDURE batch_change(); 69 | 70 | CREATE TABLE batchvotes ( 71 | batchgroup BIGINT REFERENCES batchgroups, 72 | voter BIGINT REFERENCES users, 73 | accept BIGINT[], 74 | created_on TIMESTAMP WITH TIME ZONE DEFAULT now(), 75 | updated_on TIMESTAMP WITH TIME ZONE DEFAULT now(), 76 | UNIQUE(voter, batchgroup) 77 | 78 | ); 79 | 80 | CREATE TABLE standards ( 81 | id BIGSERIAL PRIMARY KEY, 82 | description VARCHAR(127) 83 | ); 84 | 85 | CREATE TABLE votes ( 86 | id BIGSERIAL PRIMARY KEY, 87 | 88 | scores JSON, 89 | 90 | voter BIGINT REFERENCES users, 91 | proposal BIGINT REFERENCES proposals, 92 | 93 | nominate BOOLEAN DEFAULT FALSE, 94 | 95 | updated_on TIMESTAMP WITH TIME ZONE DEFAULT now(), 96 | added_on TIMESTAMP WITH TIME ZONE DEFAULT now(), 97 | UNIQUE (voter, proposal) 98 | ); 99 | 100 | CREATE OR REPLACE FUNCTION votes_change() RETURNS trigger AS 101 | $$ 102 | BEGIN 103 | UPDATE proposals SET 104 | vote_count=(SELECT count(*) FROM votes WHERE proposal=NEW.proposal), 105 | voters = ARRAY(SELECT voter FROM votes WHERE proposal=NEW.proposal) 106 | WHERE id=NEW.proposal; 107 | RETURN NEW; 108 | END; 109 | $$ LANGUAGE 'plpgsql'; 110 | 111 | CREATE TRIGGER votes_change_trigger AFTER INSERT OR UPDATE 112 | ON votes FOR EACH ROW EXECUTE PROCEDURE votes_change(); 113 | 114 | CREATE TABLE discussion ( 115 | id BIGSERIAL PRIMARY KEY, 116 | 117 | name VARCHAR(254) DEFAULT NULL, --Author feedback force 118 | frm BIGINT REFERENCES users, 119 | proposal BIGINT REFERENCES proposals, 120 | created TIMESTAMP WITH TIME ZONE DEFAULT now(), 121 | body TEXT, 122 | feedback BOOLEAN DEFAULT FALSE 123 | ); 124 | 125 | CREATE TABLE unread ( 126 | proposal BIGINT REFERENCES proposals, 127 | voter BIGINT REFERENCES users, 128 | PRIMARY KEY (proposal, voter) 129 | ); 130 | 131 | CREATE TABLE batchmessages ( 132 | id BIGSERIAL PRIMARY KEY, 133 | 134 | frm BIGINT REFERENCES users, 135 | batch BIGINT REFERENCES batchgroups, 136 | body TEXT, 137 | created TIMESTAMP WITH TIME ZONE DEFAULT now() 138 | ); 139 | 140 | CREATE TABLE batchunread ( 141 | batch BIGINT REFERENCES batchgroups, 142 | voter BIGINT REFERENCES users, 143 | PRIMARY KEY (batch, voter) 144 | ); 145 | 146 | CREATE TABLE confirmations ( 147 | id BIGSERIAL PRIMARY KEY, 148 | proposal BIGINT REFERENCES proposals, 149 | email VARCHAR(254), 150 | acknowledged BOOLEAN DEFAULT NULL 151 | ); 152 | -------------------------------------------------------------------------------- /templates/activity_button_fragment.html: -------------------------------------------------------------------------------- 1 | {%if request.user.revisit and not config.THIS_IS_BATCH %} 2 | 4 | 5 | 6 | {% endif %} 7 | {%if request.user.unread and not config.THIS_IS_BATCH %} 8 | 9 | {% endif %} 10 | 11 | -------------------------------------------------------------------------------- /templates/admin/admin_page.html: -------------------------------------------------------------------------------- 1 | {%extends "base.html"%} 2 | {%block body %} 3 | 10 | {%endblock body%} 11 | -------------------------------------------------------------------------------- /templates/admin/batchgroups.html: -------------------------------------------------------------------------------- 1 | {%extends "base.html"%} 2 | 3 | 4 | {%block body %} 5 | {%set locked_button %} 6 | 9 | {%endset%} 10 | 11 | {%set unlocked_button %} 12 | 14 | 15 | {%endset%} 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | {% for g in groups %} 30 | 31 | 32 | 33 | 34 | 35 | 36 | 43 | 44 | {% endfor %} 45 | 46 |
IdNameTalk CountSkip ConsensusTalk ConsensusesLocked
{{g.id}}{{g.name}}{{g.talk_count}}{{g.skip_consensus}}{{g.talk_consensus|join(', ')}} 37 | {%if g.locked%} 38 | {{locked_button|safe}} 39 | {%else%} 40 | {{unlocked_button|safe}} 41 | {%endif%} 42 |
47 | 48 |
49 |
50 | 51 | 53 |
54 | 55 |
56 | 57 | 60 | 63 | 64 | {%endblock body%} 65 | {%block extrajs %} 66 | 67 | 72 | 73 | 74 | 113 | {%endblock%} 114 | -------------------------------------------------------------------------------- /templates/admin/rough_scores.html: -------------------------------------------------------------------------------- 1 | {%extends "base.html"%} 2 | {%block body %} 3 |
4 |
5 | 6 | 8 |
9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |
RankProposalBatch GroupAuto GroupRough ScoreNomination 22 | Green EquivalentGreen PercentScore DeltaNominationsConsensusAccepted
32 | 60 | {%endblock body%} 61 | {%block extrajs %} 62 | 74 | 145 | {% endblock %} 146 | -------------------------------------------------------------------------------- /templates/admin/schedule.html: -------------------------------------------------------------------------------- 1 | {%extends "base.html"%} 2 | 3 | 4 | {%block body %} 5 | 6 | {% for day, rooms in schedule|groupby('day') %} 7 |

Day {{day+1}}

8 |
9 | {% for room, blocks in rooms|groupby('room') %} 10 | {% with %} 11 | {% set minimum = blocks[0].time|minutes %} 12 | {% set maximum = blocks[-1].time|minutes %} 13 |
14 |

Room {{room}}

15 | {% for block in blocks %} 16 |
19 |
20 |
{{block.time.isoformat()[:5]}}
21 |
{{block.given_duration}} Minutes
22 |
23 |
26 |
27 |
28 | {% endfor %} 29 | {%endwith%} 30 |
31 | {% endfor%} 32 |
33 | {% endfor %} 34 | 35 |
36 |
37 | 38 | {%endblock%} 39 | 40 | {%block extrajs %} 41 | 49 | 53 | 54 | 121 | {%endblock%} 122 | -------------------------------------------------------------------------------- /templates/admin/standards.html: -------------------------------------------------------------------------------- 1 | {%extends "base.html"%} 2 | {%block body %} 3 |

Standards

4 |
    5 | {% for r in standards %} 6 |
  • {{r}}
  • 7 | {% endfor %} 8 |
9 |
10 |
11 | 12 | 14 |
15 | 16 |
17 | 18 | {%endblock body%} 19 | -------------------------------------------------------------------------------- /templates/admin/user_list.html: -------------------------------------------------------------------------------- 1 | {%extends "base.html"%} 2 | {%block body %} 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | {% for u in users %} 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 35 | 36 | {% endfor %} 37 | 38 |
IdEmailNameVotesProposals MadeLast VoteCreated OnApproved
{{u.id}}{{u.email}}{{u.display_name}}{{u.votes}}{{u.proposals_made}}{{u.last_voted|date}}{{u.created_on|date}} 27 | {% if u.approved_on %} 28 | {{u.approved_on|date}} 29 | {% else %} 30 |
31 | 32 |
33 | {% endif %} 34 |
39 | {%endblock body%} 40 | -------------------------------------------------------------------------------- /templates/author_feedback.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 |
11 | {% with messages = get_flashed_messages() %} 12 | {% if messages %} 13 | {% for message in messages %} 14 | 15 | {% endfor %} 16 | {% endif %} 17 | {% endwith %} 18 |

Author Feedback

19 |

You Are: {{name}}

20 |

Proposal: {{proposal.title}} (#{{proposal.id}})

21 |

This page is for you to provide feedback to the PyCon program 22 | committee. We have some suggestions or questions, shown below.

23 | 24 |

Please be aware that any 25 | 27 | changes you make to your proposal 28 | will take 29 | a few hours to propagate here, to our proposal review system.

30 | 31 |

We frequently refer to the 32 | SpacePug Sample Proposal. 34 | In particular, we can't emphasize enough the importance of a detailed abstract and an outline with timings. 35 | Links to outside supporting material is fine, but your proposal shouldn't rely on these links! 36 | The more complete and self-contained your proposal is, the easier it is for us to evaluate it.

37 | 38 | 39 | {% for m in messages %} 40 | {% if m.feedback %} 41 | 42 | 43 | 44 | 45 | 46 | {% endif %} 47 | {% if m.name %} 48 | 49 | 50 | 51 | 52 | 53 | {% endif %} 54 | {% endfor %} 55 |
{{m.display_name}}{{m.created|date}}{{m.body}}
{{m.name}}{{m.created|date}}{{m.body}}
56 | 57 | {% if config.CUTOFF_FEEDBACK %} 58 |

Unfortunately, the deadline for submissions has passed; replies to feedback are now disabled.

59 | {% else %} 60 |
61 |
62 | 63 |
64 |
65 | 66 |
67 | 68 |
69 | {% endif %} 70 | 71 |
72 | 73 | 74 | -------------------------------------------------------------------------------- /templates/bad_feedback_key.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 |

Bad URL

10 |

Please make sure you have exactly copied the URL from the email 11 | you received, and try again!

12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | PyCon Program Committee Talk Proposal Review 5 | 6 | 7 | 8 | {%block extracss%}{%endblock%} 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 | 78 | 79 |
80 | 81 | {% with messages = get_flashed_messages() %} 82 | {% if messages %} 83 | {% for message in messages %} 84 | 85 | {% endfor %} 86 | {% endif %} 87 | {% endwith %} 88 | 89 | {% block body %} 90 | {% endblock %} 91 |
92 | 93 | 94 | 95 | 96 | {% block extrajs %} 97 | {% endblock %} 98 | 99 | 100 | -------------------------------------------------------------------------------- /templates/batch/batch.html: -------------------------------------------------------------------------------- 1 | {%extends "base.html"%} 2 | {%block body %} 3 |

Batch Review Groups

4 |
5 |
{{percent}}%
6 |
7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
NameVotedUnreadProposalsVotersMessagesTalks NominatedTotal NominationsTop Consensus ScoreLocked
25 | 51 | {%endblock%} 52 | 53 | {%block extrajs%} 54 | 60 | {%endblock%} 61 | -------------------------------------------------------------------------------- /templates/batch/batch_discussion_snippet.html: -------------------------------------------------------------------------------- 1 | {%import "batch/batch_render.html" as br%} 2 | {{br.batch_discussion(msgs)}} 3 | -------------------------------------------------------------------------------- /templates/batch/batch_render.html: -------------------------------------------------------------------------------- 1 | {% macro batch_discussion(msgs, locked=False) %} 2 |
3 |
4 |

Messages

5 |
6 | 7 | {% for m in msgs %} 8 | 9 | 10 | {% endfor %} 11 |
{{m.display_name}}{{m.created|date}}
{{m.body}}
12 | {% if not locked %} 13 |
14 |
15 |
16 | 17 | 18 |

Please be both kind and succinct with your comments.

19 |
20 | 21 |
22 |
23 | {%endif%} 24 |
25 | 26 | 27 | {% endmacro %} 28 | -------------------------------------------------------------------------------- /templates/batch/batchgroup.html: -------------------------------------------------------------------------------- 1 | {%extends "base.html"%} 2 | {%import "proposal_render.html" as pr%} 3 | {%import "batch/batch_render.html" as br%} 4 | 5 | {%block body %} 6 |

Group: {{group.name}}

7 | {% if group.locked %} 8 |

This Group is Locked

9 | {% endif%} 10 | 29 | 30 |
31 |
32 |
33 | {% for p in proposals %} 34 |
35 | {%if p.proposal.accepted != None %} 36 | {%if p.proposal.accepted%} 37 |
This talk has been accepted.
38 | {%else%} 39 |
This talk has been declined.
40 | {%endif%} 41 | {%endif%} 42 | {%if p.voters 43 | and (vote or request.user.email in config.OBSERVER_EMAILS) %} 44 |

Nominated by {{p.voters}}

45 | {%endif%} 46 | {{pr.proposal_render(p.proposal)}} 47 |
48 |

Screening Discussion

49 | {% for msg in p.discussion%} 50 |
51 |
52 | {% if msg.name %} 53 | {{msg.name}} 54 | {% else %} 55 | {{msg.display_name}} 56 | {% endif %} 57 |
58 |
59 | {{msg.body}} 60 |
61 | {% if msg.name %} 62 | 65 | {% endif %} 66 | {% if msg.feedback %} 67 | 70 | {% endif %} 71 |
72 | {% else %} 73 |

No dicussion in Screening

74 | {% endfor %} 75 | 76 |
77 | {% endfor %} 78 |
79 | 80 |
81 | 82 | 86 | 87 |
88 | 89 | {% if request.user.email in config.ADMIN_EMAILS%} 90 |
91 |
92 |

Accept/Decline Talks

93 |
94 | 95 | 96 | {% for p in proposals %} 97 | 98 | 99 | 108 | 109 | {% endfor %} 110 | 111 |
{{p.proposal.data.title}} 100 | 107 |
112 |
113 | {% endif %} 114 | 115 | {% if group.progcom_members %} 116 |
117 |
118 |

The following committee members have proposals in this group; 119 | please exercise discretion when discussing these proposals 120 | on slack or email.

121 |
    {% for m in group.progcom_members%}
  • {{m}}
  • {%endfor%}
122 |
123 |
124 | {% endif %} 125 | 126 | {% if request.user.email in config.OBSERVER_EMAILS %} 127 |

Voting disabled for observers.

128 | {% else %} 129 | {% if not group.locked %} 130 |
131 |
132 |

Select Talks

133 |
134 |
135 |
136 |

Selected Proposals

137 |
    138 | {% for v in vote.accept%} 139 | {% if v in proposal_map %} 140 |
  • 141 | 142 | {{proposal_map[v].data.title}} (#{{v}}) 143 | 144 |
  • 145 | {% endif %} 146 | {% endfor %} 147 |
148 |
No talks selected.
149 |

Remaining Proposals

150 |
    151 | {% for p in proposals%} 152 | 161 | {% endfor %} 162 |
163 |
164 | 169 |
170 |
171 | {%endif%} 172 | {%endif%} 173 | 174 | {{br.batch_discussion(msgs, group.locked)}} 175 | 176 | {% if vote or group.locked %} 177 |
178 |
179 |

Votes

180 |
181 | 182 | 183 | {% for v in all_votes %} 184 | 185 | 186 | 197 | 198 | {% endfor %} 199 | 200 |
{{v.display_name}} 187 | {% for id in v.accept %} 188 |
    189 | {% if id in proposal_map %} 190 |
  • {{proposal_map[id].data.title}} (#{{id}})
  • 191 | {% endif %} 192 |
193 | {% else %} 194 | Don't advance any proposals. 195 | {% endfor %} 196 |
201 | 202 |
203 | {% endif %} 204 | 205 |
206 |
207 | 211 | {%endblock body%} 212 | 213 | {%block extrajs%} 214 | 226 | {%endblock%} 227 | -------------------------------------------------------------------------------- /templates/batch/full_list.html: -------------------------------------------------------------------------------- 1 | {%extends "base.html"%} 2 | {%block body %} 3 |

Batch Review Groups

4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
Batch GroupIdTitleAuthor NamesProgram Committee MemberAcceptedConsensus
19 | {%endblock body%} 20 | {%block extrajs%} 21 | 48 | 61 | {%endblock%} 62 | -------------------------------------------------------------------------------- /templates/batch/my_pycon.html: -------------------------------------------------------------------------------- 1 | {%extends "base.html"%} 2 | {%block body %} 3 |

Your PyCon

4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
Batch GroupIdTitleAuthor NamesProposal ConsensusAccepted
18 | {%endblock body%} 19 | {%block extrajs%} 20 | 37 | 55 | {%endblock%} 56 | -------------------------------------------------------------------------------- /templates/batch/single_proposal.html: -------------------------------------------------------------------------------- 1 | {%extends "base.html"%} 2 | {%import "proposal_render.html" as pr%} 3 | 4 | {%block body %} 5 | 6 | {{pr.proposal_render(proposal)}} 7 |
8 |

Screening Discussion

9 | {% for msg in discussion%} 10 |
11 |
12 | {% if msg.name %} 13 | {{msg.name}} 14 | {% else %} 15 | {{msg.display_name}} 16 | {% endif %} 17 |
18 |
19 | {{msg.body}} 20 |
21 | {% if msg.name %} 22 | 25 | {% endif %} 26 | {% if msg.feedback %} 27 | 30 | {% endif %} 31 |
32 | {% else %} 33 |

No discussion in Screening

34 | {% endfor %} 35 | {%endblock body%} 36 | -------------------------------------------------------------------------------- /templates/confirmation.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | {%import "proposal_render.html" as pr%} 11 | 12 |
13 |

Congratulations!

14 |

Your proposal, listed below, was accepted to PyCon 2017! If you have any 15 | questions, want to make any changes or updates to your title or abstract, 16 | have scheduling requirements, or are unable to give your talk, please let me 17 | (njl@njl.us) know as soon as possible!

18 |
19 | {{pr.proposal_render(proposal, hidetimes=True)}} 20 |
21 | 22 | 23 | -------------------------------------------------------------------------------- /templates/discussion_snippet.html: -------------------------------------------------------------------------------- 1 | {%import "proposal_render.html" as pr%} 2 | {{ pr.discussion_render(unread, discussion) }} 3 | -------------------------------------------------------------------------------- /templates/email/accept.txt: -------------------------------------------------------------------------------- 1 | Dear {{name}}, 2 | 3 | Congratulations! Your talk, 4 | "{{title}}" 5 | has been accepted to PyCon 2017! 6 | 7 | Please read through this email carefully for your next steps. 8 | 9 | The committee judged the version of your talk that was submitted by our 10 | deadline. You can see that version of your talk here, 11 | {{url}} 12 | 13 | Visiting this link will confirm that you've received this email; please 14 | review your proposal. If you have any changes you need to make, please let 15 | me know. 16 | 17 | We'll be announcing the list of accepted talks shortly. If 18 | you can no longer give this talk, please contact me right away so I 19 | can find a replacement. If you need to make changes to your title or 20 | abstract, please contact me. 21 | 22 | We haven't yet completed the schedule, so we don't yet know whether 23 | you've been allocated a 30 or a 45 minute time slot. We'll be 24 | announcing the schedule shortly, so please stay tuned. 25 | 26 | If you have particular scheduling requirements (if you plan to come to 27 | PyCon late or leave early, for example), please let me know today or 28 | tomorrow, so that I can take your requirements into account. 29 | 30 | We'd like to have a collection of past proposals we can use in the 31 | future to help potential PyCon speakers out. If you are amenable 32 | to us publicly sharing your proposal, please reply to this email and let 33 | me know! 34 | 35 | You should register for the conference at 36 | https://us.pycon.org/2017/registration/ 37 | as soon as possible; please don't delay, tickets do sell out! 38 | 39 | Unlike many conferences, PyCon doesn't give free tickets to speakers, 40 | nor do we cover travel expenses. We do this because PyCon is a 41 | grassroots, community-organized conference and our core mission is to 42 | keep costs low. Thus, we ask everybody who can afford to pay to do so. 43 | However, we recognize that buying tickets and paying for travel can 44 | represent a financial hardship to some, and we really want to see you 45 | at PyCon! If you can't afford a ticket and/or travel expenses, 46 | please apply for financial aid. 47 | 48 | More information about financial aid can be found at 49 | https://us.pycon.org/2017/financial-assistance/ 50 | 51 | If you're applying for financial aid, make sure to mention that you're 52 | an accepted speaker. Please contact the financial aid team at 53 | pycon-aid@python.org if you have any questions. 54 | 55 | If you need assistance with visas or immigration documents, please 56 | contact our registration team (pycon2017@cteusa.com). 57 | 58 | If you have any other questions, comments, or feedback, please feel 59 | free to contact me (njl@njl.us), or the conference chair, Brandon Rhodes 60 | (pycon.brandon@gmail.com). 61 | 62 | On behalf of the PyCon staff, I want to thank you for your submission. 63 | We're really looking forward to seeing you at the conference; PyCon 2017 64 | promises to be the best PyCon yet, and that's because of our amazing 65 | speakers. We're thrilled that you're one of them. 66 | 67 | See you in Portland! 68 | 69 | 70 | Ned Jackson Lovely 71 | Program Chair, PyCon 2017 72 | njl@njl.us 73 | -------------------------------------------------------------------------------- /templates/email/decline.txt: -------------------------------------------------------------------------------- 1 | Dear {{name}}, 2 | 3 | Thank you very much for your PyCon 2017 talk proposal, 4 | "{{title}}". 5 | 6 | Unfortunately, your talk was not one of the ones selected to appear at 7 | PyCon 2017. 8 | 9 | Each year brings more talk submissions than the year before, and this 10 | year was no exception. Both the quantity and the quality of proposals 11 | we received were extraordinary. We only have 95 spaces in the schedule, 12 | and we had to make many hard choices. 13 | 14 | We'd like to have a collection of past proposals we can use in the 15 | future to help potential PyCon speakers out. If you are amenable 16 | to us publicly sharing your proposal, please reply to this email and 17 | let me know! We will work with you to make sure we anonymize your 18 | proposal properly. 19 | 20 | I hope you still decide to attend PyCon! It's an amazing conference, 21 | and we still have several venues in which you could deliver your 22 | material. Lightning talks are quick, five-minute talks on any topic, 23 | and open spaces provide dedicated rooms for informal, unscheduled 24 | proposals and discussions. 25 | 26 | Many subjects that aren't good fits for formal, scheduled talks are 27 | excellent subjects for open spaces! If you have a passion for your topic, 28 | then scheduling and hosting an open space is a great way to connect with 29 | others who share your interest. You can learn more about open spaces on 30 | the PyCon website, https://us.pycon.org/2017/events/open-spaces/ 31 | 32 | If you plan to come, you should register at 33 | https://us.pycon.org/2017/registration/ as soon as possible. We have 34 | a limited number of tickets, and we expect to sell out. Waiting to buy 35 | tickets means you might not be able to come! 36 | 37 | If you have any other questions, comments, or feedback, please feel 38 | free to contact me (njl@njl.us), or the conference chair, Brandon Rhodes 39 | (pycon.brandon@gmail.com). 40 | 41 | On behalf of the PyCon staff, I want to thank you again for your 42 | submission. You made our job of picking talks very hard, and the 43 | conference will be better for it. 44 | 45 | I hope to see you in Portland! 46 | 47 | 48 | 49 | Ned Jackson Lovely 50 | Program Chair, PyCon 2017 51 | njl@njl.us 52 | -------------------------------------------------------------------------------- /templates/email/feedback_notice.txt: -------------------------------------------------------------------------------- 1 | You have received feedback on your PyCon talk proposal, 2 | 3 | "{{proposal.title}}" 4 | 5 | A program committee member sent you the following comment, 6 | 7 | - - - - - - - - - - - - - - - - - - - - - - - 8 | {{body}} 9 | - - - - - - - - - - - - - - - - - - - - - - - 10 | 11 | To response to this message directly, please go here: 12 | 13 | {{url}} 14 | 15 | To edit your talk, please go here: 16 | 17 | {{edit_url}} 18 | 19 | Regards, 20 | 21 | The PyCon Program Committee 22 | -------------------------------------------------------------------------------- /templates/email/login_email.txt: -------------------------------------------------------------------------------- 1 | Someone has requested a login email for your PyCon program committee 2 | account. If you did not request this email, you can safely ignore this message. 3 | Otherwise, you can login into your program committee account below. 4 | 5 | {{url}} 6 | 7 | This link will expire in about twenty minutes. 8 | 9 | Regards, 10 | 11 | The PyCon Program Committee Robot 12 | -------------------------------------------------------------------------------- /templates/email/new_user_pending.txt: -------------------------------------------------------------------------------- 1 | Hi, 2 | 3 | {{name}} ({{email}}) has asked for access to the program committee 4 | application. Approve them at http://progcom.njl.us/admin/users/ 5 | 6 | Regards, 7 | 8 | The Robot 9 | -------------------------------------------------------------------------------- /templates/email/weekly_email.txt: -------------------------------------------------------------------------------- 1 | Hi, this is your friendly PyCon program committee bot. 2 | 3 | In the past seven days... 4 | {{new_proposal_count}} new proposals. 5 | {{updated_proposal_count}} updated proposals. 6 | {{votes_last_week}} votes recorded. 7 | {{active_discussions|length}} discussions contributed to. 8 | 9 | Our coverage currently looks like this: 10 | {% for sp in screening_progress %}{{sp.quantity}} proposals have {{sp.vote_count}} votes each. 11 | {% endfor %} 12 | 13 | The five busiest discussions are:{%for ad in active_discussions[:5] %} 14 | {{ad.count}} messages about {{ad.title}} 15 | http://progcom.njl.us/screening/{{ad.id}}/{% endfor %} 16 | 17 | Regards, 18 | 19 | PyCon Bot 20 | -------------------------------------------------------------------------------- /templates/email/welcome_user.txt: -------------------------------------------------------------------------------- 1 | Welcome to PyCon Program Committee Talk Review! 2 | 3 | If you aren't already subscribed to the pycon-pc mailing list, please take the 4 | time to do that now! We use the mailing list for announcements and organization. 5 | 6 | https://mail.python.org/mailman/listinfo/pycon-pc 7 | 8 | Before reviewing proposals, please read http://www.njl.us/essays/pycon-process/. 9 | It explains the screening criteria for the first stage of review in greater detail. 10 | 11 | When you're ready to get started, please log in at http://progcom.njl.us/. 12 | 13 | At that website you will see a talk proposal, chosen randomly from the pool of 14 | proposals that have the least reviews so far. Please review it based on the six 15 | criteria provided, nominate the talk for special consideration if you think it 16 | appropriate, and hit 'Save Vote' to save your vote. 17 | 18 | After you submit your evaluation, you'll be able to see how others have voted. If you 19 | disagree, feel free to start a discussion with other reviewers in the discussion box. 20 | After you have saved your evaluation, you can move on to the next proposal. Please 21 | vote for every proposal you are presented, in the state it is provided to you. We'll 22 | keep track of proposals that change after you vote on them, and allow you to go back 23 | and reevaluate them if necessary. 24 | 25 | Right now we are in the screening stage of talk proposal evaluation. Later stages 26 | of review will also occur on http://progcom.njl.us/, but we'll describe how those 27 | will work in a future mailing list message. 28 | 29 | We talk about proposals on a slack channel; you should receive an invite to the 30 | slack at https://pyconprogramcommittee.slack.com/ shortly. 31 | 32 | Thank you so much for your help in making this year's PyCon program--we can't do 33 | it without your time and thoughtful input. We hope you have fun reading these 34 | talk proposals! 35 | -------------------------------------------------------------------------------- /templates/my_votes.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html"%} 2 | {% import "vote_display.html" as vd %} 3 | {% import "progress_render.html" as prog%} 4 | {% block body %} 5 | 6 |

My Votes

7 | {{ prog.progress_render(percent)}} 8 | 9 | 10 | 11 | 12 | 13 | {% for s in standards %} 14 | 16 | {% endfor %} 17 | 18 | 19 | 21 | 22 | 23 | 24 | 25 |
IdTalk{{s.description|truncate(25)}}NominatedWhenUpdated
26 | 74 | {%endblock body%} 75 | {%block extrajs %} 76 | 90 | {%endblock%} 91 | -------------------------------------------------------------------------------- /templates/progress_render.html: -------------------------------------------------------------------------------- 1 | {%macro progress_render(percent) %} 2 |
4 |
5 | {{percent}}% 6 |
7 |
8 | {%endmacro%} 9 | -------------------------------------------------------------------------------- /templates/proposal_render.html: -------------------------------------------------------------------------------- 1 | {%import "vote_display.html" as vd%} 2 | 3 | {%macro proposal_render(proposal, anonymize=False, 4 | hidetimes=False) %} 5 | 6 | {% if not hidetimes %} 7 | 12 | {% endif %} 13 | 14 |
15 | {% for version in proposal.data_history%} 16 |
17 |

{{version.title}}

18 | 19 | 20 | 21 | 22 | {% if not anonymize %} 23 | 24 | 25 | {% endif %} 26 | 27 | 28 | {% if not hidetimes%} 29 | 30 | 31 | 32 | 33 | {%endif%} 34 | 35 |
Duration{{version.duration}}
Proposer(s){{proposal.authors|map(attribute='name')|join(", ")}}
# Speakers{{proposal.authors|length}}
Created{{proposal.added_on|date}}
Last Updated{{version.when|date}}
36 |

Description

37 |

38 | {{version.description|markdown}} 39 |

40 |

Audience

41 |

42 | {{version.audience}} 43 |

44 |

Outline

45 |

46 |

 47 |         {{version.outline}}
 48 |         
49 |

50 | {% if not anonymize %} 51 |

Additional Notes

52 |

53 | {{version.notes|markdown}} 54 |

55 | {% endif %} 56 |
57 | {%endfor%} 58 |
59 | {%endmacro%} 60 | 61 | {%macro user_vote_render(standards, existing_vote)%} 62 |
63 |
65 |

This Proposal...

66 |
67 |
68 | 69 | {% for s in standards %} 70 | 71 | 110 | 111 | 112 | {% endfor %} 113 |
72 |
73 | 83 | 92 | 101 | 108 |
109 |
{{s.description}}.
114 |
115 | {% if existing_vote and existing_vote.nominate %} 116 | 117 | 118 | {% else %} 119 | 120 | 121 | {% endif %} 122 |

123 | If you feel this talk didn't meet the objective standards but would 124 | still be a strong addition to PyCon, you can nominate it for special 125 | consideration. 126 |

127 |
128 | 139 |
140 |
141 | {%endmacro%} 142 | 143 | {%macro existing_vote_render(votes, standards) %} 144 |
145 |
146 |

Votes

147 |
148 | 149 | 150 | 151 | {% for s in standards %} 152 | 153 | {% endfor %} 154 | 155 | 156 | 157 | 158 | {% for v in votes %} 159 | 160 | {% for s in standards %} 161 | 162 | {% endfor %} 163 | 167 | 168 | {% endfor %} 169 | 170 |
{{s.id}}N
{{v.display_name}}{{vd.vote_display(v.scores[s.id])}}{% if v.nominate %} 164 | 165 | {%endif%} 166 |
171 |
172 | {%endmacro%} 173 | 174 | {%macro discussion_render(unread, discussion) %} 175 |
176 |
177 |

Discussion

178 |
179 |
    180 | {% for d in discussion %} 181 |
  • 185 |
    186 | {%if d.name%}Proposal Author{%else%}{{d.display_name}}{%endif%} 187 | {%if d.feedback%}
    To Author{%endif%} 188 |
    189 | {{d.created|date}} 190 | {%if d.name%}From Author{%endif%} 191 |
  • 192 |
  • 193 | {{d.body}} 194 |
  • 195 | {% endfor %} 196 |
197 | 198 | {% if unread %} 199 |
200 | 201 |
202 |
203 | 204 | 205 |
206 |
207 |
208 | {% endif %} 209 |
210 |
211 |
212 | 213 | 214 |

Please be both kind and succinct with your comments.

215 |
216 | 217 |
218 |
219 |
220 | {% endmacro %} 221 | -------------------------------------------------------------------------------- /templates/proposal_snippet.html: -------------------------------------------------------------------------------- 1 | {%import "proposal_render.html" as pr%} 2 | {{ pr.proposal_render(proposal, anonymize=True)}} 3 | -------------------------------------------------------------------------------- /templates/reconsider.html: -------------------------------------------------------------------------------- 1 | {%extends "base.html"%} 2 | {%block body %} 3 | 10 | {%endblock body%} 11 | -------------------------------------------------------------------------------- /templates/screening_proposal.html: -------------------------------------------------------------------------------- 1 | {%extends "base.html"%} 2 | {%import "proposal_render.html" as pr%} 3 | {%import "progress_render.html" as prog%} 4 | {%block body %} 5 |
6 |
7 | {{ prog.progress_render(percent)}} 8 |
9 |
10 |
11 |
12 | {{ pr.proposal_render(proposal, anonymize=True)}} 13 |
14 |
15 | {{ pr.user_vote_render(standards, existing_vote) }} 16 | 17 | {% if existing_vote %} 18 | {{pr.existing_vote_render(votes, standards)}} 19 | {% endif %} 20 | 21 |
22 |

Out of respect for potential speakers and your fellow committee 23 | members, please treat all of this information as confidential. 24 | The contents of proposals, votes, and discussions should not be 25 | shared outside the program committee.

26 |
27 | 28 | 29 | 30 | {{pr.discussion_render(unread, discussion)}} 31 | 32 | {% if not config.CUTOFF_FEEDBACK %} 33 |
34 |
35 |

Feedback

36 |
37 |
38 |
39 |
40 | 41 | 42 |
Messages sent here are emailed directly to the proposal author!
43 |
44 | 45 |
46 |
47 |
48 | {%endif%} 49 |
50 |
51 | {%endblock body%} 52 | -------------------------------------------------------------------------------- /templates/screening_stats.html: -------------------------------------------------------------------------------- 1 | {%extends "base.html"%} 2 | {%block body %} 3 |
4 |

Votes Per Day

5 |
6 | 7 |
8 |
9 |
10 |
11 |

Overall Vote Coverage

12 | 13 | 14 | 15 | {% for p in progress %} 16 | 17 | 18 | 19 | 20 | {% endfor %} 21 | 22 | 23 |
VotesProposals
{{p.vote_count}}{{p.quantity}}
Total{{total_proposals}}
24 |

Nominations

25 | 26 | 27 | 28 | {% for k,v in nomination_density %} 29 | 30 | {% endfor %} 31 | 32 |
NominationsProposals
{{k}}{{v}}
33 |

Active Discussions Past 7 Days

34 | 35 | 36 | 37 | {% for d in active_discussions %} 38 | 39 | 41 | {% endfor %} 42 | 43 |
DiscussionNew Messages
{{d.title}} 40 | {{d.count}}
44 | 45 |
46 |
47 |

Vote Coverage By Week Added

48 |
49 | 50 |
51 | 52 | 53 |

Votes Per Committee Member

54 | 55 | 56 | 57 | {% for user in users%} 58 | 59 | 60 | 61 | 62 | {% endfor %} 63 | 64 | 65 |
VoterVotes% Coverage
{{user.display_name}}{{user.votes}}{{"%.2f"|format(user.votes/(total_proposals-user.proposals_made)*100)}}
Total{{total_votes}}
66 |
67 | 68 | {%endblock body%} 69 | {%block extracss%} 70 | 72 | 76 | {%endblock%} 77 | {%block extrajs%} 78 | 79 | 80 | 131 | {%endblock%} 132 | -------------------------------------------------------------------------------- /templates/splash.html: -------------------------------------------------------------------------------- 1 | {%extends "base.html"%} 2 | {%block body %} 3 |

Hello

4 | {%endblock body%} 5 | -------------------------------------------------------------------------------- /templates/unread.html: -------------------------------------------------------------------------------- 1 | 2 | {%extends "base.html"%} 3 | {%block body %} 4 |

Proposals with unread discussions

5 | 6 | {% for u in unread %} 7 | 8 | 9 | {% else %} 10 | 11 | {% endfor %} 12 |
{{u.title}}#{{u.id}}
Nothing unread!
13 | {%endblock body%} 14 | -------------------------------------------------------------------------------- /templates/user/login.html: -------------------------------------------------------------------------------- 1 | {%extends "base.html"%} 2 | {%block body %} 3 |

This website is for members of the PyCon program committee. If you are 4 | interested in joining, please 5 | join the mailing list!

6 |
7 |
8 | 9 | 11 |
12 |
13 | 14 | 16 |
17 | 18 | Create Account 19 | Get Login Email 20 |
21 | {%endblock body%} 22 | -------------------------------------------------------------------------------- /templates/user/new_user.html: -------------------------------------------------------------------------------- 1 | {%extends "base.html"%} 2 | {%block body %} 3 |

This website is for members of the PyCon program committee. If you are 4 | interested in joining, please 5 | join the mailing list!

6 |

Please use the same email address you use on us.pycon.org!

7 |
8 |
9 | 10 | 11 |
12 |
13 | 14 | 16 |
17 |
18 | 19 | 21 |
22 | 23 |
24 | {%endblock body%} 25 | -------------------------------------------------------------------------------- /templates/user/request_reset.html: -------------------------------------------------------------------------------- 1 | {%extends "base.html"%} 2 | {%block body %} 3 |

If you would like a one-click login email sent, please enter your email address below. This 4 | will also allow you to reset your password.

5 |
6 |
7 | 8 | 10 |
11 | 12 |
13 | {%endblock body%} 14 | -------------------------------------------------------------------------------- /templates/user/reset_password.html: -------------------------------------------------------------------------------- 1 | 2 | {%extends "base.html"%} 3 | {%block body %} 4 |

Enter a new password below.

5 |
6 |
7 | 8 | 10 |
11 | 12 |
13 | {%endblock body%} 14 | -------------------------------------------------------------------------------- /templates/user_vote_snippet.html: -------------------------------------------------------------------------------- 1 | {%import "proposal_render.html" as pr%} 2 | {{ pr.user_vote_render(standards, existing_vote) }} 3 | {{ pr.existing_vote_render(votes, standards) }} 4 | -------------------------------------------------------------------------------- /templates/vote_display.html: -------------------------------------------------------------------------------- 1 | {%macro vote_display(vote) %} 2 | {% if vote == 2 %} 3 | 4 | {% elif vote == 1%} 5 | 6 | {% else %} 7 | 8 | {% endif%} 9 | {%endmacro%} 10 | --------------------------------------------------------------------------------