Tracking PR for registration rewrite #5

Merged
Muirrum merged 32 commits from rewrite into master 2022-12-15 18:32:08 -05:00
47 changed files with 13 additions and 8651 deletions
Showing only changes of commit 850a1538a7 - Show all commits

21
LICENSE
View file

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2017 Kevin Bohinski
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,20 +0,0 @@
# Hack@WPI 2018 Website
Used chronicel's registration system as a base but removed the need of mailchimp.
Rest is from their repo:
## Setup:
- Clone repo
- `pip3 install -r requirements.txt`
- Fill in all config files!
- Database:
```sh
python3
```
```python
from flask_app import db
db.create_all()
```
- Automatic waitlist management setup: Setup your favorite cron like tool to run `python3 manage_waitlist.py` nightly!
- `python3 flask_app.py`
- 🎉 🔥 🙌 💃 👌 💯

BIN
admin.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 529 KiB

View file

@ -1,44 +0,0 @@
import json
import csv
from io import StringIO
def json_to_csv(data):
# Opening JSON file and loading the data
# into the variable data
json_data=[]
if(type(data) is json):
json_data=data
elif(type(data) is str):
json_data=json.loads(data)
else:
json_data = json.loads(json.dumps(data))
# now we will open a file for writing
csv_out = StringIO("")
# create the csv writer object
csv_writer = csv.writer(csv_out)
# Counter variable used for writing
# headers to the CSV file
count = 0
for e in json_data:
if count == 0:
# Writing headers of CSV file
header = e.keys()
csv_writer.writerow(header)
count += 1
# Writing data of CSV file
csv_writer.writerow(e.values())
csv_out.seek(0)
return csv_out.read()
if __name__=="__main__":
with open('hack22.json') as f:
j = json.load(f)['data']
print(type(j))
print(json_to_csv(j))

View file

@ -1 +0,0 @@
sudo apt-get install python-mysqldb

View file

