Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
186 changes: 157 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,45 +1,173 @@
Simple Flask Todo App using SQLAlchemy and SQLite database.
# Flask Todo App with REST API

For styling [semantic-ui](https://semantic-ui.com/) is used.
This project enhances a simple Flask-based Todo web application by adding a full-featured REST API. The API enables external systems to create, read, update, and delete todo items programmatically, following RESTful principles.

### Setup
Create project with virtual environment
## 📌 Original Application

```console
$ mkdir myproject
$ cd myproject
$ python3 -m venv venv
The original application is a basic Todo list manager built with Flask and SQLite. It supports:

- Viewing all tasks
- Adding new tasks
- Updating tasks (marking them as complete/incomplete)
- Deleting tasks

All actions were handled through HTML forms in the web interface.

## ✨ Enhancements Made

This version introduces a REST API layer that exposes the core functionality through JSON-based HTTP endpoints. Key improvements include:

- Added full CRUD API endpoints under `/api/todos`
- Returns proper HTTP status codes (200, 201, 400, 404, etc.)
- Includes input validation and error handling
- SQLite database for local storage
- Easy to test with Postman or similar tools
- Developed a dedicated test suite using `pytest` with 100% test coverage

## 📁 Project Structure

```
📦 flask-todo
┣ 📜 app.py
┣ 📜 app_withRESTAPI.py
┣ 📜 templates/
┃ ┗ 📜 index.html
┣ 📜 test_app.py
┗ 📜 README.md
```

Activate it
```console
$ . venv/bin/activate
> All enhancements are done inside the `feature/rest-api` branch.

---

## 📋 API Documentation

### `GET /api/todos`

Fetches all todo items.

- **Response**: `200 OK`
```json
[
{
"id": 1,
"title": "Buy milk",
"description": "2 bottles",
"done": false
}
]
```

or on Windows
```console
venv\Scripts\activate
### `POST /api/todos`

Creates a new todo item.

- **Request JSON**:
```json
{
"title": "Read book",
"description": "Chapter 4",
"done": false
}
```
- **Response**: `201 Created` with new task JSON
- **Error**: `400 Bad Request` if title is missing

Install Flask
```console
$ pip install Flask
$ pip install Flask-SQLAlchemy
### `PUT /api/todos/<id>`

Updates a specific todo item.

- **Request JSON**:
```json
{
"title": "Finish report",
"done": true
}
```
- **Response**: `200 OK` with updated task
- **Error**: `404 Not Found` if the task does not exist

### `DELETE /api/todos/<id>`

Deletes a specific todo item.

- **Response**: `200 OK` with confirmation message
- **Error**: `404 Not Found` if the task does not exist

---

## 🚀 How to Run the Project

### 1. Clone the repository:

Set environment variables in terminal
```console
$ export FLASK_APP=app.py
$ export FLASK_ENV=development
```bash
git clone https://github.com/alliah2025/flask-todo.git
cd flask-todo
```

or on Windows
```console
$ set FLASK_APP=app.py
$ set FLASK_ENV=development
### 2. Create a virtual environment and activate it:

```bash
python -m venv venv
venv\Scripts\activate # On Windows
# or
source venv/bin/activate # On Unix/macOS
```

### 3. Install dependencies:

```bash
pip install Flask Flask-SQLAlchemy
```

Run the app
```console
$ flask run
### 4. Initialize the database:

```python
# Run this in a Python shell or a separate script
from app_withRESTAPI import db, app
with app.app_context():
db.create_all()
print("Database initialized.")
```

### 5. Start the Flask app:

```bash
python app_withRESTAPI.py
```

- Web interface: [http://localhost:5000](http://localhost:5000)
- API endpoint: [http://localhost:5000/api/todos](http://localhost:5000/api/todos)

You can test the API using Postman:

- `GET /api/todos` — List all todos
- `POST /api/todos` — Create a new todo
- `GET /api/todos/<id>` — Get a specific todo
- `PUT /api/todos/<id>` — Update a specific todo
- `DELETE /api/todos/<id>` — Delete a specific todo

---

## 🧪 Testing

All API endpoints have been covered by unit tests using `pytest`.

- Positive and negative test cases for all CRUD operations
- Uses an in-memory SQLite database for clean and repeatable test runs

To run tests:

```bash
pytest test_app.py --cov=.
```

---

## 🎥 Video Presentation
https://drive.google.com/drive/folders/10kQL25MgmE4RkZQBb37zw3szmufZ7SvA?usp=sharing

## 📌 Repository Links

- 🔗 Original Repository: https://github.com/patrickloeber/flask-todo.git
- 🔗 Forked with Enhancements: https://github.com/alliah2025/flask-todo/tree/feature/rest-api
79 changes: 79 additions & 0 deletions app_withRESTAPI.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
from flask import Flask, request, jsonify
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///todo.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)

class Task(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(100), nullable=False)
description = db.Column(db.String(200))
done = db.Column(db.Boolean, default=False)

def to_dict(self):
return {
'id': self.id,
'title': self.title,
'description': self.description,
'done': self.done
}

@app.route('/api/todos', methods=['GET'])
def get_todos():
todos = Task.query.all()
return jsonify([todo.to_dict() for todo in todos]), 200

@app.route('/api/todos/<int:id>', methods=['GET'])
def get_task(id):
task = db.session.get(Task, id) # updated here
if not task:
return jsonify({'error': 'Task not found'}), 404
return jsonify(task.to_dict()), 200

@app.route('/api/todos', methods=['POST'])
def create_todo():
data = request.get_json()
if not data or 'title' not in data:
return jsonify({'error': 'Title is required'}), 400
todo = Task(
title=data['title'],
description=data.get('description', ''),
done=data.get('done', False)
)
db.session.add(todo)
db.session.commit()
return jsonify(todo.to_dict()), 201

@app.route('/api/todos/<int:id>', methods=['PUT'])
def update_todo(id):
todo = db.session.get(Task, id) # updated here
if not todo:
return jsonify({'error': 'Task not found'}), 404
data = request.get_json()
if 'title' in data:
todo.title = data['title']
if 'description' in data:
todo.description = data['description']
if 'done' in data:
todo.done = data['done']
db.session.commit()
return jsonify(todo.to_dict()), 200

@app.route('/api/todos/<int:id>', methods=['DELETE'])
def delete_todo(id):
todo = db.session.get(Task, id) # updated here
if not todo:
return jsonify({'error': 'Task not found'}), 404
db.session.delete(todo)
db.session.commit()
return jsonify({'message': 'Todo deleted'}), 200

@app.route('/')
def home():
return "Welcome to the Flask TODO API!"

if __name__ == '__main__':
app.run(debug=True)

7 changes: 7 additions & 0 deletions init_db.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
##needed to run the flask app
from app import app, db

with app.app_context():
db.create_all()
print("Database initialized.")

5 changes: 5 additions & 0 deletions init_dbRESTAPI.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
##needed to run the flask app
from app_withRESTAPI import db, app
with app.app_context():
db.create_all()
print("Database initialized.")
95 changes: 95 additions & 0 deletions test_app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import pytest
import json
from app_withRESTAPI import app, db, Task

@pytest.fixture
def client():
app.config['TESTING'] = True
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///:memory:'
with app.test_client() as client:
with app.app_context():
db.create_all()
yield client
with app.app_context():
db.drop_all()

def test_create_todo_success(client):
res = client.post('/api/todos', json={'title': 'Test task'})
assert res.status_code == 201
data = res.get_json()
assert data['title'] == 'Test task'
assert data['done'] is False

def test_create_todo_fail_no_title(client):
res = client.post('/api/todos', json={'description': 'No title here'})
assert res.status_code == 400
data = res.get_json()
assert 'error' in data

def test_get_todos_empty(client):
res = client.get('/api/todos')
assert res.status_code == 200
assert res.get_json() == []

def test_get_todo_not_found(client):
res = client.get('/api/todos/999')
assert res.status_code == 404
data = res.get_json()
assert 'error' in data

def test_update_todo_not_found(client):
res = client.put('/api/todos/999', json={'title': 'New title'})
assert res.status_code == 404
assert 'error' in res.get_json()

def test_delete_todo_not_found(client):
res = client.delete('/api/todos/999')
assert res.status_code == 404
data = res.get_json()
assert 'error' in data

def test_crud_flow(client):
# Create
res = client.post('/api/todos', json={'title': 'First task'})
assert res.status_code == 201
todo = res.get_json()
todo_id = todo['id']

# Read
res = client.get(f'/api/todos/{todo_id}')
assert res.status_code == 200
assert res.get_json()['title'] == 'First task'

# Update
res = client.put(f'/api/todos/{todo_id}', json={'done': True})
assert res.status_code == 200
assert res.get_json()['done'] is True

# Delete
res = client.delete(f'/api/todos/{todo_id}')
assert res.status_code == 200

# Confirm Deletion
res = client.get(f'/api/todos/{todo_id}')
assert res.status_code == 404

def test_full_update_todo(client):
# Create a todo first
res = client.post('/api/todos', json={'title': 'Temp title'})
assert res.status_code == 201
todo = res.get_json()
todo_id = todo['id']

# Update both title and description
res = client.put(f'/api/todos/{todo_id}', json={'title': 'Updated title', 'description': 'Updated desc'})
assert res.status_code == 200
data = res.get_json()
assert data['title'] == 'Updated title'
assert data['description'] == 'Updated desc'

# Read updated todo
res = client.get(f'/api/todos/{todo_id}')
assert res.status_code == 200
data = res.get_json()
assert data['title'] == 'Updated title'
assert data['description'] == 'Updated desc'