diff --git a/.gitignore b/.gitignore index b5a8ea2..dc6ec1b 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/.gitmodules b/.gitmodules index 4952699..807cb70 100644 --- a/.gitmodules +++ b/.gitmodules @@ -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 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..12da1f2 --- /dev/null +++ b/Makefile @@ -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 ---" + diff --git a/README.md b/README.md index 7f3de50..a2538f2 100644 --- a/README.md +++ b/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 diff --git a/admin.png b/admin.png deleted file mode 100644 index ddfa335..0000000 Binary files a/admin.png and /dev/null differ diff --git a/admin/__init__.py b/admin/__init__.py deleted file mode 100644 index 9070551..0000000 --- a/admin/__init__.py +++ /dev/null @@ -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)) \ No newline at end of file diff --git a/contrib/goathacks.ini b/contrib/goathacks.ini new file mode 100644 index 0000000..a651e93 --- /dev/null +++ b/contrib/goathacks.ini @@ -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 diff --git a/contrib/goathacks.service b/contrib/goathacks.service new file mode 100644 index 0000000..2535df3 --- /dev/null +++ b/contrib/goathacks.service @@ -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 diff --git a/contrib/goathacks.socket b/contrib/goathacks.socket new file mode 100644 index 0000000..65aff27 --- /dev/null +++ b/contrib/goathacks.socket @@ -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 diff --git a/dependency_ubuntu b/dependency_ubuntu deleted file mode 100644 index 2dbf849..0000000 --- a/dependency_ubuntu +++ /dev/null @@ -1 +0,0 @@ -sudo apt-get install python-mysqldb diff --git a/flask_app.py b/flask_app.py deleted file mode 100644 index 65294b5..0000000 --- a/flask_app.py +++ /dev/null @@ -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/') -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/') -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! Return to dashboard' - 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! Return to dashboard' - 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) - diff --git a/goathacks/__init__.py b/goathacks/__init__.py new file mode 100644 index 0000000..9539750 --- /dev/null +++ b/goathacks/__init__.py @@ -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/") + def assets(path): + return send_from_directory('templates/home/assets', path) + + return app + + diff --git a/goathacks/admin/__init__.py b/goathacks/admin/__init__.py new file mode 100644 index 0000000..b650bd2 --- /dev/null +++ b/goathacks/admin/__init__.py @@ -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/") +@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/") +@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//") +@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/") +@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() diff --git a/goathacks/cli.py b/goathacks/cli.py new file mode 100644 index 0000000..340af87 --- /dev/null +++ b/goathacks/cli.py @@ -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") + diff --git a/goathacks/config.py.example b/goathacks/config.py.example new file mode 100644 index 0000000..f9421b0 --- /dev/null +++ b/goathacks/config.py.example @@ -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 " + diff --git a/goathacks/dashboard/__init__.py b/goathacks/dashboard/__init__.py new file mode 100644 index 0000000..d9ba207 --- /dev/null +++ b/goathacks/dashboard/__init__.py @@ -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! Return to dashboard' + 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'] diff --git a/goathacks/dashboard/forms.py b/goathacks/dashboard/forms.py new file mode 100644 index 0000000..ebfad2d --- /dev/null +++ b/goathacks/dashboard/forms.py @@ -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!")]) diff --git a/goathacks/database.py b/goathacks/database.py new file mode 100644 index 0000000..f5fba56 --- /dev/null +++ b/goathacks/database.py @@ -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) diff --git a/goathacks/models.py b/goathacks/models.py new file mode 100644 index 0000000..b368749 --- /dev/null +++ b/goathacks/models.py @@ -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")) diff --git a/goathacks/registration/__init__.py b/goathacks/registration/__init__.py new file mode 100644 index 0000000..0a0e839 --- /dev/null +++ b/goathacks/registration/__init__.py @@ -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) diff --git a/goathacks/registration/forms.py b/goathacks/registration/forms.py new file mode 100644 index 0000000..3fe74c5 --- /dev/null +++ b/goathacks/registration/forms.py @@ -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") + diff --git a/static/css/materialize.min.css b/goathacks/static/css/materialize.min.css similarity index 100% rename from static/css/materialize.min.css rename to goathacks/static/css/materialize.min.css diff --git a/goathacks/static/css/style.css b/goathacks/static/css/style.css new file mode 100644 index 0000000..ffdd366 --- /dev/null +++ b/goathacks/static/css/style.css @@ -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; +} diff --git a/static/css/style.css b/goathacks/static/css/style.scss similarity index 65% rename from static/css/style.css rename to goathacks/static/css/style.scss index e3daf7e..590d65f 100644 --- a/static/css/style.css +++ b/goathacks/static/css/style.scss @@ -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; +} + diff --git a/goathacks/static/img/favicon.png b/goathacks/static/img/favicon.png new file mode 100644 index 0000000..39a081f Binary files /dev/null and b/goathacks/static/img/favicon.png differ diff --git a/static/img/hackwpilogo.png b/goathacks/static/img/hackwpilogo.png similarity index 100% rename from static/img/hackwpilogo.png rename to goathacks/static/img/hackwpilogo.png diff --git a/static/js/admin.js b/goathacks/static/js/admin.js similarity index 95% rename from static/js/admin.js rename to goathacks/static/js/admin.js index 0ccdd37..45b2515 100644 --- a/static/js/admin.js +++ b/goathacks/static/js/admin.js @@ -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 = '' diff --git a/static/js/init.js b/goathacks/static/js/init.js similarity index 100% rename from static/js/init.js rename to goathacks/static/js/init.js diff --git a/static/js/jquery-2.2.4.min.js b/goathacks/static/js/jquery-2.2.4.min.js similarity index 100% rename from static/js/jquery-2.2.4.min.js rename to goathacks/static/js/jquery-2.2.4.min.js diff --git a/static/js/jquery.easing.min.js b/goathacks/static/js/jquery.easing.min.js similarity index 100% rename from static/js/jquery.easing.min.js rename to goathacks/static/js/jquery.easing.min.js diff --git a/static/js/materialize.js b/goathacks/static/js/materialize.js similarity index 100% rename from static/js/materialize.js rename to goathacks/static/js/materialize.js diff --git a/static/js/materialize.min.js b/goathacks/static/js/materialize.min.js similarity index 100% rename from static/js/materialize.min.js rename to goathacks/static/js/materialize.min.js diff --git a/goathacks/templates/.gitkeep b/goathacks/templates/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/templates/admin.html b/goathacks/templates/admin.html similarity index 89% rename from templates/admin.html rename to goathacks/templates/admin.html index 0d9c36b..4fefb63 100644 --- a/templates/admin.html +++ b/goathacks/templates/admin.html @@ -13,7 +13,7 @@ - + @@ -43,7 +43,8 @@
JSON object of users from MLH (Including dropped applications):

Do NOT share this URL.

Get registered hackers only:
-

JSON CSV

+

JSON CSV

@@ -80,11 +81,11 @@ - - -{% include 'footer.html' %} +{% endblock %} diff --git a/goathacks/templates/emails/dropped.txt b/goathacks/templates/emails/dropped.txt new file mode 100644 index 0000000..95eebe1 --- /dev/null +++ b/goathacks/templates/emails/dropped.txt @@ -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. diff --git a/goathacks/templates/emails/registration.txt b/goathacks/templates/emails/registration.txt new file mode 100644 index 0000000..81a802f --- /dev/null +++ b/goathacks/templates/emails/registration.txt @@ -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. diff --git a/goathacks/templates/emails/waitlist_promotion.txt b/goathacks/templates/emails/waitlist_promotion.txt new file mode 100644 index 0000000..eb84edb --- /dev/null +++ b/goathacks/templates/emails/waitlist_promotion.txt @@ -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. diff --git a/templates/footer.html b/goathacks/templates/footer.html similarity index 100% rename from templates/footer.html rename to goathacks/templates/footer.html diff --git a/templates/header.html b/goathacks/templates/header.html similarity index 87% rename from templates/header.html rename to goathacks/templates/header.html index 26a453b..bc6489a 100644 --- a/templates/header.html +++ b/goathacks/templates/header.html @@ -4,7 +4,10 @@ - + + {% assets 'scss' %} + + {% endassets %}