admin: Fix admin page
This commit is contained in:
parent
036ca7b21e
commit
72624a3f4e
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)
|
||||
|
||||
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