Tracking PR for registration rewrite #5
2
.gitignore
vendored
|
@ -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
|
@ -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
|
@ -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 ---"
|
||||
|
47
README.md
|
@ -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
Before Width: | Height: | Size: 529 KiB |
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
|
@ -1 +0,0 @@
|
|||
sudo apt-get install python-mysqldb
|
696
flask_app.py
|
@ -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
|
@ -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
|
@ -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
|
@ -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")
|
||||
|
16
goathacks/config.py.example
Normal 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>"
|
||||
|
56
goathacks/dashboard/__init__.py
Normal 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']
|
16
goathacks/dashboard/forms.py
Normal 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
|
@ -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
|
@ -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)
|
||||
![]() 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"))
|
83
goathacks/registration/__init__.py
Normal 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)
|
25
goathacks/registration/forms.py
Normal 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()])
|
||||
![]() This could probably get turned into an 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")
|
||||
|
185
goathacks/static/css/style.css
Normal 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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
BIN
goathacks/static/img/favicon.png
Normal file
After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 77 KiB |
|
@ -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 = ''
|
0
goathacks/templates/.gitkeep
Normal 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>
|
20
goathacks/templates/base.html
Normal 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' %}
|
|
@ -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 %}
|
13
goathacks/templates/emails/dropped.txt
Normal 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.
|
19
goathacks/templates/emails/registration.txt
Normal 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.
|
18
goathacks/templates/emails/waitlist_promotion.txt
Normal 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.
|
|
@ -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>
|
1
goathacks/templates/home
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit fba594664faeb8b6056462a0f8bc79a0d21a3768
|
32
goathacks/templates/login.html
Normal 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 %}
|
|
@ -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");
|
60
goathacks/templates/register.html
Normal 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 %}
|
||||
|
BIN
hacker.png
Before Width: | Height: | Size: 122 KiB |
|
@ -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")
|
|
@ -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
|
|
@ -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
|
@ -0,0 +1 @@
|
|||
Single-database configuration for Flask.
|
50
migrations/alembic.ini
Normal 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
|
@ -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
|
@ -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"}
|
32
migrations/versions/311c62fe5f49_.py
Normal 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 ###
|
33
migrations/versions/3f427be4ce8a_.py
Normal 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 ###
|
34
migrations/versions/55d77cdbbb49_.py
Normal 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 ###
|
32
migrations/versions/8d6ae751ec62_.py
Normal 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 ###
|
32
migrations/versions/a14a95ec57b0_.py
Normal 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 ###
|
39
migrations/versions/afb7433de2f3_create_user.py
Normal 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 ###
|
32
migrations/versions/d210860eb46a_add_waitlist_field.py
Normal 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 ###
|
BIN
options.png
Before Width: | Height: | Size: 15 KiB |
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
Before Width: | Height: | Size: 6.6 KiB |
Before Width: | Height: | Size: 141 KiB |
Before Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 100 KiB |
Before Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 68 KiB |
Before Width: | Height: | Size: 38 KiB |
Before Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 236 KiB |
Before Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 17 KiB |
|
@ -1 +0,0 @@
|
|||
Subproject commit b2a60294b8f9d37d8c486cd817f8260cc860caad
|
3
wsgi.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from goathacks import create_app
|
||||
|
||||
application = create_app()
|
@willhockey20 is this something that actually needs to be stored? I only included it bc it was in the original registration system.