diff --git a/PROJECT_FLOW.md b/PROJECT_FLOW.md new file mode 100644 index 0000000..2a00379 --- /dev/null +++ b/PROJECT_FLOW.md @@ -0,0 +1,304 @@ +# CircusCircus Project Flow + +This document explains how the current project fits together at two levels: +- quick overview for orientation +- deeper walkthrough of the runtime logic and data flow + +It is written so someone new to Flask can follow the project without reading all source files first. + +## Quick Overview + +- Tech stack: Flask + Flask-Login + Flask-SQLAlchemy + Jinja templates. +- Entry point: `forum/app.py` creates the running app and root route. +- App factory: `forum/__init__.py` builds and configures the Flask app. +- Configuration: `config.py` provides secret key and SQLAlchemy settings. +- Data models: `forum/models.py` defines User, Post, Subforum, Comment. +- Request handlers: `forum/routes.py` handles auth, browsing, posting, comments. +- Account helpers: `forum/user.py` contains username/password utility checks. +- Templates: `forum/templates/` render pages for index, subforums, posts, login. +- Startup script: `run.sh` runs the app using Flask. + +## How Startup Works + +1. Flask starts with module `forum.app` (configured by `FLASK_APP`). +2. `forum/app.py` calls `create_app()` from `forum/__init__.py`. +3. `create_app()`: + - creates Flask app + - loads `Config` from `config.py` + - registers blueprint `rt` from `forum/routes.py` + - initializes SQLAlchemy via `db.init_app(app)` + - creates DB tables with `db.create_all()` inside app context +4. Back in `forum/app.py`: + - Flask-Login is configured + - `user_loader` is registered to restore users from session IDs + - tables are ensured again and initial subforums are seeded if empty + +Note: table creation happens in both `forum/__init__.py` and `forum/app.py`. That is safe, but redundant. + +## Beginner Map: What You Are Looking At + +If you open this project and feel lost, use this sequence: + +1. Open `config.py` to see app-wide settings. +2. Open `forum/__init__.py` to see how Flask is created and wired. +3. Open `forum/app.py` to see login setup, seed data setup, and the home route. +4. Open `forum/routes.py` to see the main user actions (login, browse, post, comment). +5. Open `forum/models.py` to understand what data is stored and how tables connect. +6. Open `forum/templates/` to see what each route renders. + +This order moves from "how app starts" to "what app does" to "how page looks". + +## URL and Request Flow + +### Root and browsing + +- `GET /` in `forum/app.py`: + - fetches top-level subforums (`parent_id is None`) + - renders `subforums.html` + +- `GET /subforum?sub=` in `forum/routes.py`: + - loads the target subforum + - loads recent posts for that subforum (up to 50, newest first) + - loads child subforums + - builds breadcrumb HTML path with `generateLinkPath()` + - renders `subforum.html` + +- `GET /viewpost?post=` in `forum/routes.py`: + - loads post and comments (newest comments first) + - builds breadcrumb path from post's subforum + - renders `viewpost.html` + +### Authentication + +- `POST /action_login`: + - reads username/password from form + - finds user by username + - verifies hash with `user.check_password()` + - logs in with Flask-Login or returns login page with errors + +- `GET /action_logout` (login required): + - logs current user out + - redirects to root + +- `POST /action_createaccount`: + - validates username format and uniqueness + - validates email uniqueness + - creates `User` (password stored as hash) + - auto-logs in and redirects to root + +### Content creation + +- `GET /addpost?sub=` (login required): + - verifies subforum exists + - renders `createpost.html` + +- `POST /action_post?sub=` (login required): + - validates title/content length + - creates `Post` + - links post to current user and subforum + - commits and redirects to `viewpost` + +- `POST/GET /action_comment?post=` (login required): + - verifies post exists + - creates `Comment` + - links comment to current user and post + - commits and redirects to `viewpost` + +## Deep Logic Walkthrough + +This section explains how the system behaves internally, not just which files exist. + +### 1. App factory and global objects + +- `db` is created once in `forum/models.py` as a global SQLAlchemy object. +- `create_app()` in `forum/__init__.py` binds that `db` object to the Flask app instance. +- Route registration happens by attaching the routes blueprint (`rt`) to the app. +- Flask-Login setup happens in `forum/app.py`, where `LoginManager` is attached to the app. + +Result: every route can use `db`, model queries, and `current_user` with one shared app context. + +### 2. Database initialization and seed logic + +- On startup, tables are created if missing. +- Then the app checks whether any `Subforum` rows exist. +- If none exist, `init_site()` seeds a default category tree. +- `add_subforum()` avoids duplicate titles at the same tree level. + +Result: first run creates a usable forum structure automatically. + +### 3. Authentication lifecycle + +- Account creation stores a hashed password only. +- Login route checks plain password against hash. +- On success, Flask-Login stores user identity in session. +- On later requests, `load_user(userid)` converts session ID back into a `User` row. +- Logout clears that session identity. + +Result: auth state is cookie/session based, with DB lookup per request as needed. + +### 4. Read flow for forum pages + +- Forum index route reads top-level subforums. +- Subforum route reads: + - one subforum by ID + - posts under that subforum + - direct child subforums + - breadcrumb path text +- View-post route reads: + - one post by ID + - comments for that post + - breadcrumb path from that post's subforum + +Result: each page query is focused and template-driven. + +### 5. Write flow for posts and comments + +- Create-post route validates title/content length. +- If invalid, template is re-rendered with error messages. +- If valid, a `Post` is created and attached to: + - current user + - selected subforum +- Create-comment route does the same pattern for `Comment`: + - verify post exists + - create comment + - attach to current user and post +- Each write ends with `db.session.commit()` and redirect. + +Result: writes are simple transaction-style operations with immediate redirect. + +### 6. Why relationships are used heavily + +- Instead of manually assigning every foreign key field, code often appends objects to relationships. +- Example pattern: + - `user.posts.append(post)` + - `subforum.posts.append(post)` +- SQLAlchemy fills key IDs and persists links on commit. + +Result: route code stays short and object-oriented. + +## End-to-End Request Trace Examples + +These traces show exactly what happens in common user actions. + +### Trace A: User signs up, then lands on home page + +1. Browser submits signup form to `POST /action_createaccount`. +2. Route reads username, email, password from `request.form`. +3. Helper checks run: + - username format + - username uniqueness + - email uniqueness +4. If any check fails: + - route renders login template with error list + - DB is unchanged +5. If checks pass: + - `User` object created (password hashed) + - user row committed + - user logged in with Flask-Login + - redirect to `/` +6. `GET /` loads top-level subforums and renders index template. + +### Trace B: Logged-in user creates a post + +1. User opens `GET /addpost?sub=`. +2. Route confirms target subforum exists. +3. Create-post form renders. +4. User submits form to `POST /action_post?sub=`. +5. Route validates title/content length. +6. If invalid, route re-renders form with validation messages. +7. If valid: + - `Post` object is created + - post attached to selected subforum and current user + - session commit persists post row and foreign keys + - redirect to `GET /viewpost?post=` +8. Post detail page queries post/comments and renders. + +### Trace C: Logged-in user comments on a post + +1. User submits comment form to `POST /action_comment?post=`. +2. Route verifies post exists. +3. Route creates `Comment` with content and current timestamp. +4. Comment is linked to current user and target post. +5. Commit persists comment row and keys. +6. Redirect goes back to `GET /viewpost?post=`. +7. Page reload now includes the new comment at top (newest first). + +## How to Read Logic Quickly During Maintenance + +When trying to understand a behavior, follow this mini-checklist: + +1. Identify route function handling the URL. +2. List which model queries it runs. +3. Check which helper validators it calls. +4. Check what template it renders or redirect it returns. +5. Confirm where commit happens for data writes. + +Using this method keeps debugging predictable across the whole codebase. + +## Data Model and Relationships + +Defined in `forum/models.py`: + +- `User` + - fields: id, username, password_hash, email, admin + - relationships: one-to-many with Post and Comment + - methods: `check_password()` + +- `Subforum` + - fields: id, title, description, parent_id, hidden + - relationships: + - self-referential child subforums via `subforums` + - one-to-many with Post + +- `Post` + - fields: id, title, content, user_id, subforum_id, postdate + - relationships: one-to-many with Comment + - helper: `get_time_string()` for relative-time labels + +- `Comment` + - fields: id, content, postdate, user_id, post_id + - helper: `get_time_string()` for relative-time labels + +## Validation and Utility Logic + +From `forum/user.py` and `forum/models.py`: + +- Username regex allows 4-40 characters from `[a-zA-Z0-9!@#%&]`. +- Password regex allows 6-40 characters from same set. +- `username_taken()` and `email_taken()` query existing users. +- `valid_title()` and `valid_content()` enforce post size bounds. +- `error()` returns a simple red HTML error string. +- `generateLinkPath()` builds breadcrumb HTML by walking up parent subforums. + +## Session and Login State + +- Flask-Login tracks authenticated users in session cookies. +- `load_user(userid)` in `forum/app.py` maps session user IDs to `User` rows. +- Routes requiring auth use `@login_required`. + +## Templates and Rendering Role + +- `subforums.html`: forum index page +- `subforum.html`: single subforum view + child subforums + post list +- `viewpost.html`: post detail + comments +- `createpost.html`: post creation form +- `login.html`: login/signup form +- `layout.html` and `header.html`: shared structure and navigation + +## Important Notes About Current Logic + +- Redundant DB initialization: `db.create_all()` is called in two places. +- Error responses are plain HTML snippets, not dedicated error templates. +- Some routes trust query parameters exist and are valid integers. +- Relative-time cache fields (`lastcheck`, `savedresponce`) are in-memory only and reset per process. + +## Practical Mental Model + +Think of the app as four layers: + +1. Config layer (`config.py`): constants for app and database. +2. App wiring (`forum/__init__.py`, `forum/app.py`): create app, attach DB/login, seed initial data. +3. Domain layer (`forum/models.py`, `forum/user.py`): schema, validation helpers, utility functions. +4. Delivery layer (`forum/routes.py` + templates): map HTTP requests to queries, updates, and rendered HTML. + +When debugging, start at the route, then trace into model queries and helper functions, then verify template rendering inputs. diff --git a/Procfile b/Procfile deleted file mode 100644 index e8ccc30..0000000 --- a/Procfile +++ /dev/null @@ -1 +0,0 @@ -web: gunicorn forum.app:app diff --git a/forum/__init__.py b/forum/__init__.py index c10b0f3..351c522 100644 --- a/forum/__init__.py +++ b/forum/__init__.py @@ -1,21 +1,18 @@ from flask import Flask -from forum.routes import rt +from .routes import rt def create_app(): """Construct the core application.""" app = Flask(__name__, instance_relative_config=False) app.config.from_object('config.Config') - # I think more blueprints might be used to break routes up into things like - # post_routes - # subforum_routes - # etc + # Register the main routes blueprint. app.register_blueprint(rt) # Set globals - from forum.models import db + from .models import db db.init_app(app) with app.app_context(): - # Add some routes + # Create tables on startup so the app can run against a fresh database. db.create_all() return app diff --git a/forum/app.py b/forum/app.py index 4d8a828..8a6c32a 100644 --- a/forum/app.py +++ b/forum/app.py @@ -1,16 +1,19 @@ from flask import render_template from flask_login import LoginManager -from forum.models import Subforum, db, User +from .models import Subforum, db, User -from . import create_app +from forum import create_app +# Build the Flask app using the package factory. app = create_app() +# Simple metadata used by the templates and app config. app.config['SITE_NAME'] = 'Schooner' app.config['SITE_DESCRIPTION'] = 'a schooner forum' app.config['FLASK_DEBUG'] = 1 def init_site(): + # Create the default forum structure on first run. print("creating initial subforums") admin = add_subforum("Forum", "Announcements, bug reports, and general discussion about the forum belongs here") add_subforum("Announcements", "View forum announcements here",admin) @@ -19,6 +22,7 @@ def init_site(): add_subforum("Other", "Discuss other things here") def add_subforum(title, description, parent=None): + # Avoid duplicate subforums at the same level. sub = Subforum(title, description) if parent: for subforum in parent.subforums: @@ -35,20 +39,24 @@ def add_subforum(title, description, parent=None): db.session.commit() return sub +# Flask-Login needs a loader so it can restore the current user from the session. login_manager = LoginManager() login_manager.init_app(app) @login_manager.user_loader def load_user(userid): + # Look up the full User row for the stored session ID. return User.query.get(userid) with app.app_context(): - db.create_all() # TODO this may be redundant + # Create tables if needed, then seed the database the first time it runs. + db.create_all() if not Subforum.query.all(): init_site() @app.route('/') def index(): + # Show only top-level subforums on the home page. subforums = Subforum.query.filter(Subforum.parent_id == None).order_by(Subforum.id) return render_template("subforums.html", subforums=subforums) diff --git a/forum/models.py b/forum/models.py index 8add9ae..6766044 100644 --- a/forum/models.py +++ b/forum/models.py @@ -3,13 +3,14 @@ from flask_login import UserMixin import datetime -# create db here so it can be imported (with the models) into the App object. +# Shared SQLAlchemy object used by the app factory and all models. from flask_sqlalchemy import SQLAlchemy db = SQLAlchemy() -#OBJECT MODELS +# Database models class User(UserMixin, db.Model): + # Store account information and ownership of posts/comments. id = db.Column(db.Integer, primary_key=True) username = db.Column(db.Text, unique=True) password_hash = db.Column(db.Text) @@ -19,13 +20,17 @@ class User(UserMixin, db.Model): comments = db.relationship("Comment", backref="user") def __init__(self, email, username, password): + # Save the hashed password instead of the plain text password. self.email = email self.username = username self.password_hash = generate_password_hash(password) + def check_password(self, password): + # Compare a password guess against the stored hash. return check_password_hash(self.password_hash, password) class Post(db.Model): + # Store one forum post and link it to a user and subforum. id = db.Column(db.Integer, primary_key=True) title = db.Column(db.Text) content = db.Column(db.Text) @@ -34,16 +39,17 @@ class Post(db.Model): subforum_id = db.Column(db.Integer, db.ForeignKey('subforum.id')) postdate = db.Column(db.DateTime) - #cache stuff + # Simple in-memory cache for human-readable time labels. lastcheck = None savedresponce = None + def __init__(self, title, content, postdate): self.title = title self.content = content self.postdate = postdate + def get_time_string(self): - #this only needs to be calculated every so often, not for every request - #this can be a rudamentary chache + # Only recalculate the label every 30 seconds. now = datetime.datetime.now() if self.lastcheck is None or (now - self.lastcheck).total_seconds() > 30: self.lastcheck = now @@ -53,7 +59,6 @@ def get_time_string(self): diff = now - self.postdate seconds = diff.total_seconds() - print(seconds) if seconds / (60 * 60 * 24 * 30) > 1: self.savedresponce = " " + str(int(seconds / (60 * 60 * 24 * 30))) + " months ago" elif seconds / (60 * 60 * 24) > 1: @@ -68,6 +73,7 @@ def get_time_string(self): return self.savedresponce class Subforum(db.Model): + # Represent a forum category and its optional child subforums. id = db.Column(db.Integer, primary_key=True) title = db.Column(db.Text, unique=True) description = db.Column(db.Text) @@ -76,11 +82,13 @@ class Subforum(db.Model): posts = db.relationship("Post", backref="subforum") path = None hidden = db.Column(db.Boolean, default=False) + def __init__(self, title, description): self.title = title self.description = description class Comment(db.Model): + # Store a comment attached to a post and authored by a user. id = db.Column(db.Integer, primary_key=True) content = db.Column(db.Text) postdate = db.Column(db.DateTime) @@ -89,12 +97,13 @@ class Comment(db.Model): lastcheck = None savedresponce = None + def __init__(self, content, postdate): self.content = content self.postdate = postdate + def get_time_string(self): - #this only needs to be calculated every so often, not for every request - #this can be a rudamentary chache + # Only recalculate the label every 30 seconds. now = datetime.datetime.now() if self.lastcheck is None or (now - self.lastcheck).total_seconds() > 30: self.lastcheck = now @@ -133,9 +142,10 @@ def generateLinkPath(subforumid): return link -#Post checks +# Post validation helpers def valid_title(title): return len(title) > 4 and len(title) < 140 + def valid_content(content): return len(content) > 10 and len(content) < 5000 diff --git a/forum/routes.py b/forum/routes.py index 75993e5..dc14a14 100644 --- a/forum/routes.py +++ b/forum/routes.py @@ -3,17 +3,17 @@ from flask_login.utils import login_required import datetime from flask import Blueprint, render_template, request, redirect, url_for -from forum.models import User, Post, Comment, Subforum, valid_content, valid_title, db, generateLinkPath, error -from forum.user import username_taken, email_taken, valid_username +from .models import User, Post, Comment, Subforum, valid_content, valid_title, db, generateLinkPath, error +from .user import username_taken, email_taken, valid_username -## -# This file needs to be broken up into several, to make the project easier to work on. -## +# Route handlers for login, browsing, and content creation. +# The app is small enough to keep in one blueprint for now. rt = Blueprint('routes', __name__, template_folder='templates') @rt.route('/action_login', methods=['POST']) def action_login(): + # Read login form values and authenticate the user. username = request.form['username'] password = request.form['password'] user = User.query.filter(User.username == username).first() @@ -29,12 +29,13 @@ def action_login(): @login_required @rt.route('/action_logout') def action_logout(): - #todo + # End the current session and send the user back home. logout_user() return redirect("/") @rt.route('/action_createaccount', methods=['POST']) def action_createaccount(): + # Validate signup data, create the user, then log them in. username = request.form['username'] password = request.form['password'] email = request.form['email'] @@ -42,7 +43,7 @@ def action_createaccount(): retry = False if username_taken(username): errors.append("Username is already taken!") - retry=True + retry = True if email_taken(email): errors.append("An account already exists with this email!") retry = True @@ -65,25 +66,27 @@ def action_createaccount(): @rt.route('/subforum') def subforum(): + # Show one subforum, its posts, and its child subforums. subforum_id = int(request.args.get("sub")) subforum = Subforum.query.filter(Subforum.id == subforum_id).first() if not subforum: return error("That subforum does not exist!") posts = Post.query.filter(Post.subforum_id == subforum_id).order_by(Post.id.desc()).limit(50) - if not subforum.path: - subforumpath = generateLinkPath(subforum.id) + subforumpath = subforum.path or generateLinkPath(subforum.id) subforums = Subforum.query.filter(Subforum.parent_id == subforum_id).all() return render_template("subforum.html", subforum=subforum, posts=posts, subforums=subforums, path=subforumpath) @rt.route('/loginform') def loginform(): + # Render the shared login and signup page. return render_template("login.html") @login_required @rt.route('/addpost') def addpost(): + # Show the new post form for the selected subforum. subforum_id = int(request.args.get("sub")) subforum = Subforum.query.filter(Subforum.id == subforum_id).first() if not subforum: @@ -93,18 +96,20 @@ def addpost(): @rt.route('/viewpost') def viewpost(): + # Show one post and its comments. postid = int(request.args.get("post")) post = Post.query.filter(Post.id == postid).first() if not post: return error("That post does not exist!") - if not post.subforum.path: - subforumpath = generateLinkPath(post.subforum.id) - comments = Comment.query.filter(Comment.post_id == postid).order_by(Comment.id.desc()) # no need for scalability now + subforumpath = post.subforum.path or generateLinkPath(post.subforum.id) + # Newest comments appear first for easier reading. + comments = Comment.query.filter(Comment.post_id == postid).order_by(Comment.id.desc()) return render_template("viewpost.html", post=post, path=subforumpath, comments=comments) @login_required @rt.route('/action_comment', methods=['POST', 'GET']) def comment(): + # Create a comment, attach it to the user and post, then save it. post_id = int(request.args.get("post")) post = Post.query.filter(Post.id == post_id).first() if not post: @@ -120,15 +125,16 @@ def comment(): @login_required @rt.route('/action_post', methods=['POST']) def action_post(): + # Validate a new post before saving it. subforum_id = int(request.args.get("sub")) subforum = Subforum.query.filter(Subforum.id == subforum_id).first() if not subforum: - return redirect(url_for("subforums")) + return redirect(url_for("index")) user = current_user title = request.form['title'] content = request.form['content'] - #check for valid posting + # Collect validation errors first so the form can be shown again if needed. errors = [] retry = False if not valid_title(title): @@ -138,7 +144,7 @@ def action_post(): errors.append("Post must be between 10 and 5000 characters long!") retry = True if retry: - return render_template("createpost.html",subforum=subforum, errors=errors) + return render_template("createpost.html", subforum=subforum, errors=errors) post = Post(title, content, datetime.datetime.now()) subforum.posts.append(post) user.posts.append(post) diff --git a/run.sh b/run.sh index a39697a..5f23fad 100755 --- a/run.sh +++ b/run.sh @@ -1,6 +1,11 @@ -# export PORT=5006 +#!/bin/bash export SECRET_KEY="kristofer" -# honcho start -# you can ALSO or RATHER use the following command to run the app -cd ./forum; flask run +if [ "$1" = "--server" ]; then + cd ./forum && flask run --host=0.0.0.0 --port=8000 +else + cd ./forum && flask run --port=8000 +fi + +# To run the server, use: ./run.sh --server +# To run the server in development mode, use: ./run.sh