From 9fad260d22006a0b9324e16f8c7e746994dd4c65 Mon Sep 17 00:00:00 2001 From: alliah2025 Date: Wed, 28 May 2025 14:52:11 +0800 Subject: [PATCH 1/7] Initial commit --- init_db.py | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 init_db.py diff --git a/init_db.py b/init_db.py new file mode 100644 index 0000000..0e01912 --- /dev/null +++ b/init_db.py @@ -0,0 +1,5 @@ +from app import app, db + +with app.app_context(): + db.create_all() + print("Database initialized.") From 4f40ede9c4819b647356035c14c3813d4bde0a01 Mon Sep 17 00:00:00 2001 From: alliah2025 Date: Wed, 28 May 2025 15:15:12 +0800 Subject: [PATCH 2/7] Implemented REST API with CRUD for tasks --- app_withRESTAPI.py | 75 ++++++++++++++++++++++++++++++++++++++++++++++ init_db.py | 2 ++ init_dbRESTAPI.py | 3 ++ 3 files changed, 80 insertions(+) create mode 100644 app_withRESTAPI.py create mode 100644 init_dbRESTAPI.py diff --git a/app_withRESTAPI.py b/app_withRESTAPI.py new file mode 100644 index 0000000..301c7a2 --- /dev/null +++ b/app_withRESTAPI.py @@ -0,0 +1,75 @@ +#app_withRESTAPI + +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/', methods=['GET']) +def get_task(id): + task = Task.query.get(id) + 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/', methods=['PUT']) +def update_todo(id): + todo = Task.query.get_or_404(id) + 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/', methods=['DELETE']) +def delete_todo(id): + todo = Task.query.get_or_404(id) + db.session.delete(todo) + db.session.commit() + return jsonify({'message': 'Todo deleted'}), 200 + +if __name__ == '__main__': + app.run(debug=True) + diff --git a/init_db.py b/init_db.py index 0e01912..bc22ef5 100644 --- a/init_db.py +++ b/init_db.py @@ -1,5 +1,7 @@ from app import app, db + with app.app_context(): db.create_all() print("Database initialized.") + diff --git a/init_dbRESTAPI.py b/init_dbRESTAPI.py new file mode 100644 index 0000000..3aca5ce --- /dev/null +++ b/init_dbRESTAPI.py @@ -0,0 +1,3 @@ +from app_withRESTAPI import db, app +with app.app_context(): + db.create_all() \ No newline at end of file From e7b6c41f148ac8756a76e04e93cd45d6f54843ea Mon Sep 17 00:00:00 2001 From: alliah2025 Date: Wed, 28 May 2025 15:33:04 +0800 Subject: [PATCH 3/7] Fix issues and add unittest --- README.md | 58 +++++++++++++++----------------------------- app_withRESTAPI.py | 15 ++++++------ test_app.py | 60 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 86 insertions(+), 47 deletions(-) create mode 100644 test_app.py diff --git a/README.md b/README.md index e2a98cb..a334abb 100644 --- a/README.md +++ b/README.md @@ -1,45 +1,25 @@ -Simple Flask Todo App using SQLAlchemy and SQLite database. +# Todo REST API Documentation -For styling [semantic-ui](https://semantic-ui.com/) is used. +### Endpoints: -### Setup -Create project with virtual environment +- **GET /api/todos** + Returns list of all todos. + Response: 200 OK, JSON array of tasks. -```console -$ mkdir myproject -$ cd myproject -$ python3 -m venv venv -``` +- **GET /api/todos/** + Returns a single todo by id. + Response: 200 OK with task JSON, or 404 if not found. -Activate it -```console -$ . venv/bin/activate -``` +- **POST /api/todos** + Creates a new todo. + Request JSON: { "title": "string", "description": "string (optional)", "done": bool (optional) } + Response: 201 Created with new todo JSON, or 400 Bad Request if missing title. -or on Windows -```console -venv\Scripts\activate -``` +- **PUT /api/todos/** + Updates existing todo by id. + Request JSON may contain any of: title, description, done + Response: 200 OK with updated todo JSON, or 404 if not found. -Install Flask -```console -$ pip install Flask -$ pip install Flask-SQLAlchemy -``` - -Set environment variables in terminal -```console -$ export FLASK_APP=app.py -$ export FLASK_ENV=development -``` - -or on Windows -```console -$ set FLASK_APP=app.py -$ set FLASK_ENV=development -``` - -Run the app -```console -$ flask run -``` +- **DELETE /api/todos/** + Deletes todo by id. + Response: 200 OK with confirmation message, or 404 if not found. diff --git a/app_withRESTAPI.py b/app_withRESTAPI.py index 301c7a2..fd630c7 100644 --- a/app_withRESTAPI.py +++ b/app_withRESTAPI.py @@ -1,5 +1,3 @@ -#app_withRESTAPI - from flask import Flask, request, jsonify from flask_sqlalchemy import SQLAlchemy @@ -29,7 +27,7 @@ def get_todos(): @app.route('/api/todos/', methods=['GET']) def get_task(id): - task = Task.query.get(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 @@ -48,10 +46,11 @@ def create_todo(): db.session.commit() return jsonify(todo.to_dict()), 201 - @app.route('/api/todos/', methods=['PUT']) def update_todo(id): - todo = Task.query.get_or_404(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'] @@ -62,14 +61,14 @@ def update_todo(id): db.session.commit() return jsonify(todo.to_dict()), 200 - @app.route('/api/todos/', methods=['DELETE']) def delete_todo(id): - todo = Task.query.get_or_404(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 if __name__ == '__main__': app.run(debug=True) - diff --git a/test_app.py b/test_app.py new file mode 100644 index 0000000..73edbb7 --- /dev/null +++ b/test_app.py @@ -0,0 +1,60 @@ +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/123') + assert res.status_code == 404 + +def test_crud_flow(client): + # Create + res = client.post('/api/todos', json={'title': 'First task'}) + 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 From 87f25c6da4b84d33c69addcf187bf62681a69e3b Mon Sep 17 00:00:00 2001 From: alliah2025 Date: Wed, 28 May 2025 16:03:40 +0800 Subject: [PATCH 4/7] Ensure 100% test coverage --- app_withRESTAPI.py | 1 + test_app.py | 39 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/app_withRESTAPI.py b/app_withRESTAPI.py index fd630c7..53ddcba 100644 --- a/app_withRESTAPI.py +++ b/app_withRESTAPI.py @@ -72,3 +72,4 @@ def delete_todo(id): if __name__ == '__main__': app.run(debug=True) + diff --git a/test_app.py b/test_app.py index 73edbb7..9ae2640 100644 --- a/test_app.py +++ b/test_app.py @@ -32,12 +32,26 @@ def test_get_todos_empty(client): assert res.get_json() == [] def test_get_todo_not_found(client): - res = client.get('/api/todos/123') + 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'] @@ -55,6 +69,27 @@ def test_crud_flow(client): res = client.delete(f'/api/todos/{todo_id}') assert res.status_code == 200 - # Confirm deletion + # 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' From c38a9cd799bea17a636d8e667ce3e52973909988 Mon Sep 17 00:00:00 2001 From: alliah2025 Date: Fri, 30 May 2025 09:29:50 +0800 Subject: [PATCH 5/7] Testing API endpoints and finalizing README file. --- README.md | 164 +++++++++++++++++++++++++++++++++++++++------ app_withRESTAPI.py | 4 ++ init_db.py | 2 +- init_dbRESTAPI.py | 4 +- 4 files changed, 153 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index a334abb..f67fddf 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,151 @@ -# Todo REST API Documentation +# Flask Todo App with REST API + +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. + +## ๐Ÿ“Œ Original Application + +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 + + +> 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 + } +] + + +### 'POST /api/todos' +Creates a new todo item. +- Request JSON: +{ + "title": "Read book", + "description": "Chapter 4", + "done": false +} +Response: 201 Created with new task JSON +Error: 400 Bad Request if title is missing + + +### 'PUT /api/todos/' +Updates a specific todo item. +Request 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/' +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 +Clone the repository: +git clone +cd flask-todo +python -m venv venv + +Activate it +venv\Scripts\activate + +Install Flask +pip install Flask +pip install Flask-SQLAlchemy + +Set environment variables in terminal +$ set FLASK_APP=app_withRESTAPI.py +$ set FLASK_ENV=development + +Run the database setup +Before starting the app, make sure the SQLite database is created: +from app_withRESTAPI import db, app +with app.app_context(): + db.create_all() + print("Database initialized.") + +Run the app +python app_withRESTAPI.py + +Web interface: http://localhost:5000 +API endpoint: http://localhost:5000/api/todos + +Test the API using Postman: +GET /api/todos โ€” List all todos +POST /api/todos โ€” Create a new todo +GET /api/todos/ โ€” Get a specific todo +PUT /api/todos/ โ€” Update a specific todo +DELETE /api/todos/ โ€” 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 +Run tests with: +pytest test_app.py + + +## ๐ŸŽฅ Video Presentation + + +## ๐Ÿ“Œ Repository Links +๐Ÿ”— Original Repository: https://github.com/patrickloeber/flask-todo.git +๐Ÿ”— Forked with Enhancements: https://github.com/alliah2025/flask-todo/tree/feature/rest-api + + + + -### Endpoints: -- **GET /api/todos** - Returns list of all todos. - Response: 200 OK, JSON array of tasks. -- **GET /api/todos/** - Returns a single todo by id. - Response: 200 OK with task JSON, or 404 if not found. -- **POST /api/todos** - Creates a new todo. - Request JSON: { "title": "string", "description": "string (optional)", "done": bool (optional) } - Response: 201 Created with new todo JSON, or 400 Bad Request if missing title. -- **PUT /api/todos/** - Updates existing todo by id. - Request JSON may contain any of: title, description, done - Response: 200 OK with updated todo JSON, or 404 if not found. -- **DELETE /api/todos/** - Deletes todo by id. - Response: 200 OK with confirmation message, or 404 if not found. diff --git a/app_withRESTAPI.py b/app_withRESTAPI.py index 53ddcba..8391347 100644 --- a/app_withRESTAPI.py +++ b/app_withRESTAPI.py @@ -70,6 +70,10 @@ def delete_todo(id): 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) diff --git a/init_db.py b/init_db.py index bc22ef5..806ae3f 100644 --- a/init_db.py +++ b/init_db.py @@ -1,6 +1,6 @@ +##needed to run the flask app from app import app, db - with app.app_context(): db.create_all() print("Database initialized.") diff --git a/init_dbRESTAPI.py b/init_dbRESTAPI.py index 3aca5ce..9a02115 100644 --- a/init_dbRESTAPI.py +++ b/init_dbRESTAPI.py @@ -1,3 +1,5 @@ +##needed to run the flask app from app_withRESTAPI import db, app with app.app_context(): - db.create_all() \ No newline at end of file + db.create_all() + print("Database initialized.") \ No newline at end of file From 2a79ff95f1fab15aa02aaf93d00c594be3932136 Mon Sep 17 00:00:00 2001 From: alliah2025 Date: Fri, 30 May 2025 13:20:05 +0800 Subject: [PATCH 6/7] Finalization --- README.md | 115 ++++++++++++++++++++++++++++++++---------------------- 1 file changed, 69 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index f67fddf..6cff77a 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ This version introduces a REST API layer that exposes the core functionality thr ## ๐Ÿ“ Project Structure +``` ๐Ÿ“ฆ flask-todo โ”ฃ ๐Ÿ“œ app.py โ”ฃ ๐Ÿ“œ app_withRESTAPI.py @@ -33,7 +34,7 @@ This version introduces a REST API layer that exposes the core functionality thr โ”ƒ โ”— ๐Ÿ“œ index.html โ”ฃ ๐Ÿ“œ test_app.py โ”— ๐Ÿ“œ README.md - +``` > All enhancements are done inside the `feature/rest-api` branch. @@ -55,97 +56,119 @@ Fetches all todo items. "done": false } ] +``` +### `POST /api/todos` -### 'POST /api/todos' Creates a new todo item. -- Request JSON: + +- **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 +``` +- **Response**: `201 Created` with new task JSON +- **Error**: `400 Bad Request` if title is missing +### `PUT /api/todos/` -### 'PUT /api/todos/' Updates a specific todo item. -Request JSON + +- **Request JSON**: +```json { "title": "Finish report", "done": true } -Response: 200 OK with updated task -Error: 404 Not Found if the task does not exist +``` +- **Response**: `200 OK` with updated task +- **Error**: `404 Not Found` if the task does not exist +### `DELETE /api/todos/` -### 'DELETE /api/todos/' Deletes a specific todo item. -Response: 200 OK with confirmation message -Error: 404 Not Found if the task does not exist +- **Response**: `200 OK` with confirmation message +- **Error**: `404 Not Found` if the task does not exist + +--- ## ๐Ÿš€ How to Run the Project -Clone the repository: -git clone + +### 1. Clone the repository: + +```bash +git clone https://github.com/alliah2025/flask-todo.git cd flask-todo +``` + +### 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 +``` -Activate it -venv\Scripts\activate +### 3. Install dependencies: -Install Flask -pip install Flask -pip install Flask-SQLAlchemy +```bash +pip install Flask Flask-SQLAlchemy +``` -Set environment variables in terminal -$ set FLASK_APP=app_withRESTAPI.py -$ set FLASK_ENV=development +### 4. Initialize the database: -Run the database setup -Before starting the app, make sure the SQLite database is created: +```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.") +``` -Run the app +### 5. Start the Flask app: + +```bash python app_withRESTAPI.py +``` -Web interface: http://localhost:5000 -API endpoint: http://localhost:5000/api/todos +- Web interface: [http://localhost:5000](http://localhost:5000) +- API endpoint: [http://localhost:5000/api/todos](http://localhost:5000/api/todos) -Test the API using Postman: -GET /api/todos โ€” List all todos -POST /api/todos โ€” Create a new todo -GET /api/todos/ โ€” Get a specific todo -PUT /api/todos/ โ€” Update a specific todo -DELETE /api/todos/ โ€” Delete a specific todo +You can test the API using Postman: +- `GET /api/todos` โ€” List all todos +- `POST /api/todos` โ€” Create a new todo +- `GET /api/todos/` โ€” Get a specific todo +- `PUT /api/todos/` โ€” Update a specific todo +- `DELETE /api/todos/` โ€” 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 -Run tests with: -pytest test_app.py - - -## ๐ŸŽฅ Video Presentation - - -## ๐Ÿ“Œ Repository Links -๐Ÿ”— Original Repository: https://github.com/patrickloeber/flask-todo.git -๐Ÿ”— Forked with Enhancements: https://github.com/alliah2025/flask-todo/tree/feature/rest-api +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 +## ๐Ÿ“Œ Repository Links +- ๐Ÿ”— Original Repository: https://github.com/patrickloeber/flask-todo.git +- ๐Ÿ”— Forked with Enhancements: https://github.com/alliah2025/flask-todo/tree/feature/rest-api \ No newline at end of file From 2af47054230d5e3e502f43eec06cdc6fb66cb4cc Mon Sep 17 00:00:00 2001 From: alliah2025 Date: Fri, 30 May 2025 15:10:45 +0800 Subject: [PATCH 7/7] Add video Presentation --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 6cff77a..97a4ee5 100644 --- a/README.md +++ b/README.md @@ -165,8 +165,7 @@ pytest test_app.py --cov=. --- ## ๐ŸŽฅ Video Presentation - - +https://drive.google.com/drive/folders/10kQL25MgmE4RkZQBb37zw3szmufZ7SvA?usp=sharing ## ๐Ÿ“Œ Repository Links