23
23
24
24
from hmac import compare_digest
25
25
26
- # You can use any database you want; it'll be easier if you pick a lib that supports the MiniDataAPI spec.
27
- # Here we are using SQLite, with the FastLite library, which supports the MiniDataAPI spec.
26
+ # We define database tables using dataclasses. These are simple classes with type annotations.
27
+ # The `dataclass` decorator automatically generates a constructor, and other methods like `__repr__`, `__eq__`, etc.
28
+ @dataclass
29
+ class User : name :str ; pwd :str
30
+ @dataclass
31
+ class Todo : id :int ; title :str ; done :bool ; name :str ; details :str ; priority :int
32
+
33
+ #We use the `database` function from fastlist to create a sqlite database connection. If the data directory and
34
+ # utodos.db file don't exist, they will be created automatically.
28
35
db = database ('data/utodos.db' )
29
- # The `t` attribute is the table collection. The `todos` and `users` tables are not created if they don't exist.
30
- # Instead, you can use the `create` method to create them if needed.
31
- todos ,users = db .t .todos ,db .t .users
32
- if todos not in db .t :
33
- # You can pass a dict, or kwargs, to most MiniDataAPI methods.
34
- users .create (dict (name = str , pwd = str ), pk = 'name' )
35
- todos .create (id = int , title = str , done = bool , name = str , details = str , priority = int , pk = 'id' )
36
- # Although you can just use dicts, it can be helpful to have types for your DB objects.
37
- # The `dataclass` method creates that type, and stores it in the object, so it will use it for any returned items.
38
- Todo ,User = todos .dataclass (),users .dataclass ()
36
+ # The `create` method creates a table in the database, and returns a table object. We attach this to the `db` object
37
+ # like a namespace so there are no collisions between the database objects and various functions and variables we'll
38
+ # use in this app.
39
+ # The `pk` argument specifies the primary key for the table. The `foreign_keys` argument specifies any foreign keys.
40
+ db .users = db .create (User , pk = 'name' )
41
+ db .todos = db .create (Todo , pk = 'id' , foreign_keys = [('name' , 'user' )])
39
42
40
43
# Any Starlette response class can be returned by a FastHTML route handler.
41
44
# In that case, FastHTML won't change it at all.
@@ -54,7 +57,7 @@ def before(req, sess):
54
57
if not auth : return login_redir
55
58
# `xtra` is part of the MiniDataAPI spec. It adds a filter to queries and DDL statements,
56
59
# to ensure that the user can only see/edit their own todos.
57
- todos .xtra (name = auth )
60
+ db . todos .xtra (name = auth )
58
61
59
62
markdown_js = """
60
63
import { marked } from "https://cdn.jsdelivr.net/npm/marked/lib/marked.esm.js";
@@ -133,10 +136,10 @@ def post(login:Login, sess):
133
136
if not login .name or not login .pwd : return login_redir
134
137
# Indexing into a MiniDataAPI table queries by primary key, which is `name` here.
135
138
# It returns a dataclass object, if `dataclass()` has been called at some point, or a dict otherwise.
136
- try : u = users [login .name ]
139
+ try : u = db . users [login .name ]
137
140
# If the primary key does not exist, the method raises a `NotFoundError`.
138
141
# Here we use this to just generate a user -- in practice you'd probably to redirect to a signup page.
139
- except NotFoundError : u = users .insert (login )
142
+ except NotFoundError : u = db . users .insert (login )
140
143
# This compares the passwords using a constant time string comparison
141
144
# https://sqreen.github.io/DevelopersSecurityBestPractices/timing-attack/python
142
145
if not compare_digest (u .pwd .encode ("utf-8" ), login .pwd .encode ("utf-8" )): return login_redir
@@ -202,7 +205,7 @@ def get(auth):
202
205
# The reason we put the todo list inside a form is so that we can use the 'sortable' js library to reorder them.
203
206
# That library calls the js `end` event when dragging is complete, so our trigger here causes our `/reorder`
204
207
# handler to be called.
205
- frm = Form (* todos (order_by = 'priority' ),
208
+ frm = Form (* db . todos (order_by = 'priority' ),
206
209
id = 'todo-list' , cls = 'sortable' , hx_post = "/reorder" , hx_trigger = "end" )
207
210
# We create an empty 'current-todo' Div at the bottom of our page, as a target for the details and editing views.
208
211
card = Card (Ul (frm ), header = add , footer = Div (id = 'current-todo' ))
@@ -224,13 +227,13 @@ def get(auth):
224
227
# the parameter is a list of ints.
225
228
@rt ("/reorder" )
226
229
def post (id :list [int ]):
227
- for i ,id_ in enumerate (id ): todos .update ({'priority' :i }, id_ )
230
+ for i ,id_ in enumerate (id ): db . todos .update ({'priority' :i }, id_ )
228
231
# HTMX by default replaces the inner HTML of the calling element, which in this case is the todo list form.
229
232
# Therefore, we return the list of todos, now in the correct order, which will be auto-converted to FT for us.
230
233
# In this case, it's not strictly necessary, because sortable.js has already reorder the DOM elements.
231
234
# However, by returning the updated data, we can be assured that there aren't sync issues between the DOM
232
235
# and the server.
233
- return tuple (todos (order_by = 'priority' ))
236
+ return tuple (db . todos (order_by = 'priority' ))
234
237
235
238
# Refactoring components in FastHTML is as simple as creating Python functions.
236
239
# The `clr_details` function creates a Div with specific HTMX attributes.
@@ -242,7 +245,7 @@ def clr_details(): return Div(hx_swap_oob='innerHTML', id='current-todo')
242
245
@rt ("/todos/{id}" )
243
246
def delete (id :int ):
244
247
# The `delete` method is part of the MiniDataAPI spec, removing the item with the given primary key.
245
- todos .delete (id )
248
+ db . todos .delete (id )
246
249
# Returning `clr_details()` ensures the details view is cleared after deletion,
247
250
# leveraging HTMX's out-of-band swap feature.
248
251
# Note that we are not returning *any* FT component that doesn't have an "OOB" swap, so the target element
@@ -260,14 +263,14 @@ def get(id:int):
260
263
# `fill_form` populates the form with existing todo data, and returns the result.
261
264
# Indexing into a table (`todos`) queries by primary key, which is `id` here. It also includes
262
265
# `xtra`, so this will only return the id if it belongs to the current user.
263
- return fill_form (res , todos [id ])
266
+ return fill_form (res , db . todos [id ])
264
267
265
268
@rt ("/" )
266
269
def put (todo : Todo ):
267
270
# `update` is part of the MiniDataAPI spec.
268
271
# Note that the updated todo is returned. By returning the updated todo, we can update the list directly.
269
272
# Because we return a tuple with `clr_details()`, the details view is also cleared.
270
- return todos .update (todo ), clr_details ()
273
+ return db . todos .update (todo ), clr_details ()
271
274
272
275
@rt ("/" )
273
276
def post (todo :Todo ):
@@ -276,11 +279,11 @@ def post(todo:Todo):
276
279
new_inp = Input (id = "new-title" , name = "title" , placeholder = "New Todo" , hx_swap_oob = 'true' )
277
280
# `insert` returns the inserted todo, which is appended to the start of the list, because we used
278
281
# `hx_swap='afterbegin'` when creating the todo list form.
279
- return todos .insert (todo ), new_inp
282
+ return db . todos .insert (todo ), new_inp
280
283
281
284
@rt ("/todos/{id}" )
282
285
def get (id :int ):
283
- todo = todos [id ]
286
+ todo = db . todos [id ]
284
287
# `hx_swap` determines how the update should occur. We use "outerHTML" to replace the entire todo `Li` element.
285
288
btn = Button ('delete' , hx_delete = f'/todos/{ todo .id } ' ,
286
289
target_id = f'todo-{ todo .id } ' , hx_swap = "outerHTML" )
0 commit comments