@ -1,696 +0,0 @@
import hashlib
import json
import os
import random
import string
import smtplib
import time
from datetime import datetime
import requests
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from dateutil.relativedelta import relativedelta
from flask import Flask, render_template, redirect, url_for, request, session, jsonify, send_from_directory
from flask_sqlalchemy import SQLAlchemy
from werkzeug.utils import secure_filename
from config_hackWPI import (api_keys, SERVER_LISTEN_ADDR, SERVER_PORT, WAITLIST_LIMIT, HACKATHON_TIME,
ALLOWED_EXTENSIONS, REGISTRATION_OPEN, MCE_API_KEY)
from mail import send_message
from admin import json_to_csv
app = Flask(__name__)
app.config.from_pyfile('config.py')
db = SQLAlchemy(app)
class Hacker(db.Model):
__tablename__ = 'hackers'
mlh_id = db.Column(db.Integer, primary_key=True)
registration_time = db.Column(db.Integer)
checked_in = db.Column(db.Boolean)
waitlisted = db.Column(db.Boolean)
admin = db.Column(db.Boolean)
first_name = db.Column(db.String(100))
last_name = db.Column(db.String(100))
email = db.Column(db.String(100))
shirt_size = db.Column(db.String(4))
special_needs = db.Column(db.String(300))
class AutoPromoteKeys(db.Model):
__tablename__ = 'AutoPromoteKeys'
id = db.Column(db.Integer, primary_key=True)
key = db.Column(db.String(4096))
val = db.Column(db.String(4096))
@app.errorhandler(413)
def filesize_too_big(erro):
print("Someone tried to send something too big")
return "That file was too big, please go back and try a smaller resume pdf"
@app.errorhandler(500)
def server_error():
print("There was a server error... If you're having trouble registering, please email hack@wpi.edu with your details and what you did to break our site :P")
@app.route('/sponsor')
def sponsorindex():
return render_template('home/sponsor/index.html', registration_open=REGISTRATION_OPEN)
@app.route('/sponsor/<path:path>')
def sponsor(path):
return send_from_directory('templates/home/sponsor', path)
@app.route('/')
def root():
return render_template('home/index.html', registration_open=REGISTRATION_OPEN)
@app.route('/assets/<path:path>')
def staticassets(path):
return send_from_directory('templates/home/assets', path)
@app.route('/resumepost', methods=['POST'])
def resumepost():
if not REGISTRATION_OPEN:
return 'Registration is currently closed.', 403
"""A last minute hack to let people post their resume after they've already registered"""
if request.method == 'POST':
if 'resume' not in request.files:
return "You tried to submit a resume with no file"
resume = request.files['resume']
if resume.filename == '':
return "You tried to submit a resume with no file"
if resume and not allowed_file(resume.filename):
return jsonify(
{'status': 'error', 'action': 'register',
'more_info': 'Invalid file type... Accepted types are txt pdf doc docx and rtf...'})
if resume and allowed_file(resume.filename):
# Good file!
filename = session['mymlh']['first_name'].lower() + '_' + session['mymlh']['last_name'].lower() + '_' + str(
session['mymlh']['id']) + '.' + resume.filename.split('.')[-1].lower()
filename = secure_filename(filename)
resume.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
return 'Resume uploaded! <a href="/dashboard">Return to dashboard</a>'
return "Something went wrong. If this keeps happening, contact hack@wpi.edu for assistance"
@app.route('/shirtpost', methods=['GET'])
def shirtpost():
if not REGISTRATION_OPEN:
return 'Registration is currently closed.', 403
if not is_logged_in():
return 'Not signed in'
"""MLH removed t-shirt and accommodations fields of profile in V3, this is our hacky substitute"""
if request.method == 'GET':
size = request.args.get('size')
special_needs = request.args.get('special_needs')
id = session['mymlh']['id']
upd = {}
if size:
upd['shirt_size'] = size
if special_needs:
upd['special_needs'] = special_needs
else:
upd['special_needs'] = None
if db.session.query(db.exists().where(Hacker.mlh_id == id)).scalar():
db.session.query(Hacker).filter(Hacker.mlh_id == id).update(upd)
db.session.commit()
return 'Info saved! <a href="../dashboard">Return to dashboard</a>'
return "Something went wrong. If this keeps happening, email hack@wpi.edu for assistance"
@app.route('/register', methods=['GET', 'POST'])
def register():
if not REGISTRATION_OPEN:
return 'Registration is currently closed.', 403
if request.method == 'GET':
# Register a hacker...
if is_logged_in() and db.session.query(
db.exists().where(Hacker.mlh_id == session['mymlh']['id'])).scalar():
# Already logged in, take them to dashboard
return redirect(url_for('dashboard'))
if request.args.get('code') is None:
# Get info from MyMLH
return redirect(
'https://my.mlh.io/oauth/authorize?client_id=' + api_keys['mlh']['client_id'] + '&redirect_uri=' +
api_keys['mlh'][
'callback'] + '&response_type=code&scope=email+phone_number+demographics+birthday+education')
if is_logged_in():
return render_template('register.html', name=session['mymlh']['first_name'])
code = request.args.get('code')
oauth_redirect = requests.post(
'https://my.mlh.io/oauth/token?client_id=' + api_keys['mlh']['client_id'] + '&client_secret=' +
api_keys['mlh'][
'secret'] + '&code=' + code + '&redirect_uri=' + api_keys['mlh'][
'callback'] + '&grant_type=authorization_code')
if oauth_redirect.status_code == 200:
access_token = json.loads(oauth_redirect.text)['access_token']
user_info_request = requests.get('https://my.mlh.io/api/v3/user.json?access_token=' + access_token)
if user_info_request.status_code == 200:
print(user_info_request.text)
user = json.loads(user_info_request.text)['data']
session['mymlh'] = user
if db.session.query(db.exists().where(Hacker.mlh_id == user['id'])).scalar():
# User already exists in db, log them in
return redirect(url_for('dashboard'))
return render_template('register.html', name=user['first_name'])
return redirect(url_for('register'))
if request.method == 'POST':
if not is_logged_in() or db.session.query(
db.exists().where(Hacker.mlh_id == session['mymlh']['id'])).scalar():
# Request flow == messed up somehow, restart them
return redirect(url_for('register'))
if 'resume' not in request.files or 'tos' not in request.form:
# No file or no TOS agreement?
return redirect(url_for('register'))
resume = request.files['resume']
if resume.filename == '':
resume = False
# No file selected
#return redirect(url_for('register'))
if resume and not allowed_file(resume.filename):
resume = False
#return jsonify(
# {'status': 'error', 'action': 'register',
# 'more_info': 'Invalid file type... Accepted types are txt pdf doc docx and rtf...'})
if resume and allowed_file(resume.filename):
# Good file!
filename = session['mymlh']['first_name'].lower() + '_' + session['mymlh']['last_name'].lower() + '_' + str(
session['mymlh']['id']) + '.' + resume.filename.split('.')[-1].lower()
filename = secure_filename(filename)
resume.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
# Determine if hacker should be placed on waitlist
waitlist = False
if db.session.query(Hacker).count() + 1 > WAITLIST_LIMIT:
print(session['mymlh']['first_name'] + " put on waitlist.")
waitlist = True
else:
print(session['mymlh']['first_name'] + " put on registered.")
first_name = session['mymlh']['first_name']
last_name = session['mymlh']['last_name']
email = session['mymlh']['email']
# Add the user to the database
print(Hacker(mlh_id=session['mymlh']['id'], registration_time=int(time.time()),
checked_in=False, waitlisted=waitlist, admin=False))
db.session.add(
Hacker(mlh_id=session['mymlh']['id'], registration_time=int(time.time()),
checked_in=False, waitlisted=waitlist, admin=False,
first_name=first_name, last_name=last_name, email=email))
db.session.commit()
print(session['mymlh']['first_name'] + " put on database successfully.")
# Send a welcome email
msg = 'Dear ' + session['mymlh']['first_name'] + '\n\n'
msg += 'Thanks for applying to Hack@WPI!\n'
if waitlist:
msg += 'Sorry! We have hit our registration capacity. You have been placed on the waitlist.\n'
msg += 'We will let you know if space opens up.\n'
else:
msg += 'You are fully registered! You are guarenteed a spot at the hackathon. Be sure to see the schedule at https://hack.wpi.edu. We will be sending a follow-up email with additional information prior to the event.\n\n'
msg += 'In the meantime, make sure to join the Slack!\n'
msg += 'http://bit.ly/hack22slack\n'
send_email(session['mymlh']['email'], 'Hack@WPI - Thanks for applying', msg)
# Finally, send them to their dashboard
return redirect(url_for('dashboard'))
@app.route('/mail')
def mail():
if not is_admin():
return redirect(url_for('register'))
return render_template('mail.html', MCE_API_KEY=MCE_API_KEY, NUM_HACKERS=len(admin(True)))
@app.route('/send', methods=['POST'])
def send():
if not is_admin():
return "Not Authorized", 401
args = request.json
print(args)
recipients = args.get("recipients") or ""
subject = args.get("subject") or ""
html = args.get("html") or ""
text = args.get("text") or ""
to = []
if(recipients == "org"):
to = ["hack@wpi.edu"]
elif(recipients == "admin"):
to = ["acm-sysadmin@wpi.edu"]
elif(recipients == "all"):
to = [x["email"] for x in admin(True)]
elif(recipients == "wpi"):
to = [ x["email"] for x in admin(True) if "wpi.edu" in x["email"] or \
(x["school"] and ("WPI" in x["school"]["name"] or "Worcester Polytechnic" in x["school"]["name"])) ]
# return str(to)
send_message(to, subject, html, text)
return "Message sent successfully to {0} recipients".format(len(to))
@app.route('/hackers.csv', methods=['GET'])
def hackers_csv():
if not is_admin():
return redirect(url_for('register'))
return json_to_csv(admin(True))
@app.route('/hackers', methods=['GET'])
def hackers():
if not is_admin():
return redirect(url_for('register'))
return jsonify(admin(True))
@app.route('/admin', methods=['GET'])
def admin(return_hackers=False):
# Displays total registration information...
# As Firebase could not be used with MyMLH, use PubNub to simulate the realtime database...
if not is_admin():
return redirect(url_for('register'))
waitlist_count = 0
total_count = 0
check_in_count = 0
shirt_count = {'xxs': 0, 'xs': 0, 's': 0, 'm': 0, 'l': 0, 'xl': 0, 'xxl': 0}
male_count = 0
female_count = 0
nb_count = 0
schools = {}
majors = {}
mlh_info = get_mlh_users()
hackers = []
result = db.session.query(Hacker)
for hacker in mlh_info:
obj = result.filter(Hacker.mlh_id == hacker['id']).one_or_none()
if obj is None:
continue
if obj.waitlisted:
waitlist_count += 1
if obj.checked_in:
check_in_count += 1
if hacker['gender'] == 'Male':
male_count += 1
elif hacker['gender'] == 'Female':
female_count += 1
else:
nb_count += 1
total_count += 1
if not 'school' in hacker:
print("Hacker has no school:")
print(hacker)
else:
if hacker['school']['name'] not in schools:
schools[hacker['school']['name']] = 1
else:
schools[hacker['school']['name']] += 1
if hacker['major'] not in majors:
majors[hacker['major']] = 1
else:
majors[hacker['major']] += 1
if obj.shirt_size in shirt_count:
shirt_count[obj.shirt_size] += 1
hackers.append({
'checked_in': obj.checked_in,
'waitlisted': obj.waitlisted,
'admin': obj.admin,
'registration_time': obj.registration_time,
'id': hacker['id'],
'email': hacker['email'],
'first_name': hacker['first_name'],
'last_name': hacker['last_name'],
'phone_number': hacker['phone_number'],
'shirt_size': (obj.shirt_size or '').upper(),
'special_needs': obj.special_needs,
'school': hacker['school'] if 'school' in hacker else 'NULL'
})
if(return_hackers):
return hackers
return render_template('admin.html', hackers=hackers, total_count=total_count, waitlist_count=waitlist_count,
check_in_count=check_in_count, shirt_count=shirt_count, female_count=female_count, nb_count=nb_count,
male_count=male_count, schools=schools, majors=majors,
mlh_url='https://my.mlh.io/api/v3/users.json?client_id=' + api_keys['mlh'][
'client_id'] + '&secret=' + api_keys['mlh'][
'secret'])
@app.route('/change_admin', methods=['GET'])
def change_admin():
# Promote or drop a given hacker to/from admin status...
if not is_admin():
return jsonify({'status': 'error', 'action': 'modify_permissions',
'more_info': 'You do not have permissions to perform this action...'})
if request.args.get('mlh_id') is None or request.args.get('action') is None:
return jsonify({'status': 'error', 'action': 'change_admin', 'more_info': 'Missing required field...'})
valid_actions = ['promote', 'demote']
if request.args.get('action') not in valid_actions:
return jsonify({'status': 'error', 'action': 'change_admin', 'more_info': 'Invalid action...'})
if request.args.get('action') == 'promote':
db.session.query(Hacker).filter(Hacker.mlh_id == request.args.get('mlh_id')).update({'admin': True})
elif request.args.get('action') == 'demote':
db.session.query(Hacker).filter(Hacker.mlh_id == request.args.get('mlh_id')).update({'admin': False})
db.session.commit()
return jsonify({'status': 'success', 'action': 'change_admin:' + request.args.get('action'), 'more_info': '',
'id': request.args.get('mlh_id')})
@app.route('/check_in', methods=['GET'])
def check_in():
# Check in a hacker...
if not is_admin():
return jsonify({'status': 'error', 'action': 'check_in',
'more_info': 'You do not have permissions to perform this action...'})
if request.args.get('mlh_id') is None:
return jsonify({'status': 'error', 'action': 'check_in', 'more_info': 'Missing required field...'})
# See if hacker was already checked in...
checked_in = db.session.query(Hacker.checked_in).filter(
Hacker.mlh_id == request.args.get('mlh_id')).one_or_none()
print(db.session.query(Hacker.checked_in).filter(Hacker.mlh_id == request.args.get('mlh_id')))
print(checked_in)
if checked_in and checked_in[0]:
return jsonify({'status': 'error', 'action': 'check_in', 'more_info': 'Hacker already checked in!'})
# Update db...
db.session.query(Hacker).filter(Hacker.mlh_id == request.args.get('mlh_id')).update({'checked_in': True})
db.session.commit()
mlh_info = get_mlh_user(request.args.get('mlh_id'))
# Send a welcome email...
msg = 'Dear ' + mlh_info['first_name'] + ',\n\n'
msg += 'Thanks for checking in!\n'
msg += 'We will start shortly, please check your dashboard for updates!\n'
msg += 'If you have not done so already, make sure to join the slack: https://bit.ly/hack22slack\n'
send_email(mlh_info['email'], 'HackWPI - Thanks for checking in', msg)
return jsonify(
{'status': 'success', 'action': 'check_in', 'more_info': '', 'id': request.args.get('mlh_id')})
@app.route('/drop', methods=['GET'])
def drop():
mlh_id = request.args.get('mlh_id')
# Drop a hacker's registration...
if mlh_id is None:
return jsonify({'status': 'error', 'action': 'drop', 'more_info': 'Missing required field...'})
if not is_admin() and not is_self(mlh_id):
return jsonify({'status': 'error', 'action': 'drop',
'more_info': 'You do not have permissions to perform this action...'})
row = db.session.query(Hacker.checked_in, Hacker.waitlisted, Hacker.first_name, Hacker.last_name, Hacker.email).filter(
Hacker.mlh_id == mlh_id).one_or_none()
if row is None:
return jsonify({'status': 'error', 'action': 'drop',
'more_info': 'Could not find hacker...'})
(checked_in, waitlisted, first_name, last_name, email) = row
if checked_in:
return jsonify({'status': 'error', 'action': 'drop', 'more_info': 'Cannot drop, already checked in...'})
# mlh_info = get_mlh_user(request.args.get('mlh_id'))
print(first_name + " trying to drop.")
# Delete from db...
row = db.session.query(Hacker).filter(Hacker.mlh_id == request.args.get('mlh_id')).first()
db.session.delete(row)
db.session.commit()
# Delete resume...
for ext in ALLOWED_EXTENSIONS:
filename = first_name.lower() + '_' + last_name.lower() + '_' + mlh_id + '.' + ext
try:
os.remove(app.config['UPLOAD_FOLDER'] + '/' + filename)
except OSError:
pass
# Send a goodbye email...
msg = 'Dear ' + first_name + ',\n\n'
msg += 'Your application was dropped, sorry to see you go.\n If this was a mistake, you can re-register by going to https://hack.wpi.edu/register'
send_email(email, 'Hack@WPI - Application Dropped', msg)
print(first_name + " dropped successfully.")
if is_self(mlh_id):
session.clear()
return redirect('https://hack.wpi.edu')
return jsonify({'status': 'success', 'action': 'drop', 'more_info': '', 'id': mlh_id})
@app.route('/promote_from_waitlist', methods=['GET'])
def promote_from_waitlist():
print("Time for promotion!")
# Promote a hacker from the waitlist...
if request.args.get('mlh_id') is None:
return jsonify({'status': 'error', 'action': 'promote_from_waitlist', 'more_info': 'Missing required field...'})
(key, val) = get_auto_promote_keys()
if request.args.get(key) is None:
if not is_admin():
return jsonify({'status': 'error', 'action': 'promote_from_waitlist',
'more_info': 'You do not have permissions to perform this action...'})
else:
if request.args.get(key) != val:
return jsonify({'status': 'error', 'action': 'promote_from_waitlist',
'more_info': 'Invalid auto promote keys...'})
(checked_in, waitlisted) = db.session.query(Hacker.checked_in, Hacker.waitlisted).filter(
Hacker.mlh_id == request.args.get('mlh_id')).one_or_none()
if checked_in:
return jsonify(
{'status': 'error', 'action': 'promote_from_waitlist',
'more_info': 'Cannot promote, already checked in...'})
if not waitlisted:
return jsonify(
{'status': 'error', 'action': 'promote_from_waitlist',
'more_info': 'Cannot promote, user is not waitlisted...'})
# Update db...
db.session.query(Hacker).filter(Hacker.mlh_id == request.args.get('mlh_id')).update({'waitlisted': False})
db.session.commit()
mlh_info = get_mlh_user(request.args.get('mlh_id'))
# Send a welcome email...
msg = 'Dear ' + mlh_info['first_name'] + ',\n\n'
msg += 'You are off the waitlist!\n'
msg += 'Room has opened up, and you are now welcome to come, we look forward to seeing you!\n'
msg += '**Note** Please see https://hack.wpi.edu and the event Slack for important information regarding the event format in accordance with new COVID safety guidelines\n'
msg += 'If you cannot make it, please drop your application at https://hack.wpi.edu/dashboard.\n'
send_email(mlh_info['email'], "Hack@WPI - You're off the Waitlist!", msg)
print(mlh_info['first_name'] + "is off the waitlist!")
return jsonify(
{'status': 'success', 'action': 'promote_from_waitlist', 'more_info': '', 'id': request.args.get('mlh_id')})
@app.route('/dashboard', methods=['GET'])
def dashboard():
# Display's a hacker's options...
if not is_logged_in():
return redirect(url_for('register'))
hacker = db.session.query(Hacker).filter(Hacker.mlh_id == session['mymlh']['id']).one_or_none()
# In case application dropped but user not logged out properly
if not hacker:
session.clear()
return redirect(url_for('register'))
shirt_size = (hacker.shirt_size or 'None').upper()
return render_template('dashboard.html', name=session['mymlh']['first_name'], id=session['mymlh']['id'],
admin=is_admin(), shirt_size=shirt_size, special_needs=hacker.special_needs)
@app.route('/tos', methods=['GET'])
def tos():
return render_template('tos.html')
def is_logged_in():
if session is None:
return False
if 'mymlh' not in session:
return False
return True
def is_admin():
if not is_logged_in():
return False
user_admin = db.session.query(Hacker.admin).filter(Hacker.mlh_id == session['mymlh']['id']).one_or_none()
if user_admin is None:
return False
if not user_admin[0]:
return False
return True
def is_self(mlh_id):
mlh_id = int(mlh_id)
if not is_logged_in():
return False
if session['mymlh']['id'] != mlh_id:
return False
return True
# TODO: Migrate to new mail module
def send_email(to, subject, body):
print("Email sent to: " + to)
body += '\nPlease let your friends know about the event as well!\n'
body += 'To update your status, you can go to hack.wpi.edu/dashboard\n'
body += '\nAll the best!\nThe Hack@WPI Team'
smtp_server = api_keys['smtp_email']['smtp_server']
smtp_port = api_keys['smtp_email']['smtp_port']
server = smtplib.SMTP(smtp_server, smtp_port)
# Enable TLS if we're using secure SMTP
if(smtp_port > 25):
server.starttls()
user = api_keys['smtp_email']['user']
sender = api_keys['smtp_email']['sender']
# Get bcc info if it exists
bcc = None
if ('bcc' in api_keys['smtp_email']):
bcc = api_keys['smtp_email']['bcc']
# Login if we're using server with auth
if ('pass' in api_keys['smtp_email']):
server.login(user, api_keys['smtp_email']['pass'])
msg = _create_MIMEMultipart(subject, sender, to, body, user, bcc)
server.send_message(msg)
print("Sucess! (Email to " + to)
def _create_MIMEMultipart(subject, sender, to, body, user=None, bcc=None):
msg = MIMEMultipart()
msg['Subject'] = subject
msg['From'] = sender
if (bcc):
msg['Bcc'] = bcc
msg.add_header('reply-to', user)
if type(to) == list:
msg['To'] = ", ".join(to)
else:
msg['To'] = to
msg.attach(MIMEText(body, 'plain'))
return msg
def get_mlh_user(mlh_id):
if not isinstance(mlh_id, int):
mlh_id = int(mlh_id)
req = requests.get(
'https://my.mlh.io/api/v3/users.json?client_id=' + api_keys['mlh']['client_id'] + '&secret=' + api_keys['mlh'][
'secret'])
if req.status_code == 200:
hackers = req.json()['data']
for hacker in hackers:
if hacker['id'] == mlh_id:
return hacker
def get_mlh_users():
page_index = 1
num_pages = 1
users = []
while page_index <= num_pages:
req = requests.get('https://my.mlh.io/api/v3/users.json?client_id={client_id}&secret={secret}&page={page}'.format(
client_id=api_keys['mlh']['client_id'],
secret=api_keys['mlh']['secret'],
page=page_index
))
if req.status_code == 200:
users += req.json()['data']
num_pages = req.json()['pagination']['total_pages']
page_index += 1
return users if users else None
def gen_new_auto_promote_keys(n=50):
key = new_key(n)
val = new_key(n)
db.session.add(AutoPromoteKeys(key=key, val=val))
db.session.commit()
return (key, val)
def get_auto_promote_keys():
row = db.session.query(AutoPromoteKeys).one_or_none()
if row is not None:
db.session.delete(row)
db.session.commit()
return (row.key, row.val)
else:
return ('', '')
def new_key(n):
return ''.join(random.SystemRandom().choice(string.ascii_letters + string.digits) for _ in range(n))
def allowed_file(filename):
return '.' in filename and \
filename.split('.')[-1].lower() in ALLOWED_EXTENSIONS
if __name__ == '__main__':
app.run(host=SERVER_LISTEN_ADDR, port=SERVER_PORT, threaded=True)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

