Skip to content

Architecture Overview

This document provides a comprehensive overview of the School Clubs Management System architecture, detailing the application structure, components, and their interactions.

Application Factory Pattern

The application uses Flask's application factory pattern implemented in __init__.py. This design allows for better testing, multiple instances, and configuration flexibility.

Core Components

The application initializes several key components:

db = SQLAlchemy()
SQLAlchemy ORM instance for database operations.

turbo = Turbo()
Enables real-time UI updates without full page reloads.

bootstrap = Bootstrap5()
Provides Bootstrap 5 integration for styling.

Application Factory

def create_app(test_config=None):
    """
    Create and configure the Flask application.

    Args:
        test_config: Optional dictionary for test configuration

    Returns:
        Flask: Configured Flask application instance
    """

Configuration

The factory configures:

  • Secret Key: Session encryption
  • Database: SQLite with connection pooling
  • Extensions: SQLAlchemy, Turbo, Bootstrap, Flask-Login
  • Blueprints: Authentication and main routes
  • Admin Panel: Flask-Admin interface
  • CLI Commands: Custom commands like seed-db

Database Configuration

database_uri = "sqlite:///" + os.path.join(basedir, "instance/data.db")
app.config["SQLALCHEMY_DATABASE_URI"] = database_uri

app.config["SQLALCHEMY_ENGINE_OPTIONS"] = {
    "connect_args": {
        "timeout": 10,
        "check_same_thread": False,
    },
    "pool_pre_ping": True,
    "pool_recycle": 3600,
}

User Loader

Flask-Login requires a user loader callback:

@login_manager.user_loader
def load_user(user_id):
    """
    Load user by ID for Flask-Login.

    Args:
        user_id: Integer user ID

    Returns:
        User: User object or None
    """
    return User.query.get(int(user_id))

Unauthorized Handler

@login_manager.unauthorized_handler
def unauthorized():
    """Handle unauthorized access attempts."""
    flash("You must be logged in to access this page.", "warning")
    return redirect(url_for("main.landing_page"))

Database Models

The application uses SQLAlchemy ORM with five main models and one association table.

Entity Relationship Diagram

