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: async def store_var(db: psycopg.Cursor, key: str, value: str) -> None:
await db.execute( await db.execute(
""" """

View file

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

View file

@ -58,7 +58,17 @@ class OpenAPIRequestHandler(tornado_openapi3.handler.OpenAPIRequestHandler):
return spec 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: def url(self, url: str) -> str:
scheme_override = self.application.settings["scheme"] scheme_override = self.application.settings["scheme"]
if not scheme_override: 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 def get(self) -> None:
async with self.application.pool.connection() as conn: async with self.application.pool.connection() as conn:
async with conn.cursor() as cursor: 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 def get(self) -> None:
async with self.application.pool.connection() as conn: async with self.application.pool.connection() as conn:
async with conn.cursor() as cursor: 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: async def get(self) -> None:
self.finish( 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(
[
{ {
"decks": [], "deck_id": deck.deck_id,
"name": deck.name,
"cards": [],
} }
for deck in decks
]
)
) )
async def post(self) -> None: async def post(self) -> None:
... ...
class TemplateHandler(tornado.web.RequestHandler): class TemplateHandler(RequestHandler):
def initialize( def initialize(
self, self,
path: str, path: str,
@ -193,6 +218,7 @@ class StaticFileHandler(tornado.web.StaticFileHandler):
path = "index.html" path = "index.html"
return tornado.web.StaticFileHandler.get_absolute_path(root, path) return tornado.web.StaticFileHandler.get_absolute_path(root, path)
class Application(tornado.web.Application): class Application(tornado.web.Application):
def __init__(self, **settings): def __init__(self, **settings):
version = importlib.metadata.version(__package__) version = importlib.metadata.version(__package__)

View file

@ -157,6 +157,10 @@ update msg model =
Pages.Collection.update pageMsg pageModel Pages.Collection.update pageMsg pageModel
|> updateWith Collection CollectionMsg model |> updateWith Collection CollectionMsg model
( DeckListMsg pageMsg, DeckList pageModel ) ->
Pages.DeckList.update pageMsg pageModel
|> updateWith DeckList DeckListMsg model
( _, _ ) -> ( _, _ ) ->
( model, Cmd.none ) ( model, Cmd.none )
@ -258,6 +262,9 @@ subscriptions model =
Collection pageModel -> Collection pageModel ->
Sub.batch (Sub.map CollectionMsg (Pages.Collection.subscriptions pageModel) :: global) 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 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 (..) module Pages.DeckList exposing (..)
import Browser
import Browser.Events
import Browser.Navigation import Browser.Navigation
import Deck
import Element as E import Element as E
import Element.Background as Background
import Element.Events as Events
import Element.Font as Font import Element.Font as Font
import Http
import Paginated
import Route
import Spinner
import UI import UI
import Url import Url
import Url.Builder
type alias Model = type alias Model =
{ navigationKey : Browser.Navigation.Key { navigationKey : Browser.Navigation.Key
, url : Url.Url , url : Url.Url
, device : E.Device , device : E.Device
, spinner : Spinner.Model
, deckPage : Deckpage
} }
type Msg 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 ) init : Browser.Navigation.Key -> Url.Url -> E.Device -> ( Model, Cmd Msg )
@ -23,11 +44,88 @@ init key url device =
( { navigationKey = key ( { navigationKey = key
, url = url , url = url
, device = device , device = device
, spinner = Spinner.init
, deckPage = Loading Paginated.empty
}
, getDecks
)
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
ViewportChanged viewport ->
( { model
| device = E.classifyDevice viewport
} }
, Cmd.none , 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.Element Msg
view model = view model =
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" ] 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 exposing (Url)
import Url.Builder 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 type Route
= Collection = Collection
| DeckList | DeckList
| Deck Int
parser : Parser (Route -> a) a parser : Parser (Route -> a) a
@ -16,19 +17,22 @@ parser =
[ map Collection top [ map Collection top
, map Collection (s "collection") , map Collection (s "collection")
, map DeckList (s "decks") , map DeckList (s "decks")
, map Deck (s "decks" </> int)
] ]
toUrl : Route -> String toUrl : Route -> String
toUrl route = toUrl route =
case route of case route of
Collection -> Collection ->
Url.Builder.absolute [ "collection" ] [] Url.Builder.absolute [ "collection" ] []
DeckList -> DeckList ->
Url.Builder.absolute [ "decks" ] [] Url.Builder.absolute [ "decks" ] []
Deck deckId ->
Url.Builder.absolute [ "decks", String.fromInt deckId ] []
fromUrl : Url.Url -> Maybe Route fromUrl : Url.Url -> Maybe Route
fromUrl url = fromUrl url =