View file

@ -1,45 +0,0 @@
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from config_hackWPI import (api_keys)
user = api_keys['smtp_email']['user']
bcc = api_keys['smtp_email']['bcc']
reply = api_keys['smtp_email']['reply']
sender = api_keys['smtp_email']['sender']
smtp_server = api_keys['smtp_email']['smtp_server']
smtp_port = api_keys['smtp_email']['smtp_port']
def send_message(recipients, subject="", html="", text=""):
print("Sending email to {0} with subject {1}".format(recipients, subject))
# Create message container - the correct MIME type is multipart/alternative.
msg = MIMEMultipart('alternative')
msg['Subject'] = subject
msg['From'] = sender
msg.add_header('reply-to', reply)
# Record the MIME types of both parts - text/plain and text/html.
part1 = MIMEText(text, 'plain')
part2 = MIMEText(html, 'html')
# Attach parts into message container.
# According to RFC 2046, the last part of a multipart message, in this case
# the HTML message, is best and preferred.
msg.attach(part1)
msg.attach(part2)
server = smtplib.SMTP(smtp_server, smtp_port)
# Enable TLS if we're using secure SMTP
if(smtp_port > 25):
server.starttls()
# Login if we're using server with auth
if ('pass' in api_keys['smtp_email']):
server.login(user, api_keys['smtp_email']['pass'])
server.sendmail(sender, recipients, msg.as_string())
server.quit()
if __name__ == "__main__":
send_message(["acm-sysadmin@wpi.edu"], "Test Subject", "<b>Test HTML</b>", "Test text")

