Add deck list
This commit is contained in:
parent
f0475c2217
commit
4aa4d3b533
7 changed files with 193 additions and 21 deletions
|
@ -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(
|
||||
"""
|
||||
|
|
|
@ -115,5 +115,6 @@ class DeckCard:
|
|||
|
||||
@dataclasses.dataclass
|
||||
class Deck:
|
||||
deck_id: int
|
||||
name: str
|
||||
cards: typing.List[DeckCard]
|
||||
|
|
|
@ -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__)
|
||||
|
|
|
@ -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
17
www/src/Deck.elm
Normal 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
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 =
|
||||
|
|
Loading…
Reference in a new issue