Tracking PR for registration rewrite #5

Merged
Muirrum merged 32 commits from rewrite into master 2022-12-15 18:32:08 -05:00
9 changed files with 333 additions and 23 deletions
Showing only changes of commit 72624a3f4e - Show all commits

View file

@ -3,12 +3,14 @@ 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
db = SQLAlchemy()
migrate = Migrate()
login = LoginManager()
environment = Environment()
cors = CORS()
def create_app():
app = Flask(__name__)
@ -19,6 +21,7 @@ def create_app():
migrate.init_app(app, db)
login.init_app(app)
environment.init_app(app)
cors.init_app(app)
scss = Bundle('css/style.scss', filters='scss',
output='css/style.css')
@ -28,8 +31,10 @@ def create_app():
from . import registration
from . import dashboard
from . import admin
app.register_blueprint(registration.bp)
app.register_blueprint(dashboard.bp)
app.register_blueprint(admin.bp)
return app

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

@ -0,0 +1,182 @@
from flask import Blueprint, jsonify, redirect, render_template, url_for
from flask_login import current_user, login_required
from goathacks.models import User
bp = Blueprint("admin", __name__, url_prefix="/admin")
from goathacks import db
@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("/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"}
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()
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()

View file

@ -17,6 +17,29 @@ class User(db.Model, UserMixin):
shirt_size = Column(String, nullable=True)
accomodations = Column(String, nullable=True)
checked_in = Column(Boolean, nullable=False, default=False)
school = Column(String, nullable=True)
phone = Column(String, nullable=True)
gender = Column(String, nullable=True)
Muirrum commented 2022-12-08 20:25:22 -05:00 (Migrated from github.com)
Review

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

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

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

The data was required by mlh, still could be nice to have so I'd keep it
def create_json_output(lis):
hackers = []
for u in lis:
hackers.append({
'checked_in': u.checked_in,
'waitlisted': u.waitlisted,
'admin': u.is_admin,
'id': u.id,
'email': u.email,
'first_name': u.first_name,
'last_name': u.last_name,
'phone_number': u.phone,
'shirt_size': u.shirt_size,
'special_needs': u.accomodations,
'school': u.school
})
return hackers
@login.user_loader

View file

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

View file

@ -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">
@ -81,10 +82,12 @@
<script>
let schoolNames = []
let schoolNums = []
console.log("{{schools}}")
{% for school in schools %}
schoolNames.push('{{ school }}')
schoolNums.push({{ schools[school] }})
console.log("{{school}}")
{% endfor %}
let schoolCtx = document.getElementById('schoolCanvas')
@ -186,13 +189,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 +220,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 +254,15 @@
</td>
<td id="{{ hacker['id'] }}-checked_in">{{ hacker['checked_in'] }}</td>
<td id="{{ hacker['id'] }}-waitlisted">{{ hacker['waitlisted'] }}</td>
<td>{{ hacker['admin'] }}</td>
<td>{{ hacker.is_admin }}</td>
<td>{{ hacker['id'] }}</td>
<td>{{ hacker['registration_time'] }}</td>
<td>{{ hacker['email'] }}</td>
<td>{{ hacker['first_name'] + ' ' + hacker['last_name'] }}</td>
<td>{{ hacker['phone_number'] }}</td>
<td>{{ hacker['phone'] }}</td>
<td>{{ hacker['shirt_size'] }}</td>
<td>{{ hacker['special_needs'] }}</td>
<td>{{ hacker['school']['name'] }}</td>
<td>{{ hacker['accomodations'] }}</td>
<td>{{ hacker['school'] }}</td>
</tr>
{% endfor %}
</tbody>

View file

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

View file

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

View file

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

View file

@ -2,6 +2,7 @@ alembic==1.8.1
click==8.1.3
Flask==2.2.2
Flask-Assets
Flask-CORS
Flask-Login==0.6.2
Flask-Migrate==4.0.0
Flask-SQLAlchemy==3.0.2