View file

@ -1,15 +0,0 @@
#!/bin/bash
# cd to script dir
SCRIPT_RELATIVE_DIR=$(dirname "${BASH_SOURCE[0]}")
cd $SCRIPT_RELATIVE_DIR
# enable python venv
source ./venv/bin/activate
echo `which python`
# noot
python ./manage_waitlist.py
# disable venv
deactivate

View file

@ -1,60 +0,0 @@
import requests
from flask_app import db, Hacker, send_email, gen_new_auto_promote_keys
from config_hackWPI import WAITLIST_LIMIT, WEBHOOK_URL
num_attendees = db.session.query(Hacker).filter(Hacker.waitlisted == False).count()
num_waitlisted = db.session.query(Hacker).filter(Hacker.waitlisted == True).count()
num_to_promote = WAITLIST_LIMIT - num_attendees
if num_to_promote > num_waitlisted:
num_to_promote = num_waitlisted
print("PROMOTING " + str(num_to_promote) + " hackers")
num_to_promote_copy = num_to_promote
num_promoted = 0
errs = []
mlh_ids = db.session.query(Hacker.mlh_id).filter(Hacker.waitlisted == True).order_by(Hacker.registration_time)
for id in mlh_ids:
if num_to_promote > 0:
print('Attempting to promote: ' + str(id[0]))
(key, val) = gen_new_auto_promote_keys()
url = 'http://hack.wpi.edu/promote_from_waitlist' + '?mlh_id=' + str(id[0]) + '&' + key + '=' + val
print(url)
req = requests.get(url)
if req.status_code == 500:
errs.append('Server 500')
if not req.status_code == 200 or not req.json()['status'] == 'success':
print(req.status_code)
errs.append(req.json())
num_promoted += 1
num_to_promote -= 1
else:
break
print('\n')
msg = 'Hi, here is your daily waitlist report:\n'
msg += '\nBefore Promotion:\n'
msg += ' Reg Cap: ' + str(WAITLIST_LIMIT) + '\n'
msg += ' Num Attendees: ' + str(num_attendees) + '\n'
msg += ' Num Waitlisted: ' + str(num_waitlisted) + '\n'
msg += ' Num to Promote: ' + str(num_to_promote_copy) + '\n'
msg += '\nAfter Promotion:\n'
msg += ' Num Promoted (Attempted): ' + str(num_promoted) + '\n'
msg += ' Error Count: ' + str(len(errs)) + '\n'
msg += ' Num To Promote: ' + str(num_to_promote) + '\n'
msg += '\nPromotion Error Messages:\n'
msg += ' ' + str(errs) + '\n'
print(msg)
requests.post(WEBHOOK_URL, {
"content": msg
})
send_email('hack@wpi.edu', 'HackWPI - Daily Waitlist Report!', msg)
send_email('mikel@wpi.edu', 'HackWPI - Daily Waitlist Report!', msg)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View file

@ -1,9 +1,13 @@
Flask==1.0.0
Flask_SQLAlchemy==2.1
mailchimp3==2.0.7
pubnub==4.0.6
requests==2.20.0
Werkzeug==0.15.3
python_dateutil==2.6.0
mysql-connector-python==8.0.5
mysqlclient==1.3.12
alembic==1.8.1
click==8.1.3
Flask==2.2.2
Flask-Login==0.6.2
Flask-Migrate==4.0.0
Flask-SQLAlchemy==3.0.2
greenlet==2.0.1
itsdangerous==2.1.2
Jinja2==3.1.2
Mako==1.2.4
MarkupSafe==2.1.1
SQLAlchemy==1.4.44
Werkzeug==2.2.2

View file

@ -1,47 +0,0 @@
/* For best practice, move CSS below to an external CSS file. */
@keyframes fadeinall {
0% {
opacity: 1; }
97% {
opacity: 0; }
98% {
opacity: 0;
-webkit-transform: translateY(0);
transform: translateY(0); }
99% {
opacity: 0;
-webkit-transform: translateY(-100%);
transform: translateY(-100%); }
100% {
opacity: 0;
z-index: -1; } }
#loader {
opacity: 1;
position: fixed;
width: 100%;
height: 100%;
-webkit-transform: translateY(0);
-ms-transform: translateY(0);
transform: translateY(0);
background-color: #fff;
z-index: 999;
-webkit-animation-fill-mode: forwards;
animation-fill-mode: forwards;
-webkit-animation: fadeinall 1s normal both;
animation: fadeinall 1s normal both;
-webkit-animation-delay: 0.3s;
animation-delay: 0.3s;
}
#loader {
background-color: #222;
}
body {
background-color: #222222;
color: whitesmoke;
}
input, button, select, textarea {
border-radius: 5px;
}

File diff suppressed because one or more lines are too long

View file

@ -1,128 +0,0 @@
@font-face {font-family: "Krungthep"; src: url("//db.onlinewebfonts.com/t/736cf5b08b01082a3645e14038868e20.eot"); src: url("//db.onlinewebfonts.com/t/736cf5b08b01082a3645e14038868e20.eot?#iefix") format("embedded-opentype"), url("//db.onlinewebfonts.com/t/736cf5b08b01082a3645e14038868e20.woff2") format("woff2"), url("//db.onlinewebfonts.com/t/736cf5b08b01082a3645e14038868e20.woff") format("woff"), url("//db.onlinewebfonts.com/t/736cf5b08b01082a3645e14038868e20.ttf") format("truetype"), url("//db.onlinewebfonts.com/t/736cf5b08b01082a3645e14038868e20.svg#Krungthep") format("svg"); }
html {
height: 100%;
}
body {
background-color: #003049;
font-family: 'Montserrat', sans-serif;
font-size: 1.1rem;
color: #eee;
position: relative;
min-height: 100%;
}
p {
line-height: 2rem;
}
#logo-container {
display: flex;
justify-content: center;
flex-direction: row;
padding-top: 5px;
padding-bottom: 5px;
height: 100%;
width: 100%;
}
#goat {
height: 100%;
}
.button-collapse {
color: #26a69a;
}
.parallax-container {
min-height: 380px;
line-height: 0;
height: auto;
color: rgba(255,255,255,.9);
}
.parallax-container .section {
width: 100%;
}
label {
color: white !important;
}
@media only screen and (max-width : 992px) {
.parallax-container .section {
top: 40%;
}
#index-banner .section {
top: 10%;
}
}
@media only screen and (max-width : 600px) {
.parallax-container .section {
height: auto;
overflow: auto;
}
.container {
height: auto;
}
#index-banner .section {
top: 0;
}
}
#tagline {
font-weight: 600;
}
#event-info {
font-weight: 400;
}
#registration-banner {
min-height: 100px;
max-height: 150px;
}
#registration-banner .section{
top: auto;
}
.icon-block {
padding: 0 15px;
}
.icon-block .material-icons {
font-size: inherit;
}
.parallax img {
display: inherit;
max-width: 200%;
}
#mlh-trust-badge {
display: block;
max-width: 100px;
min-width: 60px;
position: fixed;
right: 50px;
top: 0;
width: 10%;
z-index: 10000;
}
nav {
line-height: normal !important;
font-family: "Jost", sans-serif;
font-weight: 700;
}
/*
.navbar-brand {
} */
.footer-nav {
position: absolute;
bottom: 0;
width: 100%;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 236 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

View file

@ -1,192 +0,0 @@
'use strict'
let pn = new PubNub({
publishKey: '',
subscribeKey: '',
ssl: true
})
pn.subscribe({
channels: ['hackWPI-admin']
})
pn.addListener({
message: (msg) => {
msg = msg.message
console.log(msg)
const id = msg.id
if (msg.action === 'check_in') {
document.getElementById(id + '-check_in').outerHTML = ''
document.getElementById(id + '-checked_in').innerHTML = 'True'
} else if (msg.action === 'drop') {
document.getElementById(id + '-row').outerHTML = ''
} else if (msg.action === 'promote_from_waitlist') {
document.getElementById(id + '-promote_from_waitlist').outerHTML = ''
document.getElementById(id + '-waitlisted').innerHTML = 'False'
} else if (msg.action === 'new_user' || msg.action === 'refresh' || msg.action.split(':')[0] === 'change_admin') {
window.location.reload(true)
}
}
})
let successColor = '#18BC9C'
let errColor = '#E74C3C'
$(document).ready(() => {
let timeout = setTimeout("window.location.reload(true);", 300000)
const resetTimeout = () => {
clearTimeout(timeout)
timeout = setTimeout("window.location.reload(true);", 300000)
}
$('a.check_in').click((e) => {
e.preventDefault()
let id = e.target.id.split('-')[0]
console.log('check in ' + id)
checkIn(id)
})
$('a.drop').click((e) => {
e.preventDefault()
let id = e.target.id.split('-')[0]
console.log('drop ' + id)
drop(id)
})
$('a.demote_admin').click((e) => {
e.preventDefault()
let id = e.target.id.split('-')[0]
console.log('demote admin ' + id)
changeAdmin(id, 'demote')
})
$('a.promote_admin').click((e) => {
e.preventDefault()
let id = e.target.id.split('-')[0]
console.log('promote admin ' + id)
changeAdmin(id, 'promote')
})
$('a.promote_from_waitlist').click((e) => {
e.preventDefault()
let id = e.target.id.split('-')[0]
console.log('promote waitlist ' + id)
promoteFromWaitlist(id)
})
})
const promoteFromWaitlist = (id) => {
swal({
title: 'Promote hacker ' + id + ' off the waitlist?',
text: 'Are you sure you wish to promote this hacker off the waitlist?',
type: 'info',
showCancelButton: true,
closeOnConfirm: false,
confirmButtonText: 'Yes, promote!',
confirmButtonColor: successColor
}, () => {
$.get('/promote_from_waitlist?mlh_id=' + id, (data) => {
let title = ''
let msg = ''
let type = ''
if (data.status === 'success') {
title = 'Promoted!'
msg = 'The hacker was successfully promoted off the waitlist!'
type = 'success'
} else {
title = 'Error!'
msg = JSON.stringify(data)
type = 'error'
}
swal(title, msg, type)
})
})
}
const changeAdmin = (id, action) => {
swal({
title: 'Modify:' + action + ' admin prviliges on hacker ' + id + ' ?',
text: 'Are you sure you wish to modify:' + action + ' this hacker\'s administrative privileges?',
type: 'warning',
showCancelButton: true,
closeOnConfirm: false,
confirmButtonText: 'Yes, ' + action + '!',
confirmButtonColor: errColor
}, () => {
$.get('/change_admin?mlh_id=' + id + '&action=' + action, (data) => {
let title = ''
let msg = ''
let type = ''
if (data.status === 'success') {
title = 'Modified!'
msg = 'The hacker\'s administrative privileges have been modified:' + action + '!'
type = 'success'
} else {
title = 'Error!'
msg = JSON.stringify(data)
type = 'error'
}
swal({title: title, msg: msg, type: type}, () => window.location.reload(true))
})
})
}
const drop = (id) => {
swal({
title: 'Drop hacker ' + id + ' ?',
text: 'Are you sure you wish to drop this hacker\'s application?',
type: 'warning',
showCancelButton: true,
closeOnConfirm: false,
confirmButtonText: 'Yes, drop!',
confirmButtonColor: errColor
}, () => {
$.get('/drop?mlh_id=' + id, (data) => {
let title = ''
let msg = ''
let type = ''
if (data.status === 'success') {
title = 'Dropped!'
msg = 'The hacker\'s application was successfully dropped!'
type = 'success'
} else {
title = 'Error!'
msg = JSON.stringify(data)
type = 'error'
}
swal(title, msg, type)
})
})
}
const checkIn = (id) => {
swal({
title: 'Check in hacker ' + id + ' ?',
text: 'Are you sure you wish to check in this hacker?',
type: 'info',
showCancelButton: true,
closeOnConfirm: false,
confirmButtonText: 'Yes, check in!',
confirmButtonColor: successColor
}, () => {
$.get('/check_in?mlh_id=' + id, (data) => {
let title = ''
let msg = ''
let type = ''
if (data.status === 'success') {
title = 'Checked in!'
msg = 'The hacker was checked in!'
type = 'success'
// Update table in admin.html (This is a hack, and a terrible one, but at least there's feedback on the change.
document.getElementById(id + '-checked_in').innerHTML = "True"
} else {
title = 'Error!'
msg = JSON.stringify(data)
type = 'error'
}
if (data.status === 'success' && data.action === 'check_in' && data.minor === true) {
msg += '\nATTENTION:\nHacker is a minor, please ensure they have the minor consent form!'
}
swal(title, msg, type)
})
})
}

View file

@ -1,3 +0,0 @@
$(document).ready(function(){
$('.parallax').parallax();
});

File diff suppressed because one or more lines are too long

View file

@ -1 +0,0 @@
(function(factory){if(typeof define==="function"&&define.amd){define(["jquery"],function($){return factory($)})}else if(typeof module==="object"&&typeof module.exports==="object"){exports=factory(require("jquery"))}else{factory(jQuery)}})(function($){$.easing.jswing=$.easing.swing;var pow=Math.pow,sqrt=Math.sqrt,sin=Math.sin,cos=Math.cos,PI=Math.PI,c1=1.70158,c2=c1*1.525,c3=c1+1,c4=2*PI/3,c5=2*PI/4.5;function bounceOut(x){var n1=7.5625,d1=2.75;if(x<1/d1){return n1*x*x}else if(x<2/d1){return n1*(x-=1.5/d1)*x+.75}else if(x<2.5/d1){return n1*(x-=2.25/d1)*x+.9375}else{return n1*(x-=2.625/d1)*x+.984375}}$.extend($.easing,{def:"easeOutQuad",swing:function(x){return $.easing[$.easing.def](x)},easeInQuad:function(x){return x*x},easeOutQuad:function(x){return 1-(1-x)*(1-x)},easeInOutQuad:function(x){return x<.5?2*x*x:1-pow(-2*x+2,2)/2},easeInCubic:function(x){return x*x*x},easeOutCubic:function(x){return 1-pow(1-x,3)},easeInOutCubic:function(x){return x<.5?4*x*x*x:1-pow(-2*x+2,3)/2},easeInQuart:function(x){return x*x*x*x},easeOutQuart:function(x){return 1-pow(1-x,4)},easeInOutQuart:function(x){return x<.5?8*x*x*x*x:1-pow(-2*x+2,4)/2},easeInQuint:function(x){return x*x*x*x*x},easeOutQuint:function(x){return 1-pow(1-x,5)},easeInOutQuint:function(x){return x<.5?16*x*x*x*x*x:1-pow(-2*x+2,5)/2},easeInSine:function(x){return 1-cos(x*PI/2)},easeOutSine:function(x){return sin(x*PI/2)},easeInOutSine:function(x){return-(cos(PI*x)-1)/2},easeInExpo:function(x){return x===0?0:pow(2,10*x-10)},easeOutExpo:function(x){return x===1?1:1-pow(2,-10*x)},easeInOutExpo:function(x){return x===0?0:x===1?1:x<.5?pow(2,20*x-10)/2:(2-pow(2,-20*x+10))/2},easeInCirc:function(x){return 1-sqrt(1-pow(x,2))},easeOutCirc:function(x){return sqrt(1-pow(x-1,2))},easeInOutCirc:function(x){return x<.5?(1-sqrt(1-pow(2*x,2)))/2:(sqrt(1-pow(-2*x+2,2))+1)/2},easeInElastic:function(x){return x===0?0:x===1?1:-pow(2,10*x-10)*sin((x*10-10.75)*c4)},easeOutElastic:function(x){return x===0?0:x===1?1:pow(2,-10*x)*sin((x*10-.75)*c4)+1},easeInOutElastic:function(x){return x===0?0:x===1?1:x<.5?-(pow(2,20*x-10)*sin((20*x-11.125)*c5))/2:pow(2,-20*x+10)*sin((20*x-11.125)*c5)/2+1},easeInBack:function(x){return c3*x*x*x-c1*x*x},easeOutBack:function(x){return 1+c3*pow(x-1,3)+c1*pow(x-1,2)},easeInOutBack:function(x){return x<.5?pow(2*x,2)*((c2+1)*2*x-c2)/2:(pow(2*x-2,2)*((c2+1)*(x*2-2)+c2)+2)/2},easeInBounce:function(x){return 1-bounceOut(1-x)},easeOutBounce:bounceOut,easeInOutBounce:function(x){return x<.5?(1-bounceOut(1-2*x))/2:(1+bounceOut(2*x-1))/2}})});

6478
static/js/materialize.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

View file

@ -1,279 +0,0 @@
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Hack @ WPI</title>
<link rel="icon" href="../static/favicon.png" type="image/png">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
<link rel="stylesheet" href="https://opensource.keycdn.com/fontawesome/4.7.0/font-awesome.min.css" integrity="sha384-dNpIIXE8U05kAbPhy3G1cz+yZmTzA6CY8Vg/u2L9xRnHjJiAK76m2BIEaSEV+/aU" crossorigin="anonymous">
<!--[if lt IE 9]> <script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script> <script src="https://oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js"></script><![endif]--><script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
<script src="http://www.kryogenix.org/code/browser/sorttable/sorttable.js"></script>
<script src="https://cdn.pubnub.com/sdk/javascript/pubnub.4.4.3.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/sweetalert/1.1.3/sweetalert.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/sweetalert/1.1.3/sweetalert.min.css">
<script src="//cdnjs.cloudflare.com/ajax/libs/tether/1.4.0/js/tether.min.js"></script>
<script src="../static/js/admin.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.5.0/Chart.min.js"></script>
</head>
<style>
.table-striped > tbody > tr:nth-of-type(odd) {
background-color: rgba(255, 255, 255, 0.15);
}
.table-hover > tbody > tr:hover {
background-color: rgba(255, 255, 255, 0.25);
}
table.sortable th:not(.sorttable_sorted):not(.sorttable_sorted_reverse):not(.sorttable_nosort):after {
content: " \25B4\25BE"
}
canvas {
max-width: 500px;
max-height: 500px;
}
</style>
<div class="contact-section" style="height: auto; background-repeat: repeat; background-size: contain;">
<div class="container-fluid" style="margin-left: 3%; margin-right: 3%;">
<div class="row" style="margin-top: 10%;">
<h5>JSON object of users from MLH (Including dropped applications):</h5>
<p><a href="{{ mlh_url }}"><b>Do NOT share this URL.</b></a></p>
<h5>Get registered hackers only:</h5>
<p><a href="/hackers">JSON</a> <a href="/hackers.csv">CSV</a></p>
</div>
<div class="row">
<div class="col-md-4">
<h2 style="">Gender:</h2>
<canvas id="genderCanvas" width="400" height="400"></canvas>
<script>
let genderCtx = document.getElementById('genderCanvas')
let genderChart = new Chart(genderCtx, {
type: 'doughnut',
data: {
labels: ['Female', 'Male', 'Non-Binary/Other'],
datasets: [
{
data: [{{ female_count }}, {{ male_count }}, {{ nb_count }}]
}
]
},
options: {
title: {
display: false
},
legend: {
display: false
},
labels: {
display: false
}
}
})
</script>
</div>
<div class="col-md-4">
<h2 style="">Schools:</h2>
<canvas id="schoolCanvas" width="400" height="400"></canvas>
<script>
let schoolNames = []
let schoolNums = []
{% for school in schools %}
schoolNames.push('{{ school }}')
schoolNums.push({{ schools[school] }})
{% endfor %}
let schoolCtx = document.getElementById('schoolCanvas')
let schoolChart = new Chart(schoolCtx, {
type: 'doughnut',
data: {
labels: schoolNames,
datasets: [
{
data: schoolNums
}
]
},
options: {
title: {
display: false
},
legend: {
display: false
},
labels: {
display: false
}
}
})
</script>
</div>
<div class="col-md-4">
<h2 style="">Majors:</h2>
<canvas id="majorCanvas" width="400" height="400"></canvas>
<script>
let majorNames = []
let majorNums = []
{% for major in majors %}
majorNames.push('{{ major }}')
majorNums.push({{ majors[major] }})
{% endfor %}
let majorCtx = document.getElementById('majorCanvas')
let majorChart = new Chart(majorCtx, {
type: 'doughnut',
data: {
labels: majorNames,
datasets: [
{
data: majorNums
}
]
},
options: {
title: {
display: false
},
legend: {
display: false
},
labels: {
display: false
}
}
})
</script>
</div>
</div>
<div class="row" style="">
<h2 style="">Counts:</h2>
<table id="counts" class="table table-striped table-hover table-condensed sortable">
<thead>
<tr>
<th>Total</th>
<th>Attendees</th>
<th>Waitlist</th>
<th>Checked In</th>
</tr>
</thead>
<tbody>
<tr>
<td>{{ total_count }}</td>
<td>{{ (total_count - waitlist_count) }}</td>
<td>{{ waitlist_count }}</td>
<td>{{ check_in_count }}</td>
</tr>
</tbody>
</table>
<h2 style="margin-top: 2%;">Shirts:</h2>
<table id="shirts" class="table table-striped table-hover table-condensed sortable">
<thead>
<tr>
<th>XXS</th>
<th>XS</th>
<th>S</th>
<th>M</th>
<th>L</th>
<th>XL</th>
<th>XXL</th>
</tr>
</thead>
<tbody>
<tr>
<td>{{ shirt_count['xxs'] }}</td>
<td>{{ shirt_count['xs'] }}</td>
<td>{{ shirt_count['s'] }}</td>
<td>{{ shirt_count['m'] }}</td>
<td>{{ shirt_count['l'] }}</td>
<td>{{ shirt_count['xl'] }}</td>
<td>{{ shirt_count['xxl'] }}</td>
</tr>
</tbody>
</table>
<h2 style="margin-top: 2%;">Hackers:</h2>
<table id="hackers" class="table table-striped table-hover table-condensed sortable">
<thead>
<tr>
<th>Options</th>
<th>Checked In?</th>
<th>Waitlisted?</th>
<th>Admin</th>
<th>MLH ID</th>
<th>Time Registered</th>
<th>Email</th>
<th>Name</th>
<th>Phone</th>
<th>Shirt</th>
<th>Special</th>
<th>School</th>
</tr>
</thead>
<tbody>
{% for hacker in hackers %}
<tr id="{{ hacker['id'] }}-row">
<td>
<div class="btn-group">
<a href="#" class="btn btn-primary dropdown-toggle" data-toggle="dropdown"><span
class="caret"></span></a>
<ul class="dropdown-menu">
{% if not hacker['checked_in'] %}
<li><a class="check_in" id="{{ hacker['id'] }}-check_in" href="#">Check In</a>
</li>
{% endif %}
{% if hacker['waitlisted'] and not hacker['checked_in'] %}
<li><a class="promote_from_waitlist"
id="{{ hacker['id'] }}-promote_from_waitlist"
href="#">Promote From Waitlist</a></li>
{% endif %}
<li class="divider"></li>
{% if not hacker['checked_in'] %}
<li><a class="drop" id="{{ hacker['id'] }}-drop" href="#">Drop Application</a>
</li>
{% endif %}
{% if hacker['admin'] %}
<li><a class="demote_admin" id="{{ hacker['id'] }}-demote_admin" href="#">Demote
Admin</a>
</li>
{% else %}
<li><a class="promote_admin" id="{{ hacker['id'] }}-promote_admin" href="#">Promote
Admin</a>
</li>
{% endif %}
</ul>
</div>
</td>
<td id="{{ hacker['id'] }}-checked_in">{{ hacker['checked_in'] }}</td>
<td id="{{ hacker['id'] }}-waitlisted">{{ hacker['waitlisted'] }}</td>
<td>{{ hacker['admin'] }}</td>
<td>{{ hacker['id'] }}</td>
<td>{{ hacker['registration_time'] }}</td>
<td>{{ hacker['email'] }}</td>
<td>{{ hacker['first_name'] + ' ' + hacker['last_name'] }}</td>
<td>{{ hacker['phone_number'] }}</td>
<td>{{ hacker['shirt_size'] }}</td>
<td>{{ hacker['special_needs'] }}</td>
<td>{{ hacker['school']['name'] }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<script>
$(document).ready(() => {
setTimeout('let myTh = document.getElementsByTagName("th")[14]; sorttable.innerSortFunction.apply(myTh, []); sorttable.innerSortFunction.apply(myTh, []);', 50)
})
</script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
<script src="../static/js/jquery.easing.min.js"></script>
</body>
</html>

View file

@ -1,142 +0,0 @@
{% include 'header.html' %}
<script src="https://cdnjs.cloudflare.com/ajax/libs/sweetalert/1.1.3/sweetalert.min.js"></script>
<link href="../static/css/materialize.min.css" rel="stylesheet">
<script>
function drop(id) {
if(window.confirm("Are you sure you wish to drop your application? This cannot be undone. (patiently wait after clicking the button)")) {
window.location.href = "/drop?mlh_id=" + id;
}
// swal({
// title: 'Drop your application?',
// text: 'Are you sure you wish to drop your application? This cannot be undone. (patiently wait after clicking the button)',
// type: 'warning',
// showCancelButton: true,
// closeOnConfirm: false,
// confirmButtonText: 'Yes, drop!',
// confirmButtonColor: errColor
// }, () => {
// $.get('/drop?mlh_id=' + id, (data) => {
// let title = ''
// let msg = ''
// let type = ''
// if (data.status === 'success'
// )
// {
// title = 'Dropped!'
// msg = 'Your application was successfully dropped!'
// type = 'success'
// }
// else
// {
// title = 'Error!'
// msg = JSON.stringify(data)
// type = 'error'
// }
// swal(title, msg, type)
// if (data.status === 'success') {
// setTimeout(() => {window.location = '/'
// },
// 5000
// )
// }
// })
// })
}
function resumeChange() {
let val = document.getElementById('resume').value;
val = val.split(/\\|\//gi).slice(-1)[0]
document.getElementById('filename').setAttribute('value', val);
}
</script>
<div class="contact-section" style="height: 100%;">
<div class="container">
<div class="row center justify-content-center" style="margin-top: 10%;">
<h1>Hi {{ name }}!</h1>
{% if waitlisted %}
<h2>You are waitlisted, if space opens up we will let you know...</h2>
{% else %}
<h2>You are fully registered! We look forward to seeing you!</h2>
Let us know if you have any questions by sending them to <a href="mailto:hack@wpi.edu">hack@wpi.edu</a>
<br>
<br>
Forgot to upload your resume while registering? No worries, submit it below.
</div>
<div class="row center justify-content-center">
<h5>Make sure to join the Slack and enter your shirt size below!</h5>
<p>(Please note that due to COVID-19 constraints, we can't guarantee that all participants will receive Hack@WPI t-shirts this year but we are trying to find a way!)</p>
<a href="https://join.slack.com/t/wpi-rqw4180/shared_invite/zt-1089kvjx1-3b6v152r6a_fX7NS8EGL9g" style="margin: 5px;" class="btn btn-lg btn-primary btn-invert">Slack</a>
</div>
<div class="row center justify-content-center" style="background-color: #974355; padding: 20; margin-left: 20; margin-right: 20; border-radius: 5px;">
<form method="get" action="/shirtpost">
<br>
<p><b>Optional Info:</b></p>
<div>
<p>Shirt Size (Currently selected: {{shirt_size}})</p>
<input type="radio" id="shirtxs" name="size" value="xs">
<label for="shirtxs">XS</label>
<input type="radio" id="shirts" name="size" value="s">
<label for="shirts">S</label>
<input type="radio" id="shirtm" name="size" value="m">
<label for="shirtm">M</label>
<input type="radio" id="shirtl" name="size" value="l">
<label for="shirtl">L</label>
<input type="radio" id="shirtxl" name="size" value="xl">
<label for="shirtxl">XL</label>
<input type="radio" id="shirtxxl" name="size" value="xxl">
<label for="shirtxxl">XXL</label>
<input type="radio" id="no" name="size" value="no">
<label for="no">Don't want one</label>
<p>Special Needs/Accommodations:</p>
<input type="text" name="special_needs" id="special_needs" value="{{ special_needs }}">
</div>
<br><br>
<input name="save" class="btn btn-lg btn-primary btn-invert" type="submit" value="Save"/>
<br><br><br>
</form>
</div>
<div class="row center justify-content-center">
<form method="post" action="/resumepost" enctype="multipart/form-data">
<p><b>If you'd like, add your resume to send to sponsors... </b></p>
<div class="file-field input-field">
<div class="btn">
<span>File</span>
<input id="resume" name="resume" type="file" oninput="resumeChange()"/>
</div>
<div class="file-path-wrapper white-text">
<input disabled id="filename" class="file-path validate white-text" type="text">
</div>
</div>
<input name="submit" class="btn btn-lg btn-primary btn-invert" type="submit" value="Submit"/>
</form>
{% endif %}
<br>
</div>
{% if admin %}
<br>
<div class="row justify-content-center">
<a href="/admin"><p class="btn">Admin Dashboard</p></a>
</div>
{% endif %}
<br>
</div>
<br>
<br>
<center><a onclick="drop('{{id}}')" id="drop-link"><p class="btn">Drop Application if you can't make it :(</p></a></center>
</div>
<script>
let errColor = '#E74C3C'
$(document).ready(() => {
$('#drop-link').click((e) => {
e.preventDefault()
let id = {{ id }}
drop(id)
})
})
</script>
{% include 'footer.html' %}

View file

@ -1,16 +0,0 @@
<!-- A hack to make sure the footer bar shows-->
<div style="height: 100px"></div>
<nav class="navbar navbar-inverse footer-nav" style= "margin-top: 30px">
<div class="container-fluid">
<div class="navbar-header">
</div>
<ul class="nav justify-content-end">
<li class="nav-item">
<a class="nav-link" href="mailto:hack@wpi.edu?subject=[Hackathon]">Contact Us</a>
</li>
</ul>
</div>
</nav>
</body>
</html>

View file

@ -1,42 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
<link href="../static/css/style.css" rel="stylesheet">
</head>
<style>
nav {
background-color: #F5665B;
border-color: #E7E4C6;
color: white;
}
</style>
<body>
<nav class="navbar navbar-inverse sticky-top navbar-expand">
<div class="container-fluid">
<div class="navbar-header">
<a class="navbar-brand" href="/">Hack@WPI</a>
</div>
<ul class="nav justify-content-end">
<!-- <li class="nav-item">
{% if registration_open or registration_open is not defined %}
<a class="nav-link active" href="/register">Register</a>
{% else %}
<a class="nav-link active disabled" href="#">Registration is closed</a>
{% endif %}
</li>
<li class="nav-item">
<a class="nav-link" href="../static/assets/Hack@WPI-SponsorshipInformation-2020.pdf">Sponsorship Packet</a>
</li> -->
<li class="nav-item">
<a class="nav-link" href="https://static.mlh.io/docs/mlh-code-of-conduct.pdf">Code of Conduct</a>
</li>
</ul>
</div>
</nav>

@ -1 +0,0 @@
Subproject commit b2a60294b8f9d37d8c486cd817f8260cc860caad

View file

@ -1,195 +0,0 @@
{% include 'header.html' %}
<div>
<style>
hr {
margin-top: 2rem;
margin-bottom: 2rem;
width: 70%;
border-width: 10px;
border-color: #F5665B;
}
h2 {
text-align: center;
}
h3 {
text-align: center;
}
h4 {
text-align: center;
}
h5 {
text-align: center;
}
h6 {
text-align: center;
margin-right: 100px;
margin-left: 100px;
}
div.main-logo {
margin-top: 16px;
}
.sponsors {
text-align: center;
display: block;
padding: 20px;
}
.sponsors > div {
margin-bottom: 40px;
}
.sponsors > div:last-child {
margin-bottom: 0;
}
.sponsors img {
display: inline-block;
margin-right: 40px;
/* Hack to fix overflow of bose logo*/
max-width: calc(100% - 10px);
}
.title-sponsors img{
max-height: 90px;
}
.silver-sponsors img {
max-height: 25px;
}
a {
color: black;
}
.x{
background-color: #E7E4C6;
margin-right: 100px;
margin-left: 100px;
color: black;
border-radius: 50px 50px 50px 50px;
padding-bottom: 10px;
padding-top:10px;
}
.snow-banner {
background-color: red;
width: 100%;
padding: 10px;
text-align: center;
}
</style>
<div class="snow-banner">
<b>Attention!</b> Due to the road conditions, we have delayed the schedule by an hour. We will still have live presentations, but submissions have been extended until 9am, and judging will take place at 10am.
</div>
<!--Logo + Date+ Hosted By -->
<div class="container">
<div class="row center main-logo">
<img src="../static/img/hackwpilogo.png" alt="logo" style='height: 100%; width: 100%; object-fit: contain'/>
</div>
</div>
<!--"Hackathon for all students" -->
<hr/>
<h2> A HACKATHON FOR ALL COLLEGE STUDENTS </h2>
<hr/>
<!--Location -->
<h3> WPI Campus Center </h3>
<h4>100 Institute Road</h4>
<h4> Worcester, MA 01609</h4>
<hr/>
<!-- Sponsors for 2020 -->
<h2> OUR SPONSORS FOR 2020 </h2>
<div class="container">
<div class="row center sponsors">
<div class="title-sponsors">
<img class="logo" src="../static/img/logos/bose.png"></div>
<div class="platinum-sponsors">
<h1>WPI Computer Science Department</h1>
</div>
<div class="gold-sponsors">
<img class="logo" src="../static/img/logos/iboss.png" style = "height: 150px">
<img class="logo" src="../static/img/logos/kronos.png" style = "height: 75px">
<img class="logo" src="../static/img/logos/paytronix.png" style = "height: 30px">
<img class="logo" src="../static/img/logos/abbvie.png" style = "height: 60px">
</div>
<div class="silver-sponsors">
<img class="logo" src="../static/img/logos/carbonblack.png" >
</div>
<div class="bronze-sponsors">
<img class="logo" src="../static/img/logos/wakefly.png" style = "height: 70px">
<img class="logo" src="../static/img/logos/amazon.png" style = "height: 70px">
<img class="logo" src="../static/img/logos/everquote.png" style = "height: 35px">
<img class="logo" src="../static/img/logos/synopses.png" style = "height: 20px">
<img class="logo" src="../static/img/logos/boozallenhamilton.png" style = "height: 35px">
</div>
</div>
</div>
<!--FAQs -->
<hr/>
<div class = "x">
<h2> FAQs </h2>
<div class = "FAQ">
<h4>Who can attend?</h4>
<h6> Our event is open to any college student! You dont need any experience to participate. However, due to legal issues, anyone under 18 must have a legal guardian present and cannot win prizes </h6>
</div>
<div class = "FAQ">
<h4>What is the max team size?</h4>
<h6>The maximum is 5 people per team. Minimum is 1 :P</h6>
</div>
<div class = "FAQ">
<h4>What are the awards for?</h4>
<h6>The categories this year are: </h6>
<h6>Best Overall Project
<h6>Best Game </h6>
<h6>Best Software </h6>
<h6>Best Hardware </h6>
<h6>Company Favorite </h6>
<h6>Best Use of the Bose API/SDK </h6>
<h6>Best Meme </h6>
<h6>Best Rookie </h6>
</div>
<div class = "FAQ">
<h4>What should I bring?</h4>
<h6>Bring devices, chargers, sleeping bags, toiletries, basically anything you need to be comfortable. We will provide showers at certain time slots during the weekend. Food will also be provided.</h6>
</div>
<div class = "FAQ">
<h4>What if I don't have a team?</h4>
<h6>You dont need a team to register! We will have a team formation session right after the opening ceremony for those who would like to work with a team. If you prefer to stay solo, thats also great. </h6>
</div>
<div class = "FAQ">
<h4>How should I prepare beforehand?</h4>
<h6>Make sure you have travel plans in place, especially in case of random snowfalls. Unfortunately, we do not provide travel reimbursements.
Its also nice to have an idea in mind beforehand. However, we ask that you do not bring any pre-made materials to keep the competition fair. </h6>
</div>
<div class = "FAQ">
<h4>What is the schedule?</h4>
<h6><a href = 'https://docs.google.com/spreadsheets/d/1FSh2UfZwoMwXlgrKnKjnQg2okEcLDVCP30khnPdM1H0/edit?usp=sharing' style= "color: blue">Here it is!</a> Keep in mind that this is tentative. </h6>
</div>
</div>
</div>
{% include 'footer.html' %}

View file

@ -1,101 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>🍪CookieMailer</title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<meta name="description" content="" />
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
<meta name="theme-color" content="">
<link rel="stylesheet" type="text/css" href="/static/css/mail.css" />
<script src="https://cdn.tiny.cloud/1/{{MCE_API_KEY}}/tinymce/5/tinymce.min.js" referrerpolicy="origin"></script></head>
<body>
<div id="loader"></div>
<div class="container">
<div class="row">
<div id="root" class="col">
<h2>🍪CookieMailer</h2>
<label for="recipients">To: </label>
<br/>
<select id="recipients" name="recipients">
<option value="admin">Test Email (SysAdmin)</option>
<option value="org">Test Email (Organizers)</option>
<!-- <option value="wpi">WPI Only</option> -->
<option value="all">All Participants ({{NUM_HACKERS}})</option>
</select>
<br/>
<label for="subject">Subject: </label>
<br/>
<input id="subject" name="subject" width="100%" type="text" value="Hack@WPI" />
<br/>
<br/>
<textarea id="content" name="content">
Message
<br/>
<br/>
Best,<br/>
<b>Hack@WPI Team</b><br/>
<i><a href="mailto:hack@wpi.edu">hack@wpi.edu</a></i><br/>
<img height="75px" width="75px" src="https://media.discordapp.net/attachments/829437603291856938/930311998057635880/hack317-min.png">
</textarea>
<br/>
<br/>
<input type="button" onClick="send()" value="Send"/>
</div>
</div>
</div>
<script src="https://code.jquery.com/jquery-3.6.0.min.js" crossorigin="anonymous"></script>
<script type="text/javascript">
window.onload = function(){
tinymce.init({
selector: '#content',
plugins: 'advlist,link'
});
setTimeout(function(){
document.getElementById("loader").remove();
},1000);
};
function send() {
let rec = document.getElementById("recipients").value;
let subject = document.getElementById("subject").value;
let text = tinyMCE.activeEditor.getContent({ format: 'text' });
let html = tinyMCE.activeEditor.getContent({ format: 'html' });
let body = {
"recipients": rec,
"subject": subject,
"text": text,
"html": html
}
console.log("Sending Email:"+JSON.stringify(body))
const headers = [
["Content-Type", "application/json"],
];
if((rec != "all" && window.confirm("Send test email?")) || (rec == "all" && window.confirm("Send email to {{NUM_HACKERS}} recipients?"))) {
fetch('/send', {method: 'POST', body: JSON.stringify(body), headers: headers}).then(async (res) => {
window.alert(await res.text());
}).catch((err) => {
window.alert("Error sending message - see console for details");
console.log(err);
});
} else {
window.alert("Nothing was sent");
}
// fetch('/send', {method: 'POST', body: JSON.stringify(body), headers: headers}).then(async (res) => {
// alert("Message sent");
// document.body.innerHTML = await res.text()
// }).catch((err) => {
// alert("Error sending message");
// document.body.innerHTML = err;
// })
}
</script>
</body>
</html>

View file

@ -1,69 +0,0 @@
{% include 'header.html' %}
<link href="../static/css/materialize.min.css" rel="stylesheet">
<style>
a{
color: skyblue;
}
</style>
<script>
function resumeChange() {
let val = document.getElementById('resume').value;
val = val.split(/\\|\//gi).slice(-1)[0]
document.getElementById('filename').setAttribute('value', val);
}
</script>
<div style="height: 100%;">
<div id="registration-banner" class="parallax-container valign-wrapper">
<div class="section">
<h3 class="header center text-darken-2">Registration</h3>
</div>
<div class="parallax"><img src="../static/img/background1.jpg" alt="background"></div>
</div>
<div class="container">
<div class="container">
<Center><h2>Hi {{ name }}, just a few more steps!</h2></Center>
<div class="section" style="background-color: #974355; padding: 20px;">
<form method="post" action="/register" enctype="multipart/form-data">
<div>
<p><b>If you'd like, add your resume to send to sponsors... </b></p>
<div class="file-field input-field">
<div class="btn">
<span>File</span>
<input id="resume" name="resume" type="file" oninput="resumeChange()"/>
</div>
<div class="file-path-wrapper white-text">
<input disabled id="filename" class="file-path validate white-text" type="text">
</div>
</div>
</div>
<p><i>No worries if you don't have one on hand, you can come back and upload it later via the Hack@WPI dashboard!</i></p>
<p>Please take a moment to review the following policies and check all the boxes below so we can finalize your registration:</p>
<p>
<center><a href="/tos">Hack@WPI's Terms and Conditions</a></center>
</p>
<p>
<center><a href="https://github.com/MLH/mlh-policies/blob/master/data-sharing.md">MLH's Data Sharing Notice</a></center>
</p>
<p>
<center><a href="https://mlh.io/privacy">MLH's Privacy Policy</a></center>
</p>
<p>
<center><a href="https://github.com/MLH/mlh-policies/blob/master/prize-terms-and-conditions/contest-terms.md">MLH's
Contest Terms and Conditions</a></center>
</p>
<p>
<center><a href="https://static.mlh.io/docs/mlh-code-of-conduct.pdf">MLH's Code of Conduct</a></center>
</p>
<br>
<input type="checkbox" name="tos" id="checkboxid" required><label for="checkboxid"><b style="color:white;">*I have read and agree to the Hack@WPI Terms and Conditions</b></label>
<input type="checkbox" name="mlh1" id="checkboxid1" required><label for="checkboxid1"><b style="color:white;">*I have read and agree to the MLH Code of Conduct</b></label>
<input type="checkbox" name="mlh2" id="checkboxid2" required><label for="checkboxid2"><b style="color:white;">*I authorize you to share my application/registration information with Major League Hacking for event administration, ranking, and MLH administration in-line with the MLH Privacy Policy. I further agree to the terms of both the MLH Contest Terms and Conditions and the MLH Privacy Policy.</b></label>
<input type="checkbox" name="mlh3" id="checkboxid3" required><label for="checkboxid3"><b style="color:white;">*I authorize MLH to send me pre- and post-event information emails, which contain free credit and opportunities from their partners.</b></label>
<input type="checkbox" name="mlh3" id="checkboxid4" required><label for="checkboxid4"><b style="color:white;">*I understand that in-person participation is limited to WPI students that are part of the testing protocol only.</b></label>
<br><br>
<center><input name="submit" class="btn btn-lg btn-primary btn-invert" type="submit" value="Submit"/></center>
</form>
</div>
</div>
</div>
{% include 'footer.html' %}

View file

@ -1,16 +0,0 @@
{% include 'header.html' %}
<link href="../static/css/materialize.min.css" rel="stylesheet">
<div style="margin: 16px">
<p>
I agree to hold harmless the organizers of HACK@WPI and WPIs Chapter of the Association of Computing Machinery (ACM) from any and all claims, lawsuits, demands, causes of action, liability, loss, damage and/or injury of any kind whatsoever (including without limitation all claims for monetary loss, property damager, equitable relief, personal injury and/or wrongful death), whether brought by an individual or other entity. The indemnification applies to and includes, without limitation, the payment of all penalties, fines, judgments, awards, decrees, attorneys fees, and related costs or expenses, and any reimbursements to ACM for all legal fees, expenses, and costs incurred by it.
</p>
<p>
I also acknowledge the organizers of HACK@WPI will be recording the event using video, photographs, audio recordings, and other media and give them permission to use said media in marketing/promotional materials.
</p>
</div>
<!-- This is stupid but it works -->
<div style="position: absolute; bottom: 0; width: 100%;">
{% include 'footer.html' %}
</div>