-
Notifications
You must be signed in to change notification settings - Fork 22
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
d3e3a8d
commit f881951
Showing
5 changed files
with
189 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
from fasthtml.common import * | ||
from monsterui.all import * | ||
import ast | ||
|
||
app,rt = fast_app(hdrs=Theme.blue.headers()) | ||
|
||
status_categories = ['Not Started', 'In Progress', 'Blocked by Client', 'Internal Review', 'Client Review', 'Counterparty Review', 'Out for Signature', "Archive"] | ||
|
||
client_names = ['AnswerDotAI', 'Fastai', 'Dunder Mifflin'] | ||
task_categories = ['Project', 'Hourly', 'Pro Bono', 'Other'] | ||
|
||
# Setup database | ||
db = Database('kanban.db') | ||
todos = db.t.todos | ||
# if not IS_PROD: todos.drop() | ||
if todos not in db.t: | ||
todos.create(id=int, client_name=str, task_name=str, task_description=str, status=str, url=str, name=str, task_category=str, | ||
owner=str, collaborators=str, notifiers=str, pk='id') | ||
Todo = todos.dataclass() | ||
|
||
# Setup app | ||
app, rt = fast_app(hdrs=Theme.blue.headers()) | ||
|
||
def tid(id): return f'todo-{id}' | ||
|
||
@patch | ||
def __ft__(self:Todo): | ||
delete = Button('delete', hx_delete=delete_todo.to(id=self.id), hx_target=f'#{tid(self.id)}', | ||
hx_swap='outerHTML', cls=ButtonT.danger, hx_confirm='Are you sure you want to delete this todo?') | ||
edit = Button('edit', hx_get =edit_todo. to(id=self.id), hx_target=f'#{tid(self.id)}', | ||
hx_swap='outerHTML', cls=ButtonT.primary) | ||
|
||
status_category = Select(*map(lambda s: Option(s, value=s, selected=s==self.status), status_categories), id='status_category', name='status_category', | ||
hx_trigger='change', post=update_status, | ||
hx_target='#todo-kanban', hx_vals=f'{{"id": "{self.id}"}}') | ||
|
||
def create_owner_labels(vals): | ||
parsed_vals = [o.split('@')[0] for o in ast.literal_eval(vals)] | ||
return map(Label,parsed_vals) | ||
|
||
return Card(DivLAligned(Strong("Owner: "),*create_owner_labels(self.owner)), | ||
DivFullySpaced( | ||
H4(A(self.task_name, target_id='current-todo', href=self.url, target="_blank", cls='underline')), | ||
P(self.client_name, cls=TextFont.muted_sm)), | ||
A(self.task_description, href=self.url, target="_blank", cls=TextFont.muted_lg), | ||
DivLAligned(P("Collaborators: ", cls=TextFont.muted_sm),*create_owner_labels(self.collaborators)), | ||
DivLAligned(P("Notifiers: ", cls=TextFont.muted_sm),*create_owner_labels(self.notifiers)), | ||
id=tid(self.id), | ||
footer=DivFullySpaced(edit,delete,status_category,cls='space-x-2')) | ||
|
||
# Create/edit todo form | ||
def create_input_field(name: str, todo, input_fn=Input, hidden: bool = False) -> Input: | ||
return input_fn(id=f'new-{name}',name=name,value=getattr(todo, name, None), | ||
placeholder=' '.join(word.title() for word in name.split('_')), hidden=hidden) | ||
|
||
_default_todo = Todo(client_name=None, status="Not Started", task_name=None, task_description=None, url=None, id=None) | ||
def mk_todo_form(todo=_default_todo): | ||
"""Create a form for todo creation/editing with optional pre-filled values""" | ||
_create_input_field = lambda name: create_input_field(name, todo) | ||
|
||
def _select_option(c, todo_item): | ||
return Option(c, value=c, selected=c==str(todo_item)) | ||
|
||
inputs = [UkSelect(*map(lambda x: _select_option(x,todo.client_name), client_names), | ||
id='new-client_name', name='client_name', placeholder='Select Client')] | ||
inputs += [UkSelect(*map(lambda x: _select_option(x, todo.task_category), task_categories), | ||
id='new-task_category', name='task_category', placeholder='Select Task Category')] | ||
inputs += list(map(_create_input_field, ['task_name', 'url'])) | ||
|
||
# User tagging | ||
users = list(r['name'] for r in db['users'].rows) | ||
def _un_select_option(c, todo_item): | ||
return Option(c.split('@')[0], value=c, selected=c in str(todo_item)) | ||
|
||
user_tagging = [UkSelect(*map(lambda x: _un_select_option(x, todo.owner), users), | ||
id='new-owner', name='owner', placeholder='Select Owner', multiple=True), | ||
UkSelect(*map(lambda x: _un_select_option(x, todo.collaborators), users), | ||
id='new-collaborators', name='collaborators', placeholder='Select Collaborators', multiple=True), | ||
UkSelect(*map(lambda x: _un_select_option(x, todo.notifiers), users), | ||
id='new-notifiers', name='notifiers', placeholder='Select Notifiers', multiple=True)] | ||
|
||
inputs.append(Select(*map(lambda s: Option(s, value=s, selected=s==todo.status), status_categories), id=f'new-status', name='status')) | ||
if todo.id: inputs.append(Input(id='new-id', name='id', value=todo.id, hidden=True)) | ||
|
||
return Form(Grid(*inputs),Grid(*user_tagging),TextArea(todo.task_description, id='new-task_description', name='task_description'), | ||
Button("Create/Modify Task", cls=ButtonT.primary+'w-full', post=upsert_todo,hx_target='#todo-list'), | ||
id='todo-input', cls='space-y-3 mb-6', hx_swap_oob='true') | ||
|
||
# Index page | ||
@rt | ||
async def index(sess): | ||
return Container(mk_todo_form(), Divider(), | ||
Div(mk_todo_list(sess['user_name']),id='todo-list'), | ||
Details(Summary(H1("Archived Todos")),mk_archive_list())) | ||
|
||
# Upsert todo | ||
@rt | ||
async def upsert_todo(request, todo:Todo, sess): # owners: List[str] | ||
form = await request.form() | ||
todo.owner = form.getlist('owner[]') | ||
todo.collaborators = form.getlist('collaborators[]') | ||
todo.notifiers = form.getlist('notifiers[]') | ||
print("Owners:", todo.owner) | ||
print("Collaborators:", todo.collaborators) | ||
print("Notifiers:", todo.notifiers) | ||
|
||
print("I made it into the route.") | ||
if not todo.status: todo.status = "Not Started" | ||
todos.insert(todo,replace=True) | ||
return mk_todo_list(sess['user_name']),mk_todo_form() | ||
|
||
def mk_archive_list(): return Grid(*todos(where="status='Archive'"), id='archive-list', cols=1) | ||
|
||
def mk_todo_list(user_name): | ||
un_filtered_todos = f"lower(owner) LIKE '%{user_name.lower()}%' OR lower(collaborators) LIKE '%{user_name.lower()}%' OR lower(notifiers) LIKE '%{user_name.lower()}%'" | ||
def mk_status_card(status): | ||
return Card(*todos(where=f"status='{status}' and ({un_filtered_todos})"), header=H3(status), body_cls='space-y-2') | ||
return Div(Grid(*[mk_status_card(status) for status in status_categories if status != "Archive"], cols_max=3),id='todo-kanban') | ||
|
||
@app.delete | ||
async def delete_todo(id:int): | ||
try: todos.delete(id) | ||
except NotFoundError: pass | ||
|
||
@rt | ||
async def update_status(id:int, status_category:str, sess): | ||
todo = todos.get(id) | ||
todo.status = status_category | ||
todos.update(todo) | ||
return Div(mk_todo_list(sess['user_name'])),Div(mk_archive_list()(id='archive-list',hx_swap_oob=f'outerHTML:#archive-list')) | ||
|
||
@rt | ||
async def edit_todo(id:int): return Card(mk_todo_form(todos.get(id))) | ||
|
||
|
||
serve() |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
# Intermediate Todo App (SQLite) | ||
|
||
This project is a web-based implementation of an intermediate todo app built using FastHTML, HTMX, and SQLite. This builds on the minimal todo app by adding a due date and a done field to todos. Functionality related to these new fields has been added to the app. | ||
|
||
## Key Technologies and Techniques | ||
|
||
1. **FastHTML**: A Python-based framework for building web applications with a focus on web fundamentals. | ||
2. **HTMX**: Used to create dynamic server-side content updates that let you interact with the app without page reloads. | ||
3. **SQLite**: A lightweight, serverless database used to store and manage todo items. | ||
4. **FastSQL**: A library that simplifies database operations and integrates well with FastHTML. | ||
5. **MonsterUI**: A library that creates modern UI components for FastHTML | ||
|
||
## How It Works | ||
|
||
### Server-Side Logic | ||
|
||
The app uses FastHTML to define routes and handle todo list operations. Key routes include: | ||
|
||
- `GET /`: The main page that renders the initial todo list. | ||
- `POST /`: Handles adding new todo items. | ||
- `DELETE /{id}`: Handles deleting todo items. | ||
|
||
### Data Management | ||
|
||
Todo items are stored in an SQLite database: | ||
|
||
- `todos`: A table storing todo items with `id`, `title`, `done`, and `due`fields. | ||
|
||
### Dynamic Content | ||
|
||
HTMX is used to create a dynamic user interface: | ||
|
||
- `hx-post` attribute on the form triggers a POST request to add new todos. | ||
- `hx-delete` attribute on delete links triggers DELETE requests to remove todos. | ||
- `hx-target` specifies where the response from the server should be inserted. | ||
- `hx-swap` determines how the new content should be added or replaced. | ||
+ `beforeend`: Adds the new content at the end of the target element. This is used to add the new list item to end of the todo list. | ||
+ `outerHTML`: Replaces the entire target element with the new content. This is used to replaces the todo list item completely with `None` to remove it from the list. | ||
|
||
### Key Features | ||
|
||
1. **Add Todo**: Users can add new todos using a form at the top of the list. | ||
2. **Delete Todo**: Each todo item has a delete link to remove it from the list. | ||
3. **Real-time Updates**: The list updates dynamically without full page reloads. | ||
4. **Persistent Storage**: Todos are stored in an SQLite database for data persistence. | ||
5. **Due Date**: Each todo item has a due date field and the list is sorted by due date. If the item is past due the date is displayed in red. | ||
6. **Done**: Each todo item has a done field. Items can be marked as done and the list shows completed items crossed out. | ||
7. **MonsterUI**: Simple styling is done using the MonsterUI library. | ||
8. **Edit Todo**: Each todo item has an edit link to edit the item. The edit form is displayed in a card and the todo list is updated with the new values. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
[REQUIRED] | ||
ImageAltText=Intermediate Todo application | ||
ComponentName=Intermediate Todo App (SQLite) | ||
ComponentDescription=An intermediate todo app using SQLite backend. |