Skip to content

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_name
  • last_name
  • email
  • user_type (dropdown: Student/Admin)
  • followed_clubs (multi-select)

Features

Search:

column_searchable_list = ["first_name", "last_name", "email"]

Search users by name or email address.

Filters:

column_filters = ["user_type", "created_at"]

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:

  1. Navigate to /admin/user/
  2. Find user by search
  3. Click edit
  4. Change "User Type" to "Admin"
  5. Save

View User's Followed Clubs:

  1. Navigate to user edit form
  2. 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_name
  • description
  • categories
  • is_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:

  1. Navigate to /admin/club/
  2. Find the club
  3. Click edit
  4. Check "Is Verified"
  5. Save

Create New Club:

  1. Click "Create"
  2. Fill in all required fields
  3. Check "Is Verified" if appropriate
  4. 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

  • title
  • content (textarea)
  • post_type (dropdown)
  • is_pinned (checkbox)
  • event_date (datetime picker)
  • location
  • club (dropdown)

Filters

column_filters = ["post_type", "is_pinned"]

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:

  1. Navigate to /admin/post/
  2. Find the post
  3. Click edit
  4. Check "Is Pinned"
  5. Save

Create Event on Behalf of Club:

  1. Click "Create"
  2. Fill in title and content
  3. Select "event" for Post Type
  4. Set Event Date
  5. Enter Location
  6. Select Club
  7. 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

column_filters = ["is_president", "joined_at", "student", "club"]

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:

  1. Navigate to /admin/membership/
  2. Filter by club if needed
  3. Find the membership record
  4. Click edit
  5. Check "Is President"
  6. 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:

  1. Click "Create"
  2. Select Student
  3. Select Club
  4. Check "Is President" if applicable
  5. 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

column_filters = ["status", "applied_at"]

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:

  1. Navigate to /admin/application/
  2. Filter by status="pending"
  3. Find the application
  4. Click edit
  5. Change Status to "Accepted"
  6. 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:

  1. Navigate to /admin/application/
  2. Filter by Status = "pending"
  3. 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():

with app.app_context():
    db.create_all()
    init_admin(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:

templates/
  admin/
    not_authorized.html
    custom_list.html
    custom_edit.html

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

  1. Create Club (/admin/club/)
  2. Fill in club details
  3. Set verified status
  4. Save

  5. Create Membership (/admin/membership/)

  6. Select student
  7. Select new club
  8. Check "Is President"
  9. Save

Process Application

  1. View Applications (/admin/application/)
  2. Filter by status="pending"
  3. Find application

  4. Review and Accept

  5. Change status to "accepted"
  6. Save

  7. Create Membership (/admin/membership/)

  8. Select same student and club
  9. Save

Moderate Content

  1. View All Posts (/admin/post/)
  2. Use filters to find specific posts

  3. Edit or Delete

  4. Edit to pin/unpin or modify content
  5. Delete inappropriate posts

Best Practices

Admin Best Practices

  1. Always verify data before making bulk changes
  2. Use filters to find specific records quickly
  3. Test in development before making production changes
  4. Document changes for audit purposes
  5. Backup database before major operations
  6. 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