Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
1374f36
tidy whitespace
mr-eggleton Jan 13, 2026
6279ba1
Enhance privacy policy for Auth0 integration and clarify data collect…
mr-eggleton Jan 13, 2026
a3eca66
Fixing Render
mr-eggleton Jan 13, 2026
40e1081
SQLite for now, postgress later
mr-eggleton Jan 13, 2026
bf109a2
Update Python version to 3.12.1 in render.yaml
mr-eggleton Jan 13, 2026
eaacba5
fix on render
mr-eggleton Jan 13, 2026
09b811c
gunicorn fix
mr-eggleton Jan 13, 2026
8ee1f24
gunicorn compatibility
mr-eggleton Jan 13, 2026
3e0ad4c
Render Fix
mr-eggleton Jan 13, 2026
2cfd6af
Documentation updates
mr-eggleton Jan 14, 2026
45ff8b1
Tweaks to environment variables
mr-eggleton Jan 14, 2026
638be9d
Update Auth0 callback URL handling and documentation for Render deplo…
mr-eggleton Jan 14, 2026
3bbf969
Refactor README for clarity and detail on features and setup
mr-eggleton Jan 14, 2026
44a47cf
Refactor Todo model to use SQLAlchemy's mapped_column and remove data…
mr-eggleton Jan 14, 2026
4d9483d
Add admin interface with authentication and update requirements
mr-eggleton Jan 14, 2026
1f4d388
Refactor README for improved formatting and clarity
mr-eggleton Jan 14, 2026
93a3817
Add flask_babel to requirements for internationalization support
mr-eggleton Jan 14, 2026
6007c9b
Fix formatting in README for clarity in Flask run command
mr-eggleton Jan 14, 2026
b88ab2c
Adding catergories exercise
mr-eggleton Jan 14, 2026
247f7c9
Add initial sample Todo item for new users and fix the userid creatio…
mr-eggleton Jan 15, 2026
c591150
Improve the documention
mr-eggleton Jan 15, 2026
35a8785
Update .gitignore and improve README for use as a slide show
mr-eggleton Jan 15, 2026
871be2f
Update setup documentation for clarity and consistency across files
mr-eggleton Jan 15, 2026
c24e1fd
Merge pull request #1 from UTCSheffield/main
mr-eggleton Jan 16, 2026
3327cf9
Lesson plan
mr-eggleton Jan 16, 2026
e8ca6d1
Updated the LESSON
mr-eggleton Jan 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
DATABASE_URL=sqlite:///todo.db
APP_SECRET_KEY=your_flask_secret
DATABASE_URL=sqlite:///todo.db


# GitHub OAuth for local development
GITHUB_CLIENT_ID=your-github-client-id
GITHUB_CLIENT_SECRET=your-github-client-secret
# 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=0
# OAUTHLIB_INSECURE_TRANSPORT=1


# 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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -205,3 +205,7 @@ cython_debug/
marimo/_static/
marimo/_lsp/
__marimo__/


# Obsidian-Vault
.obsidian/
3 changes: 2 additions & 1 deletion .vscode/extensions.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"recommendations": [
"ms-python.python",
"ms-python.vscode-pylance",
"qwtel.sqlite-viewer"
"qwtel.sqlite-viewer",
"mermaidchart.vscode-mermaid-chart"
]
}
338 changes: 338 additions & 0 deletions ADDING_CATERGORIES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,338 @@
# 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 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:

```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. 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:

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

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

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

**Old code:**

```html
<form method="POST" action="/add">
<input type="text" name="task" placeholder="Enter task" required>
<button type="submit">Add</button>
</form>
```

**New code:**

```html
<form method="POST" action="/add">
<input type="text" name="task" placeholder="Enter task" required>
<select name="category_id" required>
<option value="">Select category</option>
{% for category in categories %}
<option value="{{ category.id }}">{{ category.name }}</option>
{% endfor %}
</select>
<button type="submit">Add</button>
</form>
```

Find the task text being printed out and add the category next to it:

**Old code:**

```html
{{ todo.task }}
```

**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`:

**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 the instance folder.

Restart your Flask app:

```bash
python3 -m flask run --host=localhost --port=5000

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
Loading