ToDo List App in Inspira using Htmx and Bootstrap
In this step-by-step guide, you’re going to create a web app using Inspira, Htmx and Bootstrap.
Getting Started
Let's get started! Follow these steps to install Inspira and set up the project directories:
$ mkdir inspira-todo-lists
$ cd inspira-todo-lists
$ python -m venv .venv
$ source .venv/bin/activate
$ pip install inspira
$ inspira init
$ inspira new database --name todo_db --type sqlite
File Generation
Execute the following commands to generate controller
, repository
, model
, and service
files:
$ inspira new controller todo
$ inspira new repository todo
$ inspira new service todo
$ inspira new model todo
Model
Open the todo.py
model file and make the following changes
from sqlalchemy import Column, Integer, Boolean, DateTime, func, String
from database import Base
class Todo(Base):
__tablename__ = "todos"
id = Column(Integer, primary_key=True)
title = Column(String(255))
completed = Column(Boolean, default=False)
created_at = Column(DateTime(timezone=True), server_default=func.now())
Repository
Open the todo_repository.py
file and make the following changes:
from inspira.logging import log
from sqlalchemy.exc import SQLAlchemyError
from database import db_session
from src.model.todo import Todo
class TodoRepository:
def get_all_todos(self):
return db_session.query(Todo).all()
def get_todo_by_id(self, id: int):
return db_session.query(Todo).filter_by(id=id).first()
def delete_todo_by_id(self, id: int):
try:
todo = self.get_todo_by_id(id)
db_session.delete(todo)
db_session.commit()
return True
except SQLAlchemyError as e:
db_session.rollback()
log.error(f"Error deleting todo: {e}")
return False
def mark_complete(self, id: int):
try:
todo = self.get_todo_by_id(id)
todo.completed = True
db_session.commit()
return True
except SQLAlchemyError as e:
db_session.rollback()
log.error(f"Error marking todo completed: {e}")
return False
def create_todo(self, todo: Todo):
try:
db_session.add(todo)
db_session.commit()
return True
except SQLAlchemyError as e:
db_session.rollback()
log.error(f"Error creating todo: {e}")
return False
get_all_todos(self):
Fetches all Todo items from the database.
get_todo_by_id(self, id: int):
Retrieves a specific Todo item from the database based on the provided id.
delete_todo_by_id(self, id: int):
Attempts to delete a Todo item with the given id from the database.
mark_complete(self, id: int):
Marks a Todo item with the specified id as completed.
create_todo(self, todo: Todo):
Creates a new Todo item in the database.
This class encapsulates the database operations related to Todo items, providing a convenient and organized way to interact with the underlying data store.
Service
Afterward, open the todo_service.py
file and add the following:
from src.model.todo import Todo
from src.repository.todo_repository import TodoRepository
class TodoService:
def __init__(self, todo_repository: TodoRepository):
self._todo_repository = todo_repository
def get_all_todos(self):
return self._todo_repository.get_all_todos()
def create_todo(self, title: str):
todo = Todo(title=title)
return self._todo_repository.create_todo(todo)
def mark_complete(self, id: int) -> bool:
return self._todo_repository.mark_complete(id)
def delete_todo_by_id(self, id: int) -> bool:
return self._todo_repository.delete_todo_by_id(id)
The TodoService
class provides a high-level interface for managing Todo-related operations.
It utilizes TodoRepository
instance to interact with the underlying database. The class includes the following methods:
get_all_todos(self):
Retrieves all Todo items using the associated TodoRepository
instance.
create_todo(self, title: str):
Creates a new Todo item with the specified title and delegates the storage operation to the TodoRepository
.
mark_complete(self, id: int) -> bool:
Marks a Todo item with the given ID as complete, leveraging the corresponding method in the TodoRepository
.
Returns a boolean indicating the success of the operation.
delete_todo_by_id(self, id: int) -> bool:
Attempts to delete a Todo item with the provided ID using the TodoRepository
.
Returns a boolean indicating the success of the deletion operation.
In essence, the TodoService
acts as an intermediary between the application logic and the database, offering a simplified and coherent interface for handling Todo-related functionalities.
Controller
To proceed, open the todo_controller.py
file and insert the following:
from inspira.decorators.http_methods import get, post, delete, put
from inspira.decorators.path import path
from inspira.responses import TemplateResponse
from inspira.requests import Request
from src.service.todo_service import TodoService
@path("/todos")
class TodoController:
def __init__(self, todo_service: TodoService):
self._todo_service = todo_service
@get()
async def index(self, request: Request):
todos = self._todo_service.get_all_todos()
context = {
"todos": todos,
}
return TemplateResponse("index.html", context)
@post("/create")
async def create_todo(self, request: Request):
body = await request.form()
title = body['title']
self._todo_service.create_todo(title)
todos = self._todo_service.get_all_todos()
context = {
"todos": todos
}
return TemplateResponse("todo-list.html", context)
@delete("/{id}")
async def delete_todo(self, request: Request, id: int):
self._todo_service.delete_todo_by_id(id)
todos = self._todo_service.get_all_todos()
context = {
"todos": todos
}
return TemplateResponse("todo-list.html", context)
@put("/{id}")
async def mark_complete(self, request: Request, id: int):
self._todo_service.mark_complete(id)
todos = self._todo_service.get_all_todos()
context = {
"todos": todos
}
return TemplateResponse("todo-list.html", context)
The TodoController
class is a web controller responsible for handling HTTP requests related to Todo operations.
It is decorated with the @path("/todos")
attribute, indicating the base path for its endpoints.
The controller takes a TodoService
instance during initialization.
index(self, request: Request):
Handles HTTP GET requests to retrieve all Todo items. It calls the get_all_todos
method of the associated TodoService
, renders the data into an HTML template, and returns a TemplateResponse
with the context.
create_todo(self, request: Request):
Manages HTTP POST requests for creating a new Todo item. It extracts the title from the request, delegates the creation to the create_todo
method of the TodoService
, and then refreshes the Todo list by rendering a template with the updated data.
delete_todo(self, request: Request, id: int):
Processes HTTP DELETE requests to delete a Todo item by its ID. It invokes the delete_todo_by_id
method of the TodoService
and updates the Todo list for display.
mark_complete(self, request: Request, id: int):
Handles HTTP PUT requests to mark a Todo item with the given ID as complete. It calls the mark_complete
method of the TodoService
and refreshes the Todo list for presentation.
In summary, the TodoController
orchestrates the interaction between the web interface and the application logic provided by the TodoService
, ensuring seamless handling of Todo-related actions through specified HTTP methods.
Creating the Templates Folder
In the project's main directory, create a folder named templates
alongside the main.py
file.
Create the files, base.html
, index.html
and todo-list.html
, within the newly created folder using the commands below:
$ mkdir templates
$ touch templates/base.html
$ touch templates/index.html
$ touch templates/todo-list.html
Now, insert the provided content into the base.html
file:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}{% endblock %}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous">
</head>
<body>
<div class="container mt-5">
{% block content %}
{% endblock %}
</div>
<!-- Bootstrap 5 js -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"
integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz"
crossorigin="anonymous"></script>
<!-- HTMX -->
<script src="https://unpkg.com/htmx.org@1.9.2"
integrity="sha384-L6OqL9pRWyyFU3+/bjdSri+iIphTN/bvYyM37tICVyOJkWZLpP2vGn6VUEXgzg6h"
crossorigin="anonymous"></script>
</body>
</html>
Now add the following to index.html
:
{% extends 'base.html' %}
{% block title %} Todos {% endblock %}
{% block content %}
<h3 class="my-5">Welcome to Inspira Todo List App</h3>
<form hx-post="/todos/create" hx-target="#todoList" class="mx-auto">
<div class="mb-3 row align-items-center">
<label for="todoText" class="col-auto col-form-label">Enter your todo here: </label>
<div class="col-6">
<input type="text" name="title" class="form-control" id="todoText" required>
</div>
<div class="col-auto">
<button class="btn btn-success">Add</button>
</div>
</div>
</form>
<div id="todoList">
{% include 'todo-list.html' %}
</div>
{% endblock %}
Now open todo-list.html
and add the following:
{% if todos %}
<div class="card">
<ul class="list-group list-group-flush">
{% for todo in todos %}
<li class="list-group-item d-flex justify-content-between align-items-center">
<div class="{% if todo.completed %} text-success text-decoration-line-through {% endif %}">
{{ todo.title }}
</div>
<div>
<span class="action badge rounded-pill text-bg-warning" hx-put="/todos/{{todo.id}}"
hx-target="#todoList" style="cursor: pointer;">✔</span>
<span class="action badge rounded-pill text-bg-danger" hx-delete="/todos/{{todo.id}}"
hx-target="#todoList" hx-confirm="Are you sure you want to delete?"
style="cursor: pointer;">X</span>
</div>
</li>
{% endfor %}
</ul>
</div>
{% else %}
<h5>No todos</h5>
{% endif %}
Migrations
Run the following command to create a new migration file
$ inspira new migration create_table_todos
Open the migration file and add the following:
CREATE TABLE todos (
id INTEGER PRIMARY KEY,
title VARCHAR(255),
completed BOOLEAN DEFAULT FALSE,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
Now run the following command to apply the migration:
$ inspira migrate
Launching the Server
Initiate the server with the following command:
$ uvicorn main:app --reload
Testing the Service
Visit http://localhost:8000/todos
in your browser to interact with the form.
Enter the required details and submit the form for results.
Submit a title
to see the results:
Summary
Congratulations! You just used Inspira, Htmx and Bootstrap to create a ToDo list App.
The code is available on GitHub
Happy coding!