erDiagram
    User ||--o{ Membership : has
    User ||--o{ Application : makes
    User ||--o{ Post : creates
    User }o--o{ Club : follows
    Club ||--o{ Membership : has
    Club ||--o{ Application : receives
    Club ||--o{ Post : owns

    User {
        int user_id PK
        string first_name
        string last_name
        string email UK
        string password_hash
        string user_type
        datetime created_at
    }

    Club {
        int club_id PK
        string club_name
        string description
        string categories
        bool is_verified
    }

    Post {
        int post_id PK
        string title
        string content
        string post_type
        bool is_pinned
        json file_urls
        datetime post_date
        datetime event_date
        string location
        int creator_id FK
        int club_id FK
    }

    Membership {
        int student_id PK,FK
        int club_id PK,FK
        datetime joined_at
        bool is_president
    }

    Application {
        int student_id PK,FK
        int club_id PK,FK
        datetime applied_at
        string status
    }

User Model

Represents system users (students and administrators).

class User(UserMixin, db.Model):
    __tablename__ = "users"

    user_id = db.Column(db.Integer, primary_key=True)
    first_name = db.Column(db.String(100), nullable=False)
    last_name = db.Column(db.String(100), nullable=False)
    email = db.Column(db.String(80), unique=True, nullable=False)
    password_hash = db.Column(db.String(100), nullable=False)
    user_type = db.Column(db.String(10), nullable=False)  # 'student' or 'admin'
    created_at = db.Column(db.DateTime(timezone=True), server_default=func.now())

Relationships:

  • followed_clubs: Many-to-many with Club via follows table
  • memberships: One-to-many with Membership
  • applications: One-to-many with Application
  • created_posts: One-to-many with Post

Constraints:

  • user_type must be 'student' or 'admin'
  • Email must be unique

Flask-Login Integration

The UserMixin provides default implementations for is_authenticated, is_active, is_anonymous, and get_id().

Club Model

Represents student clubs in the system.

class Club(db.Model):
    __tablename__ = "clubs"

    club_id = db.Column(db.Integer, primary_key=True)
    club_name = db.Column(db.String(100), nullable=False)
    description = db.Column(db.String, nullable=False)
    categories = db.Column(db.String, nullable=False)
    is_verified = db.Column(db.Boolean, default=False, nullable=False)

Relationships:

  • followers: Many-to-many with User via follows table
  • memberships: One-to-many with Membership
  • applications: One-to-many with Application
  • posts: One-to-many with Post

Post Model

Represents posts, events, and gallery items.

class Post(db.Model):
    __tablename__ = "posts"

    post_id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String, nullable=False)
    content = db.Column(db.String, nullable=False)
    post_type = db.Column(db.String, nullable=False)  # 'post', 'event', 'galerie'
    is_pinned = db.Column(db.Boolean, nullable=False, default=False)
    file_urls = db.Column(db.JSON, nullable=True, default=list)
    post_date = db.Column(db.DateTime(timezone=True), server_default=func.now())

    # Event-specific fields
    event_date = db.Column(db.DateTime(timezone=True), nullable=True)
    location = db.Column(db.String(200), nullable=True)

    creator_id = db.Column(db.Integer, db.ForeignKey("users.user_id"), nullable=False)
    club_id = db.Column(db.Integer, db.ForeignKey("clubs.club_id"), nullable=False)

Relationships:

  • creator: Many-to-one with User
  • club: Many-to-one with Club

Constraints:

  • post_type must be 'post', 'event', or 'galerie'

Membership Model

Represents club membership with president designation.

class Membership(db.Model):
    __tablename__ = "memberships"

    student_id = db.Column(db.Integer, db.ForeignKey("users.user_id"), primary_key=True)
    club_id = db.Column(db.Integer, db.ForeignKey("clubs.club_id"), primary_key=True)
    joined_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
    is_president = db.Column(db.Boolean, default=False, nullable=False)

Composite Primary Key: (student_id, club_id)

Application Model

Tracks club membership applications.

class Application(db.Model):
    __tablename__ = "applications"

    student_id = db.Column(db.Integer, db.ForeignKey("users.user_id"), primary_key=True)
    club_id = db.Column(db.Integer, db.ForeignKey("clubs.club_id"), primary_key=True)
    applied_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
    status = db.Column(db.String(20), default="pending", nullable=False)

Composite Primary Key: (student_id, club_id)

Constraints:

  • status must be 'pending', 'accepted', or 'denied'

Follows Association Table

Simple many-to-many relationship for users following clubs.

follows = db.Table(
    "follows",
    db.Column("student_id", db.Integer, db.ForeignKey("users.user_id"), primary_key=True),
    db.Column("club_id", db.Integer, db.ForeignKey("clubs.club_id"), primary_key=True),
    db.Column("followed_at", db.DateTime(timezone=True), server_default=func.now()),
)

Blueprint Structure

The application is organized into two main blueprints:

Authentication Blueprint (auth.py)

Handles user authentication operations.

Routes:

Route Method Description
/login_modal POST Process login form via Turbo
/logout GET Log out current user
/sign_up POST Register new user

Login Handler

@auth.route("/login_modal", methods=["POST"])
def login_modal():
    """
    Handle login requests via modal form.
    Uses Turbo Flask for dynamic error display.

    Returns:
        Redirect to landing page on success
        204 No Content with Turbo update on failure
    """
    email = request.form.get("email")
    password = request.form.get("password")

    user = User.query.filter_by(email=email).first()

    if user and check_password_hash(user.password_hash, password):
        login_user(user)
        return redirect(url_for("main.landing_page"))

    # Show error via Turbo without full page reload
    turbo.push(turbo.update(
        render_template("error-login.html", error_user=error),
        target="login-error-container",
    ))
    return "", 204

Turbo Flask Integration

The application uses Turbo Flask to update specific page sections without full reloads, providing a more responsive user experience.

Sign Up Handler

@auth.route("/sign_up", methods=["POST"])
def sign_up_post():
    """
    Handle new user registration.

    Validates:
    - All fields present
    - Email not already registered
    - Password minimum 8 characters

    Returns:
        Redirect to landing page on success
        204 with Turbo error on failure
    """

Main Blueprint (main.py)

Handles all club, post, and membership operations.

Key Routes:

Route Auth Description
/ No Landing page with followed club posts
/browse Yes Browse clubs or events
/club/<int:club_id> Yes Club detail page
/club/<int:club_id>/posts Yes All posts for a club
/post/create Yes Create new post
/post/<int:post_id>/edit Yes Edit existing post
/post/<int:post_id>/delete Yes Delete post
/clubs/<int:club_id>/follow Yes Follow a club
/clubs/<int:club_id>/unfollow Yes Unfollow a club
/clubs/<int:club_id>/join Yes Apply for membership
/clubs/<int:club_id>/applications Yes View pending applications
/clubs/<int:club_id>/applications/<int:student_id>/accept Yes Accept application
/clubs/<int:club_id>/applications/<int:student_id>/deny Yes Deny application
/remove/<int:club_id>/<int:user_id> Yes Remove member
/promote/<int:club_id>/<int:user_id> Yes Promote to president

Landing Page

@main.route("/")
def landing_page():
    """
    Main landing page.

    For authenticated users:
    - Shows posts from followed clubs
    - Pinned posts appear first
    - Sorted by date descending

    Returns:
        Rendered landing page template with posts and role flags
    """

Club Detail

@main.route("/club/<int:club_id>")
@login_required
def club_detail(club_id):
    """
    Display detailed club information.

    Shows:
    - Club details and description
    - Member count and president
    - Recent posts
    - Pending application count (for presidents)
    - User's application status
    - Follow/Join/Leave actions

    Returns:
        Rendered club detail template
    """

Post Management

Create, edit, and delete posts with role-based permissions.

@main.route("/post/create", methods=["GET", "POST"])
@login_required
def create_post():
    """
    Create new post, event, or gallery item.

    POST: Process form and create post
    GET: Show creation form

    Permissions:
    - Must be member, president, or admin of the club

    Returns:
        Redirect to club detail on success
        Rendered form on GET
    """

Authorization

Post creation/editing verifies the user is a member, president, or admin of the target club.

Membership Management

@main.route("/remove/<int:club_id>/<int:user_id>", methods=["POST"])
@login_required
def remove_member(club_id, user_id):
    """
    Remove a member from a club.

    Permissions:
    - Must be club president or system admin
    - Cannot remove yourself
    - Cannot remove the president

    Returns:
        Redirect to club detail with flash message
    """
@main.route("/promote/<int:club_id>/<int:user_id>", methods=["POST"])
@login_required  
def make_president(club_id, user_id):
    """
    Promote a member to club president.

    Process:
    1. Demote current president to regular member
    2. Promote target member to president

    Permissions:
    - Must be current president or admin

    Returns:
        Redirect to club detail with flash message
    """

Application Processing

@main.route("/clubs/<int:club_id>/applications/<int:student_id>/accept", methods=["POST"])
@login_required
def accept_application(club_id, student_id):
    """
    Accept a membership application.

    Actions:
    1. Update application status to 'accepted'
    2. Create membership record
    3. Set president flag if no president exists

    Permissions:
    - Must be club president or admin

    Returns:
        Redirect to applications page with flash message
    """

Utility Functions

Role checking functions used throughout the application:

def is_member(current_user, club_id):
    """Check if user is a member, president, or admin."""
    if current_user.user_type == "admin":
        return True
    return Membership.query.filter_by(
        student_id=current_user.user_id, club_id=club_id
    ).first() is not None

def is_president(current_user, club_id):
    """Check if user is president or admin."""
    if current_user.user_type == "admin":
        return True
    membership = Membership.query.filter_by(
        student_id=current_user.user_id, club_id=club_id
    ).first()
    return membership and membership.is_president

def is_admin(current_user):
    """Check if user is an admin."""
    return current_user.user_type == "admin"

Admin Panel

The admin panel is built with Flask-Admin and provides a web interface for system administration.

Access Control

class AdminModelView(ModelView):
    """Base admin view with access control."""

    def is_accessible(self):
        """Only allow access to authenticated admins."""
        return current_user.is_authenticated and current_user.user_type == "admin"

    def inaccessible_callback(self, name, **kwargs):
        """Return 403 Forbidden for unauthorized access."""
        abort(403)

Admin Views

Club Administration

class ClubAdminView(AdminModelView):
    column_list = ["club_name", "description", "categories", "is_verified"]
    form_columns = ["club_name", "description", "categories", "is_verified"]

Features: - View all clubs - Edit club details - Verify/unverify clubs - Delete clubs (cascades to related records)

Post Administration

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"]

    def on_model_change(self, form, model, is_created):
        """Validate and set defaults before saving."""
        if is_created:
            model.creator_id = current_user.user_id

        if not model.post_date:
            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")

Features: - Create posts on behalf of clubs - Pin/unpin posts - Filter by type and pinned status - Automatic creator assignment - Event validation

User Administration

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"]

Features: - Search users by name or email - Change user type (student/admin) - Manage followed clubs - Password hash excluded from display/editing

Membership Administration

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"]

Features: - View all memberships - Designate presidents - Filter by president status - Custom formatters for student/club names

Application Administration

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")]
    }

Features: - View all applications - Change application status - Filter by status or date - Custom formatters for readable names

Admin Index

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"))

The admin home redirects to the main landing page for authenticated admins.

Initialization

def init_admin(app):
    """Initialize Flask-Admin with all model views."""
    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))

Security Considerations

Password Hashing

Passwords are hashed using Werkzeug's generate_password_hash:

from werkzeug.security import generate_password_hash, check_password_hash

# Registration
hashed_pw = generate_password_hash(password)
new_user = User(password_hash=hashed_pw, ...)

# Login
if check_password_hash(user.password_hash, password):
    login_user(user)

Session Management

Flask-Login manages user sessions:

  • Sessions are signed with SECRET_KEY
  • @login_required decorator protects routes
  • Automatic session cleanup on logout

Authorization Checks

Role-based access control throughout:

# Check membership before allowing post creation
if not is_member(current_user, club_id):
    flash("You must be a member to create posts.", "danger")
    return redirect(url_for("main.club_detail", club_id=club_id"))

# Check president status before member removal
if not is_president(current_user, club_id):
    flash("Only presidents can remove members.", "danger")
    return redirect(url_for("main.club_detail", club_id=club_id"))

SQL Injection Protection

SQLAlchemy ORM provides automatic query parameterization:

# Safe - parameterized query
User.query.filter_by(email=email).first()

# Safe - using bound parameters
User.query.filter(User.email == email).first()

Never Use String Formatting

Never use f-strings or .format() to build SQL queries. Always use SQLAlchemy's query methods.


Performance Optimization

Database Connection Pooling

app.config["SQLALCHEMY_ENGINE_OPTIONS"] = {
    "connect_args": {
        "timeout": 10,
        "check_same_thread": False,
    },
    "pool_pre_ping": True,      # Verify connections before use
    "pool_recycle": 3600,        # Recycle connections after 1 hour
}

Query Optimization

Eager Loading:

# Load related data in one query
club = Club.query.options(
    db.joinedload(Club.memberships),
    db.joinedload(Club.posts)
).get(club_id)

Selective Column Loading:

# Only load needed columns
users = User.query.with_entities(User.user_id, User.first_name, User.last_name).all()

Turbo Flask for Partial Updates

Instead of full page reloads:

turbo.push(turbo.update(
    render_template("error-login.html", error_user=error),
    target="login-error-container",
))

Only the error container is updated, reducing bandwidth and improving responsiveness.


Testing & Development

CLI Commands

Seed Database

flask seed-db

Populates the database with test data defined in seed.py.

Development Server

flask run

Starts the development server on http://localhost:5000.

Database Migrations

While the application currently uses db.create_all(), production deployments should use Flask-Migrate:

flask db init
flask db migrate -m "Initial migration"
flask db upgrade

Next Steps

__init__.py

This file creates and sets up the main Flask application. It configures the database, login system,
admin panel, UI tools, blueprints, and CLI commands, returning a ready-to-run app.

  1. db = SQLAlchemy() (function)

    • Returns:
      • db: SQLAlchemy instance for database operations.
  2. turbo variable

    turbo = Turbo()
    

    • Returns:
      • turbo: Turbo-Flask instance for real-time updates.
  3. bootstrap = Bootstrap5() (function)

    • Returns:
      • bootstrap: Flask-Bootstrap instance for styling.
  4. def create_app(test_config=None) (function)

    • Parameters:
      • test_config: Optional dictionary of config values for testing or different modes.
    • Returns:
      • app (Flask): Fully configured Flask application.

    4.1 def load_user(user_id) (method)
    * Parameters:
    - user_id: ID of the user to load.
    * Returns:
    - User object from the database.

    4.2 def unauthorized() (method)
    * Returns:
    - Redirects to the landing page and flashes a warning.

    4.3 def seed_db_command() (function)
    * Returns:
    - None — populates the database with dummy data when run from CLI.

admin.py

This file defines the admin interface for the School Clubs application, including access control, admin views for clubs and posts, and the initialization of Flask-Admin.

  1. admin_bp = Blueprint("admin", __name__, url_prefix="/admin") (function)

    • Parameters:
      • "admin": Name of the blueprint.
      • name: Current module name.
      • url_prefix: URL prefix for all admin routes.
    • Returns:
      • admin_bp (Blueprint): Blueprint used for admin-related routes.
  2. class AdminModelView(ModelView)
    Base model view that restricts access to admin users only.

    2.1 def is_accessible(self) (method)
    * Parameters:
    - None
    * Returns:
    - bool:Trueif the current user is logged in and is an admin, otherwise False.

    2.2 def inaccessible_callback(self, name, **kwargs) (method)
    * Parameters:
    - name: Name of the view being accessed.
    - **kwargs: Additional arguments passed by Flask-Admin.
    * Returns:
    - None — raises a 403 Forbidden error if access is denied.

  3. class ClubAdminView(AdminModelView)
    Admin view for managing Club records.

    • Parameters:
      • Inherits all parameters from ModelView.
    • Returns:
      • ClubAdminView: Configured admin interface for clubs.
  4. class PostAdminView(AdminModelView)
    Admin view for managing Post records, including validation and automatic field handling.

    def on_model_change(self, form, model, is_created) (method)
    * Parameters:
    - form: The submitted admin form.
    - model: The Post model being created or updated.
    - is_created:Trueif the record is being created, False if updated.
    * Returns:
    - None — modifies and validates the model before saving.

  5. class MyAdminIndexView(AdminIndexView)
    Custom admin home page with access control.

    def index(self) (method)
    * Parameters:
    - None
    * Returns:
    - Response: Renders a “not authorized” page for non-admins or redirects admins
    to the main landing page.

  6. def init_admin(app) (function)

    • Parameters:
      • app (Flask): The Flask application instance.
    • Returns:
      • None — initializes Flask-Admin and registers the Club and Post admin views.

auth.py

This file handles user authentication for the School Clubs application, including login, logout, and sign-up functionality.
It uses Flask-Login, password hashing, and Turbo-Flask for dynamic UI updates.

  1. auth = Blueprint("auth", __name__) (function)

    • Parameters:
      • "auth": Name of the blueprint.
      • __name__: Current module name.
    • Returns:
      • auth (Blueprint): Blueprint used for authentication routes.
  2. @auth.route("/login_modal", methods=["POST"]) def login_modal() (function)

    • Parameters:
      • None (form data is read from request.form).
    • Returns:
      • Redirects to the landing page on success.
      • Returns HTTP 204 on failure while pushing login errors to the Turbo target container.
  3. @auth.route("/logout")
    @login_required
    def logout() (function)

    • Parameters:
      • None
    • Returns:
      • Redirects to the main landing page.
  4. @auth.route("/sign_up", methods=["POST"])
    def sign_up_post() (function)

    • Parameters:
      • None (form data is read from request.form).
    • Returns:
      • Redirects to the landing page on successful sign-up.
      • Returns HTTP 204 on failure while pushing sign-up errors to the Turbo target container.

main-flask.py

This file defines the database models for the School Clubs application, including users, clubs, posts, memberships, applications, and follow relationships.
It also includes the seed_database function to populate the database with dummy data for development.

  1. follows = db.Table(...) (function)

    • Returns:
      • follows (Table): Association table linking students to the clubs they follow.
  2. class Membership(db.Model)
    Represents membership of a student in a club.

    Attributes:
    - student_id: ID of the student.
    - club_id: ID of the club.
    - joined_at: Timestamp when the membership was created.
    - is_president: Boolean indicating if the student is president.
    - student: Relationship to the User model.
    - club: Relationship to the Club model.

  3. class Application(db.Model)
    Represents a student’s application to join a club.

    Attributes:
    - student_id: ID of the student.
    - club_id: ID of the club.
    - applied_at: Timestamp when the application was submitted.
    - status: Application status (pending, denied, accepted).
    - student: Relationship to the User model.
    - club: Relationship to the Club model.

  4. class User(db.Model)
    Represents a user of the application (student or admin).

    Attributes:
    - user_id, first_name, last_name, email, password_hash, user_type, created_at.
    - followed_clubs: Clubs the user follows.
    - memberships: Memberships the user holds.
    - applications: Club applications made by the user.
    - created_posts: Posts created by the user.

  5. class Post(db.Model)
    Represents a post or event created by a club or user.

    Attributes:
    - post_id, title, content, post_type, is_pinned, file_urls, post_date.
    - event_date, location (for events).
    - creator_id, club_id.
    - creator: Relationship to User.
    - club: Relationship to Club.

  6. class Club(db.Model)
    Represents a club in the application.

    Attributes:
    - club_id, club_name, description, categories, is_verified.
    - followers: Users following the club.
    - memberships: Memberships in the club.
    - applications: Applications to the club.
    - posts: Posts created by the club.

  7. def seed_database() (function)

    • Returns:
      • None — populates the database with dummy users, clubs, memberships, posts, and events.
  8. @app.context_processor
    def inject_user() (function)

    • Returns:
      • dict(current_user=user): Makes the logged-in user available in all templates via {{ current_user }}.

main.py

This file defines the main Flask routes for the School Clubs application, including landing pages, club browsing, post creation and modification, membership management, and application handling.
It also provides utility functions to check user roles (member, president, admin).

  1. main = Blueprint("main", __name__) (function)

    • Parameters:
      • "main": Name of the blueprint.
      • __name__: Current module name.
    • Returns:
      • main (Blueprint): Blueprint used for main application routes.
  2. @main.route("/")
    def landing_page() (function)

    • Returns:
      • Renders the landing page (landing.html) with posts from followed clubs.
      • Includes user role flags (is_member, is_president, is_admin) and from_admin referrer flag.
  3. @main.route("/browse")
    @login_required
    def browse() (function)

    • Parameters:
      • None (query parameter type can be "clubs" or "events").
    • Returns:
      • Renders browse.html with either clubs or events, depending on the type parameter.
      • Includes user role flags (is_member, is_president, is_admin).
  4. @main.route("/club/<int:club_id>")
    @login_required
    def club_detail(club_id) (function)

    • Parameters:
      • club_id (int): ID of the club to display.
    • Returns:
      • Renders club-detail.html with club details, members count, posts, president, pending applications count, user application, and user role flags.
  5. @main.route("/post/create", methods=["GET", "POST"])
    @login_required
    def create_post() (function)

    • Parameters:
      • Form data from request.form for title, content, post type, club ID, pinned status, event date, location.
      • Optional gallery files from request.files.
    • Returns:
      • On POST: Creates a new post, commits to the database, flashes success/error messages, redirects to club detail.
      • On GET: Renders modify_post.html with clubs the user can post to, preselected club, and user role flags.
  6. @main.route("/post/<int:post_id>/edit", methods=["GET", "POST"])
    @login_required
    def modify_post(post_id) (function)

    • Parameters:
      • post_id (int): ID of the post to edit.
      • Form data from request.form and optional gallery files.
    • Returns:
      • On POST: Updates the post, handles event and gallery fields, commits to database, flashes messages, redirects to club posts.
      • On GET: Renders modify_post.html with existing post data and user role flags.
  7. @main.route("/post/<int:post_id>/delete", methods=["POST"])
    @login_required
    def delete_post(post_id) (function)

    • Parameters:
      • post_id (int): ID of the post to delete.
    • Returns:
      • Deletes the post if user is creator or admin, flashes success/error messages, redirects to referrer or landing page.
  8. @main.route("/remove/<int:club_id>/<int:user_id>", methods=["POST"])
    @login_required
    def remove_member(club_id, user_id) (function)

    • Parameters:
      • club_id (int): Club ID.
      • user_id (int): Student ID of member to remove.
    • Returns:
      • Removes a member if current user is president or admin, flashes messages, redirects to club detail.
  9. @main.route("/promote/<int:club_id>/<int:user_id>", methods=["POST"])
    @login_required
    def make_president(club_id, user_id) (function)

    • Parameters:
      • club_id (int): Club ID.
      • user_id (int): Student ID of new president.
    • Returns:
      • Promotes the member to president, demotes previous president, commits changes, flashes messages, redirects to club detail.
  10. @main.route("/club/<int:club_id>/posts")
    @login_required
    def club_posts(club_id) (function)

    • Parameters:
      • club_id (int): ID of the club.
    • Returns:
      • Renders posts.html with all posts for the given club and user role flags.
  11. @main.route("/club/<int:club_id>/<int:post_id>")
    @login_required
    def post_mod(club_id, post_id) (function)

    • Parameters:
      • club_id (int): Club ID.
      • post_id (int): Post ID.
    • Returns:
      • Renders posts.html with the given post and user role flags.
  12. def is_member(current_user, club_id) (function)

    • Parameters:
      • current_user: Logged-in user object.
      • club_id (int): Club ID.
    • Returns:
      • True if user is a member, president, or admin of the club; otherwise False.
  13. def is_president(current_user, club_id) (function)

    • Parameters:
      • current_user: Logged-in user object.
      • club_id (int): Club ID.
    • Returns:
      • True if user is president or admin; otherwise False.
  14. def is_admin(current_user) (function)

    • Parameters:
      • current_user: Logged-in user object.
    • Returns:
      • True if user is an admin; otherwise False.
  15. @main.route("/clubs/<int:club_id>/follow", methods=["POST"])
    @login_required
    def follow_club(club_id) (function)

    • Parameters:
      • club_id (int): Club ID.
    • Returns:
      • Adds the club to user’s followed clubs, flashes messages, redirects to club detail.
  16. @main.route("/clubs/<int:club_id>/unfollow", methods=["POST"])
    @login_required
    def unfollow_club(club_id) (function)

    • Parameters:
      • club_id (int): Club ID.
    • Returns:
      • Removes the club from user’s followed clubs, flashes messages, redirects to club detail.
  17. @main.route("/clubs/<int:club_id>/join", methods=["POST"])
    @login_required
    def join_club(club_id) (function)

    • Parameters:
      • club_id (int): Club ID.
    • Returns:
      • Creates a new membership application if user is not already a member or has pending application, flashes messages, redirects to club detail.
  18. @main.route("/clubs/<int:club_id>/applications")
    @login_required
    def club_applications(club_id) (function)

    • Parameters:
      • club_id (int): Club ID.
    • Returns:
      • Renders club_applications.html with pending applications for the club (presidents/admins only).
  19. @main.route("/clubs/<int:club_id>/applications/<int:student_id>/accept", methods=["POST"])
    @login_required
    def accept_application(club_id, student_id) (function)

    • Parameters:
      • club_id (int): Club ID.
      • student_id (int): Student ID of applicant.
    • Returns:
      • Accepts application, creates membership, flashes messages, redirects to pending applications page.
  20. @main.route("/clubs/<int:club_id>/applications/<int:student_id>/deny", methods=["POST"])
    @login_required
    def deny_application(club_id, student_id) (function)

    • Parameters:
      • club_id (int): Club ID.
      • student_id (int): Student ID of applicant.
    • Returns:
      • Denies application, updates database, flashes messages, redirects to pending applications page.

models.py

This file defines the database models for the School Clubs application, including users, clubs, posts, memberships, applications, and follow relationships.
It also defines the follows association table linking students to clubs they follow.

  1. follows = db.Table(...) (function)

    • Returns:
      • follows (Table): Association table linking students to the clubs they follow.
      • Columns:
        • student_id: Foreign key to users.user_id.
        • club_id: Foreign key to clubs.club_id.
        • followed_at: Timestamp of when the follow was created (default: now).
  2. class Membership(db.Model)
    Represents membership of a student in a club.

    2.1 Attributes:
    - student_id: ID of the student (primary key, foreign key to User).
    - club_id: ID of the club (primary key, foreign key to Club).
    - joined_at: Timestamp when membership was created (default: now).
    - is_president: Boolean indicating if the student is president.
    - student: Relationship to the User model.
    - club: Relationship to the Club model.

  3. class Application(db.Model)
    Represents a student’s application to join a club.

    3.1 Attributes:
    - student_id: ID of the student (primary key, foreign key to User).
    - club_id: ID of the club (primary key, foreign key to Club).
    - applied_at: Timestamp when the application was submitted (default: now).
    - status: Application status (pending, denied, accepted).
    - student: Relationship to the User model.
    - club: Relationship to the Club model.

  4. class User(UserMixin, db.Model)
    Represents a user of the application (student or admin).

    4.1 Attributes:
    - user_id, first_name, last_name, email, password_hash, user_type, created_at.
    - followed_clubs: Clubs the user follows.
    - memberships: Memberships the user holds.
    - applications: Club applications made by the user.
    - created_posts: Posts created by the user.

    4.2 Methods:
    - get_id(): Required by Flask-Login, returns the user ID as a string.

  5. class Post(db.Model)
    Represents a post or event created by a club or user.

    5.1 Attributes:
    - post_id, title, content, post_type, is_pinned, file_urls, post_date.
    - event_date, location (for events).
    - creator_id, club_id.
    - creator: Relationship to User.
    - club: Relationship to Club.

  6. class Club(db.Model)
    Represents a club in the application.

    6.1 Attributes:
    - club_id, club_name, description, categories, is_verified.
    - followers: Users following the club.
    - memberships: Memberships in the club.
    - applications: Applications to the club.
    - posts: Posts created by the club.