Backends Documentation
The documentation of our backend project
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.
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
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.
- 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.
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")
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
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')
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
if __name__ == "__main__":
# change name for testing
CORS(app)
app.run(debug=True, host="0.0.0.0", port="8086")