Merge pull request #5 from wpi-acm/rewrite
Tracking PR for registration rewrite. No longer requires MLH API and is more modular in design.
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)
|
||||
|
||||
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()])
|
||||
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">
|
||||
|
@ -80,11 +81,11 @@
|
|||
<canvas id="schoolCanvas" width="400" height="400"></canvas>
|
||||
<script>
|
||||
let schoolNames = []
|
||||
let schoolNums = []
|
||||
let schoolNums = []
|
||||
|
||||
{% for school in schools %}
|
||||
schoolNames.push('{{ school }}')
|
||||
schoolNums.push({{ schools[school] }})
|
||||
schoolNums.push({{ schools[school] }})
|
||||
{% endfor %}
|
||||
|
||||
let schoolCtx = document.getElementById('schoolCanvas')
|
||||
|
@ -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");
|
||||
|
@ -98,4 +98,4 @@
|
|||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
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()
|