Admin dashboard!

Get event management page almost working! Deleting events works,
creating new events works, editing events (almost) works! Just need to
figure out how to properly set the date and time fields when editing...

Also, user management! You can see a list of users and will
theoretically be able to promote officers from the web UI
This commit is contained in:
Cara Salter 2024-03-06 16:18:18 -05:00
parent d6302ea673
commit 5bd7d548c1
No known key found for this signature in database
GPG key ID: A8A3A601440EADA5
10 changed files with 411 additions and 1 deletions

View file

@ -48,5 +48,8 @@ def create_app():
from .dashboard import bp as dash_bp from .dashboard import bp as dash_bp
app.register_blueprint(dash_bp) app.register_blueprint(dash_bp)
from .admin import bp as admin_bp
app.register_blueprint(admin_bp)
return app return app

101
acmsite/admin/__init__.py Normal file
View file

@ -0,0 +1,101 @@
from flask import Blueprint, flash, redirect, render_template, request, url_for
import ulid
import datetime
from flask_login import current_user, login_required
from acmsite.models import User, Event
from .forms import EventForm
from acmsite import db
bp = Blueprint("admin", __name__, url_prefix="/admin")
@bp.route("/")
@login_required
def home():
if not current_user.is_admin:
flash("Unauthorized")
return redirect(url_for('dashboard.home'))
return render_template("admin/index.html")
@bp.route("/users")
@login_required
def users():
if not current_user.is_admin:
flash("Unauthorized")
return redirect(url_for('dashboard.home'))
user_list = User.query.all()
return render_template("admin/users.html", u_list=user_list)
@bp.route("/events", methods=["GET","POST"])
@login_required
def events():
if not current_user.is_admin:
flash("Unauthorized")
return redirect(url_for('dashboard.home'))
event_list = Event.query.all()
form = EventForm(request.form)
if request.method == 'POST':
name = request.form.get('name')
description = request.form.get('description')
location = request.form.get('location')
start_day = request.form.get('start_day')
start_time = request.form.get('start_time')
end_day = request.form.get('end_day')
end_time = request.form.get('end_time')
print(start_day)
print(start_time)
start = datetime.datetime.combine(datetime.date.fromisoformat(start_day),
datetime.time.fromisoformat(start_time))
end = datetime.datetime.combine(datetime.date.fromisoformat(end_day),
datetime.time.fromisoformat(end_time))
e = Event(
id=ulid.ulid(),
name=name,
description=description,
location=location,
start_time=start,
end_time=end)
db.session.add(e)
db.session.commit()
return render_template("admin/events.html", e_list=event_list, form=form)
@bp.route("/event/<string:id>")
@login_required
def event(id):
if not current_user.is_admin:
return {"status": "error", "message": "Unauthorized"}
event = Event.query.filter_by(id=id).first()
if event is None:
return {"status": "error", "message": "Invalid event ID"}
return event.create_json()
@bp.route("/event/<string:id>/delete")
@login_required
def delete_event(id):
if not current_user.is_admin:
return {"status": "error", "message": "Unauthorized"}
event = Event.query.filter_by(id=id).first()
if event is None:
return {"status": "error", "message": "Invalid event ID"}
db.session.delete(event)
db.session.commit()
return {"status": "success"}

12
acmsite/admin/forms.py Normal file
View file

@ -0,0 +1,12 @@
from flask_wtf import FlaskForm
from wtforms import DateTimeField, DateField, StringField, TextAreaField, TimeField
from wtforms.validators import DataRequired
class EventForm(FlaskForm):
name = StringField('Name', validators=[DataRequired()])
description = TextAreaField('Description')
location = StringField('Location', validators=[DataRequired()])
start_day = DateField('Start Day', validators=[DataRequired()])
start_time = TimeField('Start Time')
end_day = DateField('End Day', validators=[DataRequired()])
end_time = TimeField('End Time')

View file

@ -38,3 +38,13 @@ class Event(db.Model):
location = Column(String, nullable=False) location = Column(String, nullable=False)
start_time=Column(DateTime, nullable=False) start_time=Column(DateTime, nullable=False)
end_time=Column(DateTime, nullable=False) end_time=Column(DateTime, nullable=False)
def create_json(self):
return {
"id": self.id,
"name": self.name,
"description": self.description,
"location": self.location,
"start_time": self.start_time.isoformat(),
"end_time": self.end_time.isoformat(),
}

2
acmsite/static/js/jquery-3.6.3.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,33 @@
{% extends 'layout.html' %}
{% block navbar %}
<nav class="navbar mb-4 navbar-expand-lg">
<div class="container-fluid">
<button class="navbar-toggler" type="button" data-bs-toggle="collapse"
data-bs-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent"
aria-expanded="true"
aria-label="Toggle
navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="navbar-header">
<a class="navbar-brand" href="{{ url_for('admin.home') }}">
<img src="{{url_for('static', filename='img/logo.png')}}"
alt="Logo" width="35" height="35" class="d-inline-block
align-text-middle mx-2">ACM Admin</a>
</div>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="nav navbar-nav me-auto mb-2 mb-lg-0">
{{ render_nav_item('admin.users', 'Member List')}}
{{ render_nav_item('admin.events', 'Event List')}}
{{ render_nav_item('admin.home', 'Bulk Mail Tool')}}
</ul>
<ul class="nav navbar-nav">
{{ render_nav_item('dashboard.home', '<- Back To Site') }}
{{ render_nav_item('auth.logout', 'Logout') }}
</ul>
</div>
</div>
</nav>
{% endblock %}

View file

@ -0,0 +1,214 @@
{% extends "admin/admin-layout.html" %}
{% import "bootstrap5/form.html" as wtf %}
{% block app_content %}
<h1>Event list</h1>
<table class="table table-striped">
<thead>
<tr>
<th>Name</th>
<th>Location</th>
<th>Start Time</th>
<th>End Time</th>
<th><button type="button" class="btn btn-primary" data-bs-toggle="modal"
data-bs-target="#editModal">New</button></th>
</tr>
</thead>
<tbody>
{% for e in e_list %}
<tr>
<td>{{ e.name }}</td>
<td>{{ e.location }}</td>
<td>{{ e.start_time }}</td>
<td>{{ e.end_time }}</td>
<td>
<div class="dropdown">
<a href="#" class="btn btn-primary dropdown-toggle"
data-bs-toggle="dropdown"><span
class="caret"></span></a>
<ul class="dropdown-menu">
<li class="dropdown-item">
<a href="#editModal"
data-bs-toggle="modal" data-id="{{ e.id }}">Edit</a>
</li>
<li class="dropdown-item">
<a href="#deleteModal"
data-bs-toggle="modal" data-id="{{ e.id }}">Delete Event</a>
</ul>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<!-- Modals -->
<div class="modal" id="editModal" tabindex="-1" aria-labelledby="editModalLabel"
aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="editModalLabel">Event</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form class="form" role="form" method="post">
<div class="modal-body">
{{ form.csrf_token }}
<div class="form-floating mb-3 required">
{{ form.name(class="form-control") }}
{{ form.name.label() }}
</div>
<div class="form-floating mb-3">
{{ form.description(class="form-control") }}
{{ form.description.label() }}
</div>
<div class="form-floating mb-3 required">
{{ form.location(class="form-control") }}
{{ form.location.label() }}
</div>
<div class="row">
<div class="col">
<div class="form-floating mb-3 required">
{{ form.start_day(class="form-control") }}
{{ form.start_day.label() }}
</div>
</div>
<div class="col">
<div class="form-floating mb-3 required">
{{ form.start_time(class="form-control") }}
{{ form.start_time.label() }}
</div>
</div>
</div>
<div class="row">
<div class="col">
<div class="form-floating mb-3 required">
{{ form.end_day(class="form-control") }}
{{ form.end_day.label() }}
</div>
</div>
<div class="col">
<div class="form-floating mb-3 required">
{{ form.end_time(class="form-control") }}
{{ form.end_time.label() }}
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="submit" class="btn btn-primary">Save changes</button>
</div>
</form>
</div>
</div>
</div>
<div class="modal" id="deleteModal" tabindex="-1"
aria-labelledby="deleteModalLabel"
aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="deleteModalLabel">Delete
Event?</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal"
aria-label="Close">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary"
data-bs-dismiss="modal">Cancel</button>
<button type="button" id="delete" data-bs-dismiss="modal" class=" btn btn-danger">Delete</button>
</div>
</div>
</div>
</div>
<script src="{{ url_for('static', filename='js/jquery-3.6.3.min.js') }}" charset="utf-8"></script>
<script charset="utf-8">
const deleteButton = document.getElementById("delete")
deleteButton.addEventListener("click", (event) => {
button = $(event.relatedTarget)
id = deleteButton.dataset.id
const deleteRequest = new Request(`/admin/event/${id}/delete`)
fetch(deleteRequest)
.then(async (res) => {
window.alert(await res.text())
});
});
$('#deleteModal').on('show.bs.modal', function(event) {
var modal = $(this)
var button = $(event.relatedTarget)
var id = button.data("id")
// find delete button
delButton = document.getElementById("delete")
delButton.dataset.id = id
});
$('#editModal').on('show.bs.modal', function(event) {
var modal = $(this)
// Zero all fields
modal.find('#name').val('')
modal.find('#location').val('')
modal.find('#description').val('')
modal.find('#start_day').val('')
modal.find('#start_time').val('')
modal.find('#end_day').val('')
modal.find('#end_time').val('')
var button = $(event.relatedTarget)
var name,description,loc,start_time,start_day,end_time,end_day
id = button.data('id')
if (id) {
$.get(`/admin/event/${id}`, (data) => {
console.log(data)
if (data.status == "error") {
is_new = "yes"
} else {
name = data.name,
description = data.description,
loc = data.location
start = new Date(data.start_time)
start_day = start.toLocaleDateString()
console.log(start_day)
start_time = start.toLocaleTimeString()
end = new Date(data.end_time)
end_day = end.toLocaleDateString()
end_time = end.toLocaleTimeString()
}
modal.find('#name').val(name)
modal.find('#location').val(loc)
modal.find('#description').val(description)
modal.find('#start_day').val(start_day)
modal.find('#start_time').val(start_time)
modal.find('#end_day').val(end_day)
modal.find('#end_time').val(end_time)
});
}
});
$('#deleteModal').on('hidden.bs.modal', function(event) {
location.reload()
});
$('#editModal').on('hidden.bs.modal', function(event) {
location.reload()
});
</script>
{% endblock app_content %}
{% block scripts %}
{{super()}}
{% endblock %}

