From 1374f36a6a2f1aeb29bb5227ad599b56d0296a1c Mon Sep 17 00:00:00 2001
From: Mr Eggleton
Date: Tue, 13 Jan 2026 10:06:00 +0000
Subject: [PATCH 01/25] tidy whitespace
---
templates/layout.html | 3 ---
1 file changed, 3 deletions(-)
diff --git a/templates/layout.html b/templates/layout.html
index ffdf242..2a093c7 100644
--- a/templates/layout.html
+++ b/templates/layout.html
@@ -17,10 +17,8 @@ {{ site.WebsiteName }}
{% else %}
Login
{% endif %}
-
-
@@ -34,7 +32,6 @@ {{ site.WebsiteName }}
{% endblock %}
From 6279ba13bd38e13642bdd252fc1dc3d74e9931a0 Mon Sep 17 00:00:00 2001
From: Mr Eggleton
Date: Tue, 13 Jan 2026 10:16:51 +0000
Subject: [PATCH 02/25] Enhance privacy policy for Auth0 integration and
clarify data collection practices
---
templates/privacy.html | 89 ++++++++++++++++++++++++++++++++----------
1 file changed, 69 insertions(+), 20 deletions(-)
diff --git a/templates/privacy.html b/templates/privacy.html
index 106520c..4d60147 100644
--- a/templates/privacy.html
+++ b/templates/privacy.html
@@ -23,26 +23,45 @@ 2. Who We Are (Data Controller)
3. The Information We Collect
-We collect very limited personal information
- when you log in using your GitHub or Google account:
+{% if auth_provider == "Auth0" %}
+We collect limited personal information when you log in using Auth0, our third-party authentication provider:
-
- Google/GitHub User
- ID: When you log in using the third-party authentication service (Google or
- GitHub), we retrieve your user name and ID number and store only your unique user ID number. This is a
- persistent identifier provided
- by the third party.
+ User ID: When you log in using Auth0 (which supports authentication via Google, GitHub,
+ or other identity providers), we receive and store your unique user ID from Auth0. This is a persistent
+ identifier that allows us to recognize you on subsequent visits.
+
+ -
+ Email Address: We receive your email address from Auth0 to identify your account.
+
+ -
+ Name: We may receive your name from Auth0 as provided by your chosen identity provider.
-
Session Data: We use a session cookie to maintain your logged-in status. This cookie
- itself stores a secure, random string (session ID) that links back to your user ID & name on our server. The
+ stores a secure, random string (session ID) that links back to your user information on our server. The
+ data is only stored for the duration of your visit (session).
+
+
+We do not collect additional personal details such as location, browsing history,
+ or any other information beyond what is necessary for authentication and service provision.
+{% else %}
+We collect very limited personal information when you log in using your GitHub account:
+
+ -
+ GitHub User ID: When you log in using GitHub authentication, we retrieve your
+ user name and ID number and store only your unique user ID number. This is a persistent identifier
+ provided by GitHub.
+
+ -
+ Session Data: We use a session cookie to maintain your logged-in status. This cookie
+ stores a secure, random string (session ID) that links back to your user ID & name on our server. The
data is only stored for the duration of your visit (session).
-
-We do not collect your
- name, email address, profile picture, location, or any other personal details unless explicitly stated
- here.
+We do not collect your email address, profile picture, location, or any other personal
+ details unless explicitly stated here.
+{% endif %}
4. How We Use Your Information
We use the collected information for the
@@ -65,18 +84,40 @@
4. How We Use Your Information
analytics, or any other non-essential purpose.
5. Third-Party Data Sharing
-We use a third-party service for
- authentication:
+{% if auth_provider == "Auth0" %}
+We use third-party services for authentication and these services process your data:
+
+ -
+ Auth0 (by Okta): We use Auth0 as our authentication service provider. When you log in,
+ Auth0 handles the authentication process and provides us with your user ID, email, and name. Auth0 acts
+ as a data processor on our behalf. Auth0 may store additional information about your authentication sessions.
+ Please review the Auth0 Privacy Policy
+ for details on how they handle your data.
+
+ -
+ Identity Providers (Google, GitHub, etc.): When you choose to log in via Google, GitHub,
+ or another identity provider through Auth0, you interact with their services. These providers authenticate
+ your identity and share limited information with Auth0, which then shares it with us. Please review their
+ respective privacy policies:
+
+
- Google Privacy Policy
+ - GitHub Privacy Statement
+
+
+
+We do not share your information with any other third parties for marketing or other purposes.
+{% else %}
+We use a third-party service for authentication:
-
- GitHub/Google: When you log in, you interact with their
- services. They provide us with your unique user ID. Please review the GitHub Privacy
- Statement or Google Privacy Policy for details on
- how they handle your data.
+ GitHub: When you log in, you interact with GitHub's authentication service.
+ They provide us with your unique user ID and username. Please review the
+ GitHub Privacy Statement
+ for details on how they handle your data.
-We do not share your information with any other
- third parties.
+We do not share your information with any other third parties.
+{% endif %}
6. Our Cookie Policy
We only use strictly
@@ -126,7 +167,15 @@ 7. Your Data Protection Rights
UK Information Commissioner's Office (ICO) if you believe we
have not handled your information correctly.
+{% if auth_provider == "Auth0" %}
+8. Data Retention
+We retain your user ID, email, and name for as long as your account is active. If you wish to delete your
+ account and associated data, please contact us at the email address provided above.
+
+9. Updates to this Policy
+{% else %}
8. Updates to this Policy
+{% endif %}
We may update this policy from time to time.
The latest version will always be posted on this page.
{% endblock %}
\ No newline at end of file
From a3eca668b74811a1095a938dc06ca360edb525bb Mon Sep 17 00:00:00 2001
From: Mr Eggleton
Date: Tue, 13 Jan 2026 10:37:59 +0000
Subject: [PATCH 03/25] Fixing Render
---
README.md | 111 +++++++++++++++++++++++++++++++++++++++++++++++++---
render.yaml | 31 ++++++++++-----
2 files changed, 128 insertions(+), 14 deletions(-)
diff --git a/README.md b/README.md
index c4e9538..016fab7 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,3 @@
-
# Flask Todo App with Dual Authentication

@@ -186,10 +185,112 @@ Once you have your code how you want
## Deployment on Render
-- Add `render.yaml` to repo
-- Push to GitHub
-- Create Blueprint on Render
-- Add environment variables in Render dashboard
+### Prerequisites
+
+1. **GitHub Repository**: Your code must be pushed to a GitHub repository
+2. **Render Account**: Sign up at [render.com](https://render.com) (free tier available)
+3. **Auth0 Application**: Configured with your production callback URL
+
+### Step 1: Prepare Your Repository
+
+Ensure your repository contains:
+- `render.yaml` (blueprint configuration file)
+- `requirements.txt` (Python dependencies)
+- All application code pushed to GitHub
+
+### Step 2: Create a Blueprint on Render
+
+1. **Log in to Render Dashboard**
+ - Go to [dashboard.render.com](https://dashboard.render.com)
+
+2. **Create New Blueprint**
+ - Click the "New +" button in the top right
+ - Select "Blueprint" from the dropdown menu
+ - [Direct Link to Create Blueprint](https://dashboard.render.com/select-repo?type=blueprint)
+
+3. **Connect Your GitHub Repository**
+ - Click "Connect account" if this is your first time
+ - Authorize Render to access your GitHub repositories
+ - Search for and select `mr-eggleton/python-flask-todo` (or your fork)
+ - Click "Connect"
+
+4. **Review Blueprint Configuration**
+ - Render will detect your `render.yaml` file
+ - Review the services that will be created (web service, database, etc.)
+ - Give your blueprint instance a name (e.g., "flask-todo-app")
+ - Click "Apply"
+
+### Step 3: Configure Environment Variables
+
+After the blueprint is created, you need to add your environment variables:
+
+1. **Navigate to Your Web Service**
+ - In the Render dashboard, click on your web service
+ - Go to the "Environment" tab
+
+2. **Add Environment Variables**
+ - Click "Add Environment Variable"
+ - Add each variable from your `.env` file:
+ ```
+ APP_SECRET_KEY=
+ AUTH0_DOMAIN=.auth0.com
+ AUTH0_CLIENT_ID=
+ AUTH0_CLIENT_SECRET=
+ AUTH0_CALLBACK_URL=https://.onrender.com/callback
+ ```
+ - **Important**: Generate a NEW `APP_SECRET_KEY` for production (don't reuse your local one)
+ - **Note**: Do NOT set `OAUTHLIB_INSECURE_TRANSPORT` in production
+ - Click "Save Changes"
+
+3. **Automatic Deployment**
+ - Render will automatically build and deploy your application
+ - Wait for the build to complete (check the "Logs" tab)
+ - Your app will be available at `https://.onrender.com`
+
+### Step 4: Update Auth0 Settings
+
+1. **Add Production Callback URL**
+ - Go to [Auth0 Dashboard](https://manage.auth0.com)
+ - Navigate to your application settings
+ - Add to "Allowed Callback URLs": `https://.onrender.com/callback`
+ - Add to "Allowed Logout URLs": `https://.onrender.com/`
+ - Add to "Allowed Web Origins": `https://.onrender.com`
+ - Click "Save Changes"
+
+### Step 5: Test Your Deployment
+
+1. Visit your Render URL: `https://.onrender.com`
+2. Click "Login" - should redirect to Auth0
+3. Complete authentication
+4. Verify you can create and manage todos
+
+### Continuous Deployment
+
+Once set up, Render automatically deploys when you push to your main branch:
+
+1. Make changes to your code locally
+2. Commit and push to GitHub:
+ ```bash
+ git add .
+ git commit -m "Your commit message"
+ git push origin main
+ ```
+3. Render detects the push and automatically rebuilds/redeploys
+4. Monitor deployment progress in the Render dashboard
+
+### Troubleshooting
+
+- **Build Fails**: Check the "Logs" tab in Render dashboard for errors
+- **Auth0 Redirect Error**: Verify callback URLs match exactly (including https://)
+- **Environment Variables**: Ensure all required variables are set in Render
+- **Database Issues**: Render free tier databases sleep after inactivity; first request may be slow
+
+### Useful Links
+
+- [Render Blueprint Documentation](https://render.com/docs/infrastructure-as-code)
+- [Render Python Deployment Guide](https://render.com/docs/deploy-flask)
+- [Render Environment Variables](https://render.com/docs/environment-variables)
+- [Auth0 Production Checklist](https://auth0.com/docs/deploy-monitor/deploy/production-checklist)
## Things we are ignoring
diff --git a/render.yaml b/render.yaml
index 837f8ce..ed7197e 100644
--- a/render.yaml
+++ b/render.yaml
@@ -1,19 +1,32 @@
-
services:
- type: web
name: flask-todo-app
env: python
plan: free
- buildCommand: "pip install -r requirements.txt"
- startCommand: "gunicorn app:app"
+ buildCommand: pip install -r requirements.txt
+ startCommand: gunicorn app:app
envVars:
+ - key: PYTHON_VERSION
+ value: 3.10
+ - key: OAUTHLIB_INSECURE_TRANSPORT
+ value: "0"
- key: APP_SECRET_KEY
+ generateValue: true
+ - key: AUTH0_DOMAIN
sync: false
- - key: GITHUB_CLIENT_ID
- sync: false
- - key: GITHUB_CLIENT_SECRET
- sync: false
- - key: GOOGLE_CLIENT_ID
+ - key: AUTH0_CLIENT_ID
sync: false
- - key: GOOGLE_CLIENT_SECRET
+ - key: AUTH0_CLIENT_SECRET
sync: false
+ - key: AUTH0_CALLBACK_URL
+ value: https://${RENDER_EXTERNAL_HOSTNAME}/callback
+ - key: DATABASE_URL
+ fromDatabase:
+ name: flask-todo-db
+ property: connectionString
+
+databases:
+ - name: flask-todo-db
+ databaseName: flask_todo
+ plan: free
+ region: oregon
From 40e1081415761467d8b4539025228759738aa38b Mon Sep 17 00:00:00 2001
From: Mr Eggleton
Date: Tue, 13 Jan 2026 10:45:04 +0000
Subject: [PATCH 04/25] SQLite for now, postgress later
---
POSTGRESQL_SETUP.md | 214 ++++++++++++++++++++++++++++++++++++++++++++
README.md | 17 ++--
render.yaml | 10 ---
3 files changed, 224 insertions(+), 17 deletions(-)
create mode 100644 POSTGRESQL_SETUP.md
diff --git a/POSTGRESQL_SETUP.md b/POSTGRESQL_SETUP.md
new file mode 100644
index 0000000..7e25f0c
--- /dev/null
+++ b/POSTGRESQL_SETUP.md
@@ -0,0 +1,214 @@
+# Migrating to PostgreSQL
+
+This guide explains how to upgrade your Flask Todo App from SQLite to PostgreSQL for production use on Render.
+
+## Why Migrate to PostgreSQL?
+
+- **Reliability**: SQLite is file-based and not ideal for production web apps
+- **Concurrency**: PostgreSQL handles multiple concurrent users better
+- **Scalability**: Better performance as your app grows
+- **Data Persistence**: Render's free PostgreSQL databases are more stable than file-based SQLite
+
+## When to Migrate
+
+- Your app is getting real users
+- You need guaranteed data persistence
+- You're moving beyond testing/development
+
+## Prerequisites
+
+- Existing Flask Todo App deployed on Render
+- Access to your Render dashboard
+- Your app source code updated locally
+
+## Step 1: Create PostgreSQL Database on Render
+
+1. **Log in to Render Dashboard**
+ - Go to [dashboard.render.com](https://dashboard.render.com)
+
+2. **Create New PostgreSQL Database**
+ - Click "New +" button
+ - Select "PostgreSQL"
+ - Choose your preferred region (same as your web service recommended)
+ - Name your database (e.g., "flask-todo-db")
+ - Plan: Free tier available
+ - Click "Create Database"
+
+3. **Wait for Database to Initialize**
+ - This takes a few minutes
+ - Once ready, you'll see the connection details
+
+## Step 2: Update Your Application Code
+
+### Install PostgreSQL Driver
+
+Add psycopg2 to your `requirements.txt`:
+
+```
+Flask==2.3.0
+SQLAlchemy==2.0.0
+psycopg2-binary==2.9.6
+# ...other dependencies...
+```
+
+### Update Database Configuration
+
+In your `app.py`, update the database URI to use PostgreSQL when the `DATABASE_URL` environment variable is present:
+
+```python
+import os
+from flask import Flask
+from sqlalchemy import create_engine
+
+app = Flask(__name__)
+
+# Use DATABASE_URL if available (Render), otherwise use SQLite
+database_url = os.getenv('DATABASE_URL')
+if database_url:
+ # Fix the PostgreSQL URI format for SQLAlchemy
+ if database_url.startswith('postgres://'):
+ database_url = database_url.replace('postgres://', 'postgresql://', 1)
+ app.config['SQLALCHEMY_DATABASE_URI'] = database_url
+else:
+ app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///todo.db'
+
+app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
+```
+
+## Step 3: Update render.yaml
+
+Update your `render.yaml` to include the PostgreSQL database and link it to your web service:
+
+```yaml
+services:
+ - type: web
+ name: flask-todo-app
+ env: python
+ plan: free
+ buildCommand: pip install -r requirements.txt
+ startCommand: gunicorn app:app
+ envVars:
+ - key: PYTHON_VERSION
+ value: 3.10
+ - key: OAUTHLIB_INSECURE_TRANSPORT
+ value: "0"
+ - key: APP_SECRET_KEY
+ generateValue: true
+ - key: AUTH0_DOMAIN
+ sync: false
+ - key: AUTH0_CLIENT_ID
+ sync: false
+ - key: AUTH0_CLIENT_SECRET
+ sync: false
+ - key: AUTH0_CALLBACK_URL
+ value: https://${RENDER_EXTERNAL_HOSTNAME}/callback
+ - key: DATABASE_URL
+ fromDatabase:
+ name: flask-todo-db
+ property: connectionString
+
+databases:
+ - name: flask-todo-db
+ databaseName: flask_todo
+ plan: free
+```
+
+## Step 4: Create Database Tables
+
+When your app first connects to PostgreSQL, it needs to create the tables:
+
+### Option A: Automatic (Recommended)
+
+Update your app initialization to create tables on startup:
+
+```python
+from flask_sqlalchemy import SQLAlchemy
+
+db = SQLAlchemy(app)
+
+# In your init_todo() function or app creation:
+with app.app_context():
+ db.create_all()
+```
+
+### Option B: Manual via Shell
+
+1. SSH into your Render web service
+2. Run:
+ ```bash
+ python
+ >>> from app import app, db
+ >>> with app.app_context():
+ ... db.create_all()
+ >>> exit()
+ ```
+
+## Step 5: Deploy Updated Code
+
+1. **Commit and push your changes**
+ ```bash
+ git add requirements.txt app.py render.yaml
+ git commit -m "Add PostgreSQL support"
+ git push origin main
+ ```
+
+2. **Render automatically redeploys**
+ - Check the "Logs" tab in Render dashboard
+ - Wait for build to complete
+
+## Step 6: Verify Migration
+
+1. Visit your app: `https://.onrender.com`
+2. Create a test todo
+3. Refresh the page - data should persist
+4. Check Render PostgreSQL dashboard to confirm queries are running
+
+## Rollback to SQLite (If Needed)
+
+If you need to go back to SQLite:
+
+1. Remove the `DATABASE_URL` environment variable from Render
+2. Revert your `app.py` to use SQLite only
+3. Push to GitHub
+4. Render will redeploy with SQLite
+
+## Troubleshooting
+
+### "Ident authentication failed"
+
+- Your `DATABASE_URL` may be malformed
+- Ensure you're using `postgresql://` not `postgres://`
+- Check that the connection string includes username and password
+
+### "relation 'todo' does not exist"
+
+- Tables haven't been created yet
+- Ensure `db.create_all()` runs on app startup
+- Check application logs for errors
+
+### Connection Timeout
+
+- PostgreSQL service may still be starting
+- Wait a few minutes and try again
+- Check that your web service can reach the database (same region recommended)
+
+### Data Lost After Redeploy
+
+- Free tier databases may have limitations
+- Consider upgrading to a paid plan for production
+- Alternatively, implement regular backups
+
+## Useful Links
+
+- [Render PostgreSQL Documentation](https://render.com/docs/databases)
+- [SQLAlchemy PostgreSQL Dialect](https://docs.sqlalchemy.org/en/20/dialects/postgresql.html)
+- [PostgreSQL Connection Strings](https://www.postgresql.org/docs/current/libpq-envars.html)
+- [Database Migrations with Flask-Migrate](https://flask-migrate.readthedocs.io/)
+
+## Next Steps
+
+Once PostgreSQL is running reliably:
+
+- Implement database migrations using [Flask-Migrate](https://flask-migrate.readthedocs.io/)
+- Set up automated backups in Render
+- Consider upgrading to a paid database plan for production use
diff --git a/README.md b/README.md
index 016fab7..781603a 100644
--- a/README.md
+++ b/README.md
@@ -171,17 +171,19 @@ Without this, Auth0 cannot reach your callback URL and login will fail.
## The Database
-This code uses [SQLAlchemy](https://www.sqlalchemy.org/) to set up classes that have methods to talk to many [databases](https://docs.sqlalchemy.org/en/20/dialects/index.html) we use SQLite for simplicity here.
+This code uses [SQLAlchemy](https://www.sqlalchemy.org/) to set up classes that have methods to talk to many [databases](https://docs.sqlalchemy.org/en/20/dialects/index.html). We use **SQLite for simplicity and easy local development**.
-### SQLite Viewer extension
+### Local Development (SQLite)
-The database file is in /instance/
+The database file is stored in `/instance/todo.db`
-The database can be changed to
+### Production Deployment (SQLite)
-## First deployment
+For initial deployment on Render, SQLite works fine for testing and small user bases. The database persists in Render's filesystem.
-Once you have your code how you want
+### Upgrading to PostgreSQL
+
+Once your app grows and you need a more robust database, see [POSTGRESQL_SETUP.md](POSTGRESQL_SETUP.md) for a complete migration guide.
## Deployment on Render
@@ -230,7 +232,7 @@ After the blueprint is created, you need to add your environment variables:
2. **Add Environment Variables**
- Click "Add Environment Variable"
- - Add each variable from your `.env` file:
+ - Add each variable:
```
APP_SECRET_KEY=
AUTH0_DOMAIN=.auth0.com
@@ -246,6 +248,7 @@ After the blueprint is created, you need to add your environment variables:
- Render will automatically build and deploy your application
- Wait for the build to complete (check the "Logs" tab)
- Your app will be available at `https://.onrender.com`
+ - **Note**: The SQLite database file will persist on Render's filesystem
### Step 4: Update Auth0 Settings
diff --git a/render.yaml b/render.yaml
index ed7197e..1c9f6ad 100644
--- a/render.yaml
+++ b/render.yaml
@@ -20,13 +20,3 @@ services:
sync: false
- key: AUTH0_CALLBACK_URL
value: https://${RENDER_EXTERNAL_HOSTNAME}/callback
- - key: DATABASE_URL
- fromDatabase:
- name: flask-todo-db
- property: connectionString
-
-databases:
- - name: flask-todo-db
- databaseName: flask_todo
- plan: free
- region: oregon
From bf109a22020e637d1327b8ebc7456b4ba52de31a Mon Sep 17 00:00:00 2001
From: Mr Eggleton
Date: Tue, 13 Jan 2026 10:52:33 +0000
Subject: [PATCH 05/25] Update Python version to 3.12.1 in render.yaml
---
render.yaml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/render.yaml b/render.yaml
index 1c9f6ad..12b55d0 100644
--- a/render.yaml
+++ b/render.yaml
@@ -7,7 +7,7 @@ services:
startCommand: gunicorn app:app
envVars:
- key: PYTHON_VERSION
- value: 3.10
+ value: 3.12.1
- key: OAUTHLIB_INSECURE_TRANSPORT
value: "0"
- key: APP_SECRET_KEY
From eaacba54785b053bc63843992722223351d3a790 Mon Sep 17 00:00:00 2001
From: Mr Eggleton
Date: Tue, 13 Jan 2026 11:46:29 +0000
Subject: [PATCH 06/25] fix on render
---
requirements.txt | 1 +
1 file changed, 1 insertion(+)
diff --git a/requirements.txt b/requirements.txt
index f34dcae..f01572b 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -4,3 +4,4 @@ Flask-SQLAlchemy==3.1.1
Flask-Dance==7.0.0
gunicorn==21.2.0
python-dotenv==1.0.0
+psycopg2-binary==2.9.11
\ No newline at end of file
From 09b811cfade525294185b46efbf5bac64bd961ec Mon Sep 17 00:00:00 2001
From: Mr Eggleton
Date: Tue, 13 Jan 2026 12:06:54 +0000
Subject: [PATCH 07/25] gunicorn fix
---
render.yaml | 2 +-
requirements.txt | 3 +--
2 files changed, 2 insertions(+), 3 deletions(-)
diff --git a/render.yaml b/render.yaml
index 12b55d0..4946f01 100644
--- a/render.yaml
+++ b/render.yaml
@@ -4,7 +4,7 @@ services:
env: python
plan: free
buildCommand: pip install -r requirements.txt
- startCommand: gunicorn app:app
+ startCommand: python -m gunicorn app:app
envVars:
- key: PYTHON_VERSION
value: 3.12.1
diff --git a/requirements.txt b/requirements.txt
index f01572b..f8efd1d 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -3,5 +3,4 @@ Flask==2.3.2
Flask-SQLAlchemy==3.1.1
Flask-Dance==7.0.0
gunicorn==21.2.0
-python-dotenv==1.0.0
-psycopg2-binary==2.9.11
\ No newline at end of file
+python-dotenv==1.0.0
\ No newline at end of file
From 8ee1f24fb3dfab7bfcd011c37b1bcd2934d40ff5 Mon Sep 17 00:00:00 2001
From: Mr Eggleton
Date: Tue, 13 Jan 2026 14:06:41 +0000
Subject: [PATCH 08/25] gunicorn compatibility
---
README.md | 1 +
app.py | 12 ++++++++++++
2 files changed, 13 insertions(+)
diff --git a/README.md b/README.md
index 781603a..587aafd 100644
--- a/README.md
+++ b/README.md
@@ -199,6 +199,7 @@ Ensure your repository contains:
- `render.yaml` (blueprint configuration file)
- `requirements.txt` (Python dependencies)
- All application code pushed to GitHub
+- Use `python -m gunicorn app:app --bind 0.0.0.0:5000` to confirm that everything works ok under gunicorn before trying Render
### Step 2: Create a Blueprint on Render
diff --git a/app.py b/app.py
index 7bedf09..8115d6e 100644
--- a/app.py
+++ b/app.py
@@ -1,3 +1,7 @@
+# Load environment variables from .env file (needed for gunicorn)
+from dotenv import load_dotenv
+load_dotenv()
+
import os
from flask import Flask
from auth import auth_bp, auth0_bp, github_bp, github_auth_bp
@@ -35,5 +39,13 @@ def inject_dict_for_all_templates():
# Initialize todo module (db and tables)
init_todo(app)
+# Set AUTH0_CALLBACK_URL dynamically based on Render's hostname
+if 'RENDER_EXTERNAL_HOSTNAME' in os.environ:
+ auth0_callback_url = f"https://{os.environ['RENDER_EXTERNAL_HOSTNAME']}/callback"
+else:
+ auth0_callback_url = os.environ.get('AUTH0_CALLBACK_URL', 'http://localhost:5000/callback')
+
+# Use auth0_callback_url in your Auth0 configuration
+
if __name__ == '__main__':
app.run(debug=True)
From 3e0ad4cf32e000682d08160e04a5e16f65943a80 Mon Sep 17 00:00:00 2001
From: Mr Eggleton
Date: Tue, 13 Jan 2026 14:41:43 +0000
Subject: [PATCH 09/25] Render Fix
---
app.py | 2 ++
1 file changed, 2 insertions(+)
diff --git a/app.py b/app.py
index 8115d6e..baf4eb8 100644
--- a/app.py
+++ b/app.py
@@ -45,6 +45,8 @@ def inject_dict_for_all_templates():
else:
auth0_callback_url = os.environ.get('AUTH0_CALLBACK_URL', 'http://localhost:5000/callback')
+app.config['AUTH0_CALLBACK_URL'] = auth0_callback_url
+
# Use auth0_callback_url in your Auth0 configuration
if __name__ == '__main__':
From 2cfd6af768a5b756cdf04df4ef4049c678dc5c65 Mon Sep 17 00:00:00 2001
From: Mr Eggleton
Date: Wed, 14 Jan 2026 09:56:37 +0000
Subject: [PATCH 10/25] Documentation updates
---
CODESPACES_SETUP.md | 100 +++++++++++++++++++++++++
README.md | 175 +++-----------------------------------------
RENDER_SETUP.md | 136 ++++++++++++++++++++++++++++++++++
3 files changed, 245 insertions(+), 166 deletions(-)
create mode 100644 CODESPACES_SETUP.md
create mode 100644 RENDER_SETUP.md
diff --git a/CODESPACES_SETUP.md b/CODESPACES_SETUP.md
new file mode 100644
index 0000000..67b4632
--- /dev/null
+++ b/CODESPACES_SETUP.md
@@ -0,0 +1,100 @@
+# GitHub Codespaces Setup
+
+GitHub Codespaces is a flexible cloud-based development environment. Your editor runs in the cloud, allowing you to work from anywhere.
+
+## Creating and Starting a Codespace
+
+### Create a New Codespace
+
+1. **Go to the Repository**
+ - Navigate to [https://github.com/stretchyboy/python-todo](https://github.com/stretchyboy/python-todo) (or your fork)
+
+2. **Create a Codespace**
+ - Click the green **"Code"** button
+ - Select the **"Codespaces"** tab
+ - Click **"Create codespace on main"** (or your preferred branch)
+
+3. **Wait for Setup**
+ - GitHub will provision your Codespace (this takes 1-2 minutes)
+ - VS Code will open in your browser automatically
+ - The environment is ready when you see the terminal
+
+### Start an Existing Codespace
+
+If you've created a Codespace before:
+
+1. **Go to Your Codespaces**
+ - Visit [https://github.com/codespaces](https://github.com/codespaces)
+ - Click on your Codespace name to open it
+
+2. **Or via GitHub**
+ - Click the green **"Code"** button on the repository
+ - Select the **"Codespaces"** tab
+ - Click on an existing Codespace to resume it
+
+## Install Dependencies
+
+```bash
+py -m pip install -r requirements.txt
+```
+
+## Copy Example Environment File
+
+```bash
+# On linux or codespaces
+cp .env.example .env
+```
+
+## Auth0 Setup (Codespaces & Production)
+
+For Codespaces and Render deployment:
+
+1. **Create an Auth0 Account**
+ - Go to [auth0.com](https://auth0.com) and sign up
+
+2. **Create an Application**
+ - Dashboard → Applications → Create Application
+ - Choose "Regular Web Applications"
+ - Name: Flask Todo App
+
+3. **Configure Application Settings**
+ - "Allowed Callback URLs": `https:///callback`
+ - "Allowed Logout URLs": `https:///`
+ - "Allowed Web Origins": `https://`
+ - Codespaces URL format: `https://-5000./`
+ - Example: `https://shiny-space-pancake-g5w6pqr4v7h2pgx-5000.app.github.dev/callback`
+
+4. **Copy Credentials**
+ - Add Auth0 Domain, Client ID, and Client Secret to `.env`
+
+## Running the Application
+
+Start the Flask development server:
+
+```bash
+python -m flask run
+```
+
+The app will be available at [http://localhost:5000](http://localhost:5000)
+
+## Important: Codespaces Port Configuration
+
+If you're running in **GitHub Codespaces**, you must set the forwarded port to **Public** for Auth0 callbacks to work:
+
+1. Open the **Ports** panel (bottom of VS Code)
+2. Right-click the port 5000
+3. Select "Port Visibility" → **Public**
+
+Without this, Auth0 cannot reach your callback URL and login will fail.
+
+## Using the App
+
+1. Visit [http://localhost:5000](http://localhost:5000)
+2. Click "Login" - it will automatically route to Auth0
+3. After successful login, manage your todos
+
+## Authentication Resources
+
+- [Flask-Dance Documentation](https://flask-dance.readthedocs.io/)
+- [Auth0 Python Quickstart](https://auth0.com/docs/quickstart/webapp/python)
+- [Auth0 Dashboard](https://manage.auth0.com/)
diff --git a/README.md b/README.md
index 587aafd..79f2a4a 100644
--- a/README.md
+++ b/README.md
@@ -112,32 +112,6 @@ For local Windows development with GitHub Desktop:
3. **Enable Insecure Transport for Local Dev**
- Set `OAUTHLIB_INSECURE_TRANSPORT=1` in `.env` (only for local development)
-### Auth0 Setup (Codespaces & Production)
-
-For Codespaces and Render deployment:
-
-1. **Create an Auth0 Account**
- - Go to [auth0.com](https://auth0.com) and sign up
-
-2. **Create an Application**
- - Dashboard → Applications → Create Application
- - Choose "Regular Web Applications"
- - Name: Flask Todo App
-
-3. **Configure Application Settings**
- - "Allowed Callback URLs":
- - Local: `http://localhost:5000/callback`
- - Codespaces: `https:///callback`
- - Production: `https://your-app.onrender.com/callback`
- - "Allowed Logout URLs":
- - Local: `http://localhost:5000/`
- - Production: `https://your-app.onrender.com/`
- - "Allowed Web Origins": Same as callback URLs
- - Codespaces URL format: `https://-5000./` (use `/callback` for the callback and `/` for logout)
-
-4. **Copy Credentials**
- - Add Auth0 Domain, Client ID, and Client Secret to `.env`
-
## Running the Application
Start the Flask development server:
@@ -148,27 +122,6 @@ python -m flask run
The app will be available at [http://localhost:5000](http://localhost:5000)
-### Important: Codespaces Port Configuration
-
-If you're running in **GitHub Codespaces**, you must set the forwarded port to **Public** for Auth0 callbacks to work:
-
-1. Open the **Ports** panel (bottom of VS Code)
-2. Right-click the port 5000
-3. Select "Port Visibility" → **Public**
-
-Without this, Auth0 cannot reach your callback URL and login will fail.
-
-**To use the app:**
-1. Visit [http://localhost:5000](http://localhost:5000)
-2. Click "Login" - it will automatically route to GitHub (local) or Auth0 (Codespaces)
-3. After successful login, manage your todos
-
-## Authentication Resources
-
-- [Flask-Dance Documentation](https://flask-dance.readthedocs.io/)
-- [Auth0 Python Quickstart](https://auth0.com/docs/quickstart/webapp/python)
-- [Auth0 Dashboard](https://manage.auth0.com/)
-
## The Database
This code uses [SQLAlchemy](https://www.sqlalchemy.org/) to set up classes that have methods to talk to many [databases](https://docs.sqlalchemy.org/en/20/dialects/index.html). We use **SQLite for simplicity and easy local development**.
@@ -177,129 +130,19 @@ This code uses [SQLAlchemy](https://www.sqlalchemy.org/) to set up classes that
The database file is stored in `/instance/todo.db`
-### Production Deployment (SQLite)
+## Things we are ignoring
-For initial deployment on Render, SQLite works fine for testing and small user bases. The database persists in Render's filesystem.
+- 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
+- Storing any user data in a database (other than an id from github ). To have users on this system to store any other PII refer to [https://flask-dance.readthedocs.io/en/latest/storages.html#sqlalchemy](https://flask-dance.readthedocs.io/en/latest/storages.html#sqlalchemy) and change the privacy statement.
+- Adding extra security [https://flask-security.readthedocs.io/en/stable/quickstart.html#basic-flask-sqlalchemy-application](https://flask-security.readthedocs.io/en/stable/quickstart.html#basic-flask-sqlalchemy-application)
+- Testing. There are no tests in this code.
-### Upgrading to PostgreSQL
+## Codespaces Setup
-Once your app grows and you need a more robust database, see [POSTGRESQL_SETUP.md](POSTGRESQL_SETUP.md) for a complete migration guide.
+See [CODESPACES_SETUP.md](CODESPACES_SETUP.md) for complete GitHub Codespaces setup instructions.
## Deployment on Render
-### Prerequisites
-
-1. **GitHub Repository**: Your code must be pushed to a GitHub repository
-2. **Render Account**: Sign up at [render.com](https://render.com) (free tier available)
-3. **Auth0 Application**: Configured with your production callback URL
-
-### Step 1: Prepare Your Repository
-
-Ensure your repository contains:
-- `render.yaml` (blueprint configuration file)
-- `requirements.txt` (Python dependencies)
-- All application code pushed to GitHub
-- Use `python -m gunicorn app:app --bind 0.0.0.0:5000` to confirm that everything works ok under gunicorn before trying Render
-
-### Step 2: Create a Blueprint on Render
-
-1. **Log in to Render Dashboard**
- - Go to [dashboard.render.com](https://dashboard.render.com)
-
-2. **Create New Blueprint**
- - Click the "New +" button in the top right
- - Select "Blueprint" from the dropdown menu
- - [Direct Link to Create Blueprint](https://dashboard.render.com/select-repo?type=blueprint)
-
-3. **Connect Your GitHub Repository**
- - Click "Connect account" if this is your first time
- - Authorize Render to access your GitHub repositories
- - Search for and select `mr-eggleton/python-flask-todo` (or your fork)
- - Click "Connect"
-
-4. **Review Blueprint Configuration**
- - Render will detect your `render.yaml` file
- - Review the services that will be created (web service, database, etc.)
- - Give your blueprint instance a name (e.g., "flask-todo-app")
- - Click "Apply"
-
-### Step 3: Configure Environment Variables
-
-After the blueprint is created, you need to add your environment variables:
-
-1. **Navigate to Your Web Service**
- - In the Render dashboard, click on your web service
- - Go to the "Environment" tab
-
-2. **Add Environment Variables**
- - Click "Add Environment Variable"
- - Add each variable:
- ```
- APP_SECRET_KEY=
- AUTH0_DOMAIN=.auth0.com
- AUTH0_CLIENT_ID=
- AUTH0_CLIENT_SECRET=
- AUTH0_CALLBACK_URL=https://.onrender.com/callback
- ```
- - **Important**: Generate a NEW `APP_SECRET_KEY` for production (don't reuse your local one)
- - **Note**: Do NOT set `OAUTHLIB_INSECURE_TRANSPORT` in production
- - Click "Save Changes"
-
-3. **Automatic Deployment**
- - Render will automatically build and deploy your application
- - Wait for the build to complete (check the "Logs" tab)
- - Your app will be available at `https://.onrender.com`
- - **Note**: The SQLite database file will persist on Render's filesystem
-
-### Step 4: Update Auth0 Settings
-
-1. **Add Production Callback URL**
- - Go to [Auth0 Dashboard](https://manage.auth0.com)
- - Navigate to your application settings
- - Add to "Allowed Callback URLs": `https://.onrender.com/callback`
- - Add to "Allowed Logout URLs": `https://.onrender.com/`
- - Add to "Allowed Web Origins": `https://.onrender.com`
- - Click "Save Changes"
-
-### Step 5: Test Your Deployment
-
-1. Visit your Render URL: `https://.onrender.com`
-2. Click "Login" - should redirect to Auth0
-3. Complete authentication
-4. Verify you can create and manage todos
-
-### Continuous Deployment
-
-Once set up, Render automatically deploys when you push to your main branch:
-
-1. Make changes to your code locally
-2. Commit and push to GitHub:
- ```bash
- git add .
- git commit -m "Your commit message"
- git push origin main
- ```
-3. Render detects the push and automatically rebuilds/redeploys
-4. Monitor deployment progress in the Render dashboard
-
-### Troubleshooting
-
-- **Build Fails**: Check the "Logs" tab in Render dashboard for errors
-- **Auth0 Redirect Error**: Verify callback URLs match exactly (including https://)
-- **Environment Variables**: Ensure all required variables are set in Render
-- **Database Issues**: Render free tier databases sleep after inactivity; first request may be slow
-
-### Useful Links
-
-- [Render Blueprint Documentation](https://render.com/docs/infrastructure-as-code)
-- [Render Python Deployment Guide](https://render.com/docs/deploy-flask)
-- [Render Environment Variables](https://render.com/docs/environment-variables)
-- [Auth0 Production Checklist](https://auth0.com/docs/deploy-monitor/deploy/production-checklist)
+See [RENDER_SETUP.md](RENDER_SETUP.md) for complete Render deployment instructions, including setup, configuration, environment variables, and continuous deployment.
-## Things we are ignoring
-
-- Persistent records in a database. The current database will be destroyed each time you push to render, ( we are only testing, not building a real system that works for years).
-- 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
-- Storing any user data in a database (other than an id from github ). To have users on this system to store any other PII refer to [https://flask-dance.readthedocs.io/en/latest/storages.html#sqlalchemy](https://flask-dance.readthedocs.io/en/latest/storages.html#sqlalchemy) and change the privacy statement.
-- Adding extra security [https://flask-security.readthedocs.io/en/stable/quickstart.html#basic-flask-sqlalchemy-application](https://flask-security.readthedocs.io/en/stable/quickstart.html#basic-flask-sqlalchemy-application)
-- Testing. There are no tests in this code.
diff --git a/RENDER_SETUP.md b/RENDER_SETUP.md
new file mode 100644
index 0000000..b494983
--- /dev/null
+++ b/RENDER_SETUP.md
@@ -0,0 +1,136 @@
+# Deployment on Render
+
+## Prerequisites
+
+1. **GitHub Repository**: Your code must be pushed to a GitHub repository
+2. **Render Account**: Sign up at [render.com](https://render.com) (free tier available)
+3. **Auth0 Application**: Configured with your production callback URL
+
+## Auth0 Setup (Production)
+
+Before deploying to Render, set up Auth0 for production:
+
+1. **Create an Auth0 Account**
+ - Go to [auth0.com](https://auth0.com) and sign up
+
+2. **Create an Application**
+ - Dashboard → Applications → Create Application
+ - Choose "Regular Web Applications"
+ - Name: Flask Todo App
+
+3. **Configure Application Settings**
+ - "Allowed Callback URLs": `https://.onrender.com/callback`
+ - "Allowed Logout URLs": `https://.onrender.com/`
+ - "Allowed Web Origins": `https://.onrender.com`
+ - Example: `https://flask-todo-app.onrender.com/callback`
+
+4. **Save Credentials**
+ - Copy your Auth0 Domain, Client ID, and Client Secret
+ - You'll need these for Render environment variables in Step 3
+
+## Step 1: Prepare Your Repository
+
+Ensure your repository contains:
+- `render.yaml` (blueprint configuration file)
+- `requirements.txt` (Python dependencies)
+- All application code pushed to GitHub
+- Use `python -m gunicorn app:app --bind 0.0.0.0:5000` to confirm that everything works ok under gunicorn before trying Render
+
+## Step 2: Create a Blueprint on Render
+
+1. **Log in to Render Dashboard**
+ - Go to [dashboard.render.com](https://dashboard.render.com)
+
+2. **Create New Blueprint**
+ - Click the "New +" button in the top right
+ - Select "Blueprint" from the dropdown menu
+ - [Direct Link to Create Blueprint](https://dashboard.render.com/select-repo?type=blueprint)
+
+3. **Connect Your GitHub Repository**
+ - Click "Connect account" if this is your first time
+ - Authorize Render to access your GitHub repositories
+ - Search for and select `mr-eggleton/python-flask-todo` (or your fork)
+ - Click "Connect"
+
+4. **Review Blueprint Configuration**
+ - Render will detect your `render.yaml` file
+ - Review the services that will be created (web service, database, etc.)
+ - Give your blueprint instance a name (e.g., "flask-todo-app")
+ - Click "Apply"
+
+## Step 3: Configure Environment Variables
+
+After the blueprint is created, you need to add your environment variables:
+
+1. **Navigate to Your Web Service**
+ - In the Render dashboard, click on your web service
+ - Go to the "Environment" tab
+
+2. **Add Environment Variables**
+ - Click "Add Environment Variable"
+ - Add each variable:
+ ```
+ APP_SECRET_KEY=
+ AUTH0_DOMAIN=.auth0.com
+ AUTH0_CLIENT_ID=
+ AUTH0_CLIENT_SECRET=
+ AUTH0_CALLBACK_URL=https://.onrender.com/callback
+ ```
+ - **Important**: Generate a NEW `APP_SECRET_KEY` for production (don't reuse your local one)
+ - **Note**: Do NOT set `OAUTHLIB_INSECURE_TRANSPORT` in production
+ - Click "Save Changes"
+
+3. **Automatic Deployment**
+ - Render will automatically build and deploy your application
+ - Wait for the build to complete (check the "Logs" tab)
+ - Your app will be available at `https://.onrender.com`
+ - **Note**: The SQLite database file will persist on Render's filesystem
+
+## Step 4: Update Auth0 Settings
+
+1. **Add Production Callback URL**
+ - Go to [Auth0 Dashboard](https://manage.auth0.com)
+ - Navigate to your application settings
+ - Add to "Allowed Callback URLs": `https://.onrender.com/callback`
+ - Add to "Allowed Logout URLs": `https://.onrender.com/`
+ - Add to "Allowed Web Origins": `https://.onrender.com`
+ - Click "Save Changes"
+
+## Step 5: Test Your Deployment
+
+1. Visit your Render URL: `https://.onrender.com`
+2. Click "Login" - should redirect to Auth0
+3. Complete authentication
+4. Verify you can create and manage todos
+
+## Continuous Deployment
+
+Once set up, Render automatically deploys when you push to your main branch:
+
+1. Make changes to your code locally
+2. Commit and push to GitHub:
+ ```bash
+ git add .
+ git commit -m "Your commit message"
+ git push origin main
+ ```
+3. Render detects the push and automatically rebuilds/redeploys
+4. Monitor deployment progress in the Render dashboard
+
+## Troubleshooting
+
+- **Build Fails**: Check the "Logs" tab in Render dashboard for errors
+- **Auth0 Redirect Error**: Verify callback URLs match exactly (including https://)
+- **Environment Variables**: Ensure all required variables are set in Render
+- **Database Issues**: Render free tier databases sleep after inactivity; first request may be slow
+
+## Upgrading to PostgreSQL
+
+Once your app grows and you need a more robust database, see [POSTGRESQL_SETUP.md](POSTGRESQL_SETUP.md) for a complete migration guide.
+
+## Useful Links
+
+- [Render Blueprint Documentation](https://render.com/docs/infrastructure-as-code)
+- [Render Python Deployment Guide](https://render.com/docs/deploy-flask)
+- [Render Environment Variables](https://render.com/docs/environment-variables)
+- [Auth0 Production Checklist](https://auth0.com/docs/deploy-monitor/deploy/production-checklist)
From 45ff8b15027181f512a06b56ee6179376c332b0e Mon Sep 17 00:00:00 2001
From: Mr Eggleton
Date: Wed, 14 Jan 2026 10:07:28 +0000
Subject: [PATCH 11/25] Tweaks to environment variables
---
.env.example | 14 ++++----------
RENDER_SETUP.md | 33 +++++++++++++--------------------
app.py | 17 ++++++++++++++++-
3 files changed, 33 insertions(+), 31 deletions(-)
diff --git a/.env.example b/.env.example
index 9f78f6d..043d9db 100644
--- a/.env.example
+++ b/.env.example
@@ -1,15 +1,9 @@
-DATABASE_URL=sqlite:///todo.db
APP_SECRET_KEY=your_flask_secret
-
-
-# GitHub OAuth for local development
-GITHUB_CLIENT_ID=your-github-client-id
-GITHUB_CLIENT_SECRET=your-github-client-secret
-# Only needed for local/GitHub OAuth
OAUTHLIB_INSECURE_TRANSPORT=0
+DATABASE_URL=sqlite:///todo.db
# Example environment variables for Auth0 integration
-AUTH0_DOMAIN=your-auth0-domain.auth0.com
-AUTH0_CLIENT_ID=your-auth0-client-id
-AUTH0_CLIENT_SECRET=your-auth0-client-secret
+AUTH0_DOMAIN=your_auth0_domain
+AUTH0_CLIENT_ID=your_client_id
+AUTH0_CLIENT_SECRET=your_client_secret
AUTH0_CALLBACK_URL=http://localhost:5000/callback
diff --git a/RENDER_SETUP.md b/RENDER_SETUP.md
index b494983..29848b9 100644
--- a/RENDER_SETUP.md
+++ b/RENDER_SETUP.md
@@ -59,26 +59,19 @@ Ensure your repository contains:
- Click "Apply"
## Step 3: Configure Environment Variables
-
-After the blueprint is created, you need to add your environment variables:
-
-1. **Navigate to Your Web Service**
- - In the Render dashboard, click on your web service
- - Go to the "Environment" tab
-
-2. **Add Environment Variables**
- - Click "Add Environment Variable"
- - Add each variable:
- ```
- APP_SECRET_KEY=
- AUTH0_DOMAIN=.auth0.com
- AUTH0_CLIENT_ID=
- AUTH0_CLIENT_SECRET=
- AUTH0_CALLBACK_URL=https://.onrender.com/callback
- ```
- - **Important**: Generate a NEW `APP_SECRET_KEY` for production (don't reuse your local one)
- - **Note**: Do NOT set `OAUTHLIB_INSECURE_TRANSPORT` in production
- - Click "Save Changes"
+
+ Add these in Render Dashboard → Environment:
+
+ ```
+ AUTH0_CLIENT_ID=your_client_id
+ AUTH0_CLIENT_SECRET=your_client_secret
+ AUTH0_DOMAIN=your_auth0_domain
+ APP_SECRET_KEY=your_secret_key
+ ```
+
+ **Important:** Do NOT set `RENDER_EXTERNAL_HOSTNAME` manually. Render sets this automatically, but it's only available at runtime, not during build.
+
+ **For Auth0 Configuration:** Use your actual Render app URL (e.g., `https://python-todo.onrender.com`) in Auth0 settings, not the variable name.
3. **Automatic Deployment**
- Render will automatically build and deploy your application
diff --git a/app.py b/app.py
index baf4eb8..a3261d7 100644
--- a/app.py
+++ b/app.py
@@ -17,7 +17,7 @@
}
app = Flask(__name__)
-app.secret_key = os.getenv("APP_SECRET_KEY", "supersecret")
+app.secret_key = os.environ.get("APP_SECRET_KEY")
app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv('DATABASE_URL',
'sqlite:///todo.db')
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
@@ -47,6 +47,21 @@ def inject_dict_for_all_templates():
app.config['AUTH0_CALLBACK_URL'] = auth0_callback_url
+# Set redirect_uri based on environment
+if os.getenv('CODESPACE_NAME'):
+ # Running in GitHub Codespaces
+ redirect_uri = f"https://{os.getenv('CODESPACE_NAME')}-5000.{os.getenv('GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN')}/callback"
+elif os.getenv('RENDER'):
+ # Running on Render - use RENDER_EXTERNAL_URL which is set at runtime
+ render_url = os.getenv('RENDER_EXTERNAL_URL', '')
+ if render_url:
+ redirect_uri = f"{render_url}/callback"
+ else:
+ redirect_uri = None # Will fall back to default
+else:
+ # Local development
+ redirect_uri = "http://localhost:5000/callback"
+
# Use auth0_callback_url in your Auth0 configuration
if __name__ == '__main__':
From 638be9d5337107e4b922f177aac1292b1e07fae2 Mon Sep 17 00:00:00 2001
From: Mr Eggleton
Date: Wed, 14 Jan 2026 10:56:51 +0000
Subject: [PATCH 12/25] Update Auth0 callback URL handling and documentation
for Render deployment
---
RENDER_SETUP.md | 3 +++
app.py | 11 ++---------
render.yaml | 2 +-
3 files changed, 6 insertions(+), 10 deletions(-)
diff --git a/RENDER_SETUP.md b/RENDER_SETUP.md
index 29848b9..9259767 100644
--- a/RENDER_SETUP.md
+++ b/RENDER_SETUP.md
@@ -67,8 +67,11 @@ Ensure your repository contains:
AUTH0_CLIENT_SECRET=your_client_secret
AUTH0_DOMAIN=your_auth0_domain
APP_SECRET_KEY=your_secret_key
+ AUTH0_CALLBACK_URL=https://your-app-name.onrender.com/callback
```
+ **Important:** Replace `your-app-name.onrender.com` with your actual Render app URL (found in your Render dashboard).
+
**Important:** Do NOT set `RENDER_EXTERNAL_HOSTNAME` manually. Render sets this automatically, but it's only available at runtime, not during build.
**For Auth0 Configuration:** Use your actual Render app URL (e.g., `https://python-todo.onrender.com`) in Auth0 settings, not the variable name.
diff --git a/app.py b/app.py
index a3261d7..47ca576 100644
--- a/app.py
+++ b/app.py
@@ -51,16 +51,9 @@ def inject_dict_for_all_templates():
if os.getenv('CODESPACE_NAME'):
# Running in GitHub Codespaces
redirect_uri = f"https://{os.getenv('CODESPACE_NAME')}-5000.{os.getenv('GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN')}/callback"
-elif os.getenv('RENDER'):
- # Running on Render - use RENDER_EXTERNAL_URL which is set at runtime
- render_url = os.getenv('RENDER_EXTERNAL_URL', '')
- if render_url:
- redirect_uri = f"{render_url}/callback"
- else:
- redirect_uri = None # Will fall back to default
else:
- # Local development
- redirect_uri = "http://localhost:5000/callback"
+ # Use AUTH0_CALLBACK_URL from environment (for both local and production)
+ redirect_uri = os.getenv('AUTH0_CALLBACK_URL', 'http://localhost:5000/callback')
# Use auth0_callback_url in your Auth0 configuration
diff --git a/render.yaml b/render.yaml
index 4946f01..51eaa26 100644
--- a/render.yaml
+++ b/render.yaml
@@ -19,4 +19,4 @@ services:
- key: AUTH0_CLIENT_SECRET
sync: false
- key: AUTH0_CALLBACK_URL
- value: https://${RENDER_EXTERNAL_HOSTNAME}/callback
+ sync: false
From 3bbf969f43d5a330dc393f771e485e894a5bbfdb Mon Sep 17 00:00:00 2001
From: Mr Eggleton
Date: Wed, 14 Jan 2026 12:58:32 +0000
Subject: [PATCH 13/25] Refactor README for clarity and detail on features and
setup
---
README.md | 32 +++++++++++++++++++++++++-------
1 file changed, 25 insertions(+), 7 deletions(-)
diff --git a/README.md b/README.md
index 79f2a4a..150f89d 100644
--- a/README.md
+++ b/README.md
@@ -1,12 +1,15 @@
-# Flask Todo App with Dual Authentication
+# Flask Todo App Starter



## Features
-
+
### Flask
+ - Webserver with routing (a function for each url endpoint)
+ - Jinja templates for looping though and outputting data.
+ - todo.py contains the endpoints for the Todo app
-### SQLAlchemy & SQLite
+### SQLAlchemy & SQLite / PostgreSQL
+ - SQL Database
+ - Managed by SQLAlchemy an Object Relationship Manager 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, start with SQLite but you can move PostgreSQL or others when you are ready.
+ - todo.py includes the Todo class that provdes all you need for the building of the database and all the CRUD.
### Authentication (GitHub + Auth0)
+ - GitHub OAuth (Flask-Dance) for local Windows development
+ - Auth0 OAuth for Codespaces and Render production
-### Render
-
-### Github Actions
+### Render & Github Actions
+- Ready for Render deployment
+- GitHub Actions CI/CD
## Setup
@@ -138,6 +150,12 @@ The database file is stored in `/instance/todo.db`
- Adding extra security [https://flask-security.readthedocs.io/en/stable/quickstart.html#basic-flask-sqlalchemy-application](https://flask-security.readthedocs.io/en/stable/quickstart.html#basic-flask-sqlalchemy-application)
- Testing. There are no tests in this code.
+## Your Development
+
+
+
+
+
## Codespaces Setup
See [CODESPACES_SETUP.md](CODESPACES_SETUP.md) for complete GitHub Codespaces setup instructions.
From 44a47cfa58515437aec23ec43a598945444603a2 Mon Sep 17 00:00:00 2001
From: Mr Eggleton
Date: Wed, 14 Jan 2026 12:58:40 +0000
Subject: [PATCH 14/25] Refactor Todo model to use SQLAlchemy's mapped_column
and remove dataclass decorator
---
todo.py | 27 ++++++++++++++-------------
1 file changed, 14 insertions(+), 13 deletions(-)
diff --git a/todo.py b/todo.py
index d6361a6..299a7ed 100644
--- a/todo.py
+++ b/todo.py
@@ -1,26 +1,27 @@
# todo.py - todo functionality
from flask import Blueprint, render_template, request, redirect, session
+# models.py
from flask_sqlalchemy import SQLAlchemy
-from dataclasses import dataclass
+from sqlalchemy.orm import DeclarativeBase, MappedAsDataclass, Mapped, mapped_column
from auth import get_current_user
+
+# Base that adds dataclass behaviors to mapped classes
+class Base(MappedAsDataclass, DeclarativeBase):
+ pass
+
+
todo_bp = Blueprint('todo', __name__)
-db = SQLAlchemy()
+db = SQLAlchemy(model_class=Base)
-@dataclass
class Todo(db.Model):
- id: int
- task: str
- done: bool
- user_id: str
-
- __tablename__ = 'todos'
+ __tablename__ = "todos"
- id = db.Column(db.Integer, primary_key=True)
- task = db.Column(db.String(200), nullable=False)
- done = db.Column(db.Boolean, default=False)
- user_id = db.Column(db.String(100), nullable=False)
+ 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('/')
From 4d9483d05abb8042a8583f19709b645acf0cb12f Mon Sep 17 00:00:00 2001
From: Mr Eggleton
Date: Wed, 14 Jan 2026 13:38:47 +0000
Subject: [PATCH 15/25] Add admin interface with authentication and update
requirements
- Implemented AuthenticatedAdminIndexView and AuthenticatedModelView for secured admin access.
- Initialized admin interface in app.py.
- Added freeze.txt for dependency management.
- Updated requirements.txt to include flask-admin with necessary extras.
- Minor formatting adjustments in todo.py.
---
admin.py | 32 +++++++++++
app.py | 6 +-
freeze.txt | 145 +++++++++++++++++++++++++++++++++++++++++++++++
requirements.txt | 4 +-
todo.py | 1 +
5 files changed, 186 insertions(+), 2 deletions(-)
create mode 100644 admin.py
create mode 100644 freeze.txt
diff --git a/admin.py b/admin.py
new file mode 100644
index 0000000..be8e3a3
--- /dev/null
+++ b/admin.py
@@ -0,0 +1,32 @@
+from flask import redirect, request, url_for
+from flask_admin import Admin, AdminIndexView
+from flask_admin.contrib.sqla import ModelView
+from flask_babel import Babel
+from auth import get_current_user
+
+
+class AuthenticatedAdminIndexView(AdminIndexView):
+ def is_accessible(self):
+ return get_current_user() is not None
+
+ def inaccessible_callback(self, name, **kwargs):
+ return redirect(url_for('auth.login', next=request.url))
+
+
+class AuthenticatedModelView(ModelView):
+ def is_accessible(self):
+ return get_current_user() is not None
+
+ def inaccessible_callback(self, name, **kwargs):
+ return redirect(url_for('auth.login', next=request.url))
+
+
+def init_admin(app, db, model):
+ """Attach Babel and register secured admin views for the given model."""
+ Babel(app, locale_selector=lambda: 'en')
+ admin = Admin(app, name="Admin", template_mode="bootstrap4",
+ index_view=AuthenticatedAdminIndexView())
+ admin.add_view(AuthenticatedModelView(model, db.session,
+ endpoint="todo_admin",
+ name="Todos"))
+ return admin
diff --git a/app.py b/app.py
index 47ca576..89d09fd 100644
--- a/app.py
+++ b/app.py
@@ -7,6 +7,9 @@
from auth import auth_bp, auth0_bp, github_bp, github_auth_bp
from auth import is_codespaces, is_render
from todo import todo_bp, init_app as init_todo
+from todo import db, Todo
+from admin import init_admin
+
SITE = {
"WebsiteName": "TodoApp",
@@ -55,7 +58,8 @@ def inject_dict_for_all_templates():
# Use AUTH0_CALLBACK_URL from environment (for both local and production)
redirect_uri = os.getenv('AUTH0_CALLBACK_URL', 'http://localhost:5000/callback')
-# Use auth0_callback_url in your Auth0 configuration
+# Initialize admin interface (secured)
+init_admin(app, db, Todo)
if __name__ == '__main__':
app.run(debug=True)
diff --git a/freeze.txt b/freeze.txt
new file mode 100644
index 0000000..97f331f
--- /dev/null
+++ b/freeze.txt
@@ -0,0 +1,145 @@
+anyio==4.11.0
+argon2-cffi==25.1.0
+argon2-cffi-bindings==25.1.0
+arrow==1.4.0
+asttokens==3.0.0
+async-lru==2.0.5
+attrs==25.4.0
+babel==2.17.0
+beautifulsoup4==4.14.2
+bleach==6.3.0
+blinker==1.9.0
+boto3==1.42.27
+botocore==1.42.27
+certifi==2025.10.5
+cffi==2.0.0
+charset-normalizer==3.4.4
+click==8.3.1
+colorama==0.4.6
+comm==0.2.3
+contourpy==1.3.3
+cycler==0.12.1
+debugpy==1.8.17
+decorator==5.2.1
+defusedxml==0.7.1
+executing==2.2.1
+fastjsonschema==2.21.2
+filelock==3.19.1
+Flask==2.3.2
+Flask-Admin==2.0.2
+flask-babel==4.0.0
+Flask-Dance==7.0.0
+Flask-SQLAlchemy==3.1.1
+fonttools==4.60.1
+fqdn==1.5.1
+fsspec==2025.9.0
+gitdb==4.0.12
+GitPython==3.1.45
+greenlet==3.2.4
+gunicorn==21.2.0
+h11==0.16.0
+httpcore==1.0.9
+httpx==0.28.1
+idna==3.11
+ipykernel==7.1.0
+ipython==9.7.0
+ipython_pygments_lexers==1.1.1
+isoduration==20.11.0
+itsdangerous==2.2.0
+jedi==0.19.2
+Jinja2==3.1.6
+jmespath==1.0.1
+joblib==1.5.2
+json5==0.12.1
+jsonpointer==3.0.0
+jsonschema==4.25.1
+jsonschema-specifications==2025.9.1
+jupyter-events==0.12.0
+jupyter-lsp==2.3.0
+jupyter-server-mathjax==0.2.6
+jupyter_client==8.6.3
+jupyter_core==5.9.1
+jupyter_server==2.17.0
+jupyter_server_terminals==0.5.3
+jupyterlab==4.4.10
+jupyterlab_git==0.51.2
+jupyterlab_pygments==0.3.0
+jupyterlab_server==2.28.0
+kiwisolver==1.4.9
+lark==1.3.1
+MarkupSafe==3.0.3
+matplotlib==3.10.7
+matplotlib-inline==0.2.1
+mistune==3.1.4
+mpmath==1.3.0
+narwhals==2.10.2
+nbclient==0.10.2
+nbconvert==7.16.6
+nbdime==4.0.2
+nbformat==5.10.4
+nest-asyncio==1.6.0
+networkx==3.5
+notebook_shim==0.2.4
+numpy==2.3.4
+oauthlib==3.3.1
+packaging==25.0
+pandas==2.3.3
+pandocfilters==1.5.1
+parso==0.8.5
+pexpect==4.9.0
+pillow==12.0.0
+platformdirs==4.5.0
+plotly==6.4.0
+prometheus_client==0.23.1
+prompt_toolkit==3.0.52
+psutil==7.1.3
+psycopg2-binary==2.9.11
+ptyprocess==0.7.0
+pure_eval==0.2.3
+pycparser==2.23
+Pygments==2.19.2
+pyparsing==3.2.5
+python-dateutil==2.9.0.post0
+python-dotenv==1.0.0
+python-json-logger==4.0.0
+pytz==2025.2
+PyYAML==6.0.3
+pyzmq==27.1.0
+referencing==0.37.0
+requests==2.32.5
+requests-oauthlib==2.0.0
+rfc3339-validator==0.1.4
+rfc3986-validator==0.1.1
+rfc3987-syntax==1.1.0
+rpds-py==0.28.0
+s3transfer==0.16.0
+scikit-learn==1.7.2
+scipy==1.16.3
+seaborn==0.13.2
+Send2Trash==1.8.3
+setuptools==80.9.0
+six==1.17.0
+smmap==5.0.2
+sniffio==1.3.1
+soupsieve==2.8
+SQLAlchemy==2.0.44
+stack-data==0.6.3
+sympy==1.14.0
+tablib==3.9.0
+terminado==0.18.1
+threadpoolctl==3.6.0
+tinycss2==1.4.0
+torch==2.9.0+cpu
+tornado==6.5.2
+traitlets==5.14.3
+typing_extensions==4.15.0
+tzdata==2025.2
+uri-template==1.3.0
+urllib3==2.5.0
+URLObject==3.0.0
+wcwidth==0.2.14
+webcolors==25.10.0
+webencodings==0.5.1
+websocket-client==1.9.0
+Werkzeug==3.1.4
+WTForms==3.2.1
diff --git a/requirements.txt b/requirements.txt
index f8efd1d..45a1f4f 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -3,4 +3,6 @@ Flask==2.3.2
Flask-SQLAlchemy==3.1.1
Flask-Dance==7.0.0
gunicorn==21.2.0
-python-dotenv==1.0.0
\ No newline at end of file
+python-dotenv==1.0.0
+flask-admin[sqlalchemy,s3,images,export,translation]==1.6.1
+setuptools<81
\ No newline at end of file
diff --git a/todo.py b/todo.py
index 299a7ed..e8f2e97 100644
--- a/todo.py
+++ b/todo.py
@@ -1,5 +1,6 @@
# todo.py - todo functionality
from flask import Blueprint, render_template, request, redirect, session
+
# models.py
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.orm import DeclarativeBase, MappedAsDataclass, Mapped, mapped_column
From 1f4d3885664ae256a3c6d532daaceafc5b204537 Mon Sep 17 00:00:00 2001
From: Mr Eggleton
Date: Wed, 14 Jan 2026 14:03:04 +0000
Subject: [PATCH 16/25] Refactor README for improved formatting and clarity
---
README.md | 31 ++++++++++++++++++-------------
1 file changed, 18 insertions(+), 13 deletions(-)
diff --git a/README.md b/README.md
index 150f89d..4b6191d 100644
--- a/README.md
+++ b/README.md
@@ -18,22 +18,27 @@
- Ready for Render deployment
- GitHub Actions CI/CD
-->
+
### Flask
- - Webserver with routing (a function for each url endpoint)
- - Jinja templates for looping though and outputting data.
- - todo.py contains the endpoints for the Todo app
+
+- Webserver with routing (a function for each url endpoint)
+- Jinja templates for looping though and outputting data.
+- todo.py contains the endpoints for the Todo app
### SQLAlchemy & SQLite / PostgreSQL
- - SQL Database
- - Managed by SQLAlchemy an Object Relationship Manager 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, start with SQLite but you can move PostgreSQL or others when you are ready.
- - todo.py includes the Todo class that provdes all you need for the building of the database and all the CRUD.
+
+- SQL Database
+- Managed by SQLAlchemy an Object Relationship Manager 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, start with SQLite but you can move PostgreSQL or others when you are ready.
+- todo.py includes the Todo class that provdes all you need for the building of the database and all the CRUD.
### Authentication (GitHub + Auth0)
- - GitHub OAuth (Flask-Dance) for local Windows development
- - Auth0 OAuth for Codespaces and Render production
+
+- GitHub OAuth (Flask-Dance) for local Windows development
+- Auth0 OAuth for Codespaces and Render production
### Render & Github Actions
+
- Ready for Render deployment
- GitHub Actions CI/CD
@@ -42,12 +47,14 @@
### Clone the Repository
**Using Git Command Line:**
+
```bash
git clone https://github.com/stretchyboy/python-todo.git
cd python-todo
```
**Using GitHub Desktop:**
+
1. Open GitHub Desktop
2. Click `File` → `Clone repository`
3. Select the `URL` tab
@@ -71,12 +78,11 @@ cp .env.example .env
Open `.env.example` and save as `.env`
-
## Environment Configuration (.env)
Create a `.env` file in the root directory with the following variables:
-```
+```bash
APP_SECRET_KEY=your-secret-key-here
GITHUB_CLIENT_ID=your-github-client-id
GITHUB_CLIENT_SECRET=your-github-client-secret
@@ -129,7 +135,7 @@ For local Windows development with GitHub Desktop:
Start the Flask development server:
```bash
-python -m flask run
+py -m flask run --host=localhost --port=5000 #it maybe python3 on your machine
```
The app will be available at [http://localhost:5000](http://localhost:5000)
@@ -163,4 +169,3 @@ See [CODESPACES_SETUP.md](CODESPACES_SETUP.md) for complete GitHub Codespaces se
## Deployment on Render
See [RENDER_SETUP.md](RENDER_SETUP.md) for complete Render deployment instructions, including setup, configuration, environment variables, and continuous deployment.
-
From 93a3817d022104376f3af827685fc7da78263a54 Mon Sep 17 00:00:00 2001
From: Mr Eggleton
Date: Wed, 14 Jan 2026 14:03:15 +0000
Subject: [PATCH 17/25] Add flask_babel to requirements for
internationalization support
---
requirements.txt | 1 +
1 file changed, 1 insertion(+)
diff --git a/requirements.txt b/requirements.txt
index 45a1f4f..3c58234 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -4,5 +4,6 @@ Flask-SQLAlchemy==3.1.1
Flask-Dance==7.0.0
gunicorn==21.2.0
python-dotenv==1.0.0
+flask_babel==3.1.0
flask-admin[sqlalchemy,s3,images,export,translation]==1.6.1
setuptools<81
\ No newline at end of file
From 6007c9b10bf9f39ecd09224e5e08db4d4b06d3cb Mon Sep 17 00:00:00 2001
From: Mr Eggleton
Date: Wed, 14 Jan 2026 14:05:12 +0000
Subject: [PATCH 18/25] Fix formatting in README for clarity in Flask run
command
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 4b6191d..c7fc449 100644
--- a/README.md
+++ b/README.md
@@ -135,7 +135,7 @@ For local Windows development with GitHub Desktop:
Start the Flask development server:
```bash
-py -m flask run --host=localhost --port=5000 #it maybe python3 on your machine
+py -m flask run --host=localhost --port=5000 # it maybe python3 on your machine
```
The app will be available at [http://localhost:5000](http://localhost:5000)
From b88ab2c3b0dc8ebcc05739ae203401292b1a1364 Mon Sep 17 00:00:00 2001
From: Mr Eggleton
Date: Wed, 14 Jan 2026 14:49:04 +0000
Subject: [PATCH 19/25] Adding catergories exercise
---
.vscode/extensions.json | 3 +-
ADDING_CATERGORIES.md | 300 ++++++++++++++++++++++++++++++++++++++++
README.md | 3 +
3 files changed, 305 insertions(+), 1 deletion(-)
create mode 100644 ADDING_CATERGORIES.md
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
index 8c9cbc8..d6ad130 100644
--- a/.vscode/extensions.json
+++ b/.vscode/extensions.json
@@ -3,6 +3,7 @@
"recommendations": [
"ms-python.python",
"ms-python.vscode-pylance",
- "qwtel.sqlite-viewer"
+ "qwtel.sqlite-viewer",
+ "mermaidchart.vscode-mermaid-chart"
]
}
diff --git a/ADDING_CATERGORIES.md b/ADDING_CATERGORIES.md
new file mode 100644
index 0000000..aca93f6
--- /dev/null
+++ b/ADDING_CATERGORIES.md
@@ -0,0 +1,300 @@
+# How to Add Categories to Your Todo App
+
+This guide will walk you through adding categories (like "Urgent" and "Non-urgent") to your todo app. Follow each step carefully and copy the code exactly as shown.
+
+## What We're Building
+
+We're adding 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.
+
+```mermaid
+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
+ }
+```
+
+---
+
+## Step 1: Update `todo.py` - Add the Category Model
+
+### 1.1: Add ForeignKey import
+Find this line near the top of `todo.py`:
+```python
+from sqlalchemy.orm import DeclarativeBase, MappedAsDataclass, Mapped, mapped_column
+```
+
+Change it to:
+```python
+from sqlalchemy.orm import DeclarativeBase, MappedAsDataclass, Mapped, mapped_column
+from sqlalchemy import ForeignKey
+```
+
+### 1.2: Add the Category class
+Find the line that says `todo_bp = Blueprint('todo', __name__)`.
+
+**Just BEFORE** that line, add this new class:
+```python
+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):
+ return self.name
+
+
+```
+
+**Important:** Make sure to leave a blank line after `pass` and before `todo_bp = Blueprint`.
+
+### 1.3: Update the Todo class
+Find the `Todo` class. It should look like this:
+```python
+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)
+```
+
+Add a new line after `user_id` to add the category field:
+```python
+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)
+```
+
+---
+
+## Step 2: Update Routes in `todo.py`
+
+### 2.1: Update the home() function
+Find the `home()` function and change it to pass categories to the template:
+
+**Old code:**
+```python
+@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)
+```
+
+**New code:**
+```python
+@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)
+```
+
+### 2.2: Update the add() function
+Find the `add()` function and change it to capture the category:
+
+**Old code:**
+```python
+@todo_bp.route('/add', methods=['POST'])
+def add():
+ if 'user_id' not in session:
+ return redirect('/')
+ task_text = request.form['task']
+ new_task = Todo(task=task_text, done=False, user_id=session['user_id'])
+ db.session.add(new_task)
+ db.session.commit()
+ return redirect('/')
+```
+
+**New code:**
+```python
+@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('/')
+```
+
+### 2.3: Update the init_app() function
+Find the `init_app()` function at the bottom of `todo.py`. Add code to seed the initial categories:
+
+**Old code:**
+```python
+def init_app(app):
+ db.init_app(app)
+ with app.app_context():
+ db.create_all()
+```
+
+**New code:**
+```python
+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()
+```
+
+---
+
+## Step 3: Update `templates/index.html` - Add Category Dropdown
+
+Find the form in `index.html`:
+
+**Old code:**
+```html
+
+```
+
+**New code:**
+```html
+
+```
+
+---
+
+## Step 4: Update `app.py` - Import Category
+
+Find this line near the top of `app.py`:
+```python
+from todo import todo_bp, init_app as init_todo
+from todo import db, Todo
+```
+
+Change it to:
+```python
+from todo import todo_bp, init_app as init_todo
+from todo import db, Todo, Category
+```
+
+Then find this line near the bottom:
+```python
+init_admin(app, db, Todo)
+```
+
+Change it to:
+```python
+init_admin(app, db, Todo, Category)
+```
+
+---
+
+## Step 5: Update `admin.py` - Add Category Admin View
+
+Find the `init_admin()` function in `admin.py`:
+
+**Old code:**
+```python
+def init_admin(app, db, model):
+ """Attach Babel and register secured admin views for the given model."""
+ Babel(app, locale_selector=lambda: 'en')
+ admin = Admin(app, name="Admin", template_mode="bootstrap4",
+ index_view=AuthenticatedAdminIndexView())
+ admin.add_view(AuthenticatedModelView(model, db.session,
+ endpoint="todo_admin",
+ name="Todos"))
+ return admin
+```
+
+**New code:**
+```python
+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 admin
+```
+
+---
+
+## Step 6: Reset Your Database
+
+Because you've changed the database structure, you need to delete the old database:
+
+1. Stop your Flask app if it's running (press Ctrl+C in the terminal)
+2. Delete the database file from teh instance folder.
+
+Restart your Flask app:
+```bash
+py -m flask run --host=localhost --port=5000 # it maybe python3 on your machine
+```
+
+The app will create a new database with the "Urgent" and "Non-urgent" categories automatically.
+
+---
+
+## Testing Your Changes
+
+1. Go to the home page - you should see a dropdown to select a category when adding a task
+2. Add a task with a category selected
+3. Log in and go to `/admin/` to see the Categories section where you can add, edit, or delete categories
+
+---
+
+## Summary of Changes
+
+- **Created** a new `Category` model
+- **Added** a foreign key relationship from `Todo` to `Category`
+- **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
\ No newline at end of file
diff --git a/README.md b/README.md
index c7fc449..dfa3861 100644
--- a/README.md
+++ b/README.md
@@ -158,8 +158,11 @@ The database file is stored in `/instance/todo.db`
## Your Development
+Try [ADDING_CATERGORIES.md](ADDING_CATERGORIES.md) to add one-to-many relationship and Catergories for the tasks.
+Then what could you make with the same ideas but different entities (things)?
+Books and People could make a library etc ....
## Codespaces Setup
From 247f7c9c6a754b2172552615bb8d2472453c71c0 Mon Sep 17 00:00:00 2001
From: Mr Eggleton
Date: Thu, 15 Jan 2026 09:04:56 +0000
Subject: [PATCH 20/25] Add initial sample Todo item for new users and fix the
userid creation for github only login
---
auth/github.py | 2 +-
todo.py | 6 ++++++
2 files changed, 7 insertions(+), 1 deletion(-)
diff --git a/auth/github.py b/auth/github.py
index dace956..2922f08 100644
--- a/auth/github.py
+++ b/auth/github.py
@@ -20,7 +20,7 @@ def get_github_user():
data = session["github"]
# Prefix user id for compatibility
user = {
- "id": f"github:{data['id']}",
+ "id": f"github|{data['id']}",
"name": data["login"],
"email": data.get("email", "")
}
diff --git a/todo.py b/todo.py
index e8f2e97..d9b460c 100644
--- a/todo.py
+++ b/todo.py
@@ -4,6 +4,7 @@
# models.py
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.orm import DeclarativeBase, MappedAsDataclass, Mapped, mapped_column
+from sqlalchemy import ForeignKey
from auth import get_current_user
@@ -68,3 +69,8 @@ def init_app(app):
db.init_app(app)
with app.app_context():
db.create_all()
+
+ if Todo.query.count() == 0:
+ mreggleton = Todo(task="Mr Eggleton checking your Todo App!", done=False, user_id="github|5987806")
+ db.session.add(mreggleton)
+ db.session.commit()
\ No newline at end of file
From c5911503487561e447bdbc715786ac77a8f1fc31 Mon Sep 17 00:00:00 2001
From: Mr Eggleton
Date: Thu, 15 Jan 2026 10:46:54 +0000
Subject: [PATCH 21/25] Improve the documention
---
ADDING_CATERGORIES.md | 52 ++++++++++++++++++++-------------
CODESPACES_SETUP.md | 2 +-
README.md | 68 +++++++++++++++----------------------------
RENDER_SETUP.md | 2 +-
4 files changed, 57 insertions(+), 67 deletions(-)
diff --git a/ADDING_CATERGORIES.md b/ADDING_CATERGORIES.md
index aca93f6..863d4ce 100644
--- a/ADDING_CATERGORIES.md
+++ b/ADDING_CATERGORIES.md
@@ -30,22 +30,10 @@ erDiagram
## Step 1: Update `todo.py` - Add the Category Model
-### 1.1: Add ForeignKey import
-Find this line near the top of `todo.py`:
-```python
-from sqlalchemy.orm import DeclarativeBase, MappedAsDataclass, Mapped, mapped_column
-```
+### 1.1: Add the Category class
+Find the line that says `db = SQLAlchemy(model_class=Base)`.
-Change it to:
-```python
-from sqlalchemy.orm import DeclarativeBase, MappedAsDataclass, Mapped, mapped_column
-from sqlalchemy import ForeignKey
-```
-
-### 1.2: Add the Category class
-Find the line that says `todo_bp = Blueprint('todo', __name__)`.
-
-**Just BEFORE** that line, add this new class:
+**Just AFTER** that line, add this new class:
```python
class Category(db.Model):
__tablename__ = "categories"
@@ -53,15 +41,13 @@ class Category(db.Model):
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):
+ def __repr__(self): # When you try to print or put this object in a template represent it as it's name
return self.name
```
-**Important:** Make sure to leave a blank line after `pass` and before `todo_bp = Blueprint`.
-
-### 1.3: Update the Todo class
+### 1.2: Update the Todo class
Find the `Todo` class. It should look like this:
```python
class Todo(db.Model):
@@ -73,7 +59,7 @@ class Todo(db.Model):
done: Mapped[bool] = mapped_column(db.Boolean, default=False)
```
-Add a new line after `user_id` to add the category field:
+Add a new line after `user_id` to add the category field. And add a new function / method which will make todo.catergory return the Catergory object that is linked by the catergory_id Foreign Key:
```python
class Todo(db.Model):
__tablename__ = "todos"
@@ -83,6 +69,10 @@ class Todo(db.Model):
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)
```
---
@@ -158,6 +148,11 @@ def init_app(app):
db.init_app(app)
with app.app_context():
db.create_all()
+
+ if Todo.query.count() == 0:
+ mreggleton = Todo(task="Mr Eggleton checking your Todo App!", done=False, user_id="github|5987806")
+ db.session.add(mreggleton)
+ db.session.commit()
```
**New code:**
@@ -173,6 +168,11 @@ def init_app(app):
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()
```
---
@@ -203,6 +203,18 @@ Find the form in `index.html`:
```
+Find the task text being printed out and add the
+
+**Old code:**
+```html
+ {{ todo.task }}
+```
+
+**New code:**
+```html
+ {{ todo.task }} [{{ todo.category }}]
+```
+
---
## Step 4: Update `app.py` - Import Category
diff --git a/CODESPACES_SETUP.md b/CODESPACES_SETUP.md
index 67b4632..cdc1fe7 100644
--- a/CODESPACES_SETUP.md
+++ b/CODESPACES_SETUP.md
@@ -7,7 +7,7 @@ GitHub Codespaces is a flexible cloud-based development environment. Your editor
### Create a New Codespace
1. **Go to the Repository**
- - Navigate to [https://github.com/stretchyboy/python-todo](https://github.com/stretchyboy/python-todo) (or your fork)
+ - Navigate to [https://github.com/mr-eggleton/python-flask-todo](https://github.com/mr-eggleton/python-flask-todo) (or your fork)
2. **Create a Codespace**
- Click the green **"Code"** button
diff --git a/README.md b/README.md
index dfa3861..da73eee 100644
--- a/README.md
+++ b/README.md
@@ -4,43 +4,37 @@


+A simple Python Todo Web App to do some improvments on and be a starting point for your own more apps.
+
+
## Features
-
### Flask
-- Webserver with routing (a function for each url endpoint)
-- Jinja templates for looping though and outputting data.
+- [Flask](https://flask.palletsprojects.com/en/stable/) based Python Webserver with routing (a function for each url endpoint users can visit)
+- HTML / [Jinja templates](https://jinja.palletsprojects.com/en/stable/templates/) for looping though and outputting data.
- todo.py contains the endpoints for the Todo app
### SQLAlchemy & SQLite / PostgreSQL
-- SQL Database
-- Managed by SQLAlchemy an Object Relationship Manager 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, start with SQLite but you can move PostgreSQL or others when you are ready.
-- todo.py includes the Todo class that provdes all you need for the building of the database and all the CRUD.
+- SQL Databases the modern way
+- Managed by [SQLAlchemy](https://www.sqlalchemy.org/) an ORM / [Object Relationship Mapper](https://en.wikipedia.org/wiki/Object%E2%80%93relational_mapping) which allows you to write classes that define the data and provides the storage & [CRUD](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete) 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 proffesional systems like PostgreSQL or others when you are ready.
+- todo.py includes the Todo class that provdes all you need for the building of the database and all the [CRUD](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete).
### Authentication (GitHub + Auth0)
+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 )
+
- GitHub OAuth (Flask-Dance) for local Windows development
- Auth0 OAuth for Codespaces and Render production
### Render & Github Actions
-- Ready for Render deployment
-- GitHub Actions CI/CD
+- Ready for Render deployment so you can publish and use the site 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 daatbase server (but there are some other )
## Setup
@@ -49,8 +43,8 @@
**Using Git Command Line:**
```bash
-git clone https://github.com/stretchyboy/python-todo.git
-cd python-todo
+git clone https://github.com/mr-eggleton/python-flask-todo.git
+cd python-flask-todo
```
**Using GitHub Desktop:**
@@ -58,7 +52,7 @@ cd python-todo
1. Open GitHub Desktop
2. Click `File` → `Clone repository`
3. Select the `URL` tab
-4. Enter: `https://github.com/stretchyboy/python-todo.git`
+4. Enter: `https://github.com/mr-eggleton/python-flask-todo.git`
5. Choose a local path and click `Clone`
### Install Dependencies
@@ -74,30 +68,13 @@ py -m pip install -r requirements.txt
cp .env.example .env
```
-### On Windows in VS Code
+#### On Windows in VS Code
Open `.env.example` and save as `.env`
## Environment Configuration (.env)
-Create a `.env` file in the root directory with the following variables:
-
-```bash
-APP_SECRET_KEY=your-secret-key-here
-GITHUB_CLIENT_ID=your-github-client-id
-GITHUB_CLIENT_SECRET=your-github-client-secret
-AUTH0_DOMAIN=your-auth0-domain.auth0.com
-AUTH0_CLIENT_ID=your-auth0-client-id
-AUTH0_CLIENT_SECRET=your-auth0-client-secret
-AUTH0_CALLBACK_URL=http://localhost:5000/callback
-OAUTHLIB_INSECURE_TRANSPORT=1
-```
-
-### Generate APP_SECRET_KEY
-
-```bash
-python -c "import secrets; print(secrets.token_hex(32))"
-```
+Edit the file to put the details you need in. But @ UTC Sheffield OLP, Mr Eggleton will give you a .env file that will work with github, and you don't need to do the "Authentication Setup" and you can skip to ['Running the Application'](#running-the-application)
## Authentication Setup
@@ -152,13 +129,14 @@ The database file is stored in `/instance/todo.db`
- 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
-- Storing any user data in a database (other than an id from github ). To have users on this system to store any other PII refer to [https://flask-dance.readthedocs.io/en/latest/storages.html#sqlalchemy](https://flask-dance.readthedocs.io/en/latest/storages.html#sqlalchemy) and change the privacy statement.
+- 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](https://flask-dance.readthedocs.io/en/latest/storages.html#sqlalchemy) and change the privacy statement.
- Adding extra security [https://flask-security.readthedocs.io/en/stable/quickstart.html#basic-flask-sqlalchemy-application](https://flask-security.readthedocs.io/en/stable/quickstart.html#basic-flask-sqlalchemy-application)
- Testing. There are no tests in this code.
## Your Development
-Try [ADDING_CATERGORIES.md](ADDING_CATERGORIES.md) to add one-to-many relationship and Catergories for the tasks.
+Try [ADDING_CATERGORIES.md](ADDING_CATERGORIES.md) to add a one-to-many relationship and Categories for the tasks.
Then what could you make with the same ideas but different entities (things)?
diff --git a/RENDER_SETUP.md b/RENDER_SETUP.md
index 9259767..fbf910a 100644
--- a/RENDER_SETUP.md
+++ b/RENDER_SETUP.md
@@ -74,7 +74,7 @@ Ensure your repository contains:
**Important:** Do NOT set `RENDER_EXTERNAL_HOSTNAME` manually. Render sets this automatically, but it's only available at runtime, not during build.
- **For Auth0 Configuration:** Use your actual Render app URL (e.g., `https://python-todo.onrender.com`) in Auth0 settings, not the variable name.
+ **For Auth0 Configuration:** Use your actual Render app URL (e.g., `https://python-flask-todo.onrender.com`) in Auth0 settings, not the variable name.
3. **Automatic Deployment**
- Render will automatically build and deploy your application
From 35a87858459852cabcc5bb2b342cc7c8b1fab27c Mon Sep 17 00:00:00 2001
From: Mr Eggleton
Date: Thu, 15 Jan 2026 15:12:37 +0000
Subject: [PATCH 22/25] Update .gitignore and improve README for use as a slide
show
---
.gitignore | 4 ++++
README.md | 68 ++++++++++++++++++++++++++++++++++++++----------------
2 files changed, 52 insertions(+), 20 deletions(-)
diff --git a/.gitignore b/.gitignore
index b7faf40..0882edc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -205,3 +205,7 @@ cython_debug/
marimo/_static/
marimo/_lsp/
__marimo__/
+
+
+# Obsidian-Vault
+.obsidian/
\ No newline at end of file
diff --git a/README.md b/README.md
index da73eee..39d44d0 100644
--- a/README.md
+++ b/README.md
@@ -4,8 +4,9 @@


-A simple Python Todo Web App to do some improvments on and be a starting point for your own more apps.
+A simple Python Todo Web App to do some improvements on and be a starting point for your own simple web apps.
+---
## Features
@@ -15,13 +16,21 @@ A simple Python Todo Web App to do some improvments on and be a starting point f
- HTML / [Jinja templates](https://jinja.palletsprojects.com/en/stable/templates/) for looping though and outputting data.
- todo.py contains the endpoints for the Todo app
+---
+
### SQLAlchemy & SQLite / PostgreSQL
-- SQL Databases the modern way
+- SQL Databases the modern way
- Managed by [SQLAlchemy](https://www.sqlalchemy.org/) an ORM / [Object Relationship Mapper](https://en.wikipedia.org/wiki/Object%E2%80%93relational_mapping) which allows you to write classes that define the data and provides the storage & [CRUD](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete) for you.
+
+--
+### [Object Relationship Mapper](https://en.wikipedia.org/wiki/Object%E2%80%93relational_mapping)
+
- 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 proffesional systems like PostgreSQL or others when you are ready.
-- todo.py includes the Todo class that provdes all you need for the building of the database and all the [CRUD](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete).
+- todo.py includes the Todo class that provdes all you need for the building of the database and all the [CRUD](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete).
+
+---
### Authentication (GitHub + Auth0)
@@ -30,22 +39,23 @@ Authentication is the act of proving who you are, in this system we use external
- GitHub OAuth (Flask-Dance) for local Windows development
- Auth0 OAuth for Codespaces and Render production
+---
+
### Render & Github Actions
-- Ready for Render deployment so you can publish and use the site for free (there are some speed limitations)
+- Ready for [Render](https://render.com/) 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 daatbase server (but there are some other )
+---
+
## Setup
-### Clone the Repository
+### Start from the Template
-**Using Git Command Line:**
+1. Go to the github repository:
-```bash
-git clone https://github.com/mr-eggleton/python-flask-todo.git
-cd python-flask-todo
-```
+### Clone the Repository
**Using GitHub Desktop:**
@@ -54,27 +64,34 @@ cd python-flask-todo
3. Select the `URL` tab
4. Enter: `https://github.com/mr-eggleton/python-flask-todo.git`
5. Choose a local path and click `Clone`
+6. Click 'Open in Visual Studio Code' to open the project in VS Code
-### Install Dependencies
+---
+
+**Using Git Command Line:**
```bash
-py -m pip install -r requirements.txt
+git clone https://github.com/mr-eggleton/python-flask-todo.git
+cd python-flask-todo
```
-### Copy Example Environment File
+---
+
+### Install Dependencies
```bash
-# On linux or codespaces
-cp .env.example .env
+py -m pip install -r requirements.txt # You'll need python3 ... in linux
```
-#### On Windows in VS Code
+---
-Open `.env.example` and save as `.env`
+### Environment Configuration (.env)
-## Environment Configuration (.env)
+In VS Code open `.env.example` and save it as `.env`
-Edit the file to put the details you need in. But @ UTC Sheffield OLP, Mr Eggleton will give you a .env file that will work with github, and you don't need to do the "Authentication Setup" and you can skip to ['Running the Application'](#running-the-application)
+Edit the file to put the details you need in. But @ UTC Sheffield OLP, Mr Eggleton will give you a .env file that will work with github, and you don't need to do the "Authentication Setup" as you are using his and you can skip to ['Running the Application'](#running-the-application)
+
+---
## Authentication Setup
@@ -88,6 +105,8 @@ This app automatically detects your environment and uses the appropriate authent
The app checks for Codespaces environment variables (`CODESPACES`, `CODESPACE_NAME`) and routes accordingly.
+---
+
### GitHub OAuth Setup (Local Development)
For local Windows development with GitHub Desktop:
@@ -107,6 +126,8 @@ For local Windows development with GitHub Desktop:
3. **Enable Insecure Transport for Local Dev**
- Set `OAUTHLIB_INSECURE_TRANSPORT=1` in `.env` (only for local development)
+---
+
## Running the Application
Start the Flask development server:
@@ -117,6 +138,8 @@ py -m flask run --host=localhost --port=5000 # it maybe python3 on your machine
The app will be available at [http://localhost:5000](http://localhost:5000)
+---
+
## The Database
This code uses [SQLAlchemy](https://www.sqlalchemy.org/) to set up classes that have methods to talk to many [databases](https://docs.sqlalchemy.org/en/20/dialects/index.html). We use **SQLite for simplicity and easy local development**.
@@ -125,6 +148,8 @@ This code uses [SQLAlchemy](https://www.sqlalchemy.org/) to set up classes that
The database file is stored in `/instance/todo.db`
+---
+
## Things we are ignoring
- 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 ).
@@ -132,7 +157,9 @@ The database file is stored in `/instance/todo.db`
- 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](https://flask-dance.readthedocs.io/en/latest/storages.html#sqlalchemy) and change the privacy statement.
- Adding extra security [https://flask-security.readthedocs.io/en/stable/quickstart.html#basic-flask-sqlalchemy-application](https://flask-security.readthedocs.io/en/stable/quickstart.html#basic-flask-sqlalchemy-application)
-- Testing. There are no tests in this code.
+- Testing. There are no tests in this code, although Flask, SQL Alchemy and the other libraries used are thouroughly tested and are checked for security issues.
+
+---
## Your Development
@@ -142,6 +169,7 @@ Then what could you make with the same ideas but different entities (things)?
Books and People could make a library etc ....
+---
## Codespaces Setup
From 871be2f96069ec05f2886c23ea34b9cd6b2a47e6 Mon Sep 17 00:00:00 2001
From: Mr Eggleton
Date: Thu, 15 Jan 2026 15:26:00 +0000
Subject: [PATCH 23/25] Update setup documentation for clarity and consistency
across files
---
CODESPACES_SETUP.md | 4 ++--
README.md | 17 ++++++++++++++---
RENDER_SETUP.md | 2 +-
3 files changed, 17 insertions(+), 6 deletions(-)
diff --git a/CODESPACES_SETUP.md b/CODESPACES_SETUP.md
index cdc1fe7..b3c7d50 100644
--- a/CODESPACES_SETUP.md
+++ b/CODESPACES_SETUP.md
@@ -6,8 +6,8 @@ GitHub Codespaces is a flexible cloud-based development environment. Your editor
### Create a New Codespace
-1. **Go to the Repository**
- - Navigate to [https://github.com/mr-eggleton/python-flask-todo](https://github.com/mr-eggleton/python-flask-todo) (or your fork)
+1. **Go to your Repository**
+ - Navigate to your **python-flask-todo**
2. **Create a Codespace**
- Click the green **"Code"** button
diff --git a/README.md b/README.md
index 39d44d0..c9ecba8 100644
--- a/README.md
+++ b/README.md
@@ -53,7 +53,18 @@ Authentication is the act of proving who you are, in this system we use external
### Start from the Template
-1. Go to the github repository:
+1. Go to the github repository [https://github.com/UTCSheffield/python-flask-todo](https://github.com/UTCSheffield/python-flask-todo)
+2. Click the green "Use this template" button at the top of the page
+3. Select "Create a new repository"
+4. Fill in your new repository details:
+ - Choose a repository name (e.g., `python-flask-todo`)
+ - Add a description (optional)
+ - Choose Public or Private visibility
+5. Click "Create repository from template"
+6. Your new repository will be created with all the template files
+7. Now clone your new repository using one of the methods below
+
+---
### Clone the Repository
@@ -62,7 +73,7 @@ Authentication is the act of proving who you are, in this system we use external
1. Open GitHub Desktop
2. Click `File` → `Clone repository`
3. Select the `URL` tab
-4. Enter: `https://github.com/mr-eggleton/python-flask-todo.git`
+4. Enter: `https://github.com/UTCSheffield/python-flask-todo.git`
5. Choose a local path and click `Clone`
6. Click 'Open in Visual Studio Code' to open the project in VS Code
@@ -71,7 +82,7 @@ Authentication is the act of proving who you are, in this system we use external
**Using Git Command Line:**
```bash
-git clone https://github.com/mr-eggleton/python-flask-todo.git
+git clone https://github.com/UTCSheffield/python-flask-todo.git
cd python-flask-todo
```
diff --git a/RENDER_SETUP.md b/RENDER_SETUP.md
index fbf910a..4f0f275 100644
--- a/RENDER_SETUP.md
+++ b/RENDER_SETUP.md
@@ -49,7 +49,7 @@ Ensure your repository contains:
3. **Connect Your GitHub Repository**
- Click "Connect account" if this is your first time
- Authorize Render to access your GitHub repositories
- - Search for and select `mr-eggleton/python-flask-todo` (or your fork)
+ - Search for select your version of `python-flask-todo`
- Click "Connect"
4. **Review Blueprint Configuration**
From 3327cf9e7f425203bb2710010e691eee6a45a998 Mon Sep 17 00:00:00 2001
From: Mr Eggleton
Date: Fri, 16 Jan 2026 13:08:14 +0000
Subject: [PATCH 24/25] Lesson plan
---
.env.example | 8 +-
ADDING_CATERGORIES.md | 40 +++-
LESSON.md | 487 ++++++++++++++++++++++++++++++++++++++++++
README.md | 58 ++---
SaveAs.png | Bin 0 -> 2431 bytes
5 files changed, 560 insertions(+), 33 deletions(-)
create mode 100644 LESSON.md
create mode 100644 SaveAs.png
diff --git a/.env.example b/.env.example
index 043d9db..11505d0 100644
--- a/.env.example
+++ b/.env.example
@@ -1,7 +1,13 @@
APP_SECRET_KEY=your_flask_secret
-OAUTHLIB_INSECURE_TRANSPORT=0
DATABASE_URL=sqlite:///todo.db
+# Example environment variables for GitHub OAuth for local development
+GITHUB_CLIENT_ID=your_client_id
+GITHUB_CLIENT_SECRET=your_client_secret
+# Only needed for local/GitHub OAuth
+# OAUTHLIB_INSECURE_TRANSPORT=1
+
+
# Example environment variables for Auth0 integration
AUTH0_DOMAIN=your_auth0_domain
AUTH0_CLIENT_ID=your_client_id
diff --git a/ADDING_CATERGORIES.md b/ADDING_CATERGORIES.md
index 863d4ce..a99eeea 100644
--- a/ADDING_CATERGORIES.md
+++ b/ADDING_CATERGORIES.md
@@ -8,6 +8,8 @@ We're adding a **category system** to organize todos. Each todo must belong to o
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.
+---
+
```mermaid
erDiagram
Category ||--o{ Todo : "has many"
@@ -31,9 +33,11 @@ erDiagram
## Step 1: Update `todo.py` - Add the Category Model
### 1.1: Add the Category class
+
Find the line that says `db = SQLAlchemy(model_class=Base)`.
**Just AFTER** that line, add this new class:
+
```python
class Category(db.Model):
__tablename__ = "categories"
@@ -48,7 +52,9 @@ class Category(db.Model):
```
### 1.2: Update the Todo class
+
Find the `Todo` class. It should look like this:
+
```python
class Todo(db.Model):
__tablename__ = "todos"
@@ -59,7 +65,8 @@ class Todo(db.Model):
done: Mapped[bool] = mapped_column(db.Boolean, default=False)
```
-Add a new line after `user_id` to add the category field. And add a new function / method which will make todo.catergory return the Catergory object that is linked by the catergory_id Foreign Key:
+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:
+
```python
class Todo(db.Model):
__tablename__ = "todos"
@@ -80,9 +87,11 @@ class Todo(db.Model):
## Step 2: Update Routes in `todo.py`
### 2.1: Update the home() function
+
Find the `home()` function and change it to pass categories to the template:
**Old code:**
+
```python
@todo_bp.route('/')
def home():
@@ -95,6 +104,7 @@ def home():
```
**New code:**
+
```python
@todo_bp.route('/')
def home():
@@ -108,9 +118,11 @@ def home():
```
### 2.2: Update the add() function
+
Find the `add()` function and change it to capture the category:
**Old code:**
+
```python
@todo_bp.route('/add', methods=['POST'])
def add():
@@ -124,6 +136,7 @@ def add():
```
**New code:**
+
```python
@todo_bp.route('/add', methods=['POST'])
def add():
@@ -140,9 +153,11 @@ def add():
```
### 2.3: Update the init_app() function
+
Find the `init_app()` function at the bottom of `todo.py`. Add code to seed the initial categories:
**Old code:**
+
```python
def init_app(app):
db.init_app(app)
@@ -156,6 +171,7 @@ def init_app(app):
```
**New code:**
+
```python
def init_app(app):
db.init_app(app)
@@ -182,6 +198,7 @@ def init_app(app):
Find the form in `index.html`:
**Old code:**
+
```html
```
-Find the task text being printed out and add the
+Find the task text being printed out and add the category next to it:
**Old code:**
+
```html
{{ todo.task }}
```
**New code:**
+
```html
{{ todo.task }} [{{ todo.category }}]
```
@@ -220,23 +240,27 @@ Find the task text being printed out and add the
## Step 4: Update `app.py` - Import Category
Find this line near the top of `app.py`:
+
```python
from todo import todo_bp, init_app as init_todo
from todo import db, Todo
```
Change it to:
+
```python
from todo import todo_bp, init_app as init_todo
from todo import db, Todo, Category
```
Then find this line near the bottom:
+
```python
init_admin(app, db, Todo)
```
Change it to:
+
```python
init_admin(app, db, Todo, Category)
```
@@ -248,6 +272,7 @@ init_admin(app, db, Todo, Category)
Find the `init_admin()` function in `admin.py`:
**Old code:**
+
```python
def init_admin(app, db, model):
"""Attach Babel and register secured admin views for the given model."""
@@ -261,6 +286,7 @@ def init_admin(app, db, model):
```
**New code:**
+
```python
def init_admin(app, db, todo_model, category_model):
"""Attach Babel and register secured admin views for the given models."""
@@ -283,12 +309,12 @@ def init_admin(app, db, todo_model, category_model):
Because you've changed the database structure, you need to delete the old database:
1. Stop your Flask app if it's running (press Ctrl+C in the terminal)
-2. Delete the database file from teh instance folder.
-
+2. Delete the database file from the instance folder.
+
Restart your Flask app:
+
```bash
-py -m flask run --host=localhost --port=5000 # it maybe python3 on your machine
-```
+python3 -m flask run --host=localhost --port=5000
The app will create a new database with the "Urgent" and "Non-urgent" categories automatically.
@@ -309,4 +335,4 @@ The app will create a new database with the "Urgent" and "Non-urgent" categories
- **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
\ No newline at end of file
+- **Enabled** category management in the admin interface
diff --git a/LESSON.md b/LESSON.md
new file mode 100644
index 0000000..f7f2b61
--- /dev/null
+++ b/LESSON.md
@@ -0,0 +1,487 @@
+
+# Python as Dynamic Webserver
+
+---
+
+## Web Tech
+
+```mermaid
+mindmap
+ Web Server
+ HTML #10003;
+ Assets
+ Images #10003;
+ CSS #10003;
+ Javascript #10003;
+ Data Sources
+ Text Files #10003;
+ API
+ Database
+ Authentication
+ Types
+ Hand Built #10003;
+ Static Built #10003;
+ Dynamic
+
+```
+
+---
+## Flask Todo App Starter
+
+A simple Python Todo Web App to do some improvements on and be a starting point for your own simple web apps.
+
+## [https://github.com/UTCSheffield/python-flask-todo](https://github.com/UTCSheffield/python-flask-todo)
+
+---
+
+## Features
+
+### Flask
+
+- [Flask](https://flask.palletsprojects.com/en/stable/) based Python Webserver with routing (a function for each url endpoint users can visit)
+- HTML / [Jinja templates](https://jinja.palletsprojects.com/en/stable/templates/) for looping though and outputting data and keeping process and display seperate..
+- todo.py contains the endpoints for the Todo app
+
+---
+
+### SQLAlchemy & SQLite / PostgreSQL
+
+- SQL Databases the modern way
+- Managed by [SQLAlchemy](https://www.sqlalchemy.org/) an ORM / [Object Relationship Mapper](https://en.wikipedia.org/wiki/Object%E2%80%93relational_mapping) which allows you to write classes that define the data and provides the storage & [CRUD](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete) for you.
+
+---
+
+### [Object Relationship Mapper](https://en.wikipedia.org/wiki/Object%E2%80%93relational_mapping)
+
+- 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](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete).
+
+---
+
+### SQLAlchemy Snippet
+
+```python
+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)
+```
+
+---
+### Flask Snippet
+
+```python
+@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)
+```
+#### CRUD Admin Snippet
+
+```python
+init_admin(app, db, Todo)
+```
+
+---
+
+### Authentication (GitHub + Auth0)
+
+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
+
+---
+
+### Render & Github Actions
+
+- Ready for [Render](https://render.com/) 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)
+
+---
+
+## Setup
+
+### Start from the Template
+
+1. Login to [github.com](https://github.com/)
+2. Go to the github repository [https://github.com/UTCSheffield/python-flask-todo](https://github.com/UTCSheffield/python-flask-todo)
+3. Click the green "Use this template" button at the top of the page
+4. Select "Create a new repository"
+5. Fill in your new repository details:
+ - Choose a repository name (e.g., `python-flask-todo`)
+ - Add a description (optional)
+ - Choose Public or Private visibility
+6. Click "Create repository from template"
+7. Your new repository will be created with all the template files
+
+---
+
+### Clone your Repository locally
+
+**Using GitHub Desktop:**
+
+1. On the GitHub page for your new repository
+2. Click the green "Code" button
+3. Click "Open with GitHub Desktop"
+4. You may need to login to GitHub Desktop if you haven't already
+5. You may be prompted to choose a local path to clone the repository to
+6. Click 'Open in Visual Studio Code' to open the project in VS Code
+
+---
+
+## Top Tip
+
+Split screen with the browser with README or LESSON open on one side and VS Code on the other.
+
+---
+
+### Install Dependencies
+
+```bash
+py -m pip install -r requirements.txt # You'll need python3 ... in linux
+```
+
+---
+
+### Environment Configuration (.env)
+
+In 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,
+
+---
+
+## Running the Application
+
+Start the Flask development server:
+
+```bash
+py -m flask run --host=localhost --port=5000 # it maybe python3 on your machine
+```
+
+The app will be available at [http://localhost:5000](http://localhost:5000)
+
+Try it, login and create a few tasks!
+
+---
+
+## The Database
+
+This code uses [SQLAlchemy](https://www.sqlalchemy.org/) to set up classes that have methods to talk to many [databases](https://docs.sqlalchemy.org/en/20/dialects/index.html). We use **SQLite for simplicity and easy local development**.
+
+---
+
+### Local Development (SQLite)
+
+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?
+
+---
+
+## What We're Building Next
+
+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.
+
+```mermaid
+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
+ }
+```
+
+---
+
+## Step 1: Update `todo.py` - Add the Category Model
+
+### 1.1: Add the Category class
+
+Find the line that says `db = SQLAlchemy(model_class=Base)`.
+
+**Just AFTER** that line, add this new class:
+
+```python
+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
+
+
+```
+
+---
+
+### 1.2: Update the Todo class
+
+Find the `Todo` class. It should look like this:
+
+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:
+
+```python
+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)
+```
+
+
+---
+
+
+## Step 2: Update Routes in `todo.py`
+
+### 2.1: Update the home() function
+
+Find the `home()` function and change it to pass categories to the template:
+
+**New code:**
+
+```python
+@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)
+```
+
+---
+
+### 2.2: Update the add() function
+
+Find the `add()` function and change it to capture the category:
+
+**New code:**
+
+```python
+@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('/')
+```
+
+---
+
+### 2.3: Update the init_app() function
+
+Find the `init_app()` function at the bottom of `todo.py`. Add code to seed the initial categories:
+
+**New code:**
+
+```python
+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()
+```
+
+---
+
+## Step 3: Update `templates/index.html` - Add Category Dropdown
+
+Find the form in `index.html`:
+
+**New code:**
+
+```html
+
+```
+
+---
+
+Find the task text being printed out and add the category next to it:
+
+**New code:**
+
+```html
+ {{ todo.task }} [{{ todo.category }}]
+```
+
+---
+
+## Step 4: Update `app.py` - Import Category
+
+Find this line near the top of `app.py`:
+
+```python
+from todo import todo_bp, init_app as init_todo
+from todo import db, Todo
+```
+
+Change it to:
+
+```python
+from todo import todo_bp, init_app as init_todo
+from todo import db, Todo, Category
+```
+
+---
+
+Then find this line near the bottom:
+
+```python
+init_admin(app, db, Todo)
+```
+
+Change it to:
+
+```python
+init_admin(app, db, Todo, Category)
+```
+
+---
+
+## Step 5: Update `admin.py` - Add Category Admin View
+
+Find the `init_admin()` function in `admin.py`:
+
+**New code:**
+
+```python
+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 admin
+```
+
+---
+
+## Step 6: Reset Your Database
+
+Because you've changed the database structure, you need to delete the old database:
+
+1. Stop your Flask app if it's running (press Ctrl+C in the terminal)
+2. Delete the database file from the instance folder.
+
+Restart your Flask app:
+
+```bash
+py -m flask run --host=localhost --port=5000 # it maybe python3 on your machine
+```
+
+The app will create a new database with the "Urgent" and "Non-urgent" categories automatically.
+
+---
+
+## Testing Your Changes
+
+1. Go to the home page - you should see a dropdown to select a category when adding a task
+2. Add a task with a category selected
+3. Log in and go to [http://localhost:5000/admin/](http://localhost:5000/admin/) to see the Categories section where you can add, edit, or delete categories
+
+---
+
+## Summary of Changes
+
+- **Created** a new `Category` model
+- **Added** a foreign key relationship from `Todo` to `Category`
+- **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
+
+---
+
+## Things we are ignoring
+
+- 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.
+
+---
+## Things we are ignoring 2
+
+- 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](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.
+
+---
+
+## Your Development
+
+Then what could you make with the same ideas but different entities (things)?
+
+Books and People could make a library etc ....
diff --git a/README.md b/README.md
index c9ecba8..65cbf09 100644
--- a/README.md
+++ b/README.md
@@ -23,7 +23,7 @@ A simple Python Todo Web App to do some improvements on and be a starting point
- SQL Databases the modern way
- Managed by [SQLAlchemy](https://www.sqlalchemy.org/) an ORM / [Object Relationship Mapper](https://en.wikipedia.org/wiki/Object%E2%80%93relational_mapping) which allows you to write classes that define the data and provides the storage & [CRUD](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete) for you.
---
+---
### [Object Relationship Mapper](https://en.wikipedia.org/wiki/Object%E2%80%93relational_mapping)
- 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 .
@@ -34,7 +34,7 @@ A simple Python Todo Web App to do some improvements on and be a starting point
### Authentication (GitHub + Auth0)
-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 )
+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
@@ -45,7 +45,7 @@ Authentication is the act of proving who you are, in this system we use external
- Ready for [Render](https://render.com/) 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 daatbase server (but there are some other )
+- Can be upgraded to use a free PostgreSQL database server (but there are some other steps)
---
@@ -53,28 +53,28 @@ Authentication is the act of proving who you are, in this system we use external
### Start from the Template
-1. Go to the github repository [https://github.com/UTCSheffield/python-flask-todo](https://github.com/UTCSheffield/python-flask-todo)
-2. Click the green "Use this template" button at the top of the page
-3. Select "Create a new repository"
-4. Fill in your new repository details:
+1. Login to [github.com](https://github.com/)
+2. Go to the github repository [https://github.com/UTCSheffield/python-flask-todo](https://github.com/UTCSheffield/python-flask-todo)
+3. Click the green "Use this template" button at the top of the page
+4. Select "Create a new repository"
+5. Fill in your new repository details:
- Choose a repository name (e.g., `python-flask-todo`)
- Add a description (optional)
- Choose Public or Private visibility
-5. Click "Create repository from template"
-6. Your new repository will be created with all the template files
-7. Now clone your new repository using one of the methods below
+6. Click "Create repository from template"
+7. Your new repository will be created with all the template files
---
-### Clone the Repository
+### Clone your Repository locally
**Using GitHub Desktop:**
-1. Open GitHub Desktop
-2. Click `File` → `Clone repository`
-3. Select the `URL` tab
-4. Enter: `https://github.com/UTCSheffield/python-flask-todo.git`
-5. Choose a local path and click `Clone`
+1. On the GitHub page for your new repository
+2. Click the green "Code" button
+3. Click "Open with GitHub Desktop"
+4. You may need to login to GitHub Desktop if you haven't already
+5. You may be prompted to choose a local path to clone the repository to
6. Click 'Open in Visual Studio Code' to open the project in VS Code
---
@@ -91,16 +91,16 @@ cd python-flask-todo
### Install Dependencies
```bash
-py -m pip install -r requirements.txt # You'll need python3 ... in linux
+python3 -m pip install -r requirements.txt
```
---
### Environment Configuration (.env)
-In VS Code open `.env.example` and save it as `.env`
-
-Edit the file to put the details you need in. But @ UTC Sheffield OLP, Mr Eggleton will give you a .env file that will work with github, and you don't need to do the "Authentication Setup" as you are using his and you can skip to ['Running the Application'](#running-the-application)
+```bash
+cp .env.example .env
+```
---
@@ -144,11 +144,13 @@ For local Windows development with GitHub Desktop:
Start the Flask development server:
```bash
-py -m flask run --host=localhost --port=5000 # it maybe python3 on your machine
+python3 -m flask run --host=localhost --port=5000
```
The app will be available at [http://localhost:5000](http://localhost:5000)
+Try it, login and create a few tasks!
+
---
## The Database
@@ -159,6 +161,10 @@ This code uses [SQLAlchemy](https://www.sqlalchemy.org/) to set up classes that
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 todo.db should appear in the left hand side explorer view with a red icon.
+
+Have a look, can you see the tables and data?
+
---
## Things we are ignoring
@@ -168,7 +174,7 @@ The database file is stored in `/instance/todo.db`
- 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](https://flask-dance.readthedocs.io/en/latest/storages.html#sqlalchemy) and change the privacy statement.
- Adding extra security [https://flask-security.readthedocs.io/en/stable/quickstart.html#basic-flask-sqlalchemy-application](https://flask-security.readthedocs.io/en/stable/quickstart.html#basic-flask-sqlalchemy-application)
-- Testing. There are no tests in this code, although Flask, SQL Alchemy and the other libraries used are thouroughly tested and are checked for security issues.
+- 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.
---
@@ -182,10 +188,12 @@ Books and People could make a library etc ....
---
-## Codespaces Setup
-
-See [CODESPACES_SETUP.md](CODESPACES_SETUP.md) for complete GitHub Codespaces setup instructions.
## Deployment on Render
See [RENDER_SETUP.md](RENDER_SETUP.md) for complete Render deployment instructions, including setup, configuration, environment variables, and continuous deployment.
+
+
+## Codespaces Setup
+
+See [CODESPACES_SETUP.md](CODESPACES_SETUP.md) for complete GitHub Codespaces setup instructions.
\ No newline at end of file
diff --git a/SaveAs.png b/SaveAs.png
new file mode 100644
index 0000000000000000000000000000000000000000..857fbdd1afb252fb6c3dfc1e60e62c0c3c630d78
GIT binary patch
literal 2431
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
Gm-II@xPs&W
literal 0
HcmV?d00001
From e8ca6d194743bc26d6f336418af72a333f7217f7 Mon Sep 17 00:00:00 2001
From: Mr Eggleton
Date: Fri, 16 Jan 2026 15:22:12 +0000
Subject: [PATCH 25/25] Updated the LESSON
---
LESSON.md | 35 ++++++++++++++++++++++-------------
LESSON.pdf | Bin 0 -> 190889 bytes
2 files changed, 22 insertions(+), 13 deletions(-)
create mode 100644 LESSON.pdf
diff --git a/LESSON.md b/LESSON.md
index f7f2b61..5501d83 100644
--- a/LESSON.md
+++ b/LESSON.md
@@ -1,5 +1,5 @@
-# Python as Dynamic Webserver
+# Python as Dynamic Web Server
---
@@ -7,8 +7,22 @@
```mermaid
mindmap
- Web Server
- HTML #10003;
+ 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;
@@ -17,12 +31,6 @@ mindmap
Text Files #10003;
API
Database
- Authentication
- Types
- Hand Built #10003;
- Static Built #10003;
- Dynamic
-
```
---
@@ -59,7 +67,7 @@ A simple Python Todo Web App to do some improvements on and be a starting point
---
-### SQLAlchemy Snippet
+### SQLAlchemy Snippet
```python
class Todo(db.Model):
@@ -72,6 +80,7 @@ class Todo(db.Model):
```
---
+
### Flask Snippet
```python
@@ -85,6 +94,7 @@ def home():
todos = Todo.query.filter_by(user_id=session['user_id']).all()
return render_template('index.html', todos=todos, user=user)
```
+
#### CRUD Admin Snippet
```python
@@ -196,7 +206,7 @@ Have a look, can you see the tables and data?
---
-## What We're Building Next
+## What We're Building Next
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/`.
@@ -268,10 +278,8 @@ class Todo(db.Model):
return Category.query.get(self.category_id)
```
-
---
-
## Step 2: Update Routes in `todo.py`
### 2.1: Update the home() function
@@ -473,6 +481,7 @@ The app will create a new database with the "Urgent" and "Non-urgent" categories
- Minimal Autorisation all Authenticated users can do everything on the site.
---
+
## Things we are ignoring 2
- 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](https://flask-dance.readthedocs.io/en/latest/storages.html#sqlalchemy) and change the privacy statement.
diff --git a/LESSON.pdf b/LESSON.pdf
new file mode 100644
index 0000000000000000000000000000000000000000..b70ec8067000b5e0aca507a0a8108168b69e8f12
GIT binary patch
literal 190889
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