Tracking PR for registration rewrite #5

Merged
Muirrum merged 32 commits from rewrite into master 2022-12-15 18:32:08 -05:00
84 changed files with 1694 additions and 1037 deletions

2
.gitignore vendored
View file

@ -4,6 +4,8 @@ config_hackWPI.py
config.py
admin/*.json
admin/*.csv
goathacks/config.py
# Created by https://www.gitignore.io/api/web,vim,git,macos,linux,bower,grunt,python,pycharm,windows,eclipse,webstorm,intellij,jetbrains,virtualenv,visualstudio,visualstudiocode
# Dev ENV Stuff

7
.gitmodules vendored
View file

@ -1,3 +1,4 @@
[submodule "templates/home"]
path = templates/home
url = https://github.com/wpi-acm/hack-wpi-static.git
[submodule "goathacks/templates/home"]
path = goathacks/templates/home
url = https://github.com/WPI-ACM/Hack-WPI-Static
branch = 2023-dev

39
Makefile Normal file
View file

@ -0,0 +1,39 @@
SHELL := /bin/bash
all: clean
# Clean up temp files
#------------------------------------------------------------------
clean:
@echo "Cleaning up temp files"
@find . -name '*~' -ls -delete
@find . -name '*.bak' -ls -delete
@echo "Cleaning up __pycache__ directories"
@find . -name __pycache__ -type d -not -path "./.venv/*" -ls -exec rm -r {} +
@echo "Cleaning up logfiles"
@find ./logs -name '*.log*' -ls -delete
@echo "Cleaning up flask_session"
@find . -name flask_session -type d -not -path "./.venv/*" -ls -exec rm -r {} +
init_env:
python3 -m venv .venv
source .venv/bin/activate && pip3 install --upgrade pip
source .venv/bin/activate && pip3 install -r requirements.txt txt
upgrade_env:
source .venv/bin/activate && pip3 install --upgrade -r requirements.txt txt
make_migrations:
source .venv/bin/activate && flask db migrate
run_migrations:
source .venv/bin/activate && flask db upgrade
daemon:
@echo "--- STARTING UWSGI DAEMON ---"
@echo ""
@echo ""
source .venv/bin/activate && flask run
@echo ""
@echo ""
@echo "--- STARTING UWSGI DAEMON ---"

View file

@ -1,20 +1,31 @@
# Hack@WPI 2018 Website
Used chronicel's registration system as a base but removed the need of mailchimp.
# GoatHacks Registration Management
Rest is from their repo:
This is a rewrite of the original (commit 198f56f2c47831c2f8c192fbb45a47f7b1fb4e5b)
management system for Flask 2.1 and Postgres. The focus was on maintainability
in the future and easy modifications for future years.
## 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`
- 🎉 🔥 🙌 💃 👌 💯
## Setting up a development environment
The `Makefile` should have a bunch of useful recipes for you. Once you clone the
repo for the first time, run `make init_env`, which will set up the virtual
environment for development. If the dependencies ever change, you can run `make
upgrade_env` to install the new packages.
To test your code, run `make daemon`, which will start a development server. It
will automatically reload after your changes.
## Setting up for production
You can use your choice of WSGI server. Instructions are provided below for
uWSGI. Please ensure a current (3.x) version of Python and Pip.
1. pip install uwsgi # or the equivalent for your distribution's package manager
2. mkdir -p /etc/uwsgi/apps-available
3. mkdir -p /var/log/uwsgi
4. sudo chown -R nginx:nginx /var/log/uwsgi
5. mkdir -p /var/app
6. git clone https://github.com/WPI-ACM/Hack-WPI-Python /var/app/goathacks
7. cd /var/app/goathacks && make init_env
8. cp /var/app/goathacks/contrib/goathacks.ini /etc/uwsgi/apps-available/goathacks.ini
9. cp /var/app/goathacks/contrib/goathacks.service /etc/systemd/system/goathacks.service
10. cp /var/app/goathacks/goathacks/config.py.example /var/app/goathacks/goathacks/config.py
11. $EDITOR /var/app/goathacks/goathacks/config.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))

15
contrib/goathacks.ini Normal file
View file

@ -0,0 +1,15 @@
[uwsgi]
base = /var/app/goathacks
wsgi-file = %(base)/goathacks.py
home = %(base)/.venv
pythonpath = %(base)
socket = %(base)/%n.sock
chmod-socket=666
callable=app
logto = /var/log/uwsgi/%n.log
touch-reload = %(base/.uwsgi-touch
max-requests=1000

15
contrib/goathacks.service Normal file
View file

@ -0,0 +1,15 @@
[Unit]
Description=%i uWSGI app
After=syslog.target
[Service]
ExecStart=/usr/bin/uwsgi \
--ini /etc/uwsgi/apps-available/%i.ini \
--socket /var/run/uwsgi/%i.socket
User=www-data
Group=www-data
Restart=on-failure
KillSignal=SIGQUIT
Type=notify
StandardError=syslog
NotifyAccess=all

11
contrib/goathacks.socket Normal file
View file

@ -0,0 +1,11 @@
[Unit]
Description=Socket for uWSGI app %i
[Socket]
ListenStream=/var/run/uwsgi/%i.socket
SocketUser=www-data
SocketGroup=www-data
SocketMode=0660
[Install]
WantedBy=sockets.target

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)

62
goathacks/__init__.py Normal file
View file

@ -0,0 +1,62 @@
from flask import Flask, redirect, render_template, send_from_directory
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask_login import LoginManager
from flask_assets import Bundle, Environment
from flask_cors import CORS
from flask_mail import Mail
db = SQLAlchemy()
migrate = Migrate()
login = LoginManager()
environment = Environment()
cors = CORS()
mail = Mail()
def create_app():
app = Flask(__name__)
app.config.from_pyfile("config.py")
db.init_app(app)
migrate.init_app(app, db)
login.init_app(app)
environment.init_app(app)
cors.init_app(app)
mail.init_app(app)
scss = Bundle('css/style.scss', filters='scss',
output='css/style.css')
environment.register('scss', scss)
from .models import User
from . import registration
from . import dashboard
from . import admin
app.register_blueprint(registration.bp)
app.register_blueprint(dashboard.bp)
app.register_blueprint(admin.bp)
from goathacks import cli
app.cli.add_command(cli.gr)
# Homepage
@app.route("/")
def index_redirect():
return redirect("/index.html")
@app.route("/index.html")
def index():
return render_template("home/index.html")
# homepage assets
@app.route("/assets/<path:path>")
def assets(path):
return send_from_directory('templates/home/assets', path)
return app

234
goathacks/admin/__init__.py Normal file
View file

@ -0,0 +1,234 @@
from flask import Blueprint, jsonify, redirect, render_template, request, url_for
from flask_login import current_user, login_required
from flask_mail import Message
from goathacks.models import User
bp = Blueprint("admin", __name__, url_prefix="/admin")
from goathacks import db, mail as app_mail
@bp.route("/")
@login_required
def home():
if not current_user.is_admin:
return redirect(url_for("dashboard.home"))
male_count = 0
female_count = 0
nb_count = 0
check_in_count = 0
waitlist_count = 0
total_count = 0
shirt_count = {'XS': 0, 'S': 0, 'M': 0, 'L': 0, 'XL': 0}
hackers = db.session.execute(db.select(User)).scalars().all()
schools = {}
for h in hackers:
if h.waitlisted:
waitlist_count += 1
if h.checked_in:
check_in_count += 1
if h.gender == 'F':
female_count += 1
elif h.gender == 'M':
male_count += 1
else:
nb_count += 1
total_count += 1
if h.school not in schools:
schools[h.school] = 1
else:
schools[h.school] += 1
if h.shirt_size not in shirt_count:
shirt_count[h.shirt_size] = 1
else:
shirt_count[h.shirt_size] += 1
return render_template("admin.html", waitlist_count=waitlist_count,
total_count=total_count, shirt_count=shirt_count,
hackers=hackers, male_count=male_count,
female_count=female_count, nb_count=nb_count,
check_in_count=check_in_count, schools=schools)
@bp.route("/mail")
@login_required
def mail():
if not current_user.is_admin:
return redirect(url_for("dashboard.home"))
total_count = len(db.session.execute(db.select(User)).scalars().all())
return render_template("mail.html", NUM_HACKERS=total_count)
@bp.route("/send", methods=["POST"])
@login_required
def send():
if not current_user.is_admin:
return {"status": "error"}
json = request.json
users = User.query.all()
to = []
if json["recipients"] == "org":
to = ["hack@wpi.edu"]
elif json['recipients'] == 'admin':
to = ["acm-sysadmin@wpi.edu"]
elif json['recipients'] == "all":
to = [x.email for x in users]
with app_mail.connect() as conn:
for e in to:
msg = Message(json['subject'])
msg.add_recipient(e)
msg.html = json['html']
msg.body = json['text']
conn.send(msg)
return {"status": "success"}
@bp.route("/check_in/<int:id>")
@login_required
def check_in(id):
if not current_user.is_admin:
return redirect(url_for("dashboard.home"))
user = User.query.filter_by(id=id).one()
if user is None:
return {"status": "error", "msg": "No user found"}
user.checked_in = True
db.session.commit()
return {"status": "success"}
@bp.route("/drop/<int:id>")
@login_required
def drop(id):
if not current_user.is_admin and not current_user.id == id:
return redirect(url_for("dashboard.home"))
user = User.query.filter_by(id=id).one()
if user is None:
return {"status": "error", "msg": "user not found"}
if user.checked_in:
return {"status": "error", "msg": "Hacker is already checked in"}
msg = Message("Application Dropped")
msg.add_recipient(user.email)
msg.sender = ("GoatHacks Team", "hack@wpi.edu")
msg.body = render_template("emails/dropped.txt", user=user)
app_mail.send(msg)
db.session.delete(user)
db.session.commit()
return {"status": "success"}
@bp.route("/change_admin/<int:id>/<string:action>")
@login_required
def change_admin(id, action):
if not current_user.is_admin:
return redirect(url_for("dashboard.home"))
user = User.query.filter_by(id=id).one()
if user is None:
return {"status": "error", "msg": "user not found"}
valid_actions = ['promote', 'demote']
if action not in valid_actions:
return {"status": "error", "msg": "invalid action"}
if action == "promote":
user.is_admin = True
else:
user.is_admin = False
db.session.commit()
return {"status": "success"}
@bp.route("/promote_from_waitlist/<int:id>")
@login_required
def promote_waitlist(id):
if not current_user.is_admin:
return redirect(url_for("dashboard.home"))
user = User.query.filter_by(id=id).one()
if user is None:
return {"status": "error", "msg": "user not found"}
user.waitlisted = False
db.session.commit()
msg = Message("Waitlist Promotion")
msg.add_recipient(user.email)
msg.sender = ("GoatHacks Team", "hack@wpi.edu")
msg.body = render_template("emails/waitlist_promotion.txt", user=user)
mail.send(msg)
return {"status": "success"}
@bp.route("/hackers.csv")
@login_required
def hackers_csv():
if not current_user.is_admin:
return redirect(url_for("dashboard.home"))
users = User.query.all()
return json_to_csv(User.create_json_output(users))
@bp.route("/hackers")
@login_required
def hackers():
if not current_user.is_admin:
return redirect(url_for("dashboard.home"))
users = User.query.all()
return User.create_json_output(users)
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()

124
goathacks/cli.py Normal file
View file

@ -0,0 +1,124 @@
from datetime import datetime
import click
from flask import current_app, render_template
from flask.cli import AppGroup
from flask_mail import Message
from werkzeug.security import generate_password_hash
from goathacks.registration import bp
from goathacks import db, mail
from goathacks.models import User
gr = AppGroup("user")
@gr.command('create')
@click.option("--email", prompt=True, help="User's Email")
@click.option("--first_name", prompt=True)
@click.option("--last_name", prompt=True)
@click.option("--admin/--no-admin", prompt=True, default=False)
@click.option("--password", prompt=True, hide_input=True,
confirmation_prompt=True)
@click.option("--school", prompt=True)
@click.option("--phone", prompt=True)
@click.option("--gender", prompt=True)
def create_user(email, first_name, last_name, password, school, phone, gender,
admin):
"""
Creates a user
"""
if gender not in ['F', 'M', 'NB']:
click.echo("Invalid gender. Must be one of F, M, NB")
return
num_not_waitlisted = len(User.query.filter_by(waitlisted=False).all())
waitlisted = False
if num_not_waitlisted >= current_app.config['MAX_BEFORE_WAITLIST']:
waitlisted = True
user = User(
email=email,
password=generate_password_hash(password),
first_name=first_name,
last_name=last_name,
last_login=datetime.now(),
waitlisted=waitlisted,
school=school,
phone=phone,
gender=gender,
is_admin=admin
)
db.session.add(user)
db.session.commit()
click.echo("Created user")
@gr.command("promote")
@click.option("--email", prompt=True)
def promote_user(email):
"""
Promotes a user to administrator
"""
user = User.query.filter_by(email=email).one()
user.is_admin = True
db.session.commit()
click.echo(f"Promoted {user.first_name} to admin")
@gr.command("demote")
@click.option("--email", prompt=True)
def demote_user(email):
"""
Demotes a user from administrator
"""
user = User.query.filter_by(email=email).one()
user.is_admin = False
db.session.commit()
click.echo(f"Demoted {user.first_name} from admin")
@gr.command("waitlist")
@click.option("--email", prompt=True)
def waitlist_user(email):
"""
Toggles the waitlist status of a user
"""
user = User.query.filter_by(email=email).one()
user.waitlisted = not user.waitlisted
db.session.commit()
if user.waitlisted:
click.echo(f"Sent {user.first_name} to the waitlist")
else:
msg = Message("Waitlist Promotion")
msg.add_recipient(user.email)
msg.body = render_template("emails/waitlist_promotion.txt", user=user)
mail.send(msg)
click.echo(f"Promoted {user.first_name} from the waitlist")
@gr.command("drop")
@click.option("--email", prompt=True)
@click.option("--confirm/--noconfirm", prompt=False, default=True)
def drop_user(email, confirm):
"""
Drops a user's registration
"""
user = User.query.filter_by(email=email).one()
if not confirm:
pass
else:
if click.confirm(f"Are you sure you want to drop {user.first_name} {user.last_name}'s registration? **THIS IS IRREVERSIBLE**"):
pass
else:
return
db.session.delete(user)
db.session.commit()
click.echo(f"Dropped {user.first_name}'s registration")

View file

@ -0,0 +1,16 @@
SQLALCHEMY_DATABASE_URI="postgresql://localhost/goathacks"
MAX_BEFORE_WAITLIST=1
SECRET_KEY="bad-key-change-me"
UPLOAD_FOLDER="./uploads/"
# Mail settings
MAIL_SERVER="localhost"
MAIL_PORT=25
MAIL_USE_TLS=False
MAIL_USE_SSL=False
MAIL_USERNAME="dummy"
MAIL_PASSWORD="dummy"
MAIL_DEFAULT_SENDER="GoatHacks Team <hack@wpi.edu>"

View file

@ -0,0 +1,56 @@
from flask import Blueprint, current_app, flash, jsonify, render_template, request
from flask_login import current_user, login_required
from werkzeug.utils import secure_filename
import os
bp = Blueprint("dashboard", __name__, url_prefix="/dashboard")
from goathacks.dashboard import forms
from goathacks import db
@bp.route("/", methods=["GET", "POST"])
@login_required
def home():
form = forms.ShirtAndAccomForm(request.form)
resform = forms.ResumeForm(request.form)
if request.method == "POST" and form.validate():
current_user.shirt_size = request.form.get('shirt_size')
current_user.accomodations = request.form.get('accomodations')
db.session.commit()
flash("Updated successfully")
return render_template("dashboard.html", form=form, resform=resform)
@bp.route("/resume", methods=["POST"])
@login_required
def resume():
form = forms.ResumeForm(request.form)
"""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 = current_user.first_name.lower() + '_' + current_user.last_name.lower() + '_' + str(
current_user.id) + '.' + resume.filename.split('.')[-1].lower()
filename = secure_filename(filename)
resume.save(os.path.join(current_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"
def allowed_file(filename):
return '.' in filename and \
filename.split('.')[-1].lower() in ['pdf', 'docx', 'doc', 'txt',
'rtf']

View file

@ -0,0 +1,16 @@
from flask_wtf import FlaskForm
from flask_wtf.file import FileField, FileRequired, FileAllowed
from wtforms import RadioField, TextAreaField
from wtforms.validators import DataRequired
class ShirtAndAccomForm(FlaskForm):
shirt_size = RadioField("Shirt size", choices=["XS", "S", "M", "L", "XL",
"None"],
validators=[DataRequired()])
accomodations = TextAreaField("Special needs and/or Accomodations")
class ResumeForm(FlaskForm):
resume = FileField("Resume", validators=[FileRequired(),
FileAllowed(['pdf', 'docx', 'doc',
'txt', 'rtf'],
"Documents only!")])

14
goathacks/database.py Normal file
View file

@ -0,0 +1,14 @@
from flask_login import UserMixin
from sqlalchemy import Boolean, Column, DateTime, Integer, String
from . import db
class User(db.Model, UserMixin):
id = Column(Integer, primary_key=True)
email = Column(String, unique=True, nullable=False)
password = Column(String, nullable=False)
first_name = Column(String, nullable=False)
last_login = Column(DateTime, nullable=False)
active = Column(Boolean, nullable=False)
is_admin = Column(Boolean, nullable=False, default=False)

52
goathacks/models.py Normal file
View file

@ -0,0 +1,52 @@
from flask import flash, redirect, url_for
from flask_login import UserMixin
from sqlalchemy import Boolean, Column, DateTime, Integer, String
from . import db
from . import login
class User(db.Model, UserMixin):
id = Column(Integer, primary_key=True)
email = Column(String, unique=True, nullable=False)
password = Column(String, nullable=False)
first_name = Column(String, nullable=False)
last_name = Column(String, nullable=False)
last_login = Column(DateTime, nullable=False)
active = Column(Boolean, nullable=False, default=True)
is_admin = Column(Boolean, nullable=False, default=False)
waitlisted = Column(Boolean, nullable=False, default=False)
shirt_size = Column(String, nullable=True)
accomodations = Column(String, nullable=True)
checked_in = Column(Boolean, nullable=False, default=False)
school = Column(String, nullable=True)
phone = Column(String, nullable=True)
gender = Column(String, nullable=True)
Muirrum commented 2022-12-08 20:25:22 -05:00 (Migrated from github.com)
Review

@willhockey20 is this something that actually needs to be stored? I only included it bc it was in the original registration system.

@willhockey20 is this something that actually needs to be stored? I only included it bc it was in the original registration system.
wfryan commented 2022-12-08 20:29:57 -05:00 (Migrated from github.com)
Review

The data was required by mlh, still could be nice to have so I'd keep it

The data was required by mlh, still could be nice to have so I'd keep it
def create_json_output(lis):
hackers = []
for u in lis:
hackers.append({
'checked_in': u.checked_in,
'waitlisted': u.waitlisted,
'admin': u.is_admin,
'id': u.id,
'email': u.email,
'first_name': u.first_name,
'last_name': u.last_name,
'phone_number': u.phone,
'shirt_size': u.shirt_size,
'special_needs': u.accomodations,
'school': u.school
})
return hackers
@login.user_loader
def user_loader(user_id):
return User.query.filter_by(id=user_id).first()
@login.unauthorized_handler
def unauth():
flash("Please login first")
return redirect(url_for("registration.register"))

View file

@ -0,0 +1,83 @@
from datetime import datetime
from flask import Blueprint, config, current_app, flash, redirect, render_template, request, url_for
import flask_login
from flask_login import current_user
from goathacks.registration.forms import LoginForm, RegisterForm
from werkzeug.security import check_password_hash, generate_password_hash
from goathacks import db
from goathacks.models import User
bp = Blueprint('registration', __name__, url_prefix="/registration")
@bp.route("/", methods=["GET", "POST"])
def register():
if current_user.is_authenticated:
flash("You are already registered and logged in!")
print("got register")
form = RegisterForm(request.form)
print(vars(form.gender))
if request.method == 'POST':
print("Got form")
email = request.form.get('email')
first_name = request.form.get('first_name')
last_name = request.form.get('last_name')
password = request.form.get('password')
password_c = request.form.get('password_confirm')
school = request.form.get('school')
phone = request.form.get('phone_number')
gender = request.form.get('gender')
if password == password_c:
# Passwords match!
# Count of all non-waitlisted hackers
num_not_waitlisted = len(User.query.filter_by(waitlisted=False).all())
waitlisted = False
print(num_not_waitlisted)
print(current_app.config['MAX_BEFORE_WAITLIST'])
if num_not_waitlisted >= current_app.config['MAX_BEFORE_WAITLIST']:
waitlisted = True
user = User(
email=email,
password=generate_password_hash(password),
first_name=first_name,
last_name=last_name,
last_login=datetime.now(),
waitlisted=waitlisted,
school=school,
phone=phone,
gender=gender
)
db.session.add(user)
db.session.commit()
flask_login.login_user(user)
return redirect(url_for("dashboard.home"))
else:
flash("Passwords do not match")
return render_template("register.html", form=form)
@bp.route("/login", methods=["GET", "POST"])
def login():
form = LoginForm(request.form)
if request.method == 'POST':
email = request.form.get('email')
password = request.form.get('password')
user = User.query.filter_by(email=email).first()
if check_password_hash(user.password, password):
flask_login.login_user(user)
flash("Welcome back!")
return redirect(url_for("dashboard.home"))
else:
flash("Incorrect password")
return render_template("login.html", form=form)

View file

@ -0,0 +1,25 @@
from flask_wtf import FlaskForm
from wtforms import BooleanField, PasswordField, SelectField, StringField, SubmitField, widgets
from wtforms.validators import DataRequired
class RegisterForm(FlaskForm):
email = StringField("Email", validators=[DataRequired()])
Muirrum commented 2022-12-07 09:03:42 -05:00 (Migrated from github.com)
Review

This could probably get turned into an EmailField if we cared enough

This could probably get turned into an `EmailField` if we cared enough
first_name = StringField("Preferred First Name",
validators=[DataRequired()])
last_name = StringField("Last Name", validators=[DataRequired()])
password = PasswordField("Password", validators=[DataRequired()])
password_confirm = PasswordField("Confirm Password",
validators=[DataRequired()])
school = StringField("School/University", validators=[DataRequired()])
phone_number = StringField("Phone number", validators=[DataRequired()])
gender = SelectField("Gender", choices=[("F", "Female"), ("M", "Male"),
("NB", "Non-binary/Other")],
widget=widgets.Select())
agree_coc = BooleanField("I confirm that I have read and agree to the Code of Conduct", validators=[DataRequired()])
submit = SubmitField("Register")
class LoginForm(FlaskForm):
email = StringField("Email", validators=[DataRequired()])
password = PasswordField("Password", validators=[DataRequired()])
submit = SubmitField("Sign in")

View file

@ -0,0 +1,185 @@
@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, 0.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%;
}
label {
padding-bottom: 0.5rem;
}
form input {
border-radius: 5px;
}
form input[type="submit"] {
background: #26a69a;
border-radius: 10px;
border-color: #26a69a;
}
form input[type="radio"] {
padding-right: 5px;
}
form input[type="checkbox"]:checked {
visibility: visible;
left: unset;
position: unset;
}
form label {
font-size: 1.1rem;
padding-right: 10px;
padding-left: 25px !important;
}
form select {
display: unset;
background: #974355;
max-width: 11rem;
}
.flashes {
list-style-type: none;
display: flex;
justify-content: center;
}
.message {
width: 80%;
justify-content: center;
border: 1px solid #eee;
background-color: #26a69a;
padding: 0.2rem;
font-size: large;
color: #eee;
}

View file

@ -1,3 +1,7 @@
$color-bg: #003049;
$color-fg: #eee;
$color-section-bg: #974355;
$color-accent: #26a69a;
@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 {
@ -5,10 +9,10 @@ html {
}
body {
background-color: #003049;
background-color: $color-bg;
font-family: 'Montserrat', sans-serif;
font-size: 1.1rem;
color: #eee;
color: $color-fg;
position: relative;
min-height: 100%;
}
@ -32,7 +36,7 @@ p {
}
.button-collapse {
color: #26a69a;
color: $color-accent;
}
@ -126,3 +130,55 @@ nav {
bottom: 0;
width: 100%;
}
// Forms
label {
padding-bottom: 0.5rem;
}
form {
input {
border-radius: 5px;
}
input[type="submit"] {
background: $color-accent;
border-radius: 10px;
border-color: $color-accent;
}
input[type="radio"] {
padding-right: 5px;
}
input[type="checkbox"]:checked {
visibility: visible;
left: unset;
position: unset;
}
label {
font-size: 1.1rem;
padding-right: 10px;
padding-left: 25px !important;
}
select {
display: unset;
background: $color-section-bg;
max-width: 11rem;
}
}
// Flashed messages
.flashes {
list-style-type: none;
display: flex;
justify-content: center;
}
.message {
width: 80%;
justify-content: center;
border: 1px solid $color-fg;
background-color: $color-accent;
padding: 0.2rem;
font-size: large;
color: $color-fg;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View file

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 77 KiB

View file

@ -82,7 +82,7 @@ const promoteFromWaitlist = (id) => {
confirmButtonText: 'Yes, promote!',
confirmButtonColor: successColor
}, () => {
$.get('/promote_from_waitlist?mlh_id=' + id, (data) => {
$.get('/admin/promote_from_waitlist/' + id, (data) => {
let title = ''
let msg = ''
let type = ''
@ -110,7 +110,7 @@ const changeAdmin = (id, action) => {
confirmButtonText: 'Yes, ' + action + '!',
confirmButtonColor: errColor
}, () => {
$.get('/change_admin?mlh_id=' + id + '&action=' + action, (data) => {
$.get('/admin/change_admin/' + id + '/' + action, (data) => {
let title = ''
let msg = ''
let type = ''
@ -138,7 +138,7 @@ const drop = (id) => {
confirmButtonText: 'Yes, drop!',
confirmButtonColor: errColor
}, () => {
$.get('/drop?mlh_id=' + id, (data) => {
$.get('/admin/drop/' + id, (data) => {
let title = ''
let msg = ''
let type = ''
@ -166,7 +166,7 @@ const checkIn = (id) => {
confirmButtonText: 'Yes, check in!',
confirmButtonColor: successColor
}, () => {
$.get('/check_in?mlh_id=' + id, (data) => {
$.get('/admin/check_in/' + id, (data) => {
let title = ''
let msg = ''
let type = ''

View file

View file

@ -13,7 +13,7 @@
<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="{{url_for('static', filename='js/admin.js')}}"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.5.0/Chart.min.js"></script>
</head>
@ -43,7 +43,8 @@
<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>
<p><a href="{{url_for('admin.hackers')}}">JSON</a> <a
href="{{url_for('admin.hackers_csv')}}">CSV</a></p>
</div>
<div class="row">
<div class="col-md-4">
@ -186,13 +187,13 @@
</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>
<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>
@ -217,27 +218,27 @@
</thead>
<tbody>
{% for hacker in hackers %}
<tr id="{{ hacker['id'] }}-row">
<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>
{% 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'] %}
{% 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'] %}
{% if not hacker.checked_in %}
<li><a class="drop" id="{{ hacker['id'] }}-drop" href="#">Drop Application</a>
</li>
{% endif %}
{% if hacker['admin'] %}
{% if hacker.is_admin %}
<li><a class="demote_admin" id="{{ hacker['id'] }}-demote_admin" href="#">Demote
Admin</a>
</li>
@ -251,15 +252,15 @@
</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.is_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['phone'] }}</td>
<td>{{ hacker['shirt_size'] }}</td>
<td>{{ hacker['special_needs'] }}</td>
<td>{{ hacker['school']['name'] }}</td>
<td>{{ hacker['accomodations'] }}</td>
<td>{{ hacker['school'] }}</td>
</tr>
{% endfor %}
</tbody>

View file

@ -0,0 +1,20 @@
{% include 'header.html' %}
<div class="container">
{% with messages = get_flashed_messages() %}
{% if messages %}
<ul class="flashed-content">
{% for m in messages %}
<li class="message">{{ m }}</li>
{% endfor %}
</ul>
{% endif %}
{% endwith %}
{% block content %}
This content block is still being worked on!
{% endblock %}
</div>
{% include 'footer.html' %}

View file

@ -1,46 +1,12 @@
{% include 'header.html' %}
{% extends 'base.html' %}
{% block content %}
<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;
window.location.href = "/admin/drop/" + 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() {
@ -53,9 +19,9 @@
<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>
<h1>Hi {{ current_user.first_name }}!</h1>
{% if current_user.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>
@ -64,32 +30,24 @@
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>
<h5>Make sure to join the Discord 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>
<a href="https://discord.gg/G3pseHPRNv" style="margin: 5px;"
class="btn btn-lg btn-primary btn-invert">Discord</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">
<form method="post">
{{ form.csrf_token }}
<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>Shirt Size (Currently selected: {{current_user.shirt_size}})</p>
{% for subfield in form.shirt_size %}
{{subfield}}{{subfield.label}}
{% endfor %}
<p>Special Needs/Accommodations:</p>
<input type="text" name="special_needs" id="special_needs" value="{{ special_needs }}">
<input type="text" name="accomodations"
id="special_needs" value="{{ current_user.accomodations }}">
</div>
<br><br>
<input name="save" class="btn btn-lg btn-primary btn-invert" type="submit" value="Save"/>
@ -97,12 +55,14 @@
</form>
</div>
<div class="row center justify-content-center">
<form method="post" action="/resumepost" enctype="multipart/form-data">
<form method="post" action="{{url_for('dashboard.resume')}}" enctype="multipart/form-data">
{{ resform.csrf_token }}
<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">
@ -113,7 +73,7 @@
{% endif %}
<br>
</div>
{% if admin %}
{% if current_user.is_admin %}
<br>
<div class="row justify-content-center">
<a href="/admin"><p class="btn">Admin Dashboard</p></a>
@ -123,7 +83,7 @@
</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>
<center><a onclick="drop('{{current_user.id}}')" id="drop-link"><p class="btn">Drop Application if you can't make it :(</p></a></center>
</div>
<script>
@ -138,5 +98,4 @@
})
</script>
{% include 'footer.html' %}
{% endblock %}

View file

@ -0,0 +1,13 @@
Dear {{ user.first_name }},
Your application has been dropped. We're sorry to see you go!
If this was done in error, you can re-register by going to
https://hack.wpi.edu/registration.
Happy Hacking!
GoatHacks Team
This is an automated message. Please email hack@wpi.edu with any questions or
concerns.

View file

@ -0,0 +1,19 @@
Dear {{ user.first_name }},
Your application for GoatHacks has been confirmed! {% if user.waitlisted
%}You're on the waitlist right now, but we'll send you another email if a spot
opens up.{% else %}You've got a confirmed spot this year! Make sure to look at
the schedule at https://hack.wpi.edu.
{% if not user.waitlisted %}
We'll send another email with more details closer to the event. In the
meantime, visit your Dashboard (https://hack.wpi.edu/dashboard) to tell us about
your shirt size and any accomodations you may need.
{% endif %}
Happy Hacking!
GoatHacks Team
This is an automated message. Please email hack@wpi.edu with any questions or
concerns.

View file

@ -0,0 +1,18 @@
Hello {{ user.first_name }}!
We're writing to let you know that a spot has opened up in our registrations,
and you've been promoted off of the waitlist! Please visit our website
(https://hack.wpi.edu/dashboard) to complete your registration information and
join our Discord.
If you can no longer make the event, please visit your dashboard and use the
"Drop my registration" link.
Happy Hacking!
GoatHacks Team
This is an automated message. Please email hack@wpi.edu with any questions or
concerns.

View file

@ -4,7 +4,10 @@
<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">
<link href="../static/css/materialize.min.css" rel="stylesheet">
{% assets 'scss' %}
<link rel="stylesheet" type="text/css" href="{{ ASSET_URL }}">
{% endassets %}
</head>
<style>

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

View file

@ -0,0 +1,32 @@
{% extends 'base.html' %}
{% block content %}
<div style="height: 100%;">
<div id="registration-banner" class="parallax-container valign-wrapper">
<div class="section">
<h3 class="header-center text-darken-2">Login</h3>
</div>
<div class="parallax"><img src="{{url_for('static', filename='img/background1.jpg')}}"
alt="background"></div>
</div>
</div>
<div class="container">
<div class="section" style="background-color: #974355; padding: 20px;">
<form method="post">
{{ form.csrf_token }}
<div>
{{form.email}}<br/>{{ form.email.label}}
</div>
<div>
{{form.password}}<br/>{{form.password.label}}
</div>
<div>
{{form.submit}}
</div>
</form>
<span><p><em>Don't have an account? <a
href="{{url_for('registration.register')}}">Register
here</a>.</em></p></span>
</div>
</div>
{% endblock %}

View file

@ -34,9 +34,9 @@
<br/>
<br/>
Best,<br/>
<b>Hack@WPI Team</b><br/>
<b>GoatHacks 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">
<img height="75px" width="75px" src="{{url_for('static', filename='img/favicon.png')}}">
</textarea>
<br/>
<br/>
@ -77,7 +77,7 @@
];
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) => {
fetch('/admin/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");

View file

@ -0,0 +1,60 @@
{% extends 'base.html' %}
{% block content %}
<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="{{url_for('static', filename='img/background1.jpg')}}"
alt="background"></div>
</div>
</div>
<div class="container">
<div class="section" style="background-color: #974355; padding: 20px;">
<form method="post">
{{ form.csrf_token }}
<div>
{{ form.email}}<br/> {{ form.email.label }}
</div>
<div>
{{ form.password}}<br/>{{form.password.label}}
</div>
<div>
{{ form.password_confirm}}<br/>{{form.password_confirm.label}}
</div>
<div>
{{form.first_name}}<br/>{{form.first_name.label}}
</div>
<div>
{{form.last_name}}<br/>{{form.last_name.label}}
</div>
<hr/>
<h3>Miscellaneous Information</h3>
<div>
{{form.phone_number}}<br/>{{form.phone_number.label}}
</div>
<div>
{{form.school}}<br/>{{form.school.label}}
</div>
<div>
{{form.gender.label}}{{form.gender}}
</div>
<hr/>
<div>
<label for="agree_coc">I confirm that I have read and agree to the
Code of Conduct</label>
<input type="checkbox" id="agree_coc" name="agree_coc">
</div>
<div>
{{form.submit}}
</div>
</form>
<span><p><em>You may also want to <a
href="{{url_for('registration.login')}}">
log in</a>.</em></p></span>
</div>
</div>
{% endblock %}

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)

1
migrations/README Normal file
View file

@ -0,0 +1 @@
Single-database configuration for Flask.

50
migrations/alembic.ini Normal file
View file

@ -0,0 +1,50 @@
# A generic, single database configuration.
[alembic]
# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic,flask_migrate
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[logger_flask_migrate]
level = INFO
handlers =
qualname = flask_migrate
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

97
migrations/env.py Normal file
View file

@ -0,0 +1,97 @@
from __future__ import with_statement
import logging
from logging.config import fileConfig
from flask import current_app
from alembic import context
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
fileConfig(config.config_file_name)
logger = logging.getLogger('alembic.env')
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
config.set_main_option(
'sqlalchemy.url',
str(current_app.extensions['migrate'].db.get_engine().url).replace(
'%', '%%'))
target_db = current_app.extensions['migrate'].db
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
def get_metadata():
if hasattr(target_db, 'metadatas'):
return target_db.metadatas[None]
return target_db.metadata
def run_migrations_offline():
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url, target_metadata=get_metadata(), literal_binds=True
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online():
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
# this callback is used to prevent an auto-migration from being generated
# when there are no changes to the schema
# reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
def process_revision_directives(context, revision, directives):
if getattr(config.cmd_opts, 'autogenerate', False):
script = directives[0]
if script.upgrade_ops.is_empty():
directives[:] = []
logger.info('No changes in schema detected.')
connectable = current_app.extensions['migrate'].db.get_engine()
with connectable.connect() as connection:
context.configure(
connection=connection,
target_metadata=get_metadata(),
process_revision_directives=process_revision_directives,
**current_app.extensions['migrate'].configure_args
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

24
migrations/script.py.mako Normal file
View file

@ -0,0 +1,24 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)}
def upgrade():
${upgrades if upgrades else "pass"}
def downgrade():
${downgrades if downgrades else "pass"}

View file

@ -0,0 +1,32 @@
"""empty message
Revision ID: 311c62fe5f49
Revises: 3f427be4ce8a
Create Date: 2022-12-06 11:08:18.571528
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '311c62fe5f49'
down_revision = '3f427be4ce8a'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('user', schema=None) as batch_op:
batch_op.add_column(sa.Column('school', sa.String(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('user', schema=None) as batch_op:
batch_op.drop_column('school')
# ### end Alembic commands ###

View file

@ -0,0 +1,33 @@
"""empty message
Revision ID: 3f427be4ce8a
Revises: 55d77cdbbb49
Create Date: 2022-12-06 10:18:07.322064
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '3f427be4ce8a'
down_revision = '55d77cdbbb49'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('user', schema=None) as batch_op:
batch_op.add_column(sa.Column('checked_in', sa.Boolean(),
nullable=False, default=False))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('user', schema=None) as batch_op:
batch_op.drop_column('checked_in')
# ### end Alembic commands ###

View file

@ -0,0 +1,34 @@
"""empty message
Revision ID: 55d77cdbbb49
Revises: d210860eb46a
Create Date: 2022-12-06 10:09:50.254449
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '55d77cdbbb49'
down_revision = 'd210860eb46a'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('user', schema=None) as batch_op:
batch_op.add_column(sa.Column('shirt_size', sa.String(), nullable=True))
batch_op.add_column(sa.Column('accomodations', sa.String(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('user', schema=None) as batch_op:
batch_op.drop_column('accomodations')
batch_op.drop_column('shirt_size')
# ### end Alembic commands ###

View file

@ -0,0 +1,32 @@
"""empty message
Revision ID: 8d6ae751ec62
Revises: 311c62fe5f49
Create Date: 2022-12-06 11:08:55.896919
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '8d6ae751ec62'
down_revision = '311c62fe5f49'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('user', schema=None) as batch_op:
batch_op.add_column(sa.Column('phone', sa.String(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('user', schema=None) as batch_op:
batch_op.drop_column('phone')
# ### end Alembic commands ###

View file

@ -0,0 +1,32 @@
"""empty message
Revision ID: a14a95ec57b0
Revises: 8d6ae751ec62
Create Date: 2022-12-06 11:12:30.581556
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'a14a95ec57b0'
down_revision = '8d6ae751ec62'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('user', schema=None) as batch_op:
batch_op.add_column(sa.Column('gender', sa.String(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('user', schema=None) as batch_op:
batch_op.drop_column('gender')
# ### end Alembic commands ###

View file

@ -0,0 +1,39 @@
"""create user
Revision ID: afb7433de2f3
Revises:
Create Date: 2022-12-05 16:33:45.070436
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'afb7433de2f3'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('user',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('email', sa.String(), nullable=False),
sa.Column('password', sa.String(), nullable=False),
sa.Column('first_name', sa.String(), nullable=False),
sa.Column('last_name', sa.String(), nullable=False),
sa.Column('last_login', sa.DateTime(), nullable=False),
sa.Column('active', sa.Boolean(), nullable=False),
sa.Column('is_admin', sa.Boolean(), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('email')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('user')
# ### end Alembic commands ###

View file

@ -0,0 +1,32 @@
"""add waitlist field
Revision ID: d210860eb46a
Revises: afb7433de2f3
Create Date: 2022-12-05 17:12:08.061473
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'd210860eb46a'
down_revision = 'afb7433de2f3'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('user', schema=None) as batch_op:
batch_op.add_column(sa.Column('waitlisted', sa.Boolean(), nullable=False))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('user', schema=None) as batch_op:
batch_op.drop_column('waitlisted')
# ### end Alembic commands ###

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View file

@ -1,9 +1,23 @@
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-Assets
Flask-CORS
Flask-Mail
Flask-Login==0.6.2
Flask-Migrate==4.0.0
Flask-SQLAlchemy==3.0.2
Flask-WTF==1.0.1
greenlet==2.0.1
itsdangerous==2.1.2
Jinja2==3.1.2
Mako==1.2.4
MarkupSafe==2.1.1
msgpack==1.0.4
psycopg2==2.9.5
pynvim==0.4.3
python-dotenv==0.21.0
SQLAlchemy==1.4.44
uWSGI==2.0.21
Werkzeug==2.2.2
WTForms==3.0.1

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;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 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

Binary file not shown.

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

3
wsgi.py Normal file
View file

@ -0,0 +1,3 @@
from goathacks import create_app
application = create_app()