Giter VIP home page Giter VIP logo

atletico-crud's Introduction

Atletico Crud

Am I Responsive

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.

User Stories

  • 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.

UX

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.

Colour Scheme

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.

Coolors Colour Scheme

Typography

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'.

Mundial

Initial Visual ERD

Visual ERD

Final Visual ERD

Final Visual ERD

Wireframes

Country Wireframe

Wireframes Country

Leagues Wireframe

Wireframes League

Clubs Wireframe

Wireframes Club

Player Wireframe

Wireframes Player

Features

Existing Features

Navbar

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.

Navbar Large

Navbar Mobile

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

Logo

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.

Logo

{% 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 %}

Home Page

The home page allows users visiting the website to Register or to Log-In via clickable buttons. Home Page

Registration

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.

Registration Page

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

Log In

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.

Log In Page

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

Profile Page

The profile page is what users are directed to once logged-in or registered.

Profile Page

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

Log Out

The log-out page allows users to log-out of their profile. This is achieved via the code block below.

Log Out

@app.route("/logout")
def logout():
    # remove user from session cookie
    flash("Log Out Successful!")
    session.pop("user")
    return redirect(url_for("login"))

Flash Messages

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.

Flash Messages

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

Countries Page

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.

Countries Page

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

Add Country Button (Admin Only)

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.

Add Country Button

{% 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 %}

Add Country Page (Admin Only)

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

Edit Country Button (Admin Only)

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.

Edit Country Button

{% 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 %}

Edit Country Page (Admin Only)

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)

Delete Country Button (Admin Only)

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.

Delete Country Button

{% 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 %}

Delete Country Modal (Admin Only)

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.

Delete Modal

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

Discussion On Page Features

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.

Players Page

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.

Mongo DB Players Collection

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

Add Player Page (Admin Only)

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.

Edit Player Page (Admin Only)

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)

Delete Player Modal (Admin Only)

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

Form Page

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

Confirmation Page

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.

Footer

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.

Footer

Favicon

The favicon is an icon of the blue star which used to be the sponsor on Newcastle United's kit in the 90s.

Favicon

Features Left to Implement

Improved Admin Functionality

A toggle for username's with admin rights instead of relying on a username.

Search Functionality

The ability to search for a specific league, club or player.

Improved Route Mapping

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.

Enter Full Record Functionality

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.

Further Prevention Of Duplicate Player Entires

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.

Technologies Used

  • 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.

Set-Up

To view set-up documentation (such as import steps etc.), please refer to SETUP.md.

Testing

To view all testing documentation, please refer to TESTING.md.

Deployment

The live deployed application can be found at atletico-crud.

Heroku Deployment

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.

Heroku App Set-Up

  • From the new app Settings, click Reveal Config Vars, and set the following key/value pairs:
    • IP 0.0.0.0
    • PORT 5000
    • MONGO_URI (insert your own MongoDB URI key here). To get the MONGO_URI, follow the steps outlined below.

Click on the cluster created for the project.

Mongo URI Step One

Click on the Connect button.

Mongo URI Step Two

Click Connect Your Application.

Mongo URI Step Three

Copy the connection string and ensure to replace <password> with your own password.

Mongo URI Step Four

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). The MONGO_DBNAME is the name given to the database created within the cluster on the MongoDB website as shown in the screenshot below.

MongoDB Name

  • 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.

Heroku Postgres Add-On

  • 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.

Set Config Vars

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

Freeze requirements.txt

The Procfile can be created with the following command: echo web: python app.py > Procfile

Create Procfile

For Heroku deployment, follow these steps to connect your GitHub repository to the newly created app:

Either:

  • Connect Heroku and GitHub.

Connect Heroku and GitHub

  • Then select "Automatic Deployment" from the Heroku app.

Enable Auto Deployment

  • Click the Deploy Branch button.

App Successfully Deployed

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, and push to GitHub, you can now type: git push heroku main

The frontend terminal should now be connected and deployed to Heroku.

Local Deployment

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.

Open in Gitpod

Difference Between Local App and Heroku Deployed App

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.

Credits

Content

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.

Media

The images used for country flags, league logos, club badges and players are summarised in the table below.

Country League Club Player
England Premier League Newcastle Alan Shearer
Gary Speed
Liverpool Mohamed Salah
Kenny Dalglish
Germany Bundesliga Borussia Dortmund Robert Lewandowski
Erling Haaland
Werder Bremen Claudio Pizarro
Miroslav Klose
Spain La Liga Atletico Madrid Antoine Griezmann
Fernando Torres
Real Betis Joaquín
Denílson
Italy Serie A Fiorentina Gabriel Batistuta
Dušan Vlahovic
Roma Daniele De Rossi
Francesco Totti

Acknowledgements

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.

atletico-crud's People

Contributors

sniclasj avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.