Admin Panel Guide¶
This document covers the Flask-Admin interface for system administration in the School Clubs Management System.
Overview¶
The admin panel provides a web-based interface for managing all aspects of the system:
- User management
- Club administration and verification
- Post and event moderation
- Membership management
- Application processing
Access URL: /admin/
Access Control¶
Authentication¶
Only users with user_type='admin' can access the admin panel.
class AdminModelView(ModelView):
"""Base view with admin-only access control."""
def is_accessible(self):
"""Check if current user is authenticated and is an admin."""
return current_user.is_authenticated and current_user.user_type == "admin"
def inaccessible_callback(self, name, **kwargs):
"""Return 403 Forbidden for non-admin users."""
abort(403)
Admin Index¶
The admin home redirects to the main landing page:
class MyAdminIndexView(AdminIndexView):
@expose("/")
def index(self):
if not (current_user.is_authenticated and current_user.user_type == "admin"):
return self.render("admin/not_authorized.html")
return redirect(url_for("main.landing_page"))
Admin Views¶
User Administration¶
Path: /admin/user/
Model: User
Visible Columns¶
| Column | Type | Description |
|---|---|---|
user_id |
Integer | Unique identifier |
first_name |
String | First name |
last_name |
String | Last name |
email |
String | Email address |
user_type |
String | 'student' or 'admin' |
created_at |
DateTime | Account creation date |
Form Columns¶
first_namelast_nameemailuser_type(dropdown: Student/Admin)followed_clubs(multi-select)
Features¶
Search:
Search users by name or email address.
Filters:
Filter by user type or creation date.
Excluded Fields:
column_exclude_list = ["password_hash"]
form_excluded_columns = ["password_hash", "created_at", "created_posts", "applications", "memberships"]
Password hash is never displayed or editable for security.
Implementation¶
class UserAdminView(AdminModelView):
column_list = ["user_id", "first_name", "last_name", "email", "user_type", "created_at"]
form_columns = ["first_name", "last_name", "email", "user_type", "followed_clubs"]
column_searchable_list = ["first_name", "last_name", "email"]
column_filters = ["user_type", "created_at"]
column_exclude_list = ["password_hash"]
form_excluded_columns = ["password_hash", "created_at", "created_posts", "applications", "memberships"]
form_choices = {
"user_type": [
("student", "Student"),
("admin", "Admin"),
]
}
Common Tasks¶
Change User to Admin:
- Navigate to
/admin/user/ - Find user by search
- Click edit
- Change "User Type" to "Admin"
- Save
View User's Followed Clubs:
- Navigate to user edit form
- See selected clubs in "Followed Clubs" field
Password Management
Admins cannot view or change passwords directly. Users must use the password reset feature (to be implemented).
Club Administration¶
Path: /admin/club/
Model: Club
Visible Columns¶
| Column | Type | Description |
|---|---|---|
club_name |
String | Club name |
description |
Text | Club description |
categories |
String | Club categories/tags |
is_verified |
Boolean | Verification status |
Form Columns¶
club_namedescriptioncategoriesis_verified(checkbox)
Implementation¶
class ClubAdminView(AdminModelView):
column_list = ["club_name", "description", "categories", "is_verified"]
form_columns = ["club_name", "description", "categories", "is_verified"]
Common Tasks¶
Verify a Club:
- Navigate to
/admin/club/ - Find the club
- Click edit
- Check "Is Verified"
- Save
Create New Club:
- Click "Create"
- Fill in all required fields
- Check "Is Verified" if appropriate
- Save
Delete Club:
Cascade Delete
Deleting a club will cascade delete: - All memberships - All applications - All posts - All follow relationships
This action cannot be undone!
Post Administration¶
Path: /admin/post/
Model: Post
Visible Columns¶
| Column | Type | Description |
|---|---|---|
title |
String | Post title |
post_type |
String | Type: post/event/galerie |
is_pinned |
Boolean | Pinned status |
event_date |
DateTime | Event date (if applicable) |
location |
String | Event location (if applicable) |
club |
Relationship | Associated club |
Form Columns¶
titlecontent(textarea)post_type(dropdown)is_pinned(checkbox)event_date(datetime picker)locationclub(dropdown)
Filters¶
Filter by post type or pinned status.
Validation¶
def on_model_change(self, form, model, is_created):
"""Custom validation before save."""
# Auto-set creator for new posts
if is_created:
model.creator_id = current_user.user_id
# Auto-set post_date if not set
if not model.post_date:
from datetime import datetime
model.post_date = datetime.now()
# Validate event posts have event_date
if model.post_type == "event" and not model.event_date:
raise ValueError("Event posts must have an event date")
super(PostAdminView, self).on_model_change(form, model, is_created)
Implementation¶
class PostAdminView(AdminModelView):
column_list = ["title", "post_type", "is_pinned", "event_date", "location", "club"]
form_columns = ["title", "content", "post_type", "is_pinned", "event_date", "location", "club"]
column_filters = ["post_type", "is_pinned"]
Common Tasks¶
Pin a Post:
- Navigate to
/admin/post/ - Find the post
- Click edit
- Check "Is Pinned"
- Save
Create Event on Behalf of Club:
- Click "Create"
- Fill in title and content
- Select "event" for Post Type
- Set Event Date
- Enter Location
- Select Club
- Save (creator auto-set to current admin)
Moderate Posts:
Use filters to find posts by type or pinned status, then edit or delete as needed.
Membership Administration¶
Path: /admin/membership/
Model: Membership
Visible Columns¶
| Column | Type | Description |
|---|---|---|
student |
Relationship | Member's name |
club |
Relationship | Club name |
is_president |
Boolean | President status |
joined_at |
DateTime | Join date |
Form Columns¶
student(dropdown)club(dropdown)is_president(checkbox)
Filters¶
Custom Formatters¶
column_formatters = {
"student": lambda v, c, m, p: f"{m.student.first_name} {m.student.last_name}" if m.student else "",
"club": lambda v, c, m, p: m.club.club_name if m.club else "",
}
Display formatted names instead of object references.
Implementation¶
class MembershipAdminView(AdminModelView):
column_list = ["student", "club", "is_president", "joined_at"]
form_columns = ["student", "club", "is_president"]
column_filters = ["is_president", "joined_at", "student", "club"]
column_formatters = {
"student": lambda v, c, m, p: f"{m.student.first_name} {m.student.last_name}" if m.student else "",
"club": lambda v, c, m, p: m.club.club_name if m.club else "",
}
Common Tasks¶
Make User President:
- Navigate to
/admin/membership/ - Filter by club if needed
- Find the membership record
- Click edit
- Check "Is President"
- Save
Multiple Presidents
The system allows multiple presidents per club in the admin interface. The application logic typically ensures only one president, but admins can override this.
Add User to Club:
- Click "Create"
- Select Student
- Select Club
- Check "Is President" if applicable
- Save
Application Administration¶
Path: /admin/application/
Model: Application
Visible Columns¶
| Column | Type | Description |
|---|---|---|
student |
Relationship | Applicant's name |
club |
Relationship | Club name |
status |
String | pending/accepted/denied |
applied_at |
DateTime | Application date |
Form Columns¶
student(dropdown)club(dropdown)status(dropdown: Pending/Accepted/Denied)
Filters¶
Custom Formatters¶
column_formatters = {
"student": lambda v, c, m, p: f"{m.student.first_name} {m.student.last_name}" if m.student else "",
"club": lambda v, c, m, p: m.club.club_name if m.club else "",
}
Implementation¶
class ApplicationAdminView(AdminModelView):
column_list = ["student", "club", "status", "applied_at"]
form_columns = ["student", "club", "status"]
column_filters = ["status", "applied_at"]
form_choices = {
"status": [
("pending", "Pending"),
("accepted", "Accepted"),
("denied", "Denied"),
]
}
column_formatters = {
"student": lambda v, c, m, p: f"{m.student.first_name} {m.student.last_name}" if m.student else "",
"club": lambda v, c, m, p: m.club.club_name if m.club else "",
}
Common Tasks¶
Accept Application:
- Navigate to
/admin/application/ - Filter by status="pending"
- Find the application
- Click edit
- Change Status to "Accepted"
- Save
Manual Membership Creation
Changing application status to "accepted" does NOT automatically create a membership. You must: 1. Accept the application 2. Go to Membership admin 3. Create a new membership record
View Pending Applications:
- Navigate to
/admin/application/ - Filter by Status = "pending"
- View list of pending applications
Admin Initialization¶
The admin interface is initialized in admin.py:
def init_admin(app):
"""
Initialize Flask-Admin with all model views.
Args:
app: Flask application instance
"""
from .models import db, Club, Post, User, Membership, Application
admin = Admin(
app,
name="School Admin",
index_view=MyAdminIndexView()
)
admin.add_view(ClubAdminView(Club, db.session))
admin.add_view(PostAdminView(Post, db.session))
admin.add_view(UserAdminView(User, db.session))
admin.add_view(MembershipAdminView(Membership, db.session))
admin.add_view(ApplicationAdminView(Application, db.session))
Called from create_app():
Customization¶
Custom Column Formatters¶
Display custom formatted data:
column_formatters = {
"full_name": lambda v, c, m, p: f"{m.first_name} {m.last_name}",
"member_count": lambda v, c, m, p: len(m.memberships),
}
Custom Actions¶
Add batch operations:
from flask_admin.actions import action
class ClubAdminView(AdminModelView):
@action("verify", "Verify Clubs", "Are you sure?")
def action_verify(self, ids):
"""Verify selected clubs."""
try:
query = Club.query.filter(Club.club_id.in_(ids))
query.update({"is_verified": True}, synchronize_session=False)
db.session.commit()
flash(f"Verified {len(ids)} clubs.", "success")
except Exception as e:
flash(f"Error: {str(e)}", "error")
Custom Templates¶
Override Flask-Admin templates:
Form Widgets¶
Customize form field rendering:
from wtforms import TextAreaField
class PostAdminView(AdminModelView):
form_overrides = {
"content": TextAreaField
}
form_args = {
"content": {
"rows": 10,
"style": "width: 100%"
}
}
Security Considerations¶
Access Control¶
✅ Implemented:
- Admin-only access via
is_accessible() - 403 Forbidden for unauthorized access
- Password hash exclusion
Production Recommendations
- Log all admin actions
- Implement audit trail
- Add IP whitelisting
- Require 2FA for admin accounts
- Regular security audits
Audit Logging¶
Example implementation:
class AuditedAdminView(AdminModelView):
def after_model_change(self, form, model, is_created):
"""Log model changes."""
action = "created" if is_created else "updated"
log_entry = AuditLog(
user_id=current_user.user_id,
action=action,
model=model.__class__.__name__,
model_id=model.id,
timestamp=datetime.now()
)
db.session.add(log_entry)
db.session.commit()
Common Workflows¶
Create New Club with President¶
- Create Club (
/admin/club/) - Fill in club details
- Set verified status
-
Save
-
Create Membership (
/admin/membership/) - Select student
- Select new club
- Check "Is President"
- Save
Process Application¶
- View Applications (
/admin/application/) - Filter by status="pending"
-
Find application
-
Review and Accept
- Change status to "accepted"
-
Save
-
Create Membership (
/admin/membership/) - Select same student and club
- Save
Moderate Content¶
- View All Posts (
/admin/post/) -
Use filters to find specific posts
-
Edit or Delete
- Edit to pin/unpin or modify content
- Delete inappropriate posts
Best Practices¶
Admin Best Practices
- Always verify data before making bulk changes
- Use filters to find specific records quickly
- Test in development before making production changes
- Document changes for audit purposes
- Backup database before major operations
- Limit admin accounts to trusted users only
Efficiency Tips
- Use search to find users by email
- Use filters to narrow down lists
- Bookmark frequently used admin pages
- Learn keyboard shortcuts (if available)
- Use batch actions for multiple records
Avoid
- Never share admin credentials
- Don't delete records without understanding cascades
- Don't edit password hashes directly
- Don't grant admin access lightly
Troubleshooting¶
Cannot Access Admin Panel¶
Problem: Getting 403 Forbidden
Solutions: - Verify your user_type is "admin" - Check you're logged in - Clear browser cache and cookies - Check console for JavaScript errors
Changes Not Saving¶
Problem: Form submitted but changes not persisted
Solutions: - Check for validation errors (displayed at top of form) - Verify foreign key relationships exist - Check database constraints - Review application logs
Cascade Delete Confusion¶
Problem: Deleting a record deletes more than expected
Explanation: Relationships use cascade="all, delete-orphan"
Solution: - Understand cascade relationships before deleting - Export data before bulk operations - Consider soft deletes for important data
Next Steps¶
- Database Models - Understand data relationships
- Authentication - Admin user management
- Routes & Views - Compare admin vs. user flows