Compare commits

..

18 commits

Author SHA1 Message Date
ngolp
4e24274915 basic implementation of github issue #28 (modifying user details right from admin dashboard). Went with first name, last name, school, and phone number, since the user can change the shirt size and special accomodations on their own. 2024-10-31 13:41:12 -04:00
ngolp
c70fa7cbae Merge branch '2025' into searching-sorting-user-list 2024-10-31 11:39:20 -04:00
ngolp
228244ad2d Hacker table on admin dashboard can now be sorted by double clicking the headers. Spent a bit trying to implement this manually before learning about the sortable class. The more you know! 2024-10-27 12:28:10 -04:00
ngolp
a46a6b2831 Changed back the SQL query in /mail route 2024-10-22 20:11:34 -04:00
ngolp
44673d1b07 See Github Issue #10. Added separation of admins and users on admin dashboard.
Refactored a bit of the original admin.home endpoint into a helper function to accompany two routes that use the same template without having to copy+paste code.
2024-10-22 20:11:34 -04:00
warren yun
e9446b97c7 update user schema 2024-10-22 17:56:14 -04:00
warren yun
0268f70f9b more fields 2024-10-22 17:51:44 -04:00
warren yun
7defb52a56 countries 2024-10-20 22:15:12 -04:00
warren yun
5a3c43e98b schools dropdown 2024-10-20 21:55:25 -04:00
warren yun
2b0b2912f3 countdown (hot) 2024-10-18 23:51:22 -04:00
ngolp
b65154b865 Basic implementation of searching by email. I'd like to expand this to searching by other column values as well if necessary, and I still need to add sorting. Also, "registered users" isn't perfectly centered (its pushed off just a bit by the search button). This commit is mainly so I can save my progress. 2024-10-17 17:56:27 -04:00
warren yun
e89b2ab7b4 modified theme 2024-09-30 22:09:25 -04:00
warren yun
16f08f7750 playing around with styling and items 2024-09-17 20:01:38 -04:00
Cara Salter
d0240d15d8
Merge pull request #33 from wpi-acm/event-date-time-fields
Event Modals & Split date/time fields
2024-06-05 09:21:09 -04:00
Cara Salter
f5eb90b73d
Make unauth handler redirect to login 2024-06-03 18:15:18 -04:00
Cara Salter
a3c89dd478
Support editing event category in modals 2024-06-03 18:12:39 -04:00
Cara Salter
7fc06bde11
Enable editing/creating/deleting events
Wholly through modals, yay!
2024-06-02 12:58:07 -04:00
Cara Salter
e28912b997
Input event modal and create some supporting infra
update form to split date/time as well
2024-06-02 12:30:40 -04:00
14 changed files with 142 additions and 118 deletions

1
.gitignore vendored
View file