View file

@ -0,0 +1,7 @@
{% extends "admin/admin-layout.html" %}
{% block app_content %}
<h1>Welcome to the Officer Dashboard!</h1>
<p>Use the navbar up top to navigate to a user dashboard, event dashboard, and
bulk mail tool (when/if it works...).</p>
{% endblock app_content %}

View file

@ -0,0 +1,28 @@
{% extends "admin/admin-layout.html" %}
{% block app_content %}
<h1>Member List</h1>
<table class="table table-striped">
<thead>
<tr>
<th>Email</th>
<th>Name</th>
<th>Created</th>
<th>Last Login</th>
<th>Officer?</th>
</tr>
</thead>
<tbody>
{% for u in u_list %}
<tr>
<td>{{ u.email }}</td>
<td>{{ u.first_name }} {{ u.last_name }}</td>
<td>{{ u.created }}</td>
<td>{{ u.last_login }}</td>
<td>{{ u.is_admin }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock app_content %}

View file

@ -36,7 +36,7 @@
{% if current_user.is_authenticated %} {% if current_user.is_authenticated %}
{{ render_nav_item('dashboard.home', 'Dashboard') }} {{ render_nav_item('dashboard.home', 'Dashboard') }}
{% if current_user.is_admin %} {% if current_user.is_admin %}
{{ render_nav_item('dashboard.home', 'Admin Dash') }} {{ render_nav_item('admin.home', 'Admin Dash') }}
{% endif %} {% endif %}
{{ render_nav_item('auth.logout', 'Logout') }} {{ render_nav_item('auth.logout', 'Logout') }}
{% else %} {% else %}