1
+ from fasthtml .common import *
2
+ from fasthtml .jupyter import *
3
+ from functools import partial
4
+ from dataclasses import dataclass
5
+ from hmac import compare_digest
6
+ from monsterui .all import *
7
+
8
+ db = database (':memory:' )
9
+
10
+ class User : name :str ; pwd :str
11
+
12
+ class Todo :
13
+ id :int ; title :str ; done :bool ; name :str ; details :str ; priority :int
14
+
15
+ db .users = db .create (User , transform = True , pk = 'name' )
16
+ db .todos = db .create (Todo , transform = True )
17
+
18
+
19
+ def user_auth_before (req , sess ):
20
+ auth = req .scope ['auth' ] = sess .get ('auth' , None )
21
+ if not auth : return login_redir
22
+ db .todos .xtra (name = auth )
23
+
24
+ beforeware = Beforeware (
25
+ user_auth_before ,
26
+ skip = [r'/favicon\.ico' , r'/static/.*' , r'.*\.css' , r'.*\.js' , '/login' ]
27
+ )
28
+
29
+
30
+ app , rt = fast_app (hdrs = Theme .blue .headers (),before = beforeware )
31
+
32
+ login_redir = Redirect ('/login' )
33
+
34
+
35
+ @rt
36
+ def index (auth ):
37
+ top = Grid (Div (A ('logout' , href = '/logout' ), style = 'text-align: right' ))
38
+ # We don't normally need separate "screens" for adding or editing data. Here for instance,
39
+ # we're using an `hx-post` to add a new todo, which is added to the start of the list (using 'afterbegin').
40
+ new_inp = Input (id = "new-title" , name = "title" , placeholder = "New Todo" )
41
+ add = Form (Group (new_inp , Button ("Add" )),
42
+ hx_post = "/" , target_id = 'todo-list' , hx_swap = "afterbegin" )
43
+ # In the MiniDataAPI spec, treating a table as a callable (i.e with `todos(...)` here) queries the table.
44
+ # Because we called `xtra` in our Beforeware, this queries the todos for the current user only.
45
+ # We can include the todo objects directly as children of the `Form`, because the `Todo` class has `__ft__` defined.
46
+ # This is automatically called by FastHTML to convert the `Todo` objects into `FT` objects when needed.
47
+ # The reason we put the todo list inside a form is so that we can use the 'sortable' js library to reorder them.
48
+ # That library calls the js `end` event when dragging is complete, so our trigger here causes our `/reorder`
49
+ # handler to be called.
50
+ frm = Form (* db .todos (order_by = 'priority' ),
51
+ id = 'todo-list' , cls = 'sortable' , hx_post = "/reorder" , hx_trigger = "end" )
52
+ # We create an empty 'current-todo' Div at the bottom of our page, as a target for the details and editing views.
53
+ card = Card (Ul (frm ), header = add , footer = Div (id = 'current-todo' ))
54
+ # PicoCSS uses `<Main class='container'>` page content; `Container` is a tiny function that generates that.
55
+ # A handler can return either a single `FT` object or string, or a tuple of them.
56
+ # In the case of a tuple, the stringified objects are concatenated and returned to the browser.
57
+ # The `Title` tag has a special purpose: it sets the title of the page.
58
+ return Titled (f"{ auth } 's Todo list" , Container (top , card ))
59
+
60
+
61
+ @rt ('/login' )
62
+ def get ():
63
+ frm = Form (
64
+ LabelInput ("Name" , name = 'name' ),
65
+ LabelInput ("Password" , name = 'pwd' ),
66
+ Button ('login' ),
67
+ action = '/login' , method = 'post' )
68
+ return Titled ("Login" , frm , cls = ContainerT .sm )
69
+
70
+ @dataclass
71
+ class Login : name :str ; pwd :str
72
+
73
+ @rt ("/login" )
74
+ def post (login :Login , sess ):
75
+ if not login .name or not login .pwd : return login_redir
76
+ try : u = db .users [login .name ]
77
+ except NotFoundError : u = db .users .insert (login )
78
+ if not compare_digest (u .pwd .encode ("utf-8" ), login .pwd .encode ("utf-8" )): return login_redir
79
+ sess ['auth' ] = u .name
80
+ return Redirect ('/' )
81
+
82
+ @app .get ("/logout" )
83
+ def logout (sess ):
84
+ del sess ['auth' ]
85
+ return login_redir
86
+
87
+ serve ()
0 commit comments