@ -1,6 +1,7 @@
resumes*/
resumes*.zip
config_hackWPI.py
config.py
admin/*.json
admin/*.csv

View file

@ -1,30 +0,0 @@
import os
from dotenv import load_dotenv, dotenv_values
basedir = os.path.abspath(os.path.dirname(__file__))
load_dotenv(os.path.join(basedir, '.env'))
class Config():
TESTING = dotenv_values().get("TESTING") or False
DEBUG = dotenv_values().get("DEBUG") or False
SQLALCHEMY_DATABASE_URI = dotenv_values().get("SQLALCHEMY_DATABASE_URI") or "postgresql://localhost/goathacks"
MAX_BEFORE_WAITLIST = int(dotenv_values().get("MAX_BEFORE_WAITLIST") or 1)
MCE_API_KEY = dotenv_values().get("MCE_API_KEY")
SECRET_KEY = dotenv_values().get("SECRET_KEY") or "bad-key-change-me"
UPLOAD_FOLDER = dotenv_values().get("UPLOAD_FOLDER") or "./uploads/"
DISCORD_LINK = dotenv_values().get("DISCORD_LINK") or None
# Mail server settings
MAIL_SERVER = dotenv_values().get("MAIL_SERVER") or "localhost"
MAIL_PORT = dotenv_values().get("MAIL_PORT") or 25
MAIL_USE_TLS = dotenv_values().get("MAIL_USE_TLS") or False
MAIL_USE_SSL = dotenv_values().get("MAIL_USE_SSL") or False
MAIL_USERNAME = dotenv_values().get("MAIL_USERNAME") or "dummy"
MAIL_PASSWORD = dotenv_values().get("MAIL_PASSWORD") or "dummy"
MAIL_DEFAULT_SENDER = dotenv_values().get("MAIL_DEFAULT_SENDER") or "GoatHacks Team <hack@wpi.edu>"
MAIL_SUPPRESS_SEND = dotenv_values().get("MAIL_SUPPRESS_SEND") or TESTING

View file

@ -9,8 +9,6 @@ from flask_bootstrap import Bootstrap5
from flask_font_awesome import FontAwesome
from flask_qrcode import QRcode
from config import Config
db = SQLAlchemy()
@ -23,10 +21,10 @@ bootstrap = Bootstrap5()
font_awesome = FontAwesome()
qrcode = QRcode()
def create_app(config_class=Config):
def create_app():
app = Flask(__name__)
app.config.from_object(config_class)
app.config.from_pyfile("config.py")
db.init_app(app)
migrate.init_app(app, db)

View file

@ -4,6 +4,8 @@ from flask_mail import Message
from goathacks.models import User
from sqlalchemy.exc import IntegrityError
bp = Blueprint("admin", __name__, url_prefix="/admin")
from goathacks import db, mail as app_mail
@ -221,6 +223,44 @@ def hackers():
users = User.query.all()
return User.create_json_output(users)
@bp.route("/updateHacker", methods=["POST"])
@login_required
def updateHacker():
if not current_user.is_admin:
return redirect(url_for("dashboard.home"))
# get params from json
hacker_id = request.json['hacker_id']
change_field = request.json['change_field']
new_val = request.json['new_val']
# find the user in db
user = User.query.filter_by(id=hacker_id).one()
if user is None:
return {"status": "error", "msg": "user not found"}
# update the hacker depending on change_field
match change_field:
case "first_name":
user.first_name = new_val
case "last_name":
user.last_name = new_val
case "school":
user.school = new_val
case "phone":
user.phone = new_val
try:
db.session.commit()
except IntegrityError as err:
db.session.rollback()
flash("Could not update user information for user " + hacker_id)
return {"status": "error"}
return {"status": "success"}
import json
import csv
from io import StringIO

View file

@ -23,10 +23,8 @@ gr = AppGroup("user")
@click.option("--school", prompt=True)
@click.option("--phone", prompt=True)
@click.option("--gender", prompt=True)
@click.option("--country", prompt=True)
@click.option("--age", prompt=True)
def create_user(email, first_name, last_name, password, school, phone, gender,
admin,age, country):
admin):
"""
Creates a user
"""
@ -50,9 +48,7 @@ def create_user(email, first_name, last_name, password, school, phone, gender,
school=school,
phone=phone,
gender=gender,
is_admin=admin,
country=country,
age=age
is_admin=admin
)
db.session.add(user)
db.session.commit()

View file

@ -0,0 +1,18 @@
SQLALCHEMY_DATABASE_URI="postgresql://localhost/goathacks"
MAX_BEFORE_WAITLIST=1
SECRET_KEY="bad-key-change-me"
UPLOAD_FOLDER="./uploads/"
DISCORD_LINK=None
# Mail settings
MAIL_SERVER="localhost"
MAIL_PORT=25
MAIL_USE_TLS=False
MAIL_USE_SSL=False
MAIL_USERNAME="dummy"
MAIL_PASSWORD="dummy"
MAIL_DEFAULT_SENDER="GoatHacks Team <hack@wpi.edu>"

View file

@ -1,4 +1,4 @@
from flask import Blueprint, current_app, flash, jsonify, redirect, render_template, request, url_for
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
@ -47,17 +47,9 @@ def resume():
filename = current_user.first_name.lower() + '_' + current_user.last_name.lower() + '_' + str(
current_user.id) + '.' + resume.filename.split('.')[-1].lower()
filename = secure_filename(filename)
if not os.path.exists(current_app.config['UPLOAD_FOLDER']):
try:
os.makedirs(current_app.config['UPLOAD_FOLDER'])
except Exception:
flash("Error saving resume. Contact acm-sysadmin@wpi.edu")
return redirect(url_for("dashboard.home"))
resume.save(os.path.join(current_app.config['UPLOAD_FOLDER'], filename))
flash("Resume uploaded!")
return redirect(url_for("dashboard.home"))
flash("Something went wrong. If this keeps happening, contact hack@wpi.edu for assistance")
return redirect(url_for("dashboard.home"))
return 'Resume uploaded! <a href="/dashboard">Return to dashboard</a>'
return "Something went wrong. If this keeps happening, contact hack@wpi.edu for assistance"
def allowed_file(filename):

View file

@ -25,7 +25,6 @@ class RegisterForm(FlaskForm):
country = SelectField("Country", choices=[(country.split(",")[0], country.split(",")[0]) for country in countries_list], widget=widgets.Select())
newsletter = BooleanField("Subscribe to the MLH newsletter?")
agree_coc = BooleanField("I confirm that I have read and agree to the Code of Conduct", validators=[DataRequired()])
logistics = BooleanField("I authorize you to share my application/registration with Major League Hacking for event administration, ranking, and MLH administration in-line with the MLH privacy policy.I further agree to the terms of both the MLH Contest Terms and Conditions and the MLH Privacy Policy.", validators=[DataRequired()])
submit = SubmitField("Register")

View file

@ -14,8 +14,22 @@
{% block app_content %}
<div class="card text-center">
<div class="card-body">
<h1 class="h3 mb-3 fw-normal">Registered Users</h1>
<table id="hackers" class="table table-striped">
<div style="display:flex;flex-wrap:nowrap;align-items:center;">
<div class="dropdown">
<a href="#" class="btn btn-primary dropdown-toggle"
data-bs-toggle="dropdown">Search<span
class="caret"></span></a>
<ul class="dropdown-menu">
<input style="padding:5px;margin-left:10px;margin-right:10px;" type="text" id="searchbox" name="searchbox-text" placeholder="Search By Email" onkeyup="filterHackers()"/>
</ul>
</div>
<!-- TODO: get "Registered Users" properly centered -->
<h1 style="flex-grow:1;justify-content:center;" class="h3 mb-3 fw-normal">Registered Users</h1>
</div>
<table id="hackers" class="table table-striped sortable">
<thead>
<tr>
<th>Options</th>
@ -79,16 +93,71 @@
<td>{{ hacker.id }}</td>
<td>{{ hacker.last_login }}</td>
<td>{{ hacker.email }}</td>
<td>{{ hacker.first_name + ' ' + hacker.last_name }}</td>
<td>{{ hacker.phone }}</td>
<td>
<div style="display: flex; justify-content: flex-start;">
<input style="padding:5px; margin-left:10px; margin-right:10px;width:fit-content;max-width:100px;" type="text" id="{{hacker.id}}-namebox-first" placeholder="first_name" value="{{hacker.first_name}}" onchange="updateHacker(this.id, 'first_name', this.value)"/>
<input style="padding:5px; margin-left:10px; margin-right:10px;width:fit-content;max-width:100px;" type="text" id="{{hacker.id}}-namebox-last" placeholder="last_name" value="{{hacker.last_name}}" onchange="updateHacker(this.id, 'last_name', this.value)"/>
</div>
</td>
<td>
<input style="padding:5px; margin-left:10px; margin-right:10px;width:fit-content;max-width:100px;" type="text" id="{{hacker.id}}-phonebox" placeholder="phone" value="{{hacker.phone}}" onchange="updateHacker(this.id, 'phone', this.value)"/>
</td>
<td>{{ hacker.shirt_size }}</td>
<td>{{ hacker.accomodations }}</td>
<td>{{ hacker.school }}</td>
<td>
<input style="padding:5px; margin-left:10px; margin-right:10px;width:fit-content;max-width:75px;" type="text" id="{{hacker.id}}-schoolbox" placeholder="school" value="{{hacker.school}}" onchange="updateHacker(this.id, 'school', this.value)"/>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<script type="text/javascript">
function filterHackers() {
//get hacker table and searchbox info
const input = document.getElementById("searchbox").value.toLowerCase();
const hackertable = document.getElementById("hackers");
let rows = hackertable.getElementsByTagName("tr");
//iterate over all rows
for(let i = 1; i < rows.length; i++) {
//get the email
const cells = rows[i].getElementsByTagName("td");
const emailCell = cells[6];
//if there is an email, display or dont display based on searchbox
if(emailCell) {
const emailText = emailCell.textContent.toLowerCase();
if(!emailText.includes(input)) {
rows[i].style.display = "none";
}
else {
rows[i].style.display = "";
}
}
}
}
function updateHacker(id, change_field, new_val) {
//tell backend to update a specific field for a hacker
const headers = [
["Content-Type", "application/json"],
];
let body = {
"hacker_id": id.substr(0, id.indexOf('-')),
"change_field": change_field,
"new_val": new_val,
}
//send the post request, and report the error if there is one
fetch('/admin/updateHacker', {method: 'POST', body: JSON.stringify(body), headers: headers}).catch((err) => {
window.alert("Error updating user - see console for details");
console.log(err);
})
}
</script>
{% endblock %}

View file

@ -15,12 +15,6 @@
<p class="card-text">Let us know if you have any questions by sending
them to <a href="mailto:hack@wpi.edu">hack@wpi.edu</a></p>
{% if not current_user.waitlisted and config['DISCORD_LINK'] %}
<p>Make sure to join our Discord to get the latest updates!</p>
<button type="button" class="btn btn-primary mb-3"><a href="{{ config['DISCORD_LINK']
}}" class="link-light">Discord</a></button>
{% endif %}
<div class="row center justify-content-center">
<form method="post">
{{ form.csrf_token() }}
@ -39,7 +33,7 @@
</div>
<hr/>
<div class="row center justify-content-center">
<form method="post" action="{{url_for('dashboard.resume')}}"
<form method="post" action={{url_for('dashboard.resume')}}"
enctype="multipart/form-data">
{{ resform.csrf_token() }}
<p><b>If you'd like, add your resume to send to

@ -1 +1 @@
Subproject commit db2a7a865f9b3865fa2180b1b53b1c2d2640be81
Subproject commit a107d4daf149bac2b8bd1182b399e57e8171c1f8

View file

@ -86,20 +86,6 @@
{{ form.agree_coc }}
I confirm that I have read and agree to the <a href="https://static.mlh.io/docs/mlh-code-of-conduct.pdf" target="_blank">MLH Code of Conduct</a>
</div>
<div class="form-check mb-3 required">
{{ form.logistics }}
I authorize you to share my application/registration with Major League Hacking
for event administration, ranking, and MLH administration in-line with the MLH
privacy policy. I further agree to the terms of both the <a
href="https://github.com/MLH/mlh-policies/blob/main/contest-terms.md">MLH
Contest Terms
and
Conditions</a>
and the <a
href="https://github.com/MLH/mlh-policies/blob/main/privacy-policy.md">MLH
Privacy
Policy</a>.
</div>
<div class="form-check mb-3">
{{ form.newsletter }}
Subscribe to the MLH newsletter?

View file

@ -1,38 +0,0 @@
"""empty message
Revision ID: f5b70c6e73eb
Revises: 858e0d45876f
Create Date: 2024-10-31 13:04:48.500263
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'f5b70c6e73eb'
down_revision = '858e0d45876f'
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('newsletter', sa.Boolean(), nullable=False))
batch_op.add_column(sa.Column('country', sa.String(), nullable=False))
batch_op.add_column(sa.Column('age', sa.Integer(), nullable=False))
batch_op.add_column(sa.Column('dietary_restrictions', 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('dietary_restrictions')
batch_op.drop_column('age')
batch_op.drop_column('country')
batch_op.drop_column('newsletter')
# ### end Alembic commands ###

View file

@ -26,4 +26,3 @@ ulid
bootstrap-flask
Font-Awesome-Flask
tabulate
markupsafe