mindmap
server)Web Server(
Program that responds to HTTP requests with HTML or Assets
Static Site
Hand Built #10003;
Coded one page at a time
Static Built #10003;
Build pages and layout into static pages
Dynamic
Build from templates and data on each request
Allows for the data to be changed
Authentication
Can be an API as well as HTML
SPA
Single Page Applications
Javascript heavy
Use APIs to get data
Assets
Images #10003;
CSS #10003;
Javascript #10003;
Data Sources
Text Files #10003;
API
Database
A simple Python Todo Web App to do some improvements on and be a starting point for your own simple web apps.
- Flask based Python Webserver with routing (a function for each url endpoint users can visit)
- HTML / Jinja templates for looping though and outputting data and keeping process and display seperate..
- todo.py contains the endpoints for the Todo app
- SQL Databases the modern way
- Managed by SQLAlchemy an ORM / Object Relationship Mapper which allows you to write classes that define the data and provides the storage & CRUD for you.
- ORMs build the database for you from your classes so you define what you want to store how it connects together and any extras calculations / functions you need .
- Start with SQLite but you can move to professional systems like PostgreSQL or others when you are ready.
- todo.py includes the Todo class that provides all you need for the building of the database and all the CRUD.
class Todo(db.Model):
__tablename__ = "todos"
id: Mapped[int] = mapped_column(primary_key=True, init=False)
task: Mapped[str] = mapped_column(db.String(200), nullable=False)
user_id: Mapped[str] = mapped_column(db.String(100), nullable=False)
done: Mapped[bool] = mapped_column(db.Boolean, default=False)@todo_bp.route('/')
def home():
user = get_current_user()
if not user:
return render_template('login.html')
session['user_id'] = user["id"]
todos = Todo.query.filter_by(user_id=session['user_id']).all()
return render_template('index.html', todos=todos, user=user)init_admin(app, db, Todo)Authentication is the act of proving who you are, in this system we use external authentication systems so we aren't storing usernames & passwords (reducing the DPA responsiblilties ). There are still some so we provide a privacy policy.
- GitHub OAuth (Flask-Dance) for local Windows development
- Auth0 OAuth for Codespaces and Render production
- Ready for Render deployment so you can publish and use the site online for free (there are some speed limitations)
- GitHub Actions CI/CD to build the site when you commit a working version
- Can be upgraded to use a free PostgreSQL database server (but there are some other steps)
- Login to github.com
- Go to the github repository https://github.com/UTCSheffield/python-flask-todo
- Click the green "Use this template" button at the top of the page
- Select "Create a new repository"
- Fill in your new repository details:
- Choose a repository name (e.g.,
python-flask-todo) - Add a description (optional)
- Choose Public or Private visibility
- Choose a repository name (e.g.,
- Click "Create repository from template"
- Your new repository will be created with all the template files
Using GitHub Desktop:
- On the GitHub page for your new repository
- Click the green "Code" button
- Click "Open with GitHub Desktop"
- You may need to login to GitHub Desktop if you haven't already
- You may be prompted to choose a local path to clone the repository to
- Click 'Open in Visual Studio Code' to open the project in VS Code
Split screen with the browser with README or LESSON open on one side and VS Code on the other.
py -m pip install -r requirements.txt # You'll need python3 ... in linuxIn VS Code open .env.example and save it as .env
![[SaveAs.png]]
You will need to set Save as type to "No Extension (*.)" which is at the bottom of the list
@ UTC Sheffield OLP, Mr Eggleton will give you the contents for the .env file that will work with our github setup,
Start the Flask development server:
py -m flask run --host=localhost --port=5000 # it maybe python3 on your machineThe app will be available at http://localhost:5000
Try it, login and create a few tasks!
This code uses SQLAlchemy to set up classes that have methods to talk to many databases. We use SQLite for simplicity and easy local development.
The database file is stored in /instance/todo.db
Hopefully Visual Code has promoted you to install the recommended extensions including the SQLite extension. and so it should appear in the left hand side explorer view with a red icon.
Have a look, can you see the tables and data?
Next your going to add a category system to organize todos. Each todo must belong to one category (like "Urgent" or "Non-urgent"). Users will select a category from a dropdown menu when creating a new todo. Administrators can add, edit, or delete categories through the admin interface at /admin/.
The system uses two database tables with a one-to-many relationship: one category can have many todos, but each todo belongs to exactly one category.
erDiagram
Category ||--o{ Todo : "has many"
Category {
int id PK
string name UK
}
Todo {
int id PK
string task
string user_id
int category_id FK
boolean done
}
Find the line that says db = SQLAlchemy(model_class=Base).
Just AFTER that line, add this new class:
class Category(db.Model):
__tablename__ = "categories"
id: Mapped[int] = mapped_column(primary_key=True, init=False)
name: Mapped[str] = mapped_column(db.String(50), nullable=False, unique=True)
def __repr__(self): # When you try to print or put this object in a template represent it as it's name
return self.name
Find the Todo class.
Add a new line after user_id to add the category field. And add a new function / method which will make todo.category return the Category object that is linked by the category_id Foreign Key:
class Todo(db.Model):
__tablename__ = "todos"
id: Mapped[int] = mapped_column(primary_key=True, init=False)
task: Mapped[str] = mapped_column(db.String(200), nullable=False)
user_id: Mapped[str] = mapped_column(db.String(100), nullable=False)
category_id: Mapped[int] = mapped_column(ForeignKey('categories.id'), nullable=False)
done: Mapped[bool] = mapped_column(db.Boolean, default=False)
@property # todo.category is a property (member variable) of the todo object
def category(self): # return the category object linked to this Todo by category_id
return Category.query.get(self.category_id)Find the home() function and change it to pass categories to the template:
New code:
@todo_bp.route('/')
def home():
user = get_current_user()
if not user:
return render_template('login.html')
session['user_id'] = user["id"]
todos = Todo.query.filter_by(user_id=session['user_id']).all()
categories = Category.query.all()
return render_template('index.html', todos=todos, categories=categories, user=user)Find the add() function and change it to capture the category:
New code:
@todo_bp.route('/add', methods=['POST'])
def add():
if 'user_id' not in session:
return redirect('/')
task_text = request.form['task']
category_id = request.form.get('category_id', type=int)
if not category_id:
return redirect('/')
new_task = Todo(task=task_text, category_id=category_id, user_id=session['user_id'])
db.session.add(new_task)
db.session.commit()
return redirect('/')Find the init_app() function at the bottom of todo.py. Add code to seed the initial categories:
New code:
def init_app(app):
db.init_app(app)
with app.app_context():
db.create_all()
# Seed initial categories if they don't exist
if Category.query.count() == 0:
urgent = Category(name="Urgent")
non_urgent = Category(name="Non-urgent")
db.session.add(urgent)
db.session.add(non_urgent)
db.session.commit()
if Todo.query.count() == 0:
mreggleton_check = Todo(task="Mr Eggleton checking your Todo App!", done=False, user_id="github|5987806", category_id=non_urgent.id)
db.session.add(mreggleton_check)
db.session.commit()Find the form in index.html:
New code:
<form method="POST" action="/add">
<input type="text" name="task" placeholder="Enter task" required>
<select name="category_id" required>
<option value="">Select category</option>
{% for category in categories %}
<option value="{{ category.id }}">{{ category.name }}</option>
{% endfor %}
</select>
<button type="submit">Add</button>
</form>Find the task text being printed out and add the category next to it:
New code:
{{ todo.task }} [{{ todo.category }}]Find this line near the top of app.py:
from todo import todo_bp, init_app as init_todo
from todo import db, TodoChange it to:
from todo import todo_bp, init_app as init_todo
from todo import db, Todo, CategoryThen find this line near the bottom:
init_admin(app, db, Todo)Change it to:
init_admin(app, db, Todo, Category)Find the init_admin() function in admin.py:
New code:
def init_admin(app, db, todo_model, category_model):
"""Attach Babel and register secured admin views for the given models."""
Babel(app, locale_selector=lambda: 'en')
admin = Admin(app, name="Admin", template_mode="bootstrap4",
index_view=AuthenticatedAdminIndexView())
admin.add_view(AuthenticatedModelView(todo_model, db.session,
endpoint="todo_admin",
name="Todos"))
admin.add_view(AuthenticatedModelView(category_model, db.session,
endpoint="category_admin",
name="Categories"))
return adminBecause you've changed the database structure, you need to delete the old database:
- Stop your Flask app if it's running (press Ctrl+C in the terminal)
- Delete the database file from the instance folder.
Restart your Flask app:
py -m flask run --host=localhost --port=5000 # it maybe python3 on your machineThe app will create a new database with the "Urgent" and "Non-urgent" categories automatically. Test it on http://localhost:5000/
- Go to the home page - you should see a dropdown to select a category when adding a task
- Add a task with a category selected
- Log in and go to http://localhost:5000/admin/ to see the Categories section where you can add, edit, or delete categories
- Stop your Flask app if it's running (press Ctrl+C in the terminal)
Find the imports at the top, the last one should be
from admin import init_admin add the following line
from api import api_bpAnd just after app.register_blueprint(todo_bp) add
app.register_blueprint(api_bp)Restart your Flask app:
py -m flask run --host=localhost --port=5000 # it maybe python3 on your machineLog in and go to http://localhost:5000/api/ for api documentation and a testing environment
- Created a new
Categorymodel - Added a foreign key relationship from
TodotoCategory - Updated the form to include a category dropdown
- Modified the add route to capture the selected category
- Added automatic seeding of initial categories
- Enabled category management in the admin interface
- Added Full REST API with swagger documentation
- Persistent records in a database. The current database will be destroyed each time you push to render, ( You can modify the code once it's on Render to move to PostgreSQL ).
- Changing database structure SQLAlchemy Migrations. Currently we aren't handling changes to the database structure so you need to delete the local .db and start again (render wil do this anyway on a rebuild as mentioned above). They can be handled with Migrations
- Minimal Autorisation all Authenticated users can do everything on the site.
- Storing any user data in a database (other than an id from github or Auth0 ). To have users on this system to store any other PII refer to https://flask-dance.readthedocs.io/en/latest/storages.html#sqlalchemy and change the privacy statement.
- Testing. There are no tests in this code, although Flask, SQL Alchemy and the other libraries used are thoroughly tested and are checked for security issues.
Then what could you make with the same ideas but different entities (things)?
Books and People could make a library etc ....