Build a Journal Submission System
Build a multi-page Flask app with forms, file uploads, and a database.
Introduction
30 Second Summary
Every research paper goes through a journey before it gets published. Authors submit their work, reviewers score and critique it, and editors make the final call on whether it gets accepted or rejected.
In this project, you will build a web application with Python and Flask that simulates this entire journal workflow from submission to editorial decision.
What You'll Build
You will submit a paper in your browser, review it with a score and feedback, then make an accept or reject decision and watch the dashboard update with color-coded statuses.
By the end of this project, you'll have:
- A submission form that accepts paper titles, abstracts, and uploaded PDF documents you can download from the submissions list.
- A review system where you score papers from 1-10 and leave written feedback that appears on each paper's detail page.
- An editorial dashboard with color-coded status badges and average review scores that update the moment you accept or reject a paper.
- Secret Mission: Add search and filtering to your dashboard so you can find papers by title or status.
Are there any prerequisites?
None. This guide includes full setup instructions for Python, Flask, and everything else you need. All you need is a computer with VS Code installed.
Before We Start
Before we dive in, let's lock in what you're about to build and why it matters.
Set Up Python and Flask
Every web application needs a programming language and a framework to handle requests. Python is the language you will write your journal system in, and Flask is the lightweight framework that turns your Python code into a web server.
In this step, you will install Python, create a project folder, install Flask, and confirm everything works by visiting a live page in your browser.
In this step, get ready to:
- Install Python and verify it is available on your machine.
- Create a project folder with a virtual environment and Flask installed.
- Run a minimal Flask app and see it in your browser.
Install Python
Python is the programming language that powers your Flask application. You need version 3.13 or higher installed on your machine before you can continue.
- Open a terminal to check whether Python is already installed.
- Press Ctrl+` in VS Code to open the integrated terminal.
- Check your Python version by running this command:
Windows
python --version
macOS
python3 --version
Check your result below:
✔️ I see version 3.13 or higher
You are all set. Python is installed and ready to use. Continue to the next substep.
ⓧ I see an older version
You have Python installed, but it is an older version. Flask 3.1.3 requires Python 3.9 or higher. Upgrade to the latest version to avoid compatibility issues.
- Visit python.org/downloads in your browser.
- Click the large Download Python button to get the latest installer.
- Run the installer. On Windows, check the Add python.exe to PATH checkbox before clicking Install Now. On macOS, follow the installer prompts.
- Close and reopen your VS Code terminal after installation finishes.
- Run the version check command again to confirm it now shows 3.13 or higher.
Windows PATH note
If python --version still shows the old version after installing, the old version may be shadowing the new one. Open a fresh Command Prompt (not the VS Code terminal) and try again. If it persists, uninstall the old version from Settings > Apps and reinstall the latest Python with the PATH checkbox checked.
ⓧ Command not found
Python is not installed yet. Follow these steps to install it.
- Visit python.org/downloads in your browser.
- Click the large Download Python button to get the latest installer for your operating system.
- Run the installer. On Windows, check the Add python.exe to PATH checkbox before clicking Install Now. On macOS, follow the installer prompts.
- Close and reopen your VS Code terminal after installation completes.
- Run the version check command again. You should now see a version number like Python 3.13.14 or Python 3.14.6.
Still seeing command not found?
On macOS, try python3 --version instead. Some macOS installations use python3 as the command name.
On Windows, make sure you checked Add python.exe to PATH during installation. If you missed it, rerun the installer, select Modify, and check the PATH option.
Why isn't Python recognized after I installed it?
Create the project and install Flask
Now you will create a dedicated folder for your journal system and install Flask inside a virtual environment. A virtual environment keeps your project's packages separate from other Python projects on your machine.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir journal-system
cd journal-system
- Confirm you are in the correct folder by running:
ls
The folder is empty. That is expected. You will add files shortly.
- Open this folder in VS Code by running:
code .
What does code . do?
The code . command opens VS Code with the current folder as your workspace. The dot means "this folder."
- If a new VS Code window opens, close the old one. Then press Ctrl+` to open the terminal in your new workspace.
- Create a virtual environment by running:
python -m venv .venv
What is a virtual environment?
A virtual environment is an isolated copy of Python where you install packages just for this project. Other projects on your machine get their own environments with their own packages. This prevents version conflicts.
- Activate the virtual environment by running:
Windows
.venv\Scripts\activate
macOS
source .venv/bin/activate
Your terminal prompt now shows (.venv) at the beginning. This confirms the virtual environment is active.
- Install Flask by running:
pip install Flask==3.1.3
You should see download and installation messages ending with "Successfully installed" and a list of packages.
Seeing a permission error or pip not found?
Make sure your virtual environment is active. You should see (.venv) in your terminal prompt. If you do not, run the activate command again.
On macOS, if python -m venv .venv failed earlier, try python3 -m venv .venv and then source .venv/bin/activate.
Help me fix my Flask installation error
Create a minimal Flask app and run it
With Flask installed, you can now create the files your journal system needs. You will start with a minimal app that proves your setup works.
- In VS Code, click File > New File.
- Name the file app.py and paste the following code:
import os
import sqlite3
from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
return 'Hello, Journal!'
What does this code do?
This file creates a Flask application with a single route. When someone visits the root URL (/), Flask calls the index() function and sends back the text "Hello, Journal!" to the browser.
The import os and import sqlite3 lines prepare modules you will use in later steps for file handling and database access.
- Save app.py with Ctrl+S (Windows) or Cmd+S (macOS).
- Create another new file named schema.sql and paste this code:
DROP TABLE IF EXISTS review;
DROP TABLE IF EXISTS submission;
CREATE TABLE submission (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
abstract TEXT NOT NULL,
filename TEXT,
submitted_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
status TEXT NOT NULL DEFAULT 'Pending'
);
CREATE TABLE review (
id INTEGER PRIMARY KEY AUTOINCREMENT,
submission_id INTEGER NOT NULL,
score INTEGER NOT NULL,
comments TEXT NOT NULL,
reviewed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (submission_id) REFERENCES submission (id)
);
What does this schema define?
This file defines two database tables for your journal system. The submission table stores papers with a title, abstract, optional PDF filename, timestamp, and status. The review table stores reviews with a score, comments, and a link back to the submission it belongs to.
The DROP TABLE IF EXISTS lines at the top let you re-run this file later to reset the database without errors. The review table is dropped first because it has a foreign key that references submission.
- Save schema.sql.
- Create the two folders your project needs by running this in the terminal:
mkdir templates
mkdir uploads
- Verify your project structure by running:
ls
You should see app.py, schema.sql, templates/, uploads/, and .venv/ listed in the output.
Missing files in the output?
Make sure you saved both app.py and schema.sql in the journal-system folder, not in a subfolder or your Desktop. Check the VS Code Explorer sidebar to confirm both files appear at the top level.
If templates/ or uploads/ are missing, run the mkdir commands again.
Everything is in place. Before you run the server, think about what you expect to see when you visit the URL.
- Start the Flask development server by running:
flask --app app run --debug
What do you think you will see in your browser when you visit the URL that appears in the terminal output?
You should see the message * Running on http://127.0.0.1:5000 printed in your terminal. The server is now running.
- Open your browser and go to http://127.0.0.1:5000.
You should see the text "Hello, Journal!" displayed in your browser. Your Flask app is live.
What does --debug do?
The --debug flag enables auto-reload. Whenever you save a file, Flask automatically restarts the server so you can see changes immediately without stopping and restarting manually.
Not seeing Hello, Journal! in the browser?
Check your terminal for error messages. If you see ModuleNotFoundError: No module named 'flask', your virtual environment is not active. Run the activate command from earlier and try again.
If the terminal shows the server is running but the browser shows nothing, double-check the URL is exactly http://127.0.0.1:5000 with no extra characters.
Help me troubleshoot my Flask server
✔️ Awesome, I've got everything!
Great. Make sure you have saved both files and the server is still running in your terminal.
ⓧ I'd like to double check the full code
Here is the complete app.py file:
import os
import sqlite3
from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
return 'Hello, Journal!'
Here is the complete schema.sql file:
DROP TABLE IF EXISTS review;
DROP TABLE IF EXISTS submission;
CREATE TABLE submission (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
abstract TEXT NOT NULL,
filename TEXT,
submitted_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
status TEXT NOT NULL DEFAULT 'Pending'
);
CREATE TABLE review (
id INTEGER PRIMARY KEY AUTOINCREMENT,
submission_id INTEGER NOT NULL,
score INTEGER NOT NULL,
comments TEXT NOT NULL,
reviewed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (submission_id) REFERENCES submission (id)
);
Your environment is ready and your first Flask route is serving a live page. Next up, you will build the submission form that lets authors send papers to the system.
Build the Submission Form
Your Flask app currently returns a plain text greeting. That proves the framework is working, but a journal system needs a way for authors to submit papers and a way for editors to see what has been submitted.
In this step, you will connect your app to a SQLite database, build an HTML form for submitting papers, and display those submissions on a dashboard. By the end, you will have a full submit-and-display loop working in your browser.
In this step, get ready to:
- Set up the database connection and initialize the tables.
- Create a submission form with title and abstract fields.
- Display all submissions on the home page.
Set up the database connection and initialize it
Flask does not come with a built-in database layer. You need to write helper functions that open a connection when a request arrives and close it when the response is sent.
- In VS Code, open app.py.
- Select all the existing code and replace it with the following imports and configuration:
import os
import sqlite3
from flask import (
Flask, render_template, request, redirect,
url_for, flash, g
)
app = Flask(__name__)
app.config['SECRET_KEY'] = 'dev'
app.config['DATABASE'] = os.path.join(app.instance_path, 'journal.sqlite')
os.makedirs(app.instance_path, exist_ok=True)
What does this code do?
- The new imports bring in Flask tools for rendering templates, handling form data, redirecting, flashing messages, and storing per-request objects in g.
- SECRET_KEY is required for flash messages to work (they use a signed cookie).
- DATABASE tells Flask where to store the SQLite file, inside an instance/ folder that Flask manages.
- os.makedirs creates the instance/ folder if it does not already exist.
- Below the os.makedirs line, add the following database helper functions:
def get_db():
if 'db' not in g:
g.db = sqlite3.connect(
app.config['DATABASE'],
detect_types=sqlite3.PARSE_DECLTYPES
)
g.db.row_factory = sqlite3.Row
return g.db
@app.teardown_appcontext
def close_db(exception):
db = g.pop('db', None)
if db is not None:
db.close()
def init_db():
db = get_db()
with app.open_resource('schema.sql') as f:
db.executescript(f.read().decode('utf8'))
@app.cli.command('init-db')
def init_db_command():
init_db()
print('Initialized the database.')
What do these functions do?
- get_db() opens a database connection once per request and reuses it for subsequent queries in the same request.
- close_db() runs automatically after each response, closing the connection so it does not leak.
- init_db() reads your schema.sql file and executes the SQL to create the tables.
- init_db_command() registers a flask --app app init-db terminal command so you can create the database from the command line.
- Keep the existing @app.route('/') route at the bottom of the file for now. You will update it later in this step.
- Save app.py.
- Initialize the database by running this command in your VS Code terminal:
flask --app app init-db
You should see:
Initialized the database.
Seeing an error instead?
- If you see ModuleNotFoundError: No module named 'flask', your virtual environment is not activated. Run .venv\Scripts\activate first.
- If you see SyntaxError, double-check that your parentheses and indentation in app.py match the code above exactly.
- If you see a file-not-found error mentioning schema.sql, make sure the file is in the same folder as app.py (the root of your journal-system folder).
Help me debug the init-db command
Create the submission form template and route
Flask uses Jinja2 templates to generate HTML pages. You will create a base layout that every page shares, then a specific template for the submission form.
- Create a new file called base.html inside your templates/ folder.
- Paste the following code into templates/base.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Journal System{% endblock %}</title>
</head>
<body>
<nav>
<a href="{{ url_for('index') }}">Dashboard</a>
<a href="{{ url_for('submit') }}">Submit Paper</a>
</nav>
<main>
{% with messages = get_flashed_messages() %}
{% if messages %}
<div class="flash">
{% for message in messages %}
<p>{{ message }}</p>
{% endfor %}
</div>
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</main>
</body>
</html>
What does this template do?
- {% block title %} and {% block content %} are placeholders that child templates fill in with their own content.
- The <nav> section gives every page links to the Dashboard and Submit Paper pages.
- The get_flashed_messages() section displays one-time notifications like "Paper submitted successfully!" after form submissions.
- Save templates/base.html.
- Create another new file called submit.html inside your templates/ folder.
- Paste the following code into templates/submit.html:
{% extends 'base.html' %}
{% block title %}Submit Paper - Journal System{% endblock %}
{% block content %}
<h1>Submit a Paper</h1>
<form method="POST">
<label for="title">Title</label>
<input type="text" id="title" name="title" required>
<label for="abstract">Abstract</label>
<textarea id="abstract" name="abstract" rows="6" required></textarea>
<button type="submit">Submit Paper</button>
</form>
{% endblock %}
What does this template do?
- {% extends 'base.html' %} tells Jinja2 to wrap this page inside the base layout you just created.
- The <form method="POST"> sends the form data to the same URL when submitted. Flask will handle the POST request in the route you add next.
- The required attribute on inputs makes the browser enforce that both fields are filled in before submitting.
- Save templates/submit.html.
- Confirm both template files exist by running:
ls templates/
You should see base.html and submit.html listed in the output.
- Now add the /submit route to app.py. Paste the following code below the index() route at the bottom of the file:
@app.route('/submit', methods=['GET', 'POST'])
def submit():
if request.method == 'POST':
title = request.form['title']
abstract = request.form['abstract']
error = None
if not title:
error = 'Title is required.'
elif not abstract:
error = 'Abstract is required.'
if error is None:
db = get_db()
db.execute(
'INSERT INTO submission (title, abstract, filename) VALUES (?, ?, ?)',
(title, abstract, None)
)
db.commit()
flash('Paper submitted successfully!')
return redirect(url_for('index'))
flash(error)
return render_template('submit.html')
What does this route do?
- When a visitor opens /submit (a GET request), Flask renders the form template.
- When the form is submitted (a POST request), the route reads the title and abstract from the form data and validates them.
- The ? placeholders in the SQL query prevent SQL injection by keeping user input separate from the query structure.
- After a successful insert, flash() queues a success message and redirect() sends the user back to the home page.
- Save app.py.
- Start the Flask development server by running:
flask --app app run --debug
- Open your browser and visit http://127.0.0.1:5000/submit.
You should see a page with a "Submit a Paper" heading, a Title text field, an Abstract text area, and a Submit Paper button.
Page not loading?
- If you see a TemplateNotFound error, check that submit.html and base.html are inside the templates/ folder, not in the project root.
- If the terminal shows an IndentationError, make sure your submit() function uses consistent 4-space indentation throughout.
- If the page loads but the navigation links give errors, that is expected. The / route does not render a template yet. You will fix that next.
Help me debug my submission form page
Display submissions on the home page
The submission form works, but the home page still returns plain text. You need a template that lists all submissions and an updated route that queries the database.
- Create a new file called index.html inside your templates/ folder.
- Paste the following code into templates/index.html:
{% extends 'base.html' %}
{% block title %}Dashboard - Journal System{% endblock %}
{% block content %}
<h1>Submissions Dashboard</h1>
{% if submissions %}
<table>
<thead>
<tr>
<th>Title</th>
<th>Submitted</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{% for sub in submissions %}
<tr>
<td>{{ sub['title'] }}</td>
<td>{{ sub['submitted_at'] }}</td>
<td>{{ sub['status'] }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p>No submissions yet. <a href="{{ url_for('submit') }}">Submit a paper</a> to get started.</p>
{% endif %}
{% endblock %}
What does this template do?
- The {% if submissions %} block shows a table when papers exist, or a helpful message with a link to submit when the database is empty.
- The {% for sub in submissions %} loop creates one table row per submission, displaying the title, submission date, and status.
- Save templates/index.html.
- Now update the / route in app.py. Find the existing route:
@app.route('/')
def index():
return 'Hello, Journal!'
- Replace those three lines with the following:
@app.route('/')
def index():
db = get_db()
submissions = db.execute(
'SELECT id, title, abstract, filename, submitted_at, status FROM submission ORDER BY submitted_at DESC'
).fetchall()
return render_template('index.html', submissions=submissions)
What changed?
Instead of returning plain text, the route now queries all rows from the submission table (newest first) and passes them to index.html for rendering.
- Save app.py.
- Switch to your browser and visit http://127.0.0.1:5000. The debug server auto-reloads when you save files.
You should see the "Submissions Dashboard" heading with the message "No submissions yet" and a link to submit a paper.
Seeing an error instead of the dashboard?
- If you see OperationalError: no such table: submission, the database was not initialized. Stop the server with Ctrl+C, run flask --app app init-db, then restart the server.
- If you see BuildError mentioning url_for('submit'), make sure the submit() function exists in app.py and is decorated with @app.route('/submit').
Help me fix my dashboard route
Now test the full loop. Before you try it, what do you think will happen when you fill in the form and click Submit?
- Click the **Submit Paper** link in the navigation bar.
- Enter My First Research Paper in the **Title** field.
- Enter This paper explores the fundamentals of web development with Flask. in the **Abstract** field.
- Click **Submit Paper**.
You should be redirected to the home page. A "Paper submitted successfully!" flash message appears at the top. Your paper is now listed in the table showing its title, submission date, and "Pending" status.
Paper not appearing in the table?
- Make sure you included db.commit() after the db.execute() call in your submit() route. Without it, the insert is never saved.
- Check that your index() route uses render_template('index.html', submissions=submissions) and not the old return 'Hello, Journal!' line.
Help me debug why my submission is not showing up
✔️ Awesome, I've got everything!
Great. Double check you have saved all three template files and app.py.
⨯ I'd like to double check the full code
Your app.py is now too long to show in one block. Scroll up and confirm these sections appear in order:
- Imports (os, sqlite3, Flask tools).
- App creation and config (SECRET_KEY, DATABASE, os.makedirs).
- get_db() function.
- close_db() function with @app.teardown_appcontext.
- init_db() and init_db_command() functions.
- The / route querying all submissions.
- The /submit route handling GET and POST.
Here is templates/base.html for reference:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Journal System{% endblock %}</title>
</head>
<body>
<nav>
<a href="{{ url_for('index') }}">Dashboard</a>
<a href="{{ url_for('submit') }}">Submit Paper</a>
</nav>
<main>
{% with messages = get_flashed_messages() %}
{% if messages %}
<div class="flash">
{% for message in messages %}
<p>{{ message }}</p>
{% endfor %}
</div>
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</main>
</body>
</html>
Here is templates/submit.html:
{% extends 'base.html' %}
{% block title %}Submit Paper - Journal System{% endblock %}
{% block content %}
<h1>Submit a Paper</h1>
<form method="POST">
<label for="title">Title</label>
<input type="text" id="title" name="title" required>
<label for="abstract">Abstract</label>
<textarea id="abstract" name="abstract" rows="6" required></textarea>
<button type="submit">Submit Paper</button>
</form>
{% endblock %}
Here is templates/index.html:
{% extends 'base.html' %}
{% block title %}Dashboard - Journal System{% endblock %}
{% block content %}
<h1>Submissions Dashboard</h1>
{% if submissions %}
<table>
<thead>
<tr>
<th>Title</th>
<th>Submitted</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{% for sub in submissions %}
<tr>
<td>{{ sub['title'] }}</td>
<td>{{ sub['submitted_at'] }}</td>
<td>{{ sub['status'] }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p>No submissions yet. <a href="{{ url_for('submit') }}">Submit a paper</a> to get started.</p>
{% endif %}
{% endblock %}
Your submission loop is complete. You can submit papers and see them listed on the dashboard. Next up, you will add PDF file upload so authors can attach their actual documents to each submission.
Add PDF File Upload
Your submission form works. Authors can enter a title and abstract, and the paper shows up on the dashboard. But a journal submission without the actual document is just a list of titles.
In this step, you will add PDF file upload to the submission form. This introduces handling binary data, securing filenames, and restricting file types to only PDFs.
In this step, get ready to:
- Configure upload handling with file size limits and extension checking.
- Extend the submission form and route to accept PDF files.
- Add download links so anyone can retrieve the uploaded PDF.
Configure upload handling
Before your app can accept files, it needs to know where to store them, which file types to allow, and how large a file can be. These settings protect your server from malicious uploads and keep your project organized.
- In app.py, replace the entire import section and everything down through the os.makedirs line with the following code:
import os
import sqlite3
from flask import (
Flask, render_template, request, redirect,
url_for, flash, g, send_from_directory
)
from werkzeug.utils import secure_filename
# Configure file upload settings
UPLOAD_FOLDER = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'uploads')
ALLOWED_EXTENSIONS = {'pdf'}
app = Flask(__name__)
app.config['SECRET_KEY'] = 'dev'
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
app.config['MAX_CONTENT_LENGTH'] = 16 * 1000 * 1000
app.config['DATABASE'] = os.path.join(app.instance_path, 'journal.sqlite')
os.makedirs(app.instance_path, exist_ok=True)
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
What does this code do?
- send_from_directory is a Flask helper that safely serves files from a folder. You will use it to let users download uploaded PDFs.
- secure_filename from Werkzeug strips dangerous characters from filenames (like ../ or spaces) so attackers cannot overwrite files on your system.
- UPLOAD_FOLDER points to the uploads/ directory next to your script. This is where PDFs will be saved.
- ALLOWED_EXTENSIONS restricts uploads to only .pdf files.
- MAX_CONTENT_LENGTH caps uploads at 16 MB. Flask rejects anything larger before your code even runs.
- Now add the allowed_file() helper and a download route to app.py. Place this code directly below the init_db_command() function:
def allowed_file(filename):
# Check if the file extension is in the allowed set
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
@app.route('/uploads/<filename>')
def download_file(filename):
# Serve uploaded files from the uploads folder
return send_from_directory(app.config['UPLOAD_FOLDER'], filename)
What does this code do?
- allowed_file() splits the filename on the last dot and checks if the extension is pdf. This prevents someone from uploading a .exe or .sh file.
- The /uploads/<filename> route uses send_from_directory to serve files from the uploads/ folder. This is safer than building the file path yourself because Flask prevents path traversal attacks.
- Save app.py.
- Check your terminal. Because you are running in debug mode, Flask auto-reloads. You should see a line like Restarting with stat confirming it picked up your changes without errors.
Seeing an import error?
- If you see ImportError: cannot import name 'send_from_directory', make sure send_from_directory is inside the parentheses of the from flask import (...) block, not on a separate import line.
- If you see ModuleNotFoundError: No module named 'werkzeug', confirm your virtual environment is active. You should see (.venv) at the start of your terminal prompt.
Help me fix an import error in my Flask app.
Extend the form and route to handle files
The upload infrastructure is ready. Now you need to give users a way to select a file and teach the submit route how to receive it.
- In templates/submit.html, replace the entire <form> block (from <form method="POST"> through </form>) with this updated version:
<form method="POST" enctype="multipart/form-data">
<label for="title">Title</label>
<input type="text" id="title" name="title" required>
<label for="abstract">Abstract</label>
<textarea id="abstract" name="abstract" rows="6" required></textarea>
<!-- File upload field for PDF -->
<label for="paper">Upload PDF</label>
<input type="file" id="paper" name="paper" accept=".pdf">
<button type="submit">Submit Paper</button>
</form>
What changed in this form?
- enctype="multipart/form-data" tells the browser to send file data as binary instead of plain text. Without this, the file never reaches your server.
- The <input type="file"> with accept=".pdf" gives users a file picker that filters for PDFs. The name="paper" attribute matches what your route will look for in request.files.
- Save templates/submit.html.
- Refresh http://127.0.0.1:5000/submit in your browser. You should see a new Upload PDF label and a file chooser button below the abstract field.
Don't see the file input?
- Make sure you saved the file. Check for a dot on the tab in VS Code indicating unsaved changes.
- Confirm the new <input type="file"> line is inside the <form> tags, not outside them.
Help me figure out why my file input isn't showing.
✔️ Awesome, I've got everything!
Great. Double check you have saved templates/submit.html.
ⓧ I'd like to double check the full code
{% extends 'base.html' %}
{% block title %}Submit Paper - Journal System{% endblock %}
{% block content %}
<h1>Submit a Paper</h1>
<form method="POST" enctype="multipart/form-data">
<label for="title">Title</label>
<input type="text" id="title" name="title" required>
<label for="abstract">Abstract</label>
<textarea id="abstract" name="abstract" rows="6" required></textarea>
<label for="paper">Upload PDF</label>
<input type="file" id="paper" name="paper" accept=".pdf">
<button type="submit">Submit Paper</button>
</form>
{% endblock %}
The form can send files now, but the /submit route still ignores them. Time to wire up the backend.
- In app.py, find the submit() function. Locate this section inside the if request.method == 'POST' block:
if error is None:
db = get_db()
db.execute(
'INSERT INTO submission (title, abstract, filename) VALUES (?, ?, ?)',
(title, abstract, None)
)
db.commit()
flash('Paper submitted successfully!')
return redirect(url_for('index'))
- Replace everything from that if error is None: line down through return redirect(url_for('index')) with this code, which adds file handling above the database insert:
# Get the uploaded file from the form
file = request.files.get('paper')
filename = None
# Validate and save the file if one was uploaded
if file and file.filename != '':
if allowed_file(file.filename):
filename = secure_filename(file.filename)
file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
else:
error = 'Only PDF files are allowed.'
if error is None:
db = get_db()
db.execute(
'INSERT INTO submission (title, abstract, filename) VALUES (?, ?, ?)',
(title, abstract, filename)
)
db.commit()
flash('Paper submitted successfully!')
return redirect(url_for('index'))
What does this code do?
- request.files.get('paper') retrieves the uploaded file by the form field name paper.
- The code checks that a file was actually selected (not an empty field), then validates its extension with allowed_file().
- secure_filename() sanitizes the filename so it is safe to write to disk.
- The filename variable (either a sanitized name or None) is stored in the database alongside the title and abstract.
- Save app.py.
- Go to http://127.0.0.1:5000/submit in your browser.
- Enter My First Paper for the title.
- Enter This is a test abstract. for the abstract.
- Click the file chooser and select any PDF from your computer.
- Click Submit Paper.
You should be redirected to the home page with the flash message "Paper submitted successfully!" and your paper listed in the table.
Seeing 'Only PDF files are allowed'?
- Make sure the file you selected actually has a .pdf extension. Files like .docx or .txt will be rejected.
- If you do not have a PDF handy, try submitting without a file. The upload field is optional, so the paper will still save with no attachment.
Help me troubleshoot my file upload in Flask.
✔️ Awesome, I've got everything!
Great. Double check you have saved app.py.
ⓧ I'd like to double check the full code
Your app.py should have these sections in order. Scroll through and confirm each is present:
- Imports (os, sqlite3, Flask helpers including send_from_directory, and secure_filename from werkzeug).
- UPLOAD_FOLDER and ALLOWED_EXTENSIONS constants.
- App creation with SECRET_KEY, UPLOAD_FOLDER, MAX_CONTENT_LENGTH, and DATABASE config. Two os.makedirs calls.
- get_db(), close_db(), init_db(), and init_db_command() functions.
- allowed_file() helper function.
- index() route at /.
- submit() route with file handling (request.files.get('paper'), secure_filename, file.save()) and the INSERT using filename variable.
- download_file() route at /uploads/<filename>.
Add download links to the home page
Papers are uploading to disk and their filenames are saved in the database. The home page still does not show any way to access those files.
- In templates/index.html, find the <table> block. Replace the entire table (from <table> through </table>) with this updated version that adds a PDF column:
<table>
<thead>
<tr>
<th>Title</th>
<th>Submitted</th>
<th>Status</th>
<th>PDF</th>
</tr>
</thead>
<tbody>
{% for sub in submissions %}
<tr>
<td>{{ sub['title'] }}</td>
<td>{{ sub['submitted_at'] }}</td>
<td>{{ sub['status'] }}</td>
<td>
<!-- Show download link if a file was uploaded -->
{% if sub['filename'] %}
<a href="{{ url_for('download_file', filename=sub['filename']) }}">Download</a>
{% else %}
No file
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
What does this code do?
- A new PDF column is added to the table header.
- For each submission, Jinja2 checks if sub['filename'] exists. If a PDF was uploaded, it shows a "Download" link pointing to the download_file route. Otherwise it shows "No file".
- Save templates/index.html.
Before you check the result, what do you expect to see next to the paper you submitted earlier?
- Go to http://127.0.0.1:5000 in your browser.
You should see your paper listed with a Download link in the new PDF column. Click the link and the PDF opens in your browser.
Download link shows 'No file'?
- This means the submission was saved without a filename. The paper you submitted earlier (before adding file upload) has None stored as its filename. Submit a new paper with a PDF attached and it will show the Download link.
- If the link appears but clicking it gives a 404, check that your uploads/ folder actually contains the file. Run ls uploads/ in your terminal to check.
Help me fix my download link that isn't working.
✔️ Awesome, I've got everything!
Great. Double check you have saved templates/index.html.
ⓧ I'd like to double check the full code
Your templates/index.html should have these sections in order:
- Extends base.html and sets the title block.
- An <h1> with "Submissions Dashboard".
- An {% if submissions %} block containing a <table> with four columns: Title, Submitted, Status, PDF.
- Each row shows the title, date, status, and a conditional Download link or "No file".
- An {% else %} block with a "No submissions yet" message.
Your journal system now handles real documents. Authors can attach PDFs when submitting papers, and anyone can download them from the dashboard. Next up, you will build the review system so papers can be evaluated with scores and written feedback.
Build the Review System
In the last step, you built the submission form and file upload system. Authors can now submit papers with PDFs, and you can see them listed on the home page.
But clicking a submission title does nothing. There is no way to read a paper's full details or evaluate it. In this step, you will build a detail page for each submission and a review form that lets you score and comment on papers.
This introduces a key concept: database relationships. Each review belongs to a specific submission, and the detail page pulls related data from both tables.
In this step, get ready to:
- Create a submission detail page that shows the full paper information.
- Add a review form with a score dropdown and comments field.
- Display submitted reviews on the detail page.
Create the submission detail page
Right now the home page lists submissions, but there is nowhere to see a paper's full abstract or download its PDF from a dedicated page. You need a route that fetches one specific submission and a template that displays it.
- In app.py, find the download_file route at the bottom of the file. Add the following new route above it:
@app.route('/submission/<int:id>')
def submission(id):
db = get_db()
# Fetch the single submission by its ID
sub = db.execute('SELECT * FROM submission WHERE id = ?', (id,)).fetchone()
# Fetch all reviews that belong to this submission
reviews = db.execute(
'SELECT * FROM review WHERE submission_id = ? ORDER BY reviewed_at DESC',
(id,)
).fetchall()
return render_template('submission.html', submission=sub, reviews=reviews)
What does this code do?
This route captures the submission ID from the URL. It runs two database queries: one to fetch the submission itself, and one to fetch all reviews linked to that submission.
The ? placeholder prevents SQL injection by keeping user input separate from the query structure. The .fetchone() method returns a single row, while .fetchall() returns a list.
- Save app.py.
- Create a new file called submission.html inside your templates/ folder. Add the following code:
{% extends 'base.html' %}
{% block title %}{{ submission['title'] }} - Journal System{% endblock %}
{% block content %}
<h1>{{ submission['title'] }}</h1>
<p class="meta">
Submitted: {{ submission['submitted_at'] }} |
Status: <span class="badge badge-{{ submission['status']|lower|replace(' ', '-') }}">{{ submission['status'] }}</span>
</p>
<h2>Abstract</h2>
<p>{{ submission['abstract'] }}</p>
{% if submission['filename'] %}
<p><a href="{{ url_for('download_file', filename=submission['filename']) }}">Download PDF</a></p>
{% endif %}
{% endblock %}
What does this template do?
This template extends your base layout and displays a single submission's details. The Jinja2 template filters (lower and replace) transform the status text into a CSS class name for styling later.
The {% if submission['filename'] %} block only shows the download link when a PDF was actually uploaded.
- Save templates/submission.html.
- In your browser, type http://127.0.0.1:5000/submission/1 directly in the address bar and press Enter.
You should see the detail page for your first submission, showing its title, submission date, status, abstract, and a PDF download link (if you uploaded one).
Seeing a 404 or a build error?
- If you see a TemplateNotFound error, make sure submission.html is inside the templates/ folder (not in the project root).
- If you see a 404 page, you may not have a submission with ID 1. Go to http://127.0.0.1:5000/submit and submit a paper first, then try again.
- Make sure your Flask dev server is still running. If you stopped it, run flask --app app run --debug again.
Why isn't my submission detail page loading?
Now you need a way to reach this page from the home page without typing URLs manually.
- Open templates/index.html and find this line inside the {% for sub in submissions %} loop:
<td>{{ sub['title'] }}</td>
- Replace that line with a link that wraps the title:
<td><a href="{{ url_for('submission', id=sub['id']) }}">{{ sub['title'] }}</a></td>
- Save templates/index.html.
- Go to http://127.0.0.1:5000 in your browser and click a submission title.
You should land on the same detail page you saw before. Titles on the home page are now clickable links.
Add the review form
The detail page shows submission information, but there is no way to evaluate a paper yet. You need a form where a reviewer can score the paper and leave written feedback.
- Open templates/submission.html. Find the {% endblock %} line at the bottom and add the following code directly above it:
<h2>Add a Review</h2>
<form method="POST" action="{{ url_for('review', id=submission['id']) }}">
<label for="score">Score (1-10)</label>
<select id="score" name="score" required>
<option value="">Select a score</option>
{% for i in range(1, 11) %}
<option value="{{ i }}">{{ i }}</option>
{% endfor %}
</select>
<label for="comments">Comments</label>
<textarea id="comments" name="comments" rows="4" required></textarea>
<button type="submit">Submit Review</button>
</form>
What does this form do?
The form POSTs to a /submission/<id>/review route (which you will create next). The {% for i in range(1, 11) %} loop generates score options from 1 to 10 without writing ten option tags manually.
- Save templates/submission.html.
- Now open app.py. Add the following route below the existing submission route and above the download_file route:
@app.route('/submission/<int:id>/review', methods=['POST'])
def review(id):
score = request.form['score']
comments = request.form['comments']
error = None
if not score:
error = 'Score is required.'
elif not comments:
error = 'Comments are required.'
if error is None:
db = get_db()
# Insert the review linked to this submission
db.execute(
'INSERT INTO review (submission_id, score, comments) VALUES (?, ?, ?)',
(id, int(score), comments)
)
# Update the submission status to show it's being reviewed
db.execute(
'UPDATE submission SET status = ? WHERE id = ?',
('Under Review', id)
)
db.commit()
flash('Review submitted successfully!')
else:
flash(error)
return redirect(url_for('submission', id=id))
What does this route do?
This route validates that both a score and comments were provided. If valid, it inserts a new row into the review table with the submission_id linking it back to the paper.
It also updates the submission's status from "Pending" to "Under Review". This is a relational pattern: one action (submitting a review) changes data in two tables.
- Save app.py.
- In your browser, click a submission title from the home page to reach the detail page.
- Select a score from the dropdown (e.g. 7).
- Type a comment like Well-structured argument with clear methodology. in the comments field.
- Click Submit Review.
You should see a green flash message saying "Review submitted successfully!" and the page reloads. The review is saved, but you cannot see it yet because the template does not display reviews.
Seeing a method not allowed error?
- Check that you included methods=['POST'] in the @app.route decorator for the review route.
- Verify the form's action attribute points to url_for('review', id=submission['id']) with the correct function name.
Why is my review form giving me an error when I submit?
✔️ Awesome, I've got everything!
Great. Make sure both app.py and templates/submission.html are saved.
ⓧ I'd like to double check the full code
Your app.py should now contain the two new routes you added in this step. Scroll up and confirm these are present in order:
- The submission route (fetches one submission and its reviews).
- The review route (validates input, inserts review, updates status).
- Both appear above the download_file route.
Display existing reviews on the detail page
You submitted a review, but the page shows no evidence of it. The route already passes a reviews list to the template. You just need to render it.
- Open templates/submission.html. Find the <h2>Add a Review</h2> line you added earlier. Add the following code directly above it:
<h2>Reviews ({{ reviews|length }})</h2>
{% if reviews %}
{% for rev in reviews %}
<div class="review-card">
<p><strong>Score: {{ rev['score'] }}/10</strong></p>
<p>{{ rev['comments'] }}</p>
<p class="meta">{{ rev['reviewed_at'] }}</p>
</div>
{% endfor %}
{% else %}
<p>No reviews yet.</p>
{% endif %}
What does this code do?
The {{ reviews|length }} filter shows the total count of reviews in the heading. The {% for rev in reviews %} loop renders each review as a card with its score, comments, and timestamp.
If no reviews exist yet, it shows a friendly "No reviews yet." message instead of an empty section.
- Save templates/submission.html.
Before you refresh the page, what do you expect to see in the Reviews section? Think about the review you submitted a moment ago.
- Refresh the submission detail page in your browser.
You should see "Reviews (1)" as the section heading, followed by your review showing the score you chose (e.g. "Score: 7/10"), the comments you wrote, and the timestamp.
Not seeing your review?
- Make sure you placed the reviews section above the "Add a Review" form, not below it or outside the {% block content %} block.
- Confirm you actually submitted a review in the previous substep. If the flash message did not appear earlier, the review was not saved.
- Check that the variable names match: rev['score'], rev['comments'], and rev['reviewed_at'] must match the column names in your schema.sql.
Why aren't my reviews showing up on the submission detail page?
- Go back to the home page at http://127.0.0.1:5000 and check the status column for the paper you reviewed.
The status should now show "Under Review" instead of "Pending". The review route updated it automatically.
✔️ Awesome, I've got everything!
Great. Double check you have saved templates/submission.html with both the reviews display and the review form.
ⓧ I'd like to double check the full code
Your templates/submission.html should contain these sections in order. Scroll up and confirm each is present:
- The {% extends 'base.html' %} line and title block at the top.
- The submission title (<h1>), meta paragraph (submitted date and status badge), abstract section, and PDF download link.
- The "Reviews" section with the {% for rev in reviews %} loop showing score, comments, and date.
- The "Add a Review" form with the score dropdown and comments textarea at the bottom.
Your review system is working. You can click into any submission, read its abstract, download the PDF, leave a review, and see all reviews displayed. Next up, you will add a decision dashboard with status badges and accept/reject buttons to complete the editorial workflow.
Add the Decision Dashboard
You have a working submission and review system. Authors can submit papers, and reviewers can score them.
But look at the home page right now. Every paper looks identical regardless of whether it has been reviewed, accepted, or rejected. There is no average score, no review count, and no way to make an editorial decision. This step closes that gap with a proper dashboard.
In this step, get ready to:
- Update the home page query to show review scores and counts alongside each submission.
- Add accept/reject decision buttons so an editor can finalize a paper's status.
- Create a stylesheet that color-codes status badges and polishes the entire interface.
Observe the gap on the home page
Before you fix anything, take a moment to see the problem firsthand.
- Switch back to your browser at http://127.0.0.1:5000.
- Look at the submissions list on the home page.
What do you expect to see for a paper that already has a review? You submitted a review with a score in the previous step.
Every paper shows the same plain text status with no scores, no review count, and no visual distinction. That is the gap you will fix now.
Update the home page query to include review data
The current / route queries only the submission table. To show average scores and review counts, you need a LEFT JOIN that pulls in data from the review table.
- In app.py, find the index() function. Locate these lines:
submissions = db.execute(
'SELECT id, title, abstract, filename, submitted_at, status FROM submission ORDER BY submitted_at DESC'
).fetchall()
- Replace those lines with this updated query:
submissions = db.execute(
'SELECT s.id, s.title, s.abstract, s.filename, s.submitted_at, s.status,'
' AVG(r.score) AS avg_score, COUNT(r.id) AS review_count'
' FROM submission s LEFT JOIN review r ON s.id = r.submission_id'
' GROUP BY s.id ORDER BY s.submitted_at DESC'
).fetchall()
What does this query do?
LEFT JOIN connects each submission to its reviews. If a submission has no reviews yet, it still appears in the results (that is what LEFT means).
AVG(r.score) calculates the average review score. COUNT(r.id) counts how many reviews exist. GROUP BY s.id ensures you get one row per submission with its aggregated review data.
Now you need a template that actually displays these new columns.
- Replace the entire contents of templates/index.html with the following. Start with the top section:
{% extends 'base.html' %}
{% block title %}Dashboard - Journal System{% endblock %}
{% block content %}
<h1>Submissions Dashboard</h1>
{% if submissions %}
<table>
<thead>
<tr>
<th>Title</th>
<th>Submitted</th>
<th>Status</th>
<th>Avg Score</th>
<th>Reviews</th>
<th>PDF</th>
</tr>
</thead>
- Directly below that, paste the table body and closing tags:
<tbody>
{% for sub in submissions %}
<tr>
<td><a href="{{ url_for('submission', id=sub['id']) }}">{{ sub['title'] }}</a></td>
<td>{{ sub['submitted_at'] }}</td>
<td><span class="badge badge-{{ sub['status']|lower|replace(' ', '-') }}">{{ sub['status'] }}</span></td>
<td>{{ '%.1f'|format(sub['avg_score']) if sub['avg_score'] else 'N/A' }}</td>
<td>{{ sub['review_count'] }}</td>
<td>
{% if sub['filename'] %}
<a href="{{ url_for('download_file', filename=sub['filename']) }}">Download</a>
{% else %}
No file
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p>No submissions yet. <a href="{{ url_for('submit') }}">Submit a paper</a> to get started.</p>
{% endif %}
{% endblock %}
What changed in this template?
Two new columns (Avg Score, Reviews) show the aggregated data from your updated query.
The status cell now wraps the text in a <span> with a class like badge-pending or badge-under-review. These class names will map to colored styles once you add CSS later in this step.
- Save both app.py and templates/index.html.
- Refresh http://127.0.0.1:5000 in your browser.
You should see the table now has Avg Score and Reviews columns. Papers with reviews show a numeric score. Papers without reviews show N/A and a count of 0.
Seeing an error instead of the table?
Check that your query in app.py uses s.id and r.score (with the table aliases), not bare column names like id or score.
Also confirm that templates/index.html references sub['avg_score'] and sub['review_count'] exactly as shown above.
help me debug my index route query
Add accept/reject decision buttons
An editor needs to finalize whether a reviewed paper is accepted or rejected. You will add a new route to handle that decision and update the submission detail template to show the buttons.
- In app.py, find the download_file function at the bottom of the file. Add this new route directly above it:
@app.route('/submission/<int:id>/decide', methods=['POST'])
def decide(id):
decision = request.form['decision']
db = get_db()
db.execute(
'UPDATE submission SET status = ? WHERE id = ?',
(decision, id)
)
db.commit()
flash(f'Paper {decision.lower()}.')
return redirect(url_for('submission', id=id))
What does this route do?
The form will send either "Accepted" or "Rejected" as the decision value. This route writes that value directly into the submission's status column, then redirects back to the submission detail page.
Now add the decision buttons to the submission detail template.
- In templates/submission.html, find this line near the top:
{% if submission['filename'] %}
<p><a href="{{ url_for('download_file', filename=submission['filename']) }}">Download PDF</a></p>
{% endif %}
- Add the following decision form directly below that {% endif %} block:
{% if submission['status'] == 'Under Review' %}
<h2>Editorial Decision</h2>
<form method="POST" action="{{ url_for('decide', id=submission['id']) }}" class="decision-form">
<button type="submit" name="decision" value="Accepted" class="btn-accept">Accept</button>
<button type="submit" name="decision" value="Rejected" class="btn-reject">Reject</button>
</form>
{% endif %}
Why check for 'Under Review' status?
The decision buttons only appear when a submission has at least one review (status is "Under Review"). A paper with status "Pending" has not been reviewed yet, so offering Accept/Reject would be premature.
- Save both app.py and templates/submission.html.
- Refresh your browser and click on a submission that you previously reviewed.
Do you expect to see the Accept and Reject buttons? If this submission's status is "Under Review" from your earlier review, the buttons should appear below the PDF download link.
- Click Accept to accept the paper.
The page reloads and shows a flash message confirming the decision. The decision buttons disappear because the status is no longer "Under Review".
Don't see the Accept/Reject buttons?
The buttons only appear when the submission status is exactly "Under Review". If you have not submitted a review for this paper yet, go to the review form at the bottom of the page, submit a review, then reload.
Also confirm the new decide route in app.py is placed above the download_file route, not nested inside another function.
help me debug my editorial decision buttons
✔️ Awesome, I've got everything!
Great. Double check you have saved both app.py and templates/submission.html.
ⓧ I'd like to double check the full code
Your app.py is over 30 lines so scroll up and confirm you have both changes: the updated index() query with the LEFT JOIN, and the new decide() route. For templates/submission.html, confirm the decision form block sits between the PDF download {% endif %} and the Reviews heading.
Style the dashboard with CSS
The dashboard works but looks like unstyled HTML. A stylesheet will add layout structure, color-coded status badges, and polished form elements.
- Create a new folder called static inside your journal-system folder.
- Inside static/, create a new file called style.css.
- Paste the following page layout styles into static/style.css:
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
line-height: 1.6;
color: #333;
max-width: 900px;
margin: 0 auto;
padding: 20px;
}
nav {
display: flex;
gap: 20px;
padding: 15px 0;
border-bottom: 2px solid #eee;
margin-bottom: 30px;
}
- Add the navigation link and heading styles below your existing code:
nav a {
text-decoration: none;
color: #2563eb;
font-weight: 500;
}
nav a:hover {
text-decoration: underline;
}
h1 {
margin-bottom: 20px;
color: #1e293b;
}
h2 {
margin-top: 30px;
margin-bottom: 15px;
color: #334155;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 15px;
}
- Add the table cell and form layout styles below:
th, td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #e2e8f0;
}
th {
background-color: #f8fafc;
font-weight: 600;
}
a {
color: #2563eb;
}
form {
max-width: 600px;
}
label {
display: block;
margin-top: 15px;
margin-bottom: 5px;
font-weight: 500;
}
- Save static/style.css and continue adding below. Next, paste the input and button styles:
input[type="text"],
textarea,
select {
width: 100%;
padding: 10px;
border: 1px solid #d1d5db;
border-radius: 6px;
font-size: 14px;
}
textarea {
resize: vertical;
}
input[type="file"] {
margin-top: 5px;
}
button {
margin-top: 20px;
padding: 10px 20px;
background-color: #2563eb;
color: white;
border: none;
border-radius: 6px;
font-size: 14px;
cursor: pointer;
}
- Add the button hover state and all status badge styles:
button:hover {
background-color: #1d4ed8;
}
.badge {
display: inline-block;
padding: 3px 10px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
}
.badge-pending {
background-color: #fef3c7;
color: #92400e;
}
.badge-under-review {
background-color: #dbeafe;
color: #1e40af;
}
.badge-accepted {
background-color: #d1fae5;
color: #065f46;
}
- Add the rejected badge, flash messages, and review card styles:
.badge-rejected {
background-color: #fee2e2;
color: #991b1b;
}
.flash {
background-color: #eff6ff;
border: 1px solid #bfdbfe;
padding: 12px;
border-radius: 6px;
margin-bottom: 20px;
}
.flash p {
color: #1e40af;
}
.meta {
color: #64748b;
font-size: 14px;
}
.review-card {
background-color: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 15px;
margin-bottom: 12px;
}
- Finally, add the decision form button styles at the bottom:
.decision-form {
display: flex;
gap: 10px;
}
.btn-accept {
background-color: #059669;
}
.btn-accept:hover {
background-color: #047857;
}
.btn-reject {
background-color: #dc2626;
}
.btn-reject:hover {
background-color: #b91c1c;
}
What do the badge classes do?
Each .badge-pending, .badge-under-review, .badge-accepted, and .badge-rejected class assigns a different background and text color. The template generates these class names automatically from the submission status using Jinja2 filters.
The .btn-accept and .btn-reject classes override the default blue button with green and red to visually signal the editorial decision.
- Save static/style.css.
The stylesheet exists but your templates do not load it yet. You need to link it in the base template.
- In templates/base.html, find this line:
<title>{% block title %}Journal System{% endblock %}</title>
</head>
- Add the stylesheet link between the <title> line and the closing </head> tag, so it looks like this:
<title>{% block title %}Journal System{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
- Save templates/base.html.
- Refresh http://127.0.0.1:5000 in your browser.
Before you look, predict what the home page will show now. Do you expect color-coded badges? A styled table?
You should see a clean, centered layout with a navigation bar at the top, a table with column headers on a light gray background, and color-coded status badges. "Accepted" papers show a green badge, "Under Review" shows blue, and "Pending" shows amber.
Page still looks unstyled?
Make sure the static folder is at the same level as app.py (not inside templates/). Flask serves static files from a folder named exactly static by default.
Check your browser's developer tools (right-click the page, click Inspect, then the Console tab). If you see a 404 for style.css, the file path or folder name is wrong.
help me debug why my Flask CSS stylesheet is not loading
✔️ Awesome, I've got everything!
Great. Confirm you have saved all files and your dashboard looks styled with colored badges.
ⓧ I'd like to double check the full code
Your static/style.css file is long. Scroll up and confirm you have all seven sections in order: page layout, navigation links and headings, table cells and form layout, inputs and buttons, badge styles, flash and review card styles, and decision form buttons.
For templates/base.html, confirm this line appears inside the <head> section:
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
Your journal system now has a complete editorial workflow. Papers go from Pending to Under Review when scored, then to Accepted or Rejected when an editor decides. The dashboard gives you a clear picture of every submission at a glance. Next up, you will take on the Secret Mission and add search and filtering to handle a growing list of papers.
Secret mission
Add Search and Filtering
Your dashboard lists every submission in one long scroll. As papers pile up, finding a specific one becomes a chore. Can you add search and status filtering so editors can zero in on what they need?
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally, so there are no ongoing costs.
Resources you used:
- The journal-system project folder on your Desktop (contains all source code, templates, and static files).
- The .venv virtual environment with Flask installed (inside journal-system).
- The instance/journal.sqlite database file (inside journal-system).
- Uploaded PDF files in the uploads/ folder (inside journal-system).
Keep everything running
No action needed. Choose this if you are still actively building or want to continue experimenting with your journal system.
- The Flask development server only runs when you start it manually. It does not consume resources in the background.
- Your database and uploaded files remain on disk, ready for next time.
- To start the app again later, open the terminal in your journal-system folder, activate the virtual environment, and run flask --app app run --debug.
Pause - I'll come back to this later
Shut down the running Flask server to free up your terminal. All files stay on disk so you can resume later.
- Switch to the terminal where Flask is running.
- Press Ctrl+C to stop the development server.
- Deactivate the virtual environment by running:
deactivate
Your project folder, database, and uploads remain exactly as you left them. To resume, reactivate the virtual environment and start the server again.
Delete - I don't want to use this again
Remove all project resources and start fresh. This permanently deletes your code, database, and uploaded files.
- Switch to the terminal where Flask is running.
- Press Ctrl+C to stop the development server.
- Deactivate the virtual environment by running:
deactivate
- Navigate up to the Desktop by running:
cd ~/Desktop
- Delete the entire project folder. On macOS or Linux, run:
rm -rf journal-system
- On Windows (Command Prompt), run:
rmdir /s /q journal-system
This removes all source code, templates, the virtual environment, the SQLite database, and any uploaded PDFs in one step.
Nice Work!
Nice Work!
You just built a complete journal submission and review system from scratch using Flask, SQLite, and Jinja2 templates. You can submit papers, review them, make editorial decisions, and watch the dashboard update in real time.
What you learned:
- Built a multi-page Flask web application with HTML forms that handle both text data and PDF file uploads.
- Stored and retrieved data using SQLite with parameterized queries and implemented a relational data model where submissions have many reviews.
- Created a full status workflow (Pending, Under Review, Accepted, Rejected) with color-coded badges and average review scores on a live dashboard.
- Secret Mission: Added dynamic search and filtering with safe SQL query construction that prevents injection while building WHERE clauses conditionally.
Ready to quiz yourself?