diff --git a/.env.example b/.env.example index 9f78f6d..11505d0 100644 --- a/.env.example +++ b/.env.example @@ -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 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/.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..a99eeea --- /dev/null +++ b/ADDING_CATERGORIES.md @@ -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 +
+ + +
+``` + +**New code:** + +```html +
+ + + +
+``` + +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 diff --git a/CODESPACES_SETUP.md b/CODESPACES_SETUP.md new file mode 100644 index 0000000..b3c7d50 --- /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 your Repository** + - Navigate to your **python-flask-todo** + +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/LESSON.md b/LESSON.md new file mode 100644 index 0000000..5501d83 --- /dev/null +++ b/LESSON.md @@ -0,0 +1,496 @@ + +# Python as Dynamic Web Server + +--- + +## Web Tech + +```mermaid +mindmap + server)Web Server( + Program that responds to HTTP requests with HTML or Assets + Static Site + Hand Built #10003; + Coded one page at a time + Static Built #10003; + Build pages and layout into static pages + Dynamic + Build from templates and data on each request + Allows for the data to be changed + Authentication + Can be an API as well as HTML + SPA + Single Page Applications + Javascript heavy + Use APIs to get data + Assets + Images #10003; + CSS #10003; + Javascript #10003; + Data Sources + Text Files #10003; + API + Database +``` + +--- +## 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/LESSON.pdf b/LESSON.pdf new file mode 100644 index 0000000..b70ec80 Binary files /dev/null and b/LESSON.pdf differ 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 c4e9538..65cbf09 100644 --- a/README.md +++ b/README.md @@ -1,87 +1,109 @@ - -# Flask Todo App with Dual Authentication +# Flask Todo App Starter ![Python](https://img.shields.io/badge/Python-3.10-blue) ![Flask](https://img.shields.io/badge/Flask-2.3-green) ![Render](https://img.shields.io/badge/Deploy-Render-purple) -## Features +A simple Python Todo Web App to do some improvements on and be a starting point for your own simple web apps. -- Flask + SQLAlchemy ORM -- **Dual Authentication:** - - GitHub OAuth (Flask-Dance) for local Windows development - - Auth0 OAuth for Codespaces and Render production -- Automatic provider detection based on environment -- SQLite (easy to switch to PostgreSQL) -- Ready for Render deployment -- GitHub Actions CI/CD +--- + +## Features ### Flask -### SQLAlchemy & SQLite +- [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 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). + +--- ### Authentication (GitHub + Auth0) -### Render +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 Actions +- GitHub OAuth (Flask-Dance) for local Windows development +- Auth0 OAuth for Codespaces and Render production -## Setup +--- -### Clone the Repository +### Render & Github Actions -**Using Git Command Line:** -```bash -git clone https://github.com/stretchyboy/python-todo.git -cd python-todo -``` +- 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) -**Using GitHub Desktop:** -1. Open GitHub Desktop -2. Click `File` → `Clone repository` -3. Select the `URL` tab -4. Enter: `https://github.com/stretchyboy/python-todo.git` -5. Choose a local path and click `Clone` +--- -### Install Dependencies +## Setup -```bash -py -m pip install -r requirements.txt -``` +### Start from the Template -### Copy Example Environment File +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 -```bash -# On linux or codespaces -cp .env.example .env -``` +--- -### On Windows in VS Code +### Clone your Repository locally -Open `.env.example` and save as `.env` +**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 -## Environment Configuration (.env) +--- -Create a `.env` file in the root directory with the following variables: +**Using Git Command Line:** +```bash +git clone https://github.com/UTCSheffield/python-flask-todo.git +cd python-flask-todo ``` -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 + +--- + +### Install Dependencies + +```bash +python3 -m pip install -r requirements.txt ``` -### Generate APP_SECRET_KEY +--- + +### Environment Configuration (.env) ```bash -python -c "import secrets; print(secrets.token_hex(32))" +cp .env.example .env ``` +--- + ## Authentication Setup ### How It Works @@ -94,6 +116,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: @@ -113,88 +137,63 @@ 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: ```bash -python -m flask run +python3 -m flask run --host=localhost --port=5000 ``` The app will be available at [http://localhost:5000](http://localhost:5000) -### Important: Codespaces Port Configuration +Try it, login and create a few tasks! -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** +## The Database -Without this, Auth0 cannot reach your callback URL and login will fail. +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**. -**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 +### Local Development (SQLite) -## Authentication Resources +The database file is stored in `/instance/todo.db` -- [Flask-Dance Documentation](https://flask-dance.readthedocs.io/) -- [Auth0 Python Quickstart](https://auth0.com/docs/quickstart/webapp/python) -- [Auth0 Dashboard](https://manage.auth0.com/) +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. -## The Database +Have a look, can you see the tables and data? -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. +--- + +## 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. +- 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 thoroughly tested and are checked for security issues. -### SQLite Viewer extension +--- -The database file is in /instance/ +## Your Development -The database can be changed to +Try [ADDING_CATERGORIES.md](ADDING_CATERGORIES.md) to add a one-to-many relationship and Categories for the tasks. -## First deployment +Then what could you make with the same ideas but different entities (things)? + +Books and People could make a library etc .... + +--- -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 +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. +## Codespaces Setup + +See [CODESPACES_SETUP.md](CODESPACES_SETUP.md) for complete GitHub Codespaces setup instructions. \ No newline at end of file diff --git a/RENDER_SETUP.md b/RENDER_SETUP.md new file mode 100644 index 0000000..4f0f275 --- /dev/null +++ b/RENDER_SETUP.md @@ -0,0 +1,132 @@ +# 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 select your version of `python-flask-todo` + - 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 + + 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 + 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-flask-todo.onrender.com`) in Auth0 settings, not the variable name. + +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) diff --git a/SaveAs.png b/SaveAs.png new file mode 100644 index 0000000..857fbdd Binary files /dev/null and b/SaveAs.png differ 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 7bedf09..89d09fd 100644 --- a/app.py +++ b/app.py @@ -1,8 +1,15 @@ +# 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 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", @@ -13,7 +20,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 @@ -35,5 +42,24 @@ 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') + +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" +else: + # Use AUTH0_CALLBACK_URL from environment (for both local and production) + redirect_uri = os.getenv('AUTH0_CALLBACK_URL', 'http://localhost:5000/callback') + +# Initialize admin interface (secured) +init_admin(app, db, Todo) + if __name__ == '__main__': app.run(debug=True) 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/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/render.yaml b/render.yaml index 837f8ce..51eaa26 100644 --- a/render.yaml +++ b/render.yaml @@ -1,19 +1,22 @@ - 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: python -m gunicorn app:app envVars: + - key: PYTHON_VERSION + value: 3.12.1 + - key: OAUTHLIB_INSECURE_TRANSPORT + value: "0" - key: APP_SECRET_KEY + generateValue: true + - key: AUTH0_DOMAIN sync: false - - key: GITHUB_CLIENT_ID + - key: AUTH0_CLIENT_ID sync: false - - key: GITHUB_CLIENT_SECRET + - key: AUTH0_CLIENT_SECRET sync: false - - key: GOOGLE_CLIENT_ID - sync: false - - key: GOOGLE_CLIENT_SECRET + - key: AUTH0_CALLBACK_URL sync: false diff --git a/requirements.txt b/requirements.txt index f34dcae..3c58234 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,3 +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 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 %} 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 diff --git a/todo.py b/todo.py index d6361a6..d9b460c 100644 --- a/todo.py +++ b/todo.py @@ -1,26 +1,29 @@ # 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 sqlalchemy import ForeignKey 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('/') @@ -66,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