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
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
Simple Flask Todo App using SQLAlchemy and SQLite database.
Updated with full REST API feature.

For styling, [semantic-ui](https://semantic-ui.com/) was used on the original version of the repository.
To lessen the workload of browsing into semantic-ui.com, I just made the styles inside the Base.html using CSS in this new repository.


### New Feature (Full REST API)
The new feature allows the Todo list to be used not just through the HTML interface, but also programmatically—via API calls.
This means other apps, JavaScript front-ends, or even mobile apps can now create, read, update, and delete todos.
It adds flexibility and modern integration potential, making the app much more powerful and scalable.

I aldo added a Due Date feature for the to-do list. So that the user will be able to add the to-dos deadlines.

For styling [semantic-ui](https://semantic-ui.com/) is used.

### Setup
Create project with virtual environment
Expand Down
204 changes: 154 additions & 50 deletions app.py
Original file line number Diff line number Diff line change
@@ -1,50 +1,154 @@
from flask import Flask, render_template, request, redirect, url_for
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)

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


class Todo(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(100))
complete = db.Column(db.Boolean)


@app.route("/")
def home():
todo_list = Todo.query.all()
return render_template("base.html", todo_list=todo_list)


@app.route("/add", methods=["POST"])
def add():
title = request.form.get("title")
new_todo = Todo(title=title, complete=False)
db.session.add(new_todo)
db.session.commit()
return redirect(url_for("home"))


@app.route("/update/<int:todo_id>")
def update(todo_id):
todo = Todo.query.filter_by(id=todo_id).first()
todo.complete = not todo.complete
db.session.commit()
return redirect(url_for("home"))


@app.route("/delete/<int:todo_id>")
def delete(todo_id):
todo = Todo.query.filter_by(id=todo_id).first()
db.session.delete(todo)
db.session.commit()
return redirect(url_for("home"))

if __name__ == "__main__":
db.create_all()
app.run(debug=True)
from flask import Flask, render_template, request, redirect, url_for, jsonify, flash
from flask_sqlalchemy import SQLAlchemy
from datetime import datetime

app = Flask(__name__)
app.secret_key = "your_secret_key_here"

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

class Todo(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(100))
complete = db.Column(db.Boolean)
due_date = db.Column(db.Date)

@app.route("/")
def home():
# ✅ Sort by due date, nulls (None) last
todo_list = Todo.query.order_by(Todo.due_date.asc().nullslast()).all()
return render_template("base.html", todo_list=todo_list)

@app.route("/add", methods=["POST"])
def add():
title = request.form.get("title")
due_date_str = request.form.get("due_date")

if not title or title.strip() == "":
flash("Todo title cannot be empty.", "warning")
return redirect(url_for("home"))

due_date = None
if due_date_str:
try:
due_date = datetime.strptime(due_date_str, "%Y-%m-%d").date()
except ValueError:
flash("Invalid due date format.", "warning")
return redirect(url_for("home"))

new_todo = Todo(title=title.strip(), complete=False, due_date=due_date)
db.session.add(new_todo)
db.session.commit()
flash(f'Todo "{title}" added.', "success")
return redirect(url_for("home"))

@app.route("/toggle/<int:todo_id>")
def toggle(todo_id):
todo = Todo.query.get_or_404(todo_id)
todo.complete = not todo.complete
db.session.commit()
flash(f'Todo "{todo.title}" toggled to {"done" if todo.complete else "not done"}.', "info")
return redirect(url_for("home"))

@app.route("/delete/<int:todo_id>")
def delete(todo_id):
todo = Todo.query.filter_by(id=todo_id).first()
if todo:
db.session.delete(todo)
db.session.commit()
flash(f'Todo "{todo.title}" deleted.', "error")
else:
flash("Todo not found.", "error")
return redirect(url_for("home"))

# -----------------------
# REST API ENDPOINTS
# -----------------------

@app.route("/api/todos", methods=["GET"])
def api_get_todos():
todos = Todo.query.order_by(Todo.due_date.asc().nullslast()).all()
return jsonify([
{
"id": t.id,
"title": t.title,
"complete": t.complete,
"due_date": t.due_date.strftime("%Y-%m-%d") if t.due_date else None
} for t in todos
]), 200

@app.route("/api/todos/<int:todo_id>", methods=["GET"])
def api_get_single_todo(todo_id):
todo = Todo.query.get_or_404(todo_id)
return jsonify({
"id": todo.id,
"title": todo.title,
"complete": todo.complete,
"due_date": todo.due_date.strftime("%Y-%m-%d") if todo.due_date else None
}), 200

@app.route("/api/todos", methods=["POST"])
def api_create_todo():
data = request.get_json()
title = data.get("title")
due_date_str = data.get("due_date")

if not title:
return jsonify({"error": "Title is required"}), 400

due_date = None
if due_date_str:
try:
due_date = datetime.strptime(due_date_str, "%Y-%m-%d").date()
except ValueError:
return jsonify({"error": "Invalid due_date format. Use YYYY-MM-DD"}), 400

new_todo = Todo(title=title, complete=False, due_date=due_date)
db.session.add(new_todo)
db.session.commit()
return jsonify({"message": "Todo created", "id": new_todo.id}), 201

@app.route("/api/todos/<int:todo_id>", methods=["PUT"])
def api_update_todo(todo_id):
todo = Todo.query.get_or_404(todo_id)
data = request.get_json()
todo.title = data.get("title", todo.title)
todo.complete = data.get("complete", todo.complete)

due_date_str = data.get("due_date")
if due_date_str is not None:
try:
todo.due_date = datetime.strptime(due_date_str, "%Y-%m-%d").date()
except ValueError:
return jsonify({"error": "Invalid due_date format. Use YYYY-MM-DD"}), 400

db.session.commit()
return jsonify({"message": "Todo updated"}), 200

@app.route("/api/todos/<int:todo_id>", methods=["DELETE"])
def api_delete_todo(todo_id):
todo = Todo.query.get_or_404(todo_id)
db.session.delete(todo)
db.session.commit()
return jsonify({"message": "Todo deleted"}), 200

@app.route("/api/todos/<int:todo_id>/done", methods=["PATCH"])
def api_mark_done(todo_id):
todo = Todo.query.get_or_404(todo_id)
todo.complete = True
db.session.commit()
return jsonify({"message": f"Todo {todo_id} marked as done"}), 200

@app.route("/api/todos/<int:todo_id>/toggle", methods=["PATCH"])
def api_toggle_done(todo_id):
todo = Todo.query.get_or_404(todo_id)
todo.complete = not todo.complete
db.session.commit()
return jsonify({"message": f"Todo {todo_id} toggled to {'done' if todo.complete else 'not done'}"}), 200

if __name__ == "__main__":
with app.app_context():
db.create_all()
app.run(debug=True)
148 changes: 148 additions & 0 deletions base.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>To-Do App</title>

<style>
body {
background: url("/static/images/bg.jpg") no-repeat center center fixed;
font-family: 'Franklin Gothic Medium', 'Arial Narrow', Arial, sans-serif;
background-size:contain;
margin: 0;
padding: 0;
}

.container {
max-width: 600px;
margin: 95px auto;
padding: 20px;
background-color: white;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
}

h1 {
text-align: center;
color: #333;
}

form {
margin-bottom: 20px;
}

label {
display: block;
margin-bottom: 6px;
font-weight: bold;
}

input[type="text"] {
width: 100%;
padding: 10px;
margin-bottom: 10px;
border: 1px solid #ccc;
border-radius: 4px;
}

button, .button-link {
display: inline-block;
padding: 8px 16px;
margin: 4px 4px 0 0;
font-size: 14px;
color: white;
background-color: #007bff;
border: none;
border-radius: 4px;
text-decoration: none;
cursor: pointer;
}

.green { background-color: #28a745; }
.orange { background-color: #fd7e14; }
.red { background-color: #dc3545; }
.gray { background-color: #6c757d; }

.segment {
padding: 15px;
border: 1px solid #ddd;
border-radius: 6px;
margin-bottom: 10px;
}

.label {
display: inline-block;
padding: 4px 8px;
margin-top: 6px;
border-radius: 3px;
font-size: 13px;
color: white;
}

hr {
margin: 30px 0;
border: none;
border-top: 1px solid #ccc;
}
</style>

<script>
function confirmDelete(todoTitle) {
return confirm(`Are you sure you want to delete "${todoTitle}"?`);
}
</script>
</head>

<body>
<div class="container">
<h1>To Do App</h1>

<!-- Flash messages -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="label {{ category }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}

<form action="/add" method="post">
<label>Todo Title</label>
<input type="text" name="title" placeholder="Enter Todo..." required />
<button type="submit">Add</button>
</form>

<hr />

{% for todo in todo_list %}
<div class="segment">
<p><strong>{{ todo.id }}. {{ todo.title }}</strong></p>

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

{% if not todo.complete %}
<a class="button-link green" href="{{ url_for('toggle', todo_id=todo.id) }}">Mark Done</a>
{% else %}
<a class="button-link orange" href="{{ url_for('toggle', todo_id=todo.id) }}">Mark Undone</a>
{% endif %}

<a
class="button-link red"
href="{{ url_for('delete', todo_id=todo.id) }}"
onclick="return confirmDelete('{{ todo.title }}');"
>
Delete
</a>
</div>
{% else %}
<p>No todos yet. Add one above!</p>
{% endfor %}

</div>
</body>
</html>
Binary file added bg.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added db.sqlite
Binary file not shown.
6 changes: 6 additions & 0 deletions init_db.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# init_db.py
from app import app, db

with app.app_context():
db.create_all()
print("Database initialized successfully.")
Binary file added static/images/bg.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading