Add deck list

This commit is contained in:
Correl Roush 2023-01-09 21:22:46 -05:00
parent f0475c2217
commit 4aa4d3b533
7 changed files with 193 additions and 21 deletions

View file

@ -346,6 +346,25 @@ async def store_deck_card(
)
async def get_decks(
db: psycopg.Cursor, limit: int = 10, offset: int = 0
) -> typing.List[tutor.models.Deck]:
db.row_factory = psycopg.rows.dict_row
await db.execute(
"""
SELECT * FROM "decks"
ORDER BY "decks"."deck_id"
LIMIT %(limit)s OFFSET %(offset)s
""",
{"limit": limit, "offset": offset},
)
rows = await db.fetchall()
return [
tutor.models.Deck(deck_id=row["deck_id"], name=row["name"], cards=[])
for row in rows
]
async def store_var(db: psycopg.Cursor, key: str, value: str) -> None:
await db.execute(
"""

View file

@ -115,5 +115,6 @@ class DeckCard:
@dataclasses.dataclass
class Deck:
deck_id: int
name: str
cards: typing.List[DeckCard]

View file

@ -58,7 +58,17 @@ class OpenAPIRequestHandler(tornado_openapi3.handler.OpenAPIRequestHandler):
return spec
class SearchHandler(tornado.web.RequestHandler):
class RequestHandler(tornado.web.RequestHandler):
def set_links(self, **links) -> None:
self.set_header(
"Link",
", ".join(
[f'<{self.url(url)}>; rel="{rel}"' for rel, url in links.items()]
),
)
class SearchHandler(RequestHandler):
def url(self, url: str) -> str:
scheme_override = self.application.settings["scheme"]
if not scheme_override:
@ -74,14 +84,6 @@ class SearchHandler(tornado.web.RequestHandler):
)
)
def set_links(self, **links) -> None:
self.set_header(
"Link",
", ".join(
[f'<{self.url(url)}>; rel="{rel}"' for rel, url in links.items()]
),
)
async def get(self) -> None:
async with self.application.pool.connection() as conn:
async with conn.cursor() as cursor:
@ -146,7 +148,7 @@ class SearchHandler(tornado.web.RequestHandler):
)
class CollectionHandler(tornado.web.RequestHandler):
class CollectionHandler(RequestHandler):
async def get(self) -> None:
async with self.application.pool.connection() as conn:
async with conn.cursor() as cursor:
@ -157,19 +159,42 @@ class CollectionHandler(tornado.web.RequestHandler):
)
class DecksHandler(tornado.web.RequestHandler):
class DecksHandler(RequestHandler):
async def get(self) -> None:
self.finish(
{
"decks": [],
}
page = max(1, int(self.get_argument("page", 1)))
limit = int(self.get_argument("limit", 10))
async with self.application.pool.connection() as conn:
async with conn.cursor() as cursor:
decks = await tutor.database.get_decks(
cursor, limit=limit + 1, offset=limit * (page - 1)
)
has_more = decks and len(decks) > limit
self.set_header("Content-Type", "application/json")
self.set_header("Access-Control-Allow-Origin", "*")
links = {}
if page > 1:
links["prev"] = update_args(self.request.full_url(), page=page - 1)
if has_more:
links["next"] = update_args(self.request.full_url(), page=page + 1)
self.set_links(**links)
self.write(
json.dumps(
[
{
"deck_id": deck.deck_id,
"name": deck.name,
"cards": [],
}
for deck in decks
]
)
)
async def post(self) -> None:
...
class TemplateHandler(tornado.web.RequestHandler):
class TemplateHandler(RequestHandler):
def initialize(
self,
path: str,
@ -193,6 +218,7 @@ class StaticFileHandler(tornado.web.StaticFileHandler):
path = "index.html"
return tornado.web.StaticFileHandler.get_absolute_path(root, path)
class Application(tornado.web.Application):
def __init__(self, **settings):
version = importlib.metadata.version(__package__)

View file

@ -157,6 +157,10 @@ update msg model =
Pages.Collection.update pageMsg pageModel
|> updateWith Collection CollectionMsg model
( DeckListMsg pageMsg, DeckList pageModel ) ->
Pages.DeckList.update pageMsg pageModel
|> updateWith DeckList DeckListMsg model
( _, _ ) ->
( model, Cmd.none )
@ -258,6 +262,9 @@ subscriptions model =
Collection pageModel ->
Sub.batch (Sub.map CollectionMsg (Pages.Collection.subscriptions pageModel) :: global)
DeckList pageModel ->
Sub.batch (Sub.map DeckListMsg (Pages.DeckList.subscriptions pageModel) :: global)
_ ->
Sub.batch global

17
www/src/Deck.elm Normal file
View file

@ -0,0 +1,17 @@
module Deck exposing (..)
import Json.Decode
import Json.Decode.Pipeline as JDP
type alias Deck =
{ id : Int
, name : String
}
decode : Json.Decode.Decoder Deck
decode =
Json.Decode.succeed Deck
|> JDP.required "deck_id" Json.Decode.int
|> JDP.required "name" Json.Decode.string

View file

@ -1,21 +1,42 @@
module Pages.DeckList exposing (..)
import Browser
import Browser.Events
import Browser.Navigation
import Deck
import Element as E
import Element.Background as Background
import Element.Events as Events
import Element.Font as Font
import Http
import Paginated
import Route
import Spinner
import UI
import Url
import Url.Builder
type alias Model =
{ navigationKey : Browser.Navigation.Key
, url : Url.Url
, device : E.Device
, spinner : Spinner.Model
, deckPage : Deckpage
}
type Msg
= None
= ViewportChanged UI.Dimensions
| SpinnerMsg Spinner.Msg
| GotDecks (Result Http.Error (Paginated.Page Deck.Deck))
| DeckClicked Int
type Deckpage
= Ready (Paginated.Page Deck.Deck)
| Loading (Paginated.Page Deck.Deck)
| Failed
init : Browser.Navigation.Key -> Url.Url -> E.Device -> ( Model, Cmd Msg )
@ -23,11 +44,88 @@ init key url device =
( { navigationKey = key
, url = url
, device = device
, spinner = Spinner.init
, deckPage = Loading Paginated.empty
}
, Cmd.none
, getDecks
)
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
ViewportChanged viewport ->
( { model
| device = E.classifyDevice viewport
}
, Cmd.none
)
SpinnerMsg msg_ ->
( { model | spinner = Spinner.update msg_ model.spinner }, Cmd.none )
GotDecks (Ok deckPage) ->
( { model | deckPage = Ready deckPage }, Cmd.none )
GotDecks (Err _) ->
( { model | deckPage = Failed }, Cmd.none )
DeckClicked deckId ->
( model, Browser.Navigation.pushUrl model.navigationKey (Route.toUrl (Route.Deck deckId)) )
view : Model -> E.Element Msg
view model =
E.column [ E.padding 40, E.centerX, Font.italic ] [ UI.text "No decks yet" ]
case model.deckPage of
Failed ->
E.none
Loading _ ->
E.el [ E.height E.fill, E.centerX ] <|
E.html <|
Spinner.view UI.manaSpinner
model.spinner
Ready deckPage ->
if deckPage == Paginated.empty then
E.column [ E.padding 40, E.centerX, Font.italic ] [ UI.text "No decks yet" ]
else
let
deckRow deck =
E.row
[ E.width E.fill
, E.padding 10
, E.spacing 10
, E.pointer
, E.mouseOver [ Background.color UI.colors.hover ]
, Events.onClick <| DeckClicked deck.id
]
[ UI.title deck.name ]
in
E.column [ E.width E.fill, E.centerX ] <| List.map deckRow deckPage.values
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.batch
[ Browser.Events.onResize
(\w h -> ViewportChanged { width = w, height = h })
, Sub.map SpinnerMsg Spinner.subscription
]
getDecks : Cmd Msg
getDecks =
loadPage <|
Url.Builder.absolute
[ "api", "decks" ]
[ Url.Builder.int "limit" 18 ]
loadPage : String -> Cmd Msg
loadPage url =
Http.get
{ url = url
, expect = Paginated.expectJson GotDecks Deck.decode
}

View file

@ -2,12 +2,13 @@ module Route exposing (Route(..), fromUrl, toUrl)
import Url exposing (Url)
import Url.Builder
import Url.Parser exposing (Parser, map, oneOf, parse, s, top)
import Url.Parser exposing ((</>), Parser, int, map, oneOf, parse, s, top)
type Route
= Collection
| DeckList
| Deck Int
parser : Parser (Route -> a) a
@ -16,19 +17,22 @@ parser =
[ map Collection top
, map Collection (s "collection")
, map DeckList (s "decks")
, map Deck (s "decks" </> int)
]
toUrl : Route -> String
toUrl route =
case route of
Collection ->
Url.Builder.absolute [ "collection" ] []
DeckList ->
Url.Builder.absolute [ "decks" ] []
Deck deckId ->
Url.Builder.absolute [ "decks", String.fromInt deckId ] []
fromUrl : Url.Url -> Maybe Route
fromUrl url =