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:
|
async def store_var(db: psycopg.Cursor, key: str, value: str) -> None:
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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))
|
||||||
"decks": [],
|
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:
|
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__)
|
||||||
|
|
|
@ -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
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 (..)
|
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
|
||||||
}
|
}
|
||||||
, 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.Element Msg
|
||||||
view model =
|
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 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 =
|
||||||
|
|
Loading…
Reference in a new issue