Atletico Crud is an online soccer/football database.
The database contains players who are renowned as having 'legend status' for a particular club, in a particular league within a particular country e.g. Alan Shearer who played for Newcastle United in the Premier League in England.
The aim of the website is for users to view players who have already been inducted to the database and for them to submit their own suggestions for players to be entered into the database via a Form.
The website's admin will then review the submitted forms and decide whether to create a new entry. The admin has the ability to create/add, read/view, update/edit and delete data.
- As a user, I want to be able to register as a user on the website.
- As a user, I want to be able to log in and log out from the website.
- As a user, I want to be able to read/view countries, leagues, clubs and players that have already been inducted into the database.
- As a user, I want to be able to submit a form to suggest a player that should be added to the database.
- As a user, I want to be able to receive confirmation that my form has been sent.
- As a user, I want to be able to navigate to view all countries, leagues, clubs and players.
- As a user I want to be able to view the website across multiple devices and screen sizes.
- As an admin, I want to be able to create/add data to the database.
- As an admin, I want to be able to read/view data on the database.
- As an admin, I want to be able to updtae/edit data within the database.
- As an admin, I want to be able to delete data from the database.
- As an admin, I want to receive confirmation that actions involving creating, editing or deleting have been successful or unsuccessful.
- As an admin I want to be able to create/add, read/view, update/edit and delete content on the website across multiple devices and screen sizes.
The aim of the site is to be as minimal as possible so that attention is focused on the database content.
Once registered or logged in, users are taken their profile page. From here, the ever present navigation bar allows the user to navigate to Countries, Leagues, Clubs or Players. When utilising the links in the navbar directly e.g. clicking Clubs, the user will be directed to a view showing all clubs across all leagues present in the database. However, if the player were to navigate to Leagues first before selecting Premier League, the club's visible would be filitered so that only Premier League clubs are displayed. This gives the user the felxibility to look at what they are most interested in viewing.
Users can also navigate to a Form page to submit their suggestions for players who they believe should be inducted into the database.
The website utilises Materialize CSS to responsively react to different screen sizes so that the user can view content on multiple devices.
I selected the dark-cyan and white colour scheme as they contrasted nicely with each other e.g. white text on a dark-cyan background or vice versa. The dark-cyan also didn't clash with any country flag, league logo or club crest/kit and was a more interesting colour than a dark grey or black.
The colour scheme below was generated by coolors.
The font used for the logo and the page headings is Monoton from Google Fonts. I chose this font as it reminded me of the font utilised by the football/soccer magazine 'Mundial'.
The navbar is always present at the top of the page for large screens and as a collapsible side navbar on smaller screen devices. The links visible depend on the user session status. For example, if no user is logged in, the only links available are Home, Log In and Register. Similarly, the Form link is only visible when logged in as a non-admin user and is hidden when logged in as an admin. This variation in navbar display is achieved via the code snippet below.
<ul class="left hide-on-med-and-down">
{% if session.user %}
<li><a href="{{ url_for('profile', username=session['user']) }}">Profile</a></li>
<li><a href="{{ url_for('countries') }}">Countries</a></li>
<li><a href="{{ url_for('leagues', country_id=0) }}">Leagues</a></li>
<li><a href="{{ url_for('clubs', league_id=0) }}">Clubs</a></li>
<li><a href="{{ url_for('playersa', club_id=0) }}">Players</a></li>
{% if session.user != "admin"|lower %}
<li><a href="{{ url_for('form') }}">Form</a></li>
{% endif %}
<li><a href="{{ url_for('logout') }}">Log Out</a></li>
{% else %}
<li><a href="{{ url_for('home') }}">Home</a></li>
<li><a href="{{ url_for('login') }}">Log In</a></li>
<li><a href="{{ url_for('register') }}">Register</a></li>
{% endif %}
</ul>
The logo is always visible at the top of the page for large and small screen devices. The logo will link to the profile page if a user is logged in and will link to the home page if no user is logged in. The code snippet below demonstrates how this is achieved.
{% if session.user %}
<a href="{{ url_for('profile', username=session['user']) }}" class="brand-logo right main-title">
Atletico Crud
</a>
{% else %}
<a href="{{ url_for('home') }}" class="brand-logo right main-title">Atletico Crud</a>
{% endif %}
The home page allows users visiting the website to Register or to Log-In via clickable buttons.
The registration page allows users to create a username and password to gain access to the site. It will not allow for duplicate usernames which is achieved via the code block below. There is also a link at the bottom of the registration page to take an user to the log-in page in the event that they have already created an account.
@app.route("/register", methods=["GET", "POST"])
def register():
if request.method == "POST":
# check if username already exists in db
existing_user = Users.query.filter(
Users.user_name == request.form.get("username").lower()).all()
if existing_user:
flash("Username already exists")
return redirect(url_for("register"))
user = Users(
user_name=request.form.get("username").lower(),
password=generate_password_hash(request.form.get("password"))
)
db.session.add(user)
db.session.commit()
# put the new user into 'session' cookie
session["user"] = request.form.get("username").lower()
flash("Registration Successful!")
return redirect(url_for("profile", username=session["user"]))
return render_template("register.html")
The log-in page allows users to log in using their username and password to gain access to the site. It will alert the user if their credentials are incorrect via the code block below. There is also a link at the bottom of the log-in page to take an user to the registration page in the event that they need to create an account.
@app.route("/login", methods=["GET", "POST"])
def login():
if request.method == "POST":
# check if username exists in db
existing_user = Users.query.filter(
Users.user_name == request.form.get("username").lower()).all()
if existing_user:
print(request.form.get("username"))
# ensure hashed password matches user input
if check_password_hash(
existing_user[0].password, request.form.get("password")):
session["user"] = request.form.get("username").lower()
flash("Welcome, {}".format(
request.form.get("username")))
return redirect(url_for(
"profile", username=session["user"]))
else:
# invalid password match
flash("Incorrect Username and/or Password")
return redirect(url_for("login"))
else:
# username doesn't exist
flash("Incorrect Username and/or Password")
return redirect(url_for("login"))
return render_template("login.html")
The profile page is what users are directed to once logged-in or registered.
@app.route("/profile/<username>", methods=["GET", "POST"])
def profile(username):
if "user" in session:
return render_template("profile.html", username=session["user"])
return redirect(url_for("login"))
The log-out page allows users to log-out of their profile. This is achieved via the code block below.
@app.route("/logout")
def logout():
# remove user from session cookie
flash("Log Out Successful!")
session.pop("user")
return redirect(url_for("login"))
Flash messages pop up on the page when users complete actions such as logging in, logging out and entry of incorrect credentials. Additionally, when the user is an admin, the flash messages confirm actions such as successfully adding, editing or deleting data. These flash messages can be seen in the code snippets above and the format of these messages is set in the file base.html which extends to other pages via template inheritance as shown in the code snippet below.
<section>
<!-- flash messages -->
{% with messages = get_flashed_messages() %}
{% if messages %}
{% for message in messages %}
<div class="row flashes">
<h4 class="cyan-text text-darken-4 center-align">{{ message }}</h4>
</div>
{% endfor %}
{% endif %}
{% endwith %}
</section>
The countries page displays flags for the countries currently available in the database. When logged in as an admin, the 'Edit' and 'Delete' buttons are visible however these are hidden when logged in as a non-admin user. Clicking on the Country name below the flag image will take the user to the leagues associated with this particular country in the database e.g. Clicking England will display the English league called Premier League.
<div class="row">
{% for country in countries %}
<div class="col s12 m6 l3 card-style">
<div class="card">
<div class="card-image">
<img src="{{ country.country_image_url }}" alt="{{ country.country_name }} Flag">
</div>
<div class="card-action">
<a class="card-text-style" href="{{ url_for('leagues', country_id=country.id ) }}">{{ country.country_name }}</a><br>
{% if session.user|lower == "admin"|lower %}
<a href="{{ url_for('edit_country', country_id=country.id) }}" class="btn-small green">Edit</a>
<a href="#modal-{{ country.id }}" class="btn-small red modal-trigger">Delete</a>
{% endif %}
<!-- Modal to confirm delete -->
<div id="modal-{{ country.id }}" class="modal">
<div class="modal-content">
<h4>Are You Sure?</h4>
<p>Warning! Deleting cannot be undone!</p>
</div>
<div class="modal-footer">
<a href="{{ url_for('delete_country', country_id=country.id) }}" class="btn red">Delete</a>
<a href="{{ url_for('countries') }}" class="modal-close btn green">Cancel</a>
</div>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
class Country(db.Model):
# schema for the Country model
id = db.Column(db.Integer, primary_key=True)
country_name = db.Column(db.String(25), unique=True, nullable=False)
country_image_url = db.Column(db.String(250), unique=True, nullable=True)
leagues = db.relationship(
"League", backref="country", cascade="all, delete", lazy=True)
def __repr__(self):
# __repr__ to represent itself in the form of a string
return
f"#{self.country_name} - Country Image URL: {self.country_image_url}"
# Route for all countries
@app.route("/countries")
def countries():
countries = list(Country.query.order_by(Country.country_name).all())
return render_template("countries.html", countries=countries)
The Add Country button only appears on the countries page if the user logged in is an admin. Clicking the button takes the admin user to the Add Country page which is discussed below.
{% if session.user|lower == "admin"|lower %}
<div class="row">
<div class="col s12 center-align">
<a href="{{ url_for('add_country') }}" class="btn-large cyan darken-4 white-text">
Add Country +
</a>
</div>
</div>
{% endif %}
{% endblock %}
The Add Country page allows the admin user to type in a country name to the form as well as an image url to represent that particular country. The code snippets below show the html form along with the python route. The python route for Add Country below in conjunction with the country model above contain code which prevents duplicate entries such as adding the same country name twice.
<div class="row">
<form class="col s12" method="POST" action="{{ url_for('add_country') }}">
<!-- Country Name -->
<div class="row">
<div class="input-field col s12">
<input placeholder="Input Country Name" name="country_name" id="country_name" minlength="3"
maxlength="25" type="text" class="validate" required>
<label for="country_name">Country Name</label>
</div>
</div>
<!-- Country Image URL -->
<div class="row">
<div class="input-field col s12">
<input placeholder="Input Country Image URL" name="country_image_url" id="country_image_url" minlength="3"
maxlength="250" type="text" class="validate" required>
<label for="country_image_url">Country Image URL</label>
</div>
</div>
<!-- Submit Button -->
<div class="row">
<div class="col s12 center-align">
<button type="submit" class="btn-large cyan darken-4">
Add Country +
</button>
</div>
</div>
</form>
</div>
# Route to add a country if admin
@app.route("/add_country", methods=["GET", "POST"])
def add_country():
if session["user"] != "admin":
return redirect(url_for("countries"))
if request.method == "POST":
existing_country = Country.query.filter(
func.lower(Country.country_name) == request.form.get(
"country_name").lower()).first()
if existing_country:
flash("Country Already Exists!")
return redirect(url_for("countries"))
exisitng_country_img = Country.query.filter(
func.lower(Country.country_image_url) == request.form.get(
"country_image_url").lower()).first()
if exisitng_country_img:
flash("Image Already in Use!")
return redirect(url_for("countries"))
country = Country(
country_name=request.form.get("country_name"),
country_image_url=request.form.get("country_image_url")
)
db.session.add(country)
db.session.commit()
flash("Country Successfully Added!")
return redirect(url_for("countries"))
return render_template("add_country.html")
The Edit Country button only appears on the countries page if the user logged in is an admin. Clicking the button takes the admin user to the Edit Country page which is discussed below.
{% if session.user|lower == "admin"|lower %}
<a href="{{ url_for('edit_country', country_id=country.id) }}" class="btn-small green">Edit</a>
<a href="#modal-{{ country.id }}" class="btn-small red modal-trigger">Delete</a>
{% endif %}
The Edit Country page allows the admin user to edit the details for a country such as the country name and the image url to represent that particular country. The code snippets below show the html form along with the python route. The python route for Edit Country below in conjunction with the country model above contain code which prevents duplicate entries such as editing an existing country to be the same as another country which already exists in the database.
<div class="row">
<form class="col s12" method="POST" action="{{ url_for('edit_country', country_id=country.id) }}">
<!-- Country Name -->
<div class="row">
<div class="input-field col s12">
<input placeholder="Input Country Name" name="country_name" value="{{ country.country_name}}"
id="country_name" minlength="3" maxlength="25" type="text" class="validate" required>
<label for="country_name">Country Name</label>
</div>
</div>
<!-- Country Image URL -->
<div class="row">
<div class="input-field col s12">
<input placeholder="Input Country Image URL" name="country_image_url" value="{{ country.country_image_url}}"
id="country_image_url" minlength="3" maxlength="250" type="text" class="validate" required>
<label for="country_image_url">Country Image URL</label>
</div>
</div>
<!-- Submit Button -->
<div class="row">
<div class="col s12 center-align">
<button type="submit" class="btn-large cyan darken-4">
Edit Country +
</button>
</div>
</div>
</form>
</div>
# Route to edit country if admin
@app.route("/edit_country/<int:country_id>", methods=["GET", "POST"])
def edit_country(country_id):
if session["user"] != "admin":
return redirect(url_for("leagues", country_id=0))
country = Country.query.get_or_404(country_id)
if request.method == "POST":
existing_country = Country.query.filter(
func.lower(Country.country_name) == request.form.get(
"country_name").lower()).first()
if existing_country:
flash("Country Already Exists!")
return redirect(url_for("countries"))
exisitng_country_img = Country.query.filter(
func.lower(Country.country_image_url) == request.form.get(
"country_image_url").lower()).first()
if exisitng_country_img:
flash("Image Already in Use!")
return redirect(url_for("countries"))
country.country_name = request.form.get("country_name")
country.country_image_url = request.form.get("country_image_url")
db.session.commit()
flash("Country Successfully Updated!")
return redirect(url_for("countries"))
return render_template("edit_country.html", country=country)
The Delete Country button only appears on the countries page if the user logged in is an admin. Clicking the button initiates a modal which pops up asking the user if they definitely wish to proceed with the deletion which is discussed in the next section.
{% if session.user|lower == "admin"|lower %}
<a href="{{ url_for('edit_country', country_id=country.id) }}" class="btn-small green">Edit</a>
<a href="#modal-{{ country.id }}" class="btn-small red modal-trigger">Delete</a>
{% endif %}
The modal pop-up allows a user to confirm the deletion by clicking Delete or to cancel the deletion by clicking Cancel. Clicking Cancel takes the user back to the countries page.
<!-- Modal to confirm delete -->
<div id="modal-{{ country.id }}" class="modal">
<div class="modal-content">
<h4>Are You Sure?</h4>
<p>Warning! Deleting cannot be undone!</p>
</div>
<div class="modal-footer">
<a href="{{ url_for('delete_country', country_id=country.id) }}" class="btn red">Delete</a>
<a href="{{ url_for('countries') }}" class="modal-close btn green">Cancel</a>
</div>
</div>
If the user proceeds with confirming the deletion, the python route for Delete Country is shown below. Although the Country model is a Postgres model, the delete route below also contains code for the deletion of data from MongoDB. The reason for this is that whilst Country, League and Club are all stored in Postgres, the data for Players is stored in MongoDB. The Postgres models have been set up so that the deletion of a table e.g. Country cascades to any tables linked to it as per the models.
For example, the country England, has a League (Premier League) which in turn has two Clubs (Newcastle and Liverpool). The Clubs Newcastle and Liverpool have two players respectively. If the Country England was deleted, the League (Premier League) and the Clubs (Newcastle and Liverpool) would also be deleted as per due to the relationship set-up between the Postgres tables in the models. As the MongoDB Players table is not linked to the Postgres tables via the Postgres models, the Players associated with any Leagues or Clubs within the Country of England would not be deleted without the inclusion of the MongoDB line of code in the Delete Country route highlighted below.
# Route to delete country if admin
@app.route("/delete_country/<int:country_id>")
def delete_country(country_id):
if session["user"] != "admin":
return redirect(url_for("countries"))
else:
country = Country.query.get_or_404(country_id)
mongo.db.players.delete_many({"country_id": (country_id)})
db.session.delete(country)
db.session.commit()
flash("Country Successfully Deleted!")
return redirect(url_for("countries"))
The features discussed above for Countries are applicable for the following:
- Leagues
- Add League
- Edit League
- Delete League
- Clubs
- Add Club
- Edit Club
- Delete Club
- Players
- Add Player
- Edit Player
- Delete Player
The only difference is in the routes for Leagues, Clubs and Players with regards to filtered and unfiltered views. This is shown in the code and explained below.
# Route to display all leagues if country_id == 0
# Otherwise route displays leagues for a specific country_id
@app.route("/leagues/<int:country_id>")
def leagues(country_id):
if country_id == 0:
leagues = list(League.query.order_by(League.league_name).all())
return render_template("leagues.html", leagues=leagues)
else:
leagues = list(League.query.order_by(League.id).filter(
League.country_id == country_id))
return render_template("leagues.html", leagues=leagues)
The code above shows that if Leagues is clicked via the navbar, the value of html <int:country_id>
is equal to 0 and it will therefore return a view of ALL Leagues across ALL Countries. However, if the user clicks on England, the value of html <int:country_id>
is equal to 1 and therefore the Leagues displayed are filtered for Leagues associated with that specific Country.
This logic also applies to the Clubs and Players routes e.g. if Clubs is clicked via the navbar, the value of html <int:league_id>
is equal to 0 and it will therefore return a view of ALL Clubs across ALL Leagues. However, if the user clicks on Premier League, the value of html <int:league_id>
is equal to 1 and therefore the Clubs displayed are filtered for Clubs associated with that specific League.
The data for Players is stored in a MongoDB collection called players. Due to this, the code in the Players route is different to Countries, Leagues and Clubs as shown below. A screenshot of the Mongo DB collection and an example of a document entry is also shown below.
# Route to display all players if league_id == 0
# Otherwise route displays players for a specific league_id
@app.route("/playersa/<club_id>")
def playersa(club_id):
if club_id == "0":
playersa = mongo.db.players.find()
return render_template("playersa.html", playersa=playersa)
else:
playersa = mongo.db.players.find({"club_id": club_id})
return render_template("playersa.html", playersa=playersa)
The data for Players is stored in a MongoDB collection called players. Due to this, the code in the Add Player route is different to Add Country, Add League and Add Club as shown below.
# Route to add player if admin
@app.route("/add_playera", methods=["GET", "POST"])
def add_playera():
if session["user"] != "admin":
return redirect(url_for("playersa", club_id=0))
else:
if request.method == "POST":
new_playera = request.form.get("player_image_url")
if mongo.db.players.count_documents(
{"image_url": new_playera}, limit=1) == 0:
club = Club.query.get_or_404(request.form.get("club_id"))
league = club.league_id
get_league = League.query.get_or_404(league)
country = get_league.country_id
playersa = {
"name": request.form.get("player_name"),
"dob": request.form.get("player_dob"),
"nationality": request.form.get(
"player_nationality"),
"position": request.form.get("player_position"),
"club_id": request.form.get("club_id"),
"league_id": league,
"country_id": country,
"image_url": request.form.get("player_image_url")
}
mongo.db.players.insert_one(playersa)
flash("Player Successfully Added!")
return redirect(url_for("playersa", club_id=club.id))
else:
flash("This Player Already Exists!")
return redirect(url_for("add_playera"))
clubs = list(Club.query.order_by(Club.club_name).all())
return render_template("add_playera.html", clubs=clubs)
Additionally, it was not possible to prevent duplicate player entries by preventing the addition of a player with the same name. The reason for this is that there may be numerous players with the same name and there may be multiple entries of the same player if they were deemed to be legends for multiple clubs. For this reason, the image_url field was used as a unique identifier to prevent duplicate entries which can be seen in the code snippet above. This is discussed further in the Features Left to Implement section.
The data for Players is stored in a MongoDB collection called players. Due to this, the code in the Edit Player route is different to Edit Country, Edit League and Edit Club as shown below.
# Route to edit player if admin
@app.route("/edit_playera/<player_id>", methods=["GET", "POST"])
def edit_playera(player_id):
if session["user"] != "admin":
return redirect(url_for("playersa", club_id=0))
else:
clubs = list(Club.query.order_by(Club.club_name).all())
player = mongo.db.players.find_one({"_id": ObjectId(player_id)})
if request.method == "POST":
submit = {
"name": request.form.get("player_name"),
"dob": request.form.get("player_dob"),
"nationality": request.form.get(
"player_nationality"),
"position": request.form.get("player_position"),
"club_id": request.form.get("club_id"),
"image_url": request.form.get("player_image_url")
}
mongo.db.players.update_one(
{"_id": ObjectId(player_id)}, {"$set": submit})
flash("Player Successfully Updated!")
return redirect(url_for("playersa", club_id=0))
return render_template("edit_playera.html", player=player, clubs=clubs)
The data for Players is stored in a MongoDB collection called players. Due to this, the code in the Delete Player route is different to Delete Country, Delete League and Delete Club as shown below.
# Route to delete player if admin
@app.route("/delete_playera/<player_id>")
def delete_playera(player_id):
if session["user"] != "admin":
return redirect(url_for("playersa", club_id=0))
else:
mongo.db.players.delete_one({"_id": ObjectId(player_id)})
flash("Player Successfully Deleted!")
return redirect(url_for("playersa", club_id=0))
The Form page is only visible in the navbar if the user is not an admin as discussed in the navbar section. The purpose of the form is for a non-admin user to submit a form to request that a player be added to the database. The form in the project is a dummy form and does not have a POST method as shown in the route code below.
# Route to form if user != admin
@app.route("/form")
def form():
if session["user"] == "admin":
return redirect(url_for("profile", username=session["user"]))
else:
return render_template("form.html")
The form's action directs the user to confirmation.html as shown below.
<h3 class="cyan-text text-darken-4 center-align">Player Submission Form</h3>
<div class="row">
<form class="col s12" action="{{ url_for('confirmation') }}">
The Confirmation page is only accessible if the user is not an admin as shown in the route below.
# Route to confirmation page after form submission
@app.route("/confirmation")
def confirmation():
if session["user"] == "admin":
return redirect(url_for("profile", username=session["user"]))
else:
return render_template("confirmation.html")
The user is directed to the confirmation page after submitting an entry via the form page.
The footer is very simple and is generated via Materialize CSS. It simply contains the name of the database and the name of the developer. The name of the developer acts as a hyperlink to the developer's GitHub profile which opens in a new tab.
The favicon is an icon of the blue star which used to be the sponsor on Newcastle United's kit in the 90s.
A toggle for username's with admin rights instead of relying on a username.
The ability to search for a specific league, club or player.
Route for clubs would be country/league/club instead of club/league_id. Route for players would be country/league/club/player instead of player/club_id.
A method for a full record to be added whereby a country, league, club and player could be added from one form with data being stored in the relevant tables.
I would like to create a method where multiple criteria are assessed to prevent duplicate player entries. For example, if the player name, club name and nationality were the same but the DOB was different, this would be allowed. However, if all fields were the same, this would not be allowed.
- Git - used for version control.
- GitHub - used to secure my code online.
- Gitpod - used as the cloud-based IDE.
- GitHub Projects - used to track project progress here.
- Flask - used to create the Python web application.
- Flask-SQLAlchemy - used as an extension to Flask to simplify use of SQLAlchemy.
- Flask Migrate - used to handle SQLAlchemy database migrations for the Flask application.
- PyMongo - used as the Python distribution containing tools for working with MongoDB.
- Flask-PyMongo - used to bridge Flask and PyMongo.
- SQLAlchemy - used for the relational database provider.
- MongoDB - used as the non-relational database provider.
- Heroku - used to deploy the site.
- amiresponsive - used for the mockup image.
- Materialize CSS - used to create the layout and structure of the website.
- HTML - used to write the code for the website.
- Python - used to write the code for the website.
- JavaScript - used to initialize Materialize CSS scripts.
- CSS - used to style the website.
- Error Handling - used to create custom error handling pages for error code 404 and 500.
To view set-up documentation (such as import steps etc.), please refer to SETUP.md.
To view all testing documentation, please refer to TESTING.md.
The live deployed application can be found at atletico-crud.
This project uses Heroku, a platform as a service (PaaS) that enables developers to build, run, and operate applications entirely in the cloud.
Deployment steps are as follows, after account setup:
- Select New in the top-right corner of your Heroku Dashboard, and select Create new app from the dropdown menu.
- Your app name must be unique, and then choose a region closest to you (EU or USA), and finally, select Create App.
- From the new app Settings, click Reveal Config Vars, and set the following key/value pairs:
IP
0.0.0.0PORT
5000MONGO_URI
(insert your own MongoDB URI key here). To get theMONGO_URI
, follow the steps outlined below.
Click on the cluster created for the project.
Click on the Connect button.
Click Connect Your Application.
Copy the connection string and ensure to replace <password>
with your own password.
Paste this string into the env.py file and Heroku config var as the value for the MONGO_URI key.
MONGO_DBNAME
(insert your own MongoDB DB Name key here). TheMONGO_DBNAME
is the name given to the database created within the cluster on the MongoDB website as shown in the screenshot below.
DATABASE_URL
(this comes from the Resources tab, you can get your own Postgres Database using the Free Hobby Tier)- Click on the Resources tab.
- Search for Postgres in the 'Add-ons' search bar.
- Select Heroku Postgres as shown in the screenshot below.
SECRET_KEY
(this can be any random secret key)
The below screenshot shows the completed config vars page on Heroku with sensitive information in the value boxes redacted.
Heroku needs two additional files in order to deploy properly.
- requirements.txt
- Procfile
You can install this project's requirements (where applicable) using: pip3 install -r requirements.txt
. If you have your own packages that have been installed, then the requirements file needs updated using: pip3 freeze --local > requirements.txt
The Procfile can be created with the following command: echo web: python app.py > Procfile
For Heroku deployment, follow these steps to connect your GitHub repository to the newly created app:
Either:
- Connect Heroku and GitHub.
- Then select "Automatic Deployment" from the Heroku app.
- Click the Deploy Branch button.
Or:
- In the Terminal/CLI, connect to Heroku using this command:
heroku login -i
- Set the remote for Heroku:
heroku git:remote -a <app_name>
(replace app_name with your app, without the angle-brackets) - After performing the standard Git
add
,commit
, andpush
to GitHub, you can now type:git push heroku main
The frontend terminal should now be connected and deployed to Heroku.
Gitpod IDE was used to write the code for this project.
To make a local copy of this repository, you can clone the project by typing the follow into your IDE terminal:
git clone https://github.com/sniclasj/atletico-crud.git
You can install this project's requirements (where applicable) using: pip3 install -r requirements.txt
.
Create an env.py
file, and add the following environment variables:
import os
os.environ.setdefault("IP", "0.0.0.0")
os.environ.setdefault("PORT", "5000")
os.environ.setdefault("MONGO_URI", "insert your own MongoDB URI key here")
os.environ.setdefault("MONGO_DBNAME", "insert your own MongoDB DB Name key here")
os.environ.setdefault("DATABASE_URL", "from your Hobby Tier on the Resources tab from Heroku")
os.environ.setdefault("SECRET_KEY", "this can be any random secret key")
Alternatively, if using Gitpod, you can click below to create your own workspace using this repository.
The only difference between the local app and the app deployed on Heroku is that the players are not 'wired up' to the clubs on the local app with the exception of Newcastle United. The reason for this is that players are linked to clubs via club_ids. The club_id for Atletico Madrid in the local app is 43 whereas the club_id for Atletico Madrid in the Heroku app is 7. Newcastle United has the club_id of 1 both in the local app and the app deployed on Heroku. The reason for this is that Newcastle United (the club I support) was the first club added to both databases (local and deployed). However, due to the number of iterative tests completed in the local version of the app, there are mismatches between local club_ids and Heroku deployed club_ids.
When clicking 'View Players' for Atletico Madrid in the local app, no players will display as no player has the club_id of 43 on MongoDB. When clicking 'View Players' for Atletico Madrid on the Heroku app, two players will display as two players have the club_id of 7 on MongoDB.
I utilised the Mini Project on the Code Institute LMS to help me create this project.
I utilised the Flask Migrate documentation to install and utilise Flask-Migrate.
I utilised Error Handling documentation to create customised error handling pages for error codes 404 and 500.
The images used for country flags, league logos, club badges and players are summarised in the table below.
I would like to thank my Code Institute mentor Tim Nelson for his support during the course of this project.
I would also like to thank the Code Institute tutor team for the assistance provided during the course of this project.