Introduction

Our backend was based on a Flask server with a SQLAlchemy database storing the user's data.

What is Flask?

Python Flask is a web framework that allows developers to build web applications using Python. It is lightweight, flexible, and easy to use, making it popular for creating small to medium-sized web applications or APIs (Application Programming Interfaces). Flask follows the Model-View-Controller (MVC) architectural pattern, but it's known for its simplicity and minimalistic approach.

Flask provides the basic tools and features needed to develop web applications, but it doesn't impose a specific structure or design pattern. This flexibility allows developers to choose how they organize their code and design their application. Flask's core philosophy is to keep things simple and allow developers to add additional functionality as needed through third-party extensions.

With Flask, developers can define routes to handle different URLs and HTTP methods, allowing developers to define the behavior of their application. It also includes a template engine, Jinja2, which enables developers to create dynamic HTML pages. Flask supports various extensions for database integration, form handling, user authentication, and more, making it highly customizable and adaptable to different project requirements.

Overall, Flask is a popular choice for Python web development due to its simplicity, flexibility, and large community support. It allows developers to quickly build web applications with minimal overhead and provides a solid foundation for expanding functionality as needed.

What is SQLAlchemy?

SQLAlchemy is a powerful and popular Object-Relational Mapping (ORM) library for Python. It provides a high-level, intuitive interface for interacting with relational databases, making it easier to work with database records using Python objects and queries.

The main goal of SQLAlchemy is to bridge the gap between the object-oriented world of Python and the relational world of databases. It allows developers to define database models as Python classes, where each class represents a table in the database and its attributes represent the columns. SQLAlchemy then handles the translation of these Python objects into SQL statements, such as SELECT, INSERT, UPDATE, and DELETE, to interact with the database.

Overall, SQLAlchemy is widely used in Python web development and other database-driven applications. It provides a robust and efficient way to interact with databases using Python, abstracting away much of the complexities of database management and allowing developers to focus on their application logic.

Initiation

The __init__.py file defines all things needed for our project.

Module Importation

  • Flask from flask is the main module for creating and running a Flask web Application
  • LoginManager from flask_login provides with uder session management and authentication features.
    • It was originally intended for a login functionality, but was not implemented.
  • SQLAlchemy from flask_sqlalchemy us an ORM (Object-Relational Mapping) library for Flask enabling interaction with databases using python objects
  • Migrate from flask_migrate provides databast migration functionality for Flask applications
from flask import Flask
from flask_login import LoginManager
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate

Object Definition and Flask Application Configuration

  • The Flask object app was created by passing __name__ (a builtin parameter) as the argument
  • The SQLAlchemy object db was created by initializing the SQLALchemy class
  • SQLALCHEMY_TRACK_MODIFICATIONS is set to False to disable modification tracking for SQLAlchemy.
  • SQLALCHEMY_DATABASE_URI is set to a SQLite database URI (sqlite:///volumes/sqlite.db) to specify the database's location within the backend.
  • SECRET_KEY is set to 'SECRET_KEY', which is used for cryptographic purposes in Flask applications.
  • A migration object is created by calling Migrate and passing the Flask application instance (app) and the SQLAlchemy object (db).
  • MAX_CONTENT_LENGTH is set to 5 1024 1024, which represents the maximum size (in bytes) allowed for uploaded content.
  • UPLOAD_EXTENSIONS lists the supported file types, including .jpg, .png, and .gif.
  • UPLOAD_FOLDER is set to 'volumes/uploads/', specifying the location where user-uploaded content will be stored.
"""
These object can be used throughout project.
1.) Objects from this file can be included in many blueprints
2.) Isolating these object definitions avoids duplication and circular dependencies
"""

# Setup of key Flask object (app)
app = Flask(__name__)
# Setup SQLAlchemy object and properties for the database (db)
dbURI = 'sqlite:///volumes/sqlite.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['SQLALCHEMY_DATABASE_URI'] = dbURI
app.config['SECRET_KEY'] = 'SECRET_KEY'
db = SQLAlchemy()
Migrate(app, db)

# Images storage
app.config['MAX_CONTENT_LENGTH'] = 5 * 1024 * 1024  # maximum size of uploaded content
app.config['UPLOAD_EXTENSIONS'] = ['.jpg', '.png', '.gif']  # supported file types
app.config['UPLOAD_FOLDER'] = 'volumes/uploads/'  # location of user uploaded content

Model

Our Project uses the file leaders1.py as the model for defining a class called LeaderUser that represents our user object to create our leaderboards.

Module Importation

  • json for working with json data.
  • datetime and date from datetime to record the date that the user made their record.
  • app and db defined from before for usage in Flask application and SQLALchemy database.
import json
import datetime
from datetime import date
from __init__ import app, db
from sqlalchemy.exc import IntegrityError

Class Definition

  • The LeaderUser class inherits from db.Model, making it a database model based on SQLALchemy.
  • The class has a __tablename__ attribute set to 'userLeaderboard', specifying the table name in the database.
  • The class defines several instance variables (id, _name, _score, _locations, _tot_distance, _calc_distance, _dateG) that correspond to columns in the database table.
  • The __init__ method is the constructor that initializes the instance variables of a LeaderUser object.
  • There are getter and setter methods (@property and corresponding @(attribute name).setter decorators) for each instance variable, allowing access and modification of the object's attributes.
    • The dateG getter method, in particular, converts the _dateG attribute to a formatted string.
  • The __str__ method returns the object's content in readable form as JSON string using json.dumps().
  • The CRUD (Create, Read, Update, Delete) methods (create, read, update, delete) is provided for interacting with the database.
    • create() method is responsible for creating and adding a new record to the database table. It tries to add LeaderUser object into the database via a commit of a session.
      • If the operation encounters an IntegrityError (e.g., due to violating unique constraints), it rolls back the session and returns None.
      • Otherwise, it returns the self object (the newly created LeaderUser).
    • read() method converts the LeaderUser object (self) into a dictionary containing its attributes. It returns the dictionary representation of the object, which is used for API response generation.
    • update() method allows modifying attributes to the existing LeaderUser Object. If any of the optional parameters are different from their default values, the function change the corresponding attributes of the LeaderUser object and commits the changes.
    • delete() method removes the LeaderUser object from the database.
class LeaderUser(db.Model):
    __tablename__ = 'userLeaderboard'  # table name is plural, class name is singular

    # Define the User schema with "vars" from object
    id = db.Column(db.Integer, primary_key=True)
    _name = db.Column(db.String(255), unique=False, nullable=False)
    _score = db.Column(db.Integer, unique=False, nullable=False)
    _locations = db.Column(db.JSON, unique=False, nullable=False)
    _tot_distance = db.Column(db.Integer, unique=False, nullable=False)
    _calc_distance = db.Column(db.Integer, unique=False, nullable=False)
    _dateG = db.Column(db.Date)


    # constructor of a User object, initializes the instance variables within object (self)
    def __init__(self, name, score, locations, tot_distance, calc_distance, dateG=date.today()):
        self._name = name    # variables with self prefix become part of the object, 
        self._score = score
        self._locations = locations
        self._tot_distance = tot_distance
        self._calc_distance = calc_distance
        self._dateG = dateG


    # a name getter method, extracts name from object
    @property
    def name(self):
        return self._name
    
    # a setter function, allows name to be updated after initial object creation
    @name.setter
    def name(self, name):
        self._name = name
    
    # a getter method, extracts score from object
    @property
    def score(self):
        return self._score
    
    # a setter function, allows score to be updated after initial object creation
    @score.setter
    def score(self, score):
        self._score = score
    
    # a getter method, extracts locations from object
    @property
    def locations(self):
        return self._locations
    
    # a setter function, allows locations to be updated after initial object creation
    @locations.setter
    def locations(self, locations):
        self._locations = locations

    # a getter method, extracts tot_distance from object
    @property
    def tot_distance(self):
        return self._tot_distance
    
    # a setter function, allows tot_distance to be updated after initial object creation
    @tot_distance.setter
    def tot_distance(self, tot_distance):
        self._tot_distance = tot_distance

    # a getter method, extracts cal_distance from object
    @property
    def calc_distance(self):
        return self._tot_distance
    
    # a setter function, allows calc_distance to be updated after initial object creation
    @calc_distance.setter
    def tot_distance(self, tot_distance):
        self._tot_distance = tot_distance
    
    # a getter method, extracts dateG from object
    @property
    def dateG(self):
        dateG_string = self._dateG.strftime('%m-%d-%Y')
        return dateG_string
    
    # a setter function, allows dateG to be updated after initial object creation
    @dateG.setter
    def dateG(self, dateG):
        self._dateG = dateG
    
   
    # output content using str(object) in human readable form, uses getter
    # output content using json dumps, this is ready for API response
    def __str__(self):
        return json.dumps(self.read())

    # CRUD create/add a new record to the table
    # returns self or None on error
    def create(self):
        try:
            # creates a person object from User(db.Model) class, passes initializers
            db.session.add(self)  # add prepares to persist person object to Users table
            db.session.commit()  # SqlAlchemy "unit of work pattern" requires a manual commit
            return self
        except IntegrityError:
            db.session.remove()
            return None

    # CRUD read converts self to dictionary
    # returns dictionary
    def read(self):
        return {
            "id": self.id,
            "name": self.name,
            "score": self.score,
            "locations": self.locations,
            "tot_distance": self.tot_distance,
            "calc_distance": self.calc_distance,
            "dateG": self.dateG
        }

    # CRUD update: updates user name, password, phone
    # returns self
    def update(self, name="", score=0,locations="",tot_distance=0,calc_distance=0):
        """only updates values with length"""
        if len(name) > 0:
            self.name = name
        if score > 0:
            self.score = score
        if len(locations) > 0:
            self.locations = locations
        if tot_distance > 0:
            self.tot_distance = tot_distance
        if calc_distance > 0:
            self.calc_distance = calc_distance
        
        db.session.commit()
        return self

    # CRUD delete: remove self
    # None
    def delete(self):
        db.session.delete(self)
        db.session.commit()
        return None

Initialization and Testing

  • app and db objects are used to create the database tables within the initLeaderUsers function.
  • An instance of LeaderUser called score1 is used to test the database.
  • The previously defined create method is used to add it to the database.
  • Any IntegrityError is caught and handled by removing th esession and printing an error message.
def initLeaderUsers():
    with app.app_context():
        """Create database and tables"""
        db.create_all()
        """Tester data for table"""
        score1 = LeaderUser(name='Chester', score = 100, locations={"list":["Balboa Park"]},tot_distance=150,calc_distance=100,dateG=datetime.date(2023,5,29))

        leaders = [score1]

        for leader in leaders:
            try:
                leader.create()
            except IntegrityError:
                db.session.remove()
                print(f"error try again later")

API

The leaderboard1.py file sets up a Flask REST API for managing a leaderboard of users.

Module Importation

  • json provides functionalities for working with JSON data.
  • Blueprint from flask is used to organize related routes and views into modular components.
  • request from flask provides an object that represents HTTP request sent by the client and allows the response to such requests.
  • jsonify from flask can create a JSON file from a Python dictionary or object.
  • Api from flask_restful is used to define API endpoints and handle different HTTP methods.
  • Resource from flask_restful provides a unit of your API that handles specific HTTP methods for a given URL path.
  • datetime provide classes representing dates and times, it is use for working in dates in the leaderboard data.
  • flask_cors is used for enabling Cross-Origin Resource Sharing (CORS) in Flask applications, which allows web pages to make requests to a different domain than the one serving the page.
  • LeaderUser is our previously object from model.leaders1; it is used to interact with the database.
import json
from flask import Blueprint, request, jsonify
from flask_restful import Api, Resource # used for REST API building
from datetime import *
from flask_cors import cross_origin
from model.leaders1 import LeaderUser

API Initiation

  • A Flask blueprint named leaderboard with a prefix of (base url)/api/LeaderboardUser is created
  • An instance of Api called leaders_api is created, which is associated with the leaderboard blueprint.
leaderboard = Blueprint('leaderUser_api', __name__,
                   url_prefix='/api/leaderboardUser')

# API docs https://flask-restful.readthedocs.io/en/latest/api.html
leaders_api = Api(leaderboard)

API Definition

  • LeaderBoardAPI is created, which contains several nested classes that represent different API endpoints and their corresponding HTTP methods
    • LeaderBoardAPI.AddScore represents the endpoint for creating a new score in the leaderboard. It handles the HTTP POST method. It extracts the necessary data from the request's JSON body, validates it, creates a new LeaderUser object, and adds it to the leaderboard by calling the create() method. If successful, it returns the created user's information in JSON format.
    • LeaderBoardAPI.LeaderGet represents the endpoint for retrieving all users' information from the leaderboard. It handles the HTTP GET method. It retrieves all LeaderUser objects from the database and converts them into JSON format.
    • LeaderBoardAPI.LeaderUpdate represents the endpoint for updating a user's score in the leaderboard. It handles the HTTP PUT method. It retrieves the necessary data from the request's JSON body, finds the corresponding LeaderUser object based on the provided name, updates the user's score, total distance, and calculated distance, and returns the updated user's information in JSON format.
    • LeaderBoardAPI.LeaderDelete represents the endpoint for deleting a user from the leaderboard. It handles the HTTP DELETE method. It retrieves the user's ID from the request's JSON body, finds the corresponding LeaderUser object, deletes it from the database, and returns a message confirming the deletion.
    • LeaderBoardAPI.Search represents the endpoint for searching for a user in the leaderboard based on their name. It handles the HTTP POST method. It retrieves the name from the request's JSON body, queries the leaderboard for users with the matching name, and returns their information in JSON format.
    • LeaderBoardAPI.GetUsersHighestScore represents the endpoint for retrieving the users with the highest scores from the leaderboard. It handles the HTTP GET method. It retrieves all LeaderUser objects from the database, orders them by score in descending order, and returns their information in JSON format.
    • Each endpoint is added to the leaders_api instance, specifying the class and the url for each class, which is added to the original url (base url)/api/LeaderboardUser.
      • /addScore for AddScore, the POST method
      • /get for LeaderGet, the GET method
      • /update for LeaderUpdate, the UPDATE method
      • /delete for LeaderDelete, the DELETE method
      • /getSearch for the Search
      • /getMaxScore for GetUserHighestScore
class LeaderBoardAPI:        
    class AddScore(Resource):  # User API operation for Create, Read.  THe Update, Delete methods need to be implemeented
        def post(self): # Create method
            ''' Read data for json body '''
            body = request.get_json()
            
            ''' Avoid garbage in, error checking '''
            # validate name
            name = body.get('name')
            if name is None or len(name) < 1:
                return {'message': f'Name missing or too short'}, 400
            
            # validate uid
            score = float(body.get('score'))
            if score is None or score <= 0:
                return {'message': f'No username or score is too low'}, 400
            
            locations = body.get('locations')
            if locations is None or len(locations) <= 0:
                return {'message': f'No username or score is too low'}, 400
            
            tot_distance = float(body.get('tot_distance'))
            if tot_distance is None or tot_distance <= 0:
                return {'message': f'Total distance by user is nonexistnet or unrealistic'}, 400
            
            calc_distance = float(body.get('calc_distance'))
            if calc_distance is None or calc_distance <= 0:
                return {'message': f'Calculated distance is nonexistent or unrealistic'}, 400
            
            dateG = body.get('date')

            ''' #1: Key code block, setup USER OBJECT '''
            userMade = LeaderUser(name=name, score=score, locations=locations, tot_distance=tot_distance, calc_distance=calc_distance)

            # Checks if date of score exists, reformats it to mm-dd-yyyy
            if dateG is not None:
                try:
                    userMade.dateG = datetime.strptime(dateG, '%m-%d-%Y').date()
                except:
                    return {'message': f'Date obtained has a format error {dateG}, must be mm-dd-yyyy'}, 210
            
            user = userMade.create()
            # success returns json of user
            if user:
                return jsonify(user.read())
            # failure returns error
            return {'message': f'Processed {name}, either a format error or User ID {score} is duplicate'}, 400

    class LeaderGet(Resource):
        def get(self): # Read Method
            leaders = LeaderUser.query.all()    # read/extract all users from database
            json_ready = [leader.read() for leader in leaders]  # prepare output in json
            return jsonify(json_ready)  # jsonify creates Flask response object, more specific to APIs than json.dumps

    # PUT method updates data in the API
    class LeaderUpdate(Resource):
        # def put(self) does the PUT method
        def put(self):
            # Gets the data from postman or frontend
            body = request.get_json()

            # Gets the username
            name = body.get('name')

            # Gets the score, score is going to be updated
            score = body.get('score')

            tot_distance = float(body.get('tot_distance'))
            
            calc_distance = float(body.get('calc_distance'))

            # Gets the user through the username
            userUpdating = LeaderUser.query.filter_by(_name = name).first()
            if userUpdating:
                # Updates the score for the user
                userUpdating.update(score = score)
                userUpdating.update(tot_distance = tot_distance)
                userUpdating.update(calc_distance = calc_distance)
                # Returns a dictionary to confirm that the score was updated
                return jsonify(userUpdating.read())
            else:
                # Error message if update fails
                return {'message': f'{name} not found'}, 210

    # Delete method deletes data in the API
    class LeaderDelete(Resource):
        # def delete(self) does the DELETE method
        def delete(self):
            # Gets the data from postman or frontend
            body = request.get_json() 
 
            # Gets the ID
            getID = body.get('id')

            # Gets the user through the ID
            leaderDeleting = LeaderUser.query.get(getID)
            if leaderDeleting:
                # Deletes the user according to its ID number
                leaderDeleting.delete()
                return {'message': f'Profile #{getID} deleted'}, 210
            else:
                # Error message if delete fails
                return {'message': f'Profile #{getID} not found'}, 210


    class Search(Resource):
        def post(self):
            body = request.get_json()
            name = body.get('name')
            if name is None or len(name) < 1:
                return {'message': f'Name missing or too short'}, 400
            
            leaders = LeaderUser.query.order_by(LeaderUser._score.desc()).all()
            user_scores = [leader.read() for leader in leaders if leader._name == name]
            if len(user_scores) > 0:
                return jsonify(user_scores)
            else: 
                return {'message': f'No user found with name {name}'}, 400
            
    class GetUsersHighestScore(Resource):
        def get(self):
            leaders = LeaderUser.query.order_by(LeaderUser._score.desc()).all()
            json_ready = [leader.read() for leader in leaders]
            return jsonify(json_ready)
    
    leaders_api.add_resource(AddScore, '/addscore')
    leaders_api.add_resource(LeaderGet, '/get')
    leaders_api.add_resource(LeaderUpdate, '/update')
    leaders_api.add_resource(LeaderDelete, '/delete')
    leaders_api.add_resource(Search, '/getSearch')
    leaders_api.add_resource(GetUsersHighestScore, '/getMaxScore')

Main

The main.py file completes the process, sets up routes, registers blueprints and initializes components.

Importing Modules

  • render_template from flask can be used to render HTML templates by taking a template file as input and returns the rendered HTML content.
  • CORS from flask_cors provides Cross-Origin Resource Sharing (CORS) support for Flask applications. It allows the application to handle requests from different origins.
  • app and db from __init__.py is used once again, with app representing the Flask Application instance and db representing the SQLAlchemy database instance.
  • initLeaderUsers from model.leaders1 is a function used to initialize the database data.
  • leaderboard from api.leaderboard1 is a Flask blueprint that defines the API routes and endpoints related to the leaderboard.
import threading

# import "packages" from flask
from flask import render_template  # import render_template from "public" flask libraries
from flask_cors import CORS
# import "packages" from "this" project
from __init__ import app,db  # Definitions initialization
# from model.jokes import initJokes
# from model.users import initLeaders
# from model.players import initPlayers
from model.leaders1 import initLeaderUsers
# from model.logins import initLogins

# setup APIs
# from api.covid import covid_api # Blueprint import api definition
# from api.joke import joke_api # Blueprint import api definition
# from api.user import user_api # Blueprint import api definition
# from api.login import login_api
from api.leaderboard1 import leaderboard

SQLAlchemy Initialization

  • The leaderboard blueprint is registered with app.register_blueprint().
  • This connects the routes to the handlers so that appropriate handlers are invoked when the url is accessed.
  • The activate_job function runs before the first request, which calls initLeaderUsers() to setup the database.
app.register_blueprint(leaderboard)
app.register_blueprint(app_projects) # ignore this line

@app.errorhandler(404)  # catch for URL not found
def page_not_found(e):
    # note that we set the 404 status explicitly
    return render_template('404.html'), 404

@app.route('/')  # connects default URL to index() function
def index():
    return render_template("index.html")

@app.route('/table/')  # connects /stub/ URL to stub() function
def table():
    return render_template("table.html")

@app.before_first_request
def activate_job():  # activate these items 
    # initJokes()
    initLeaderUsers()
    # initPlayers()
    # initLogins

Running the Server

  • An if __name__ == "__main__": block to ensure that the application is only run when the script is executed directly.
  • The Flask application runs to specify debug, host, and port settings.
if __name__ == "__main__":
    # change name for testing
    CORS(app)
    app.run(debug=True, host="0.0.0.0", port="8086")