- Published on
Baseball Team Manager using Flask and MongoDB
- Authors
- Name
- Andrew Nicholas
Table of Contents
- Introduction
- Setting Up the Project
- Project Structure
- Configuration Files
- Create the Flask Application
- Create the HTML Templates
- Setting Up MongoDB
- Deploy to Vercel
Introduction
This application allows users to manage a baseball team, including adding, editing, and removing players. In this post, I will cover the key components and steps required to build it.
Technologies used include:
- HTML
- CSS
- Python
- Flask
- MongoDB
- Vercel
Setting Up the Project
Initial Setup
Install Python: Ensure you have Python installed. You can download it from the official website.
Create a Virtual Environment:
python -m venv venv
- Activate the Virtual Environment:
- On Windows:
.\venv\Scripts\Activate
- On Mac/Linux:
source venv/bin/Activate
- Install Flask and Other Dependencies:
pip install Flask flask-bcrypt flask-login Flask-WTF Flask-PyMongo
- Set Up Poetry for Dependency Management:
- Install Poetry:
curl -sSL https://install.python-poetry.org | python3 -
- Initialize Poetry:
poetry init
- Add Dependencies:
poetry add flask flask-bcrypt flask-login Flask-WTF flask-PyMongo
- Install Dependencies:
poetry install
Project Structure
Now set up the project structure. This can be done manually or by using the command line.
Create the Project Structure
- Create the project directory and navigate to it:
mkdir baseballteammanager
cd baseballteammanager
- Create the following directories and files:
mkdir templates static static/css
touch application.py baseball_manager.py index.py requirements.txt pyproject.toml
touch templates/index.html templates/add_player.html templates/edit_player.html templates/login.html templates/register.html
touch static/css/styles.css
The project structure should now look like this:
baseballteammanager/
application.py
baseball_manager.py
index.py
templates/
add_player.html
edit_player.html
index.html
login.html
register.html
static/
css/
styles.css
requirements.txt
pyproject.toml
Configuration Files
'pyproject.toml'
This file is used to define the project and its dependencies when using Poetry. Open 'pyproject.toml' and check to make sure nothing is missing:
[tool.poetry]
name = "baseballteammanager"
version = "0.1.0"
description = "A Baseball Team Manager application"
authors = ["Your Name <you@example.com>"]
[tool.poetry.dependencies]
python = "^3.8"
Flask = "^2.1.2"
flask-bcrypt = "^1.0.1"
flask-login = "^0.6.3"
Flask-WTF = "^1.2.1"
Flask-PyMongo = "^2.3.0"
WTForms = "^3.1.1"
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
'requirements.txt'
This lists the dependencies required by the project. Ensure it contains the following:
bcrypt==4.1.2
click==8.1.7
flask==2.1.2
flask-bcrypt==1.0.1
flask-login==0.6.3
flask-pymongo==2.3.0
flask-wtf==1.2.1
pymongo==4.6.1
Create the Flask Application
In this application, 'baseball_manager.py' serves as a utility module to separate the business logic from the application logic in 'application.py'. This is beneficial for maintainability and clarity.
Overview
'application.py'
- Flask app setup
- User and form classes
- Routes for managing players and user authentication
- Calls functions from 'baseball_manager.py' for handling player and user data
'baseball_manager.py'
- Functions to handle CRUD operations for players
- Functions for user management
- A helper function to calculate batting average
Creating the Flask Application
Set up the main application file ('application.py'):
from flask import Flask, render_template, request, redirect, url_for, flash
from flask_bcrypt import Bcrypt
from flask_login import LoginManager, login_user, current_user, login_required, logout_user
import baseball_manager
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField
from wtforms.validators import DataRequired, Length, EqualTo, ValidationError
from flask_pymongo import PyMongo
import os
from bson import ObjectId
import logging
logging.basicConfig(level=logging.DEBUG)
mongo = PyMongo()
class User:
def __init__(self, user_data):
self.user_data = user_data
def is_active(self):
return True
def is_authenticated(self):
return True
def is_anonymous(self):
return False
def get_id(self):
return str(self.user_data['_id'])
def create_app():
app = Flask(__name__)
app.config["MONGO_URI"] = os.environ.get('MONGODB_URI')
app.config["SECRET_KEY"] = os.environ.get('SECRET_KEY') or 'fgqAaLGwkP'
mongo.init_app(app)
bcrypt = Bcrypt(app)
login_manager = LoginManager(app)
login_manager.login_view = 'login'
class RegistrationForm(FlaskForm):
username = StringField('Username', validators=[DataRequired(), Length(min=2, max=20)])
password = PasswordField('Password', validators=[DataRequired()])
confirm_password = PasswordField('Confirm Password', validators=[DataRequired(), EqualTo('password')])
submit = SubmitField('Sign Up')
def validate_username(self, username):
users_collection = mongo.db.users
user = users_collection.find_one({'username': username.data})
if user:
raise ValidationError('That username is already taken. Please choose a different one.')
class LoginForm(FlaskForm):
username = StringField('Username', validators=[DataRequired(), Length(min=2, max=20)])
password = PasswordField('Password', validators=[DataRequired()])
submit = SubmitField('Login')
@app.route('/')
def index():
if current_user.is_authenticated:
user_id = current_user.get_id() # Use get_id() method to get the user's ID
logging.debug(f"Fetching lineup for user_id: {user_id}")
lineup = baseball_manager.get_players(mongo, user_id)
logging.debug(f"Lineup: {lineup}")
else:
lineup = [] # Empty list for unauthenticated users
return render_template('index.html', lineup=lineup, is_authenticated=current_user.is_authenticated)
@app.route('/add_player', methods=['GET', 'POST'])
def add_player_route():
error_message = None
if request.method == 'POST':
name = request.form['name']
position = request.form['position']
at_bats = request.form['at_bats']
hits = request.form['hits']
# Data validation
if not name or not position or not at_bats.isdigit() or not hits.isdigit():
error_message = "Invalid input. Please fill out all fields correctly."
elif position not in ['C', '1B', '2B', '3B', 'SS', 'LF', 'CF', 'RF', 'P']:
error_message = "Invalid position. Please enter a valid position."
if not error_message:
user_id = current_user.get_id()
baseball_manager.add_player(mongo, name, position, int(at_bats), int(hits), user_id)
return render_template('add_player.html', error=error_message)
@app.route('/remove_player/<string:player_id>')
def remove_player(player_id):
user_id = current_user.get_id()
baseball_manager.remove_player(mongo, ObjectId(player_id), user_id)
return redirect(url_for('index'))
@app.route('/edit_player/<string:player_id>', methods=['GET'])
@login_required
def edit_player(player_id):
player = baseball_manager.get_player(mongo, ObjectId(player_id))
if player is None:
return redirect(url_for('index')) # Redirect if player not found
return render_template('edit_player.html', player=player)
@app.route('/update_player', methods=['POST'])
@login_required
def update_player():
error_message = None
player_id = request.form.get('player_id')
player_id = ObjectId(player_id)
original_name = request.form.get('original_name')
name = request.form.get('name', '')
position = request.form.get('position', '')
at_bats = request.form.get('at_bats', '')
hits = request.form.get('hits', '')
player = baseball_manager.get_player(mongo, ObjectId(player_id))
if player:
if name:
player.name = name
if position:
player.position = position
if at_bats.isdigit():
player.at_bats = int(at_bats)
if hits.isdigit():
player.hits = int(hits)
player.avg = baseball_manager.get_batting_avg(player.at_bats, player.hits)
else:
error_message = "Player not found."
if error_message:
return render_template('edit_player.html', player=[original_name, position, at_bats, hits], error=error_message)
return redirect(url_for('index'))
@app.route("/register", methods=['GET', 'POST'])
def register():
form = RegistrationForm()
if form.validate_on_submit():
hashed_password = bcrypt.generate_password_hash(form.password.data).decode('utf-8')
user_data = {'username': form.username.data, 'password': hashed_password}
user_id = baseball_manager.add_user(mongo, user_data)
baseball_manager.clone_default_lineup_for_user(mongo, user_id)
return redirect(url_for('login'))
return render_template('register.html', title='Register', form=form)
@app.route("/login", methods=['GET', 'POST'])
def login():
form = LoginForm()
if form.validate_on_submit():
users_collection = mongo.db.users
user = users_collection.find_one({'username': form.username.data})
if user and bcrypt.check_password_hash(user['password'], form.password.data):
user_obj = User(user) # Wrap dict in User class
login_user(user_obj)
return redirect(url_for('index'))
else:
flash('Login Unsuccessful. Please check username and password', 'danger')
return render_template('login.html', title='Login', form=form)
@login_manager.user_loader
def load_user(user_id):
users_collection = mongo.db.users
user = users_collection.find_one({'_id': ObjectId(user_id)})
return User(user) if user else None
@app.route('/logout')
@login_required
def logout():
logout_user()
return redirect(url_for('index'))
if __name__ == '__main__':
app.run(debug=True)
return app
Explanation:
- Import Statements: Import necessary modules and initialize the MongoDB connection.
- User Class: Represents a user in the application.
- create_app Function: Sets up the Flask application, initializes extensions, and defines routes for handling requests.
- RegistrationForm and LoginForm: Define the forms for user registration and login.
- Routes: Define the endpoints for the application, including adding, editing, and removing players, as well as user registration, login, and logout.
Set up the business logic module ('baseball_manager.py'):
from bson import ObjectId
import logging
logging.basicConfig(level=logging.DEBUG)
# Define the default lineup
default_lineup = [
{"name": "Jordan Bell", "position": "P", "at_bats": 30, "hits": 5},
{"name": "John Doe", "position": "C", "at_bats": 50, "hits": 15},
{"name": "Jane Smith", "position": "1B", "at_bats": 70, "hits": 20},
{"name": "Mike Johnson", "position": "2B", "at_bats": 60, "hits": 18},
{"name": "Sarah Brown", "position": "3B", "at_bats": 55, "hits": 16},
{"name": "Alex Lee", "position": "SS", "at_bats": 65, "hits": 22},
{"name": "Chris Green", "position": "LF", "at_bats": 45, "hits": 12},
{"name": "Pat Morgan", "position": "CF", "at_bats": 80, "hits": 30},
{"name": "Taylor King", "position": "RF", "at_bats": 40, "hits": 10}
]
def get_batting_avg(at_bats, hits):
if at_bats == 0:
return 0
avg = hits / at_bats
return round(avg, 3)
def clone_default_lineup_for_user(mongo, user_id):
players_collection = mongo.db.players # Access the 'players' collection
for player_data in default_lineup:
player_data['user_id'] = user_id
player_data['avg'] = get_batting_avg(player_data["at_bats"], player_data["hits"])
logging.debug(f"Cloning player for user {user_id}: {player_data}")
players_collection.insert_one(player_data)
def get_players(mongo, user_id):
players_collection = mongo.db.players
return list(players_collection.find({'user_id': ObjectId(user_id)}))
def add_player(mongo, name, position, at_bats, hits, user_id):
players_collection = mongo.db.players
avg = get_batting_avg(at_bats, hits)
new_player = {
"name": name,
"position": position,
"at_bats": at_bats,
"hits": hits,
"avg": avg,
"user_id": user_id
}
players_collection.insert_one(new_player)
def update_player(mongo, player_id, name, position, at_bats, hits):
players_collection = mongo.db.players
avg = get_batting_avg(at_bats, hits)
players_collection.update_one(
{'_id': ObjectId(player_id)},
{'$set': {
"name": name,
"position": position,
"at_bats": at_bats,
"hits": hits,
"avg": avg
}}
)
def remove_player(mongo, player_id, user_id):
players_collection = mongo.db.players
players_collection.delete_one({'_id': player_id, 'user_id': user_id})
def get_player(mongo, player_id):
players_collection = mongo.db.players
return players_collection.find_one({'_id': player_id})
def add_user(mongo, user_data):
users_collection = mongo.db.users
new_user = users_collection.insert_one(user_data)
return new_user.inserted_id
def get_user(mongo, username):
users_collection = mongo.db.users
return users_collection.find_one({'username': username})
Explanation:
- Default Lineup: A predefined list of players to initialize for a new user.
- CRUD Functions: Functions for creating, reading, updating, and deleting player data.
- Helper Functions: Functions to calculate batting average and manage user data.
Create the WSGI entry point ('wsgi.py') in the root folder:
import sys, os
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
from application import create_app
app = create_app()
if __name__ == "__main__":
app.run()
Explanation:
- WSGI Configuration: Sets up the WSGI entry point for the application. This is necessary for deploying the application.
Create the index script ('index.py') in the root folder:
from wsgi import app
Explanation:
- Index Script: Simplifies Vercel deployment by importing the app from the WSGI configuration.
Create the HTML templates
'index.html':
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Baseball Team Manager</title>
<!-- Add Bootstrap for styling -->
<link
rel="stylesheet"
href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
/>
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}" />
</head>
<body>
<!-- Navigation Bar -->
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<a class="navbar-brand" href="{{ url_for('index') }}">Baseball Team Manager</a>
<button
class="navbar-toggler"
type="button"
data-toggle="collapse"
data-target="#navbarNav"
aria-controls="navbarNav"
aria-expanded="false"
aria-label="Toggle navigation"
>
<span class="navbar-toggler-icon"></span>
</button>
<div class="navbar-collapse collapse" id="navbarNav">
<ul class="navbar-nav ml-auto">
{% if is_authenticated %}
<li class="nav-item">
<a class="nav-link" href="#">{{ current_user.username }}</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('logout') }}">Logout</a>
</li>
{% else %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('login') }}">Login</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('register') }}">Register</a>
</li>
{% endif %}
</ul>
</div>
</nav>
<div class="container">
{% if is_authenticated %}
<p>Welcome, {{ current_user.username }}!</p>
<a href="{{ url_for('add_player_route') }}" class="btn btn-primary mb-3">Add Player</a>
<table class="table">
<thead>
<tr>
<th>Name</th>
<th>Pos</th>
<th>AB</th>
<th>H</th>
<th>AVG</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% if lineup %} {% if lineup|length > 0 %} {% for player in lineup %}
<tr>
<td>{{ player['name'] }}</td>
<td>{{ player['position'] }}</td>
<td>{{ player['at_bats'] }}</td>
<td>{{ player['hits'] }}</td>
<td>{{ player['avg'] }}</td>
<td>
<a href="{{ url_for('edit_player', player_id=player['_id']|string) }}">Edit</a>
|
<a href="{{ url_for('remove_player', player_id=player['_id']|string) }}">Remove</a>
</td>
</tr>
{% endfor %} {% else %}
<tr>
<td colspan="6">No players in your lineup.</td>
</tr>
{% endif %} {% endif %}
</tbody>
</table>
{% else %}
<p>
Welcome to Baseball Team Manager! Please
<a href="{{ url_for('login') }}">log in</a> or
<a href="{{ url_for('register') }}">register</a>.
</p>
{% endif %}
</div>
<!-- Bootstrap JS and dependencies -->
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"></script>
</body>
</html>
Explanation:
The 'index.html' template is the main page of the application. It includes a navigation bar with login and register options, or logout if the user is authenticated. It displays a table with the player's lineup if the user is logged in, or prompts the user to log in or register if not.
'add_player.html':
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Add Player</title>
<link
rel="stylesheet"
href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
/>
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}" />
</head>
<body>
<div class="container">
<h1>Add Player</h1>
<form method="post" action="{{ url_for('add_player_route') }}">
{% if error %}
<div class="alert alert-danger">{{ error }}</div>
{% endif %}
<div class="form-group">
<input
type="text"
name="name"
class="form-control"
placeholder="Player's Name"
required
/>
</div>
<div class="form-group">
<input
type="text"
name="position"
class="form-control"
placeholder="Position (e.g., C, 1B)"
required
/>
</div>
<div class="form-group">
<input type="number" name="at_bats" class="form-control" placeholder="At Bats" required />
</div>
<div class="form-group">
<input type="number" name="hits" class="form-control" placeholder="Hits" required />
</div>
<button type="submit" class="btn btn-primary">Add Player</button>
</form>
<a href="{{ url_for('index') }}" class="btn btn-secondary">Back to Lineup</a>
</div>
<!-- Bootstrap JS and dependencies -->
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"></script>
</body>
</html>
Explanation:
The 'add_player.html' template provides a form for adding a new player to the team. It includes input fields for the player's name, position, at-bats, and hits. If there are any errors in the form submission, an alert message will be displayed.
'edit_player.html':
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Edit Player</title>
<!-- Add Bootstrap for styling -->
<link
rel="stylesheet"
href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
/>
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}" />
</head>
<body>
<div class="container">
<h1>Edit Player</h1>
<form method="post" action="{{ url_for('update_player') }}">
{% if error %}
<div class="alert alert-danger">{{ error }}</div>
{% endif %}
<!-- Hidden input for player_id -->
<input type="hidden" name="player_id" value="{{ player._id }}" />
<!-- Original name, used for reference in case of changes -->
<input type="hidden" name="original_name" value="{{ player.name }}" />
<div class="form-group">
<label>Name</label>
<input type="text" name="name" class="form-control" value="{{ player.name }}" />
</div>
<div class="form-group">
<label>Position</label>
<input type="text" name="position" class="form-control" value="{{ player.position }}" />
</div>
<div class="form-group">
<label>At Bats</label>
<input type="number" name="at_bats" class="form-control" value="{{ player.at_bats }}" />
</div>
<div class="form-group">
<label>Hits</label>
<input type="number" name="hits" class="form-control" value="{{ player.hits }}" />
</div>
<button type="submit" class="btn btn-primary">Update Player</button>
</form>
<a href="{{ url_for('index') }}" class="btn btn-secondary">Back to Lineup</a>
</div>
</body>
</html>
Explanation:
The 'edit_player.html' template provides a form for editing an existing player's details. The form is pre-filled with the current details of the player, and includes hidden fields for the player ID and original name. If there are any errors, an alert message will be displayed.
'login.html':
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Login</title>
<link
rel="stylesheet"
href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
/>
</head>
<body>
<div class="container">
<h1>Login</h1>
<form method="post">
{{ form.hidden_tag() }}
<div class="form-group">
{{ form.username.label(class="form-control-label") }} {{
form.username(class="form-control") }} {% for error in form.username.errors %}
<span style="color: red">[{{ error }}]</span>
{% endfor %}
</div>
<div class="form-group">
{{ form.password.label(class="form-control-label") }} {{
form.password(class="form-control") }} {% for error in form.password.errors %}
<span style="color: red">[{{ error }}]</span>
{% endfor %}
</div>
<div class="form-group">{{ form.submit(class="btn btn-primary") }}</div>
</form>
</div>
</body>
</html>
Explanation:
The 'login.html' template provides a form for user login. It uses Flask-WTF to generate the form fields and handle validation. Errors will be displayed next to the respective fields if there are any validation issues.
'register.html':
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Register</title>
<link
rel="stylesheet"
href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
/>
</head>
<body>
<div class="container">
<h1>Register</h1>
<form method="post">
{{ form.hidden_tag() }}
<div class="form-group">
{{ form.username.label(class="form-control-label") }} {{
form.username(class="form-control") }} {% for error in form.username.errors %}
<span style="color: red">[{{ error }}]</span>
{% endfor %}
</div>
<div class="form-group">
{{ form.password.label(class="form-control-label") }} {{
form.password(class="form-control") }} {% for error in form.password.errors %}
<span style="color: red">[{{ error }}]</span>
{% endfor %}
</div>
<div class="form-group">
{{ form.confirm_password.label(class="form-control-label") }} {{
form.confirm_password(class="form-control") }} {% for error in
form.confirm_password.errors %}
<span style="color: red">[{{ error }}]</span>
{% endfor %}
</div>
<div class="form-group">{{ form.submit(class="btn btn-primary") }}</div>
</form>
</div>
</body>
</html>
Explanation:
The 'register.html' template provides a form for user registration. It includes fields for the username, password, and password confirmation, and uses Flask-WTF for validation. Errors are displayed next to the respective fields if there are any issues.
Setting Up MongoDB
Create a MongoDB Atlas Account: If you don't already have a MongoDB Atlas account, create one at mongodb.com
Create a Cluster: Follow the instructions on the Mongo website to create a new cluster. Choose the free tier for development purposes.
Create a Database User:
- Go to the Database Access section in the MongoDB Atlas dashboard.
- Click on "Add New Database User."
- Create a username and password for the database user. Ensure these credentials are stored securely, as they are needed to connect the Flask application to MongoDB.
- Get the Connection String:
- Go to the Clusters section in the MongoDB Atlas dashboard.
- Click on "Connect" for the relevant cluster.
- Choose "Connect your application."
- Copy the connection string provided.
- Modify the Connection String:
- Replace '<username>' and '<password>' in the connection string with the database user's credentials.
- Replace '<dbname>' with the name of the relevant database.
Example:
mongodb+srv://<username>:<password>@cluster0.mongodb.net/<dbname>?retryWrites=true&w=majority
Replace '<username>', '<password>', and '<dbname>' with the actual credentials and database name.
Deploy to Vercel
- Install Vercel CLI:
npm install -g vercel
- Log In to Vercel:
vercel login
- Create 'vercel.json' Configuration File in root folder:
touch vercel.json
Add the following configuration:
{
"version": 2,
"builds": [
{
"src": "./index.py",
"use": "@vercel/python"
}
],
"routes": [
{
"src": "/(.*)",
"dest": "/"
}
]
}
- Deploy the Project: Deploy the project to Vercel for the first time.
vercel
Follow the prompts to complete the initial deployment. Vercel will automatically detect the 'index.py' file and use it to start the Flask application.
- Set Up Environment Variables on Vercel:
- Go to the project's dashboard on vercel.com.
- Navigate to the "Settings" tab.
- Under "Environment Variables", add the following variables:
- 'MONGODB_URI': Your MongoDB connection string.
- 'SECRET_KEY': A secret key for Flask sessions. This can be genereated using Python:
Copy the generated key and use it as the 'SECRET_KEY'import os os.urandom(24).hex()
- Redeploy the Project: After adding the environment variables, redeploy the project to Vercel to apply the changes.
vercel --prod