Tracking PR for registration rewrite #5
9 changed files with 333 additions and 23 deletions
|
@ -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
182
goathacks/admin/__init__.py
Normal 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()
|
|
@ -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)
|
||||
![]() 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
|
||||
|
|
|
@ -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 = ''
|
||||
|
|
|
@ -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>
|
||||
|
|
32
migrations/versions/311c62fe5f49_.py
Normal file
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 ###
|
32
migrations/versions/8d6ae751ec62_.py
Normal file
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
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 ###
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue
@willhockey20 is this something that actually needs to be stored? I only included it bc it was in the original registration system.