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
3 changes: 3 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[run]
omit =
venv/*
43 changes: 43 additions & 0 deletions README_Garcellano.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
Required Installs:
Python
Git

Dependencies:
pip install Flask
pip install Flask-SQLAlchemy
pip install pytest
pip install pytest-cov

Step by step to run the app:
1. Clone the repository
git clone https://github.com/DCG06/flask-todo.git
cd flask-todo
2. Switch to my branch
git checkout feature/todo-rest-api
3. Create Virtual Env
python -m venv venv
venv\Scripts\activate
4.Install Dependencies Above
5. initialize database

python

from app import app, db
with app.app_context():
db.create_all()
exit()

6. Run the app

flask run

7. Run Tests
pytest test_api.py -v
pytest --cov=app --cov-report=term-missing test_api.py

CHANGES
- REST API with full CRUD endpoints for Todo items
- New inputs: description, due date, and priority
- Error handling with HTTP status codes (200, 201, 400, 404)
- Unit tests with 100% coverage using pytest
- Both positive and negative test cases for all endpoints
185 changes: 178 additions & 7 deletions app.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,41 @@
from flask import Flask, render_template, request, redirect, url_for
from flask import Flask, render_template, request, redirect, url_for, jsonify
from flask_sqlalchemy import SQLAlchemy
from datetime import date

app = Flask(__name__)

# /// = relative path, //// = absolute path
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///db.sqlite'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

db = SQLAlchemy(app)

VALID_PRIORITIES = ["low", "medium", "high"]


class Todo(db.Model):
"""Model representing a Todo item."""
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(100))
complete = db.Column(db.Boolean)
title = db.Column(db.String(100), nullable=False)
complete = db.Column(db.Boolean, default=False)
description = db.Column(db.String(500), nullable=True)
due_date = db.Column(db.Date, nullable=True)
priority = db.Column(db.String(10), nullable=True)

def to_dict(self):
"""Serialize the Todo object to a dictionary."""
return {
"id": self.id,
"title": self.title,
"complete": self.complete,
"description": self.description,
"due_date": self.due_date.isoformat() if self.due_date else None,
"priority": self.priority
}


# Existing HTML routes (unchanged behavior)

@app.route("/")
def home():
todo_list = Todo.query.all()
Expand All @@ -24,7 +45,24 @@ def home():
@app.route("/add", methods=["POST"])
def add():
title = request.form.get("title")
new_todo = Todo(title=title, complete=False)
description = request.form.get("description") or None
due_date_str = request.form.get("due_date") or None
priority = request.form.get("priority") or None

due_date = None
if due_date_str:
try:
due_date = date.fromisoformat(due_date_str)
except ValueError:
due_date = None

new_todo = Todo(
title=title,
complete=False,
description=description,
due_date=due_date,
priority=priority
)
db.session.add(new_todo)
db.session.commit()
return redirect(url_for("home"))
Expand All @@ -45,6 +83,139 @@ def delete(todo_id):
db.session.commit()
return redirect(url_for("home"))

if __name__ == "__main__":
db.create_all()
app.run(debug=True)

# REST API routes

@app.route("/api/todos", methods=["GET"])
def api_get_todos():
"""Return a list of all todo items."""
todos = Todo.query.all()
return jsonify([todo.to_dict() for todo in todos]), 200


@app.route("/api/todos/<int:todo_id>", methods=["GET"])
def api_get_todo(todo_id):
"""Return a single todo item by ID. Returns 404 if not found."""
todo = db.session.get(Todo, todo_id)
if todo is None:
return jsonify({"error": "Todo not found"}), 404
return jsonify(todo.to_dict()), 200


@app.route("/api/todos", methods=["POST"])
def api_create_todo():
"""
Create a new todo item.
Expects JSON body: {
"title": "some text", (required)
"description": "some text", (optional)
"due_date": "YYYY-MM-DD", (optional)
"priority": "low|medium|high" (optional)
}
Returns 400 if title is missing, empty, or other fields are invalid.
"""
data = request.get_json()
if not data or not data.get("title", "").strip():
return jsonify({"error": "Title is required"}), 400

# Validate due_date if provided
due_date = None
if "due_date" in data and data["due_date"] is not None:
try:
due_date = date.fromisoformat(data["due_date"])
except ValueError:
return jsonify({"error": "due_date must be in YYYY-MM-DD format"}), 400

# Validate priority if provided
priority = None
if "priority" in data and data["priority"] is not None:
if data["priority"] not in VALID_PRIORITIES:
return jsonify({"error": f"priority must be one of: {', '.join(VALID_PRIORITIES)}"}), 400
priority = data["priority"]

description = data.get("description") or None

new_todo = Todo(
title=data["title"].strip(),
complete=False,
description=description,
due_date=due_date,
priority=priority
)
db.session.add(new_todo)
db.session.commit()
return jsonify(new_todo.to_dict()), 201


@app.route("/api/todos/<int:todo_id>", methods=["PUT"])
def api_update_todo(todo_id):
"""
Update an existing todo item by ID.
Accepts any combination of:
{
"title": "new text",
"complete": true,
"description": "new text",
"due_date": "YYYY-MM-DD",
"priority": "low|medium|high"
}
Returns 404 if not found, 400 if fields are invalid.
"""
todo = db.session.get(Todo, todo_id)
if todo is None:
return jsonify({"error": "Todo not found"}), 404

data = request.get_json()
if not data:
return jsonify({"error": "No data provided"}), 400 # pragma: no cover

if "title" in data:
if not isinstance(data["title"], str) or not data["title"].strip():
return jsonify({"error": "Title cannot be empty"}), 400
todo.title = data["title"].strip()

if "complete" in data:
if not isinstance(data["complete"], bool):
return jsonify({"error": "'complete' must be a boolean"}), 400
todo.complete = data["complete"]

if "description" in data:
todo.description = data["description"] or None

if "due_date" in data:
if data["due_date"] is None:
todo.due_date = None
else:
try:
todo.due_date = date.fromisoformat(data["due_date"])
except ValueError:
return jsonify({"error": "due_date must be in YYYY-MM-DD format"}), 400

if "priority" in data:
if data["priority"] is None:
todo.priority = None
elif data["priority"] not in VALID_PRIORITIES:
return jsonify({"error": f"priority must be one of: {', '.join(VALID_PRIORITIES)}"}), 400
else:
todo.priority = data["priority"]

db.session.commit()
return jsonify(todo.to_dict()), 200


@app.route("/api/todos/<int:todo_id>", methods=["DELETE"])
def api_delete_todo(todo_id):
"""Delete a todo item by ID. Returns 404 if not found."""
todo = db.session.get(Todo, todo_id)
if todo is None:
return jsonify({"error": "Todo not found"}), 404

db.session.delete(todo)
db.session.commit()
return jsonify({"message": "Todo deleted successfully"}), 200


if __name__ == "__main__": # pragma: no cover
with app.app_context():
db.create_all()
app.run(debug=True)
49 changes: 43 additions & 6 deletions templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,26 @@ <h1 class="ui center aligned header">To Do App</h1>
<form class="ui form" action="/add" method="post">
<div class="field">
<label>Todo Title</label>
<input type="text" name="title" placeholder="Enter Todo..."><br>
<input type="text" name="title" placeholder="Enter Todo..." required>
</div>
<div class="field">
<label>Description (optional)</label>
<textarea name="description" placeholder="Enter description..." rows="2"></textarea>
</div>
<div class="two fields">
<div class="field">
<label>Due Date (optional)</label>
<input type="date" name="due_date">
</div>
<div class="field">
<label>Priority (optional)</label>
<select name="priority" class="ui dropdown">
<option value="">-- No Priority --</option>
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
</select>
</div>
</div>
<button class="ui blue button" type="submit">Add</button>
</form>
Expand All @@ -26,14 +45,32 @@ <h1 class="ui center aligned header">To Do App</h1>

{% for todo in todo_list %}
<div class="ui segment">
<p class="ui big header">{{todo.id }} | {{ todo.title }}</p>
<p class="ui big header">{{ todo.id }} | {{ todo.title }}</p>

{% if todo.complete == False %}
<span class="ui gray label">Not Complete</span>
{% else %}
<span class="ui green label">Completed</span>
{% if todo.description %}
<p>{{ todo.description }}</p>
{% endif %}

<div style="margin-bottom: 8px;">
{% if todo.due_date %}
<span class="ui label"><i class="calendar icon"></i>Due: {{ todo.due_date }}</span>
{% endif %}

{% if todo.priority == "high" %}
<span class="ui red label">High Priority</span>
{% elif todo.priority == "medium" %}
<span class="ui yellow label">Medium Priority</span>
{% elif todo.priority == "low" %}
<span class="ui green label">Low Priority</span>
{% endif %}

{% if todo.complete == False %}
<span class="ui gray label">Not Complete</span>
{% else %}
<span class="ui green label">Completed</span>
{% endif %}
</div>

<a class="ui blue button" href="/update/{{ todo.id }}">Update</a>
<a class="ui red button" href="/delete/{{ todo.id }}">Delete</a>
</div>
Expand Down
Loading