aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorstuebinm2022-08-14 19:30:44 +0200
committerstuebinm2022-08-14 19:30:44 +0200
commitddbbd02dc816c076faaa9141b3aa4853da83749f (patch)
tree2a20abdfac9577feedb522bd77ef6e8a10e5a0f4
parentce14bc010a8f97cd3bab6f5cbd998f614b811546 (diff)
control room: translations & form input
still basic, but works pretty well overall
-rw-r--r--lib/API.hs4
-rw-r--r--lib/Persist.hs2
-rw-r--r--lib/Server.hs4
-rw-r--r--lib/Server/ControlRoom.hs348
-rw-r--r--lib/Server/Util.hs5
-rw-r--r--messages/de.msg11
-rw-r--r--messages/en.msg11
-rw-r--r--tracktrain.cabal1
8 files changed, 185 insertions, 201 deletions
diff --git a/lib/API.hs b/lib/API.hs
index 9400187..99e96ae 100644
--- a/lib/API.hs
+++ b/lib/API.hs
@@ -18,8 +18,8 @@ import Servant (Application, FormUrlEncoded,
FromHttpApiData (parseUrlPiece),
Server, err401, err404, type (:>))
import Servant.API (Capture, Get, JSON, NoContent,
- PlainText, Post, QueryParam,
- ReqBody, type (:<|>) ((:<|>)), Raw)
+ PlainText, Post, QueryParam, Raw,
+ ReqBody, type (:<|>) ((:<|>)))
import Servant.API.WebSocket (WebSocket)
import Servant.GTFS.Realtime (Proto)
import Servant.Swagger (HasSwagger (..))
diff --git a/lib/Persist.hs b/lib/Persist.hs
index c9c7901..611da9e 100644
--- a/lib/Persist.hs
+++ b/lib/Persist.hs
@@ -88,8 +88,8 @@ TripPing json sql=tt_trip_ping
Announcement json sql=tt_announcements
Id UUID default=uuid_generate_v4()
trip TripID
- message Text
header Text
+ message Text
day Day
url Text Maybe
announcedAt UTCTime Maybe
diff --git a/lib/Server.hs b/lib/Server.hs
index 24f29f9..f7ee81b 100644
--- a/lib/Server.hs
+++ b/lib/Server.hs
@@ -47,9 +47,9 @@ import Persist
import Server.ControlRoom
import Server.GTFS_RT (gtfsRealtimeServer)
import Server.Util (Service, ServiceM, runService)
-import Yesod (toWaiAppPlain)
+import Yesod (toWaiAppPlain)
-import System.IO.Unsafe
+import System.IO.Unsafe
application :: GTFS -> Pool SqlBackend -> IO Application
application gtfs dbpool = do
diff --git a/lib/Server/ControlRoom.hs b/lib/Server/ControlRoom.hs
index 0971e79..0e3f01e 100644
--- a/lib/Server/ControlRoom.hs
+++ b/lib/Server/ControlRoom.hs
@@ -1,89 +1,98 @@
-{-# LANGUAGE DataKinds #-}
-{-# LANGUAGE DefaultSignatures #-}
-{-# LANGUAGE DeriveAnyClass #-}
-{-# LANGUAGE DeriveGeneric #-}
-{-# LANGUAGE FlexibleContexts #-}
-{-# LANGUAGE FlexibleInstances #-}
-{-# LANGUAGE LambdaCase #-}
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RecordWildCards #-}
-{-# LANGUAGE ScopedTypeVariables #-}
-{-# LANGUAGE TypeApplications #-}
-{-# LANGUAGE TypeOperators #-}
-{-# LANGUAGE TypeFamilies #-}
-{-# LANGUAGE TemplateHaskell #-}
-{-# LANGUAGE QuasiQuotes #-}
-{-# LANGUAGE ViewPatterns #-}
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE DefaultSignatures #-}
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE RecordWildCards #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TemplateHaskell #-}
+{-# LANGUAGE TypeApplications #-}
+{-# LANGUAGE TypeFamilies #-}
+{-# LANGUAGE TypeOperators #-}
+{-# LANGUAGE ViewPatterns #-}
module Server.ControlRoom (ControlRoom(..)) where
-import Control.Monad (unless, when)
-import Control.Monad.IO.Class (MonadIO (liftIO))
-import qualified Data.ByteString.Char8 as C8
-import qualified Data.Map as M
-import Data.Swagger (ToSchema (..))
-import Data.Text (Text)
-import qualified Data.Text as T
-import Web.FormUrlEncoded (ToForm)
-import Web.Internal.FormUrlEncoded (Form)
-
-import Control.Monad (forM_)
-import Control.Monad.Extra (maybeM, whenM)
-import Data.Functor ((<&>))
-import Data.Map (Map)
-import Data.Pool (Pool)
-import Data.Time (getCurrentTime, utctDay)
-import Data.Time.Calendar (Day)
-import Data.Time.Format.ISO8601 (iso8601Show)
-import Data.UUID (UUID)
-import qualified Data.UUID as UUID
-import Database.Persist (Entity (..), delete, entityVal,
- get, insert, selectList, (==.))
-import Database.Persist.Sql (SqlBackend, PersistFieldSql,
- runSqlPool)
-import GHC.Generics (Generic)
-import Persist (AmendmentStatus,
- Announcement (..),
- EntityField (..), Key (..),
- ScheduleAmendment (ScheduleAmendment),
- runSql)
-import Server.Util (Service, redirect)
-import Text.ProtocolBuffers (Default (defaultValue))
-import Web.FormUrlEncoded (FromForm)
-import Fmt ((+|), (|+))
-import Yesod
-import Data.List (lookup)
-import Text.Read (readMaybe)
-import Text.Blaze.Html (ToMarkup(..))
-import qualified Data.Vector as V
+import Control.Monad (forM_)
+import Control.Monad.Extra (maybeM)
+import Control.Monad.IO.Class (MonadIO (liftIO))
+import qualified Data.ByteString.Char8 as C8
+import Data.Functor ((<&>))
+import Data.List (lookup)
+import Data.Map (Map)
+import qualified Data.Map as M
+import Data.Pool (Pool)
+import Data.Text (Text)
+import qualified Data.Text as T
+import Data.Time (getCurrentTime, utctDay)
+import Data.Time.Calendar (Day)
+import Data.Time.Format.ISO8601 (iso8601Show)
+import Data.UUID (UUID)
+import qualified Data.UUID as UUID
+import qualified Data.Vector as V
+import Database.Persist (Entity (..), delete, entityVal, get,
+ insert, selectList, (==.))
+import Database.Persist.Sql (PersistFieldSql, SqlBackend,
+ runSqlPool)
+import Fmt ((+|), (|+))
+import GHC.Generics (Generic)
+import Persist (AmendmentStatus, Announcement (..),
+ EntityField (..), Key (..),
+ ScheduleAmendment (ScheduleAmendment),
+ runSql)
+import Server.Util (Service)
+import Text.Blaze.Html (ToMarkup (..))
+import Text.ProtocolBuffers (Default (defaultValue))
+import Text.Read (readMaybe)
+import Yesod
+import Yesod.Form
import GTFS
data ControlRoom = ControlRoom
{ getBaseurl :: Text
- , getGtfs :: GTFS
- , getPool :: Pool SqlBackend
+ , getGtfs :: GTFS
+ , getPool :: Pool SqlBackend
}
+mkMessage "ControlRoom" "messages" "en"
+
mkYesod "ControlRoom" [parseRoutes|
-/main MainR GET
-/train/#TripID/#Day TrainViewR GET
+/ RootR GET
+/trains TrainsR GET
+/train/id/#TripID/#Day TrainViewR GET
+/train/announce/#TripID/#Day AnnounceR POST
+/train/del-announce/#UUID DelAnnounceR GET
+/trips TripsViewR GET
+/trip/#TripID TripViewR GET
|]
instance Yesod ControlRoom where
approot = ApprootMaster (\cr -> getBaseurl cr)
+instance RenderMessage ControlRoom FormMessage where
+ renderMessage _ _ = defaultFormMessage
+
-- which backend we're using and how to run an action.
instance YesodPersist ControlRoom where
type YesodPersistBackend ControlRoom = SqlBackend
-
runDB action = do
pool <- getYesod <&> getPool
runSqlPool action pool
-getMainR :: Handler Html
-getMainR = do
+
+
+getRootR :: Handler Html
+getRootR = redirect (TrainsR)
+
+getTrainsR :: Handler Html
+getTrainsR = do
req <- getRequest
let maybeDay = lookup "day" (reqGetParams req) >>= (readMaybe . T.unpack)
@@ -91,30 +100,34 @@ getMainR = do
gtfs <- getYesod <&> getGtfs
let trips = tripsOnDay gtfs day
defaultLayout [whamlet|
-<h1>Trips on #{iso8601Show day}
+<h1>Trains on #{iso8601Show day}
<ol>
$forall Trip{..} <- trips
<li><a href="@{TrainViewR tripTripID day}">#{tripTripID}</a>
: #{stopDeparture (V.head tripStops)} #{stationName (stopStation (V.head tripStops))}
-|] -- TODO: display stuff
+|]
getTrainViewR :: TripID -> Day -> Handler Html
getTrainViewR trip day = do
GTFS{..} <- getYesod <&> getGtfs
+ (widget, enctype) <- generateFormPost (announceForm day trip)
case M.lookup trip trips of
Nothing -> notFound
Just res@Trip{..} -> do
anns <- runDB $ selectList [ AnnouncementTrip ==. trip, AnnouncementDay ==. day ] []
defaultLayout [whamlet|
-<h1>#{tripTripID} on #{iso8601Show day}
-<h2>Announcements
+<h1><a href="@{TripViewR tripTripID}">#{tripTripID}</a> _{Msgon} <a href="@?{(TrainsR, [("day", T.pack (iso8601Show day))])}">#{iso8601Show day}</a>
+<h2>_{MsgAnnouncements}
<ul>
$forall Entity (AnnouncementKey uuid) Announcement{..} <- anns
- <li><em>#{announcementHeader}: #{announcementMessage}</em> <a href="">delete</a>
+ <li><em>#{announcementHeader}: #{announcementMessage}</em> <a href="@{DelAnnounceR uuid}">delete</a>
$if null anns
- <li><em>(none)</em>
- <li><a href="">Add Announcement</a>
-<h2>Stops
+ <li><em>(_{MsgNone})</em>
+<h3>_{MsgNewAnnouncement}
+<form method=post action=@{AnnounceR trip day} enctype=#{enctype}>
+ ^{widget}
+ <button>Submit
+<h2>_{MsgStops}
<ol>
$forall Stop{..} <- tripStops
<div>(#{stopSequence}) #{stopArrival} #{stationName stopStation}
@@ -124,130 +137,81 @@ getTrainViewR trip day = do
|]
+getTripsViewR :: Handler Html
+getTripsViewR = do
+ GTFS{..} <- getYesod <&> getGtfs
+ defaultLayout [whamlet|
+<h1>List of Trips
+<ul>
+ $forall Trip{..} <- trips
+ <li><a href="@{TripViewR tripTripID}">#{tripTripID}</a>
+ : #{stopDeparture (V.head tripStops)} #{stationName (stopStation (V.head tripStops))}
+|]
+
+
+getTripViewR :: TripID -> Handler Html
+getTripViewR tripId = do
+ GTFS{..} <- getYesod <&> getGtfs
+ case M.lookup tripId trips of
+ Nothing -> notFound
+ Just Trip{..} -> defaultLayout [whamlet|
+<h1>Trip #{tripTripID}
+<h2>Stops
+<ol>
+ $forall Stop{..} <- tripStops
+ <div>(#{stopSequence}) #{stopArrival} #{stationName stopStation}
+<h2>Dates
+<ul>
+ TODO!
+|]
+
+
+postAnnounceR :: TripID -> Day -> Handler Html
+postAnnounceR trip day = do
+ ((result, widget), enctype) <- runFormPost (announceForm day trip)
+ case result of
+ FormSuccess ann -> do
+ runDB $ insert ann
+ redirect (TrainViewR trip day)
+ _ -> defaultLayout
+ [whamlet|
+ <p>Invalid input, let's try again.
+ <form method=post action=@{AnnounceR trip day} enctype=#{enctype}>
+ ^{widget}
+ <button>Submit
+ |]
+
+getDelAnnounceR :: UUID -> Handler Html
+getDelAnnounceR uuid = do
+ ann <- runDB $ do
+ a <- get (AnnouncementKey uuid)
+ delete (AnnouncementKey uuid)
+ pure a
+ case ann of
+ Nothing -> notFound
+ Just Announcement{..} ->
+ redirect (TrainViewR announcementTrip announcementDay)
instance ToMarkup Time where
toMarkup time =
toMarkup (show time)
--- type ControlRoomAPI =
--- "main" :> QueryParam "day" Day :> Get '[HTML] (Day, Map TripID (Trip Deep Deep))
--- :<|> "train" :> Capture "tripID" TripID :> Capture "day" Day :> Get '[HTML] TrainView
--- :<|> "trips" :> Get '[HTML] (Map TripID (Trip Deep Deep))
--- :<|> "trip" :> Capture "tripId" TripID :> Get '[HTML] (Trip Deep Deep)
--- :<|> ControlRoomCranks
-
--- -- | train infra seems to involve turning lots of cranks, so here have some!
--- type ControlRoomCranks =
--- "train" :> "cancel" :> ReqBody '[FormUrlEncoded] Form :> Post '[PlainText] NoContent
--- :<|> "train" :> "del-announce" :> Capture "uuid" UUID :> Get '[PlainText] NoContent
--- :<|> "train" :> "announce" :> Capture "tripId" TripID :> Capture "day" Day :> ReqBody '[FormUrlEncoded] TrainAnnounceF :> Post '[PlainText] NoContent
--- :<|> "train" :> "date" :> Capture "tripId" TripID :> ReqBody '[FormUrlEncoded] TrainDateF :> Post '[PlainText] NoContent
--- :<|> "train" :> "date" :> Capture "tripId" TripID :> QueryParam' '[Required] "" TrainDateF :> Post '[PlainText] NoContent
--- :<|> "train" :> "delay" :> ReqBody '[FormUrlEncoded] Form :> Post '[PlainText] NoContent
--- :<|> "train" :> "metainfo" :> ReqBody '[FormUrlEncoded] Form :> EmptyAPI
--- :<|> "trip" :> "new" :> ReqBody '[FormUrlEncoded] Form :> EmptyAPI
-
--- data TrainAnnounceF = TrainAnnounceF
--- { taHeader :: Text
--- , taMsg :: Text
--- , taLogTime :: Maybe Bool
--- } deriving (Generic, ToHtmlForm, FromForm)
-
--- -- | TODO: can also be "normal"?
--- data TrainDateF = TrainDateF
--- { tdDay :: Day
--- , tdStatus :: AmendmentStatus
--- } deriving (Generic, ToHtmlForm, FromForm)
-
-
--- controlRoomServer :: GTFS -> Pool SqlBackend -> Service ControlRoomAPI
--- controlRoomServer gtfs@GTFS{..} dbpool = handleTimetable :<|> handleTrain :<|> handleTrips :<|> handleTrip
--- :<|> controlRoomCranks dbpool
--- where handleTrain trip day = case M.lookup trip trips of
--- Just res -> do
--- as <- runSql dbpool
--- $ selectList [ AnnouncementTrip ==. trip, AnnouncementDay ==. day ] []
--- pure (TrainView day res as True) -- TODO: can't just assume it runs that day …
--- Nothing -> throwError err404
--- handleTimetable maybeDay = do
--- -- TODO: resolve "overlay" trips (perhaps just additional CalendarDates?)
--- day <- liftIO $ maybeM (getCurrentTime <&> utctDay) pure (pure maybeDay)
--- pure $ (day, tripsOnDay gtfs day)
--- handleTrips = pure trips
--- handleTrip tripId = case M.lookup tripId trips of
--- Just trip -> pure trip
--- Nothing -> throwError err404
-
--- controlRoomCranks :: Pool SqlBackend -> Service ControlRoomCranks
--- controlRoomCranks dbpool = undefined :<|> handleAnnounceDel :<|> handleAnnounce :<|> handleDate :<|> handleDate :<|> undefined
--- where handleAnnounce tripID day TrainAnnounceF{..} = do
--- now <- liftIO $ getCurrentTime
--- AnnouncementKey uuid <- runSql dbpool $ insert $ Announcement
--- { announcementTrip = tripID
--- , announcementMessage = taMsg
--- , announcementHeader = taHeader
--- , announcementDay = day
--- , announcementUrl = Nothing
--- , announcementAnnouncedAt =
--- fmap (const now) taLogTime
--- }
--- redirect ("/cr/train/"<>C8.pack (T.unpack tripID)<>"/"<>C8.pack (iso8601Show day))
--- handleAnnounceDel uuid = do
--- ann <- runSql dbpool $ do
--- a <- get (AnnouncementKey uuid)
--- delete (AnnouncementKey uuid)
--- pure a
--- case ann of
--- Nothing -> throwError err404
--- Just Announcement{..} ->
--- redirect ("/cr/train/"+|announcementTrip|+"/"+| iso8601Show announcementDay|+"")
--- handleDate tripId TrainDateF{..} = do
--- -- TODO: check that tripId exists
--- runSql dbpool $ insert $ ScheduleAmendment tripId tdDay tdStatus
--- redirect ("/cr/train/"+|tripId|+"/"+| iso8601Show tdDay|+"")
-
-
--- instance ToHtmlFormInput CalendarExceptionType
--- instance ToHtmlForm CalendarDate
-
-
--- instance ToHtml TrainView where
--- toHtml (TrainView day Trip{..} as running) = crPage tripTripID $ do
--- unless running $ do
--- div_ [class_ "warning"] "Warning: Trip not running on this day!"
-
--- h2_ "Announcements"
--- ul_ $ do
--- unless (null as) $ forM_ as $ \(Entity (AnnouncementKey uuid) Announcement{..}) -> do
--- li_ $ do
--- em_ (toHtml announcementHeader); ": "; toHtml announcementMessage
--- " "; a_ [href_ $ "/cr/train/del-announce/"<>UUID.toText uuid] "delete"
--- li_ $ do
--- "Add Announcement:"
--- toHtmlForm (defaultValue {formAction = Just ("/cr/train/announce/"<>tripTripID<>"/"<>(T.pack . iso8601Show) day) })
--- (Proxy @TrainAnnounceF)
-
--- h2_ "Stops"
--- ol_ $ forM_ tripStops $ \Stop{..} -> do
--- div_ (toHtml (stationName stopStation))
-
--- h2_ "Vehicle Position"
--- div_ "todo!"
-
--- h2_ "Cancellation Status"
--- a_ [href_ ("/cr/train/date/"+|tripTripID|+"?tdDate="+|iso8601Show day|+"&tdStatus=Cancelled")] "Cancel"
-
-
--- instance ToHtml (Day, Map TripID (Trip Deep Deep)) where
--- toHtml (day, trips) = crPage ("trips on " <> shownDay) $ do
--- ol_ $ forM_ trips $ \Trip{..} -> li_ $ do
--- a_ [href_ ("/cr/train/"<>tripTripID<>"/"<>shownDay)] (toHtml tripTripID)
--- when (null trips) $ do
--- em_ "(none)"
--- where shownDay = T.pack (iso8601Show day)
-
--- -- | control room page
--- crPage :: Monad m => Text -> HtmlT m () -> HtmlT m ()
--- crPage title content = do
--- h1_ (toHtml title)
--- main_ content
+data TrainAnnounceF = TrainAnnounceF
+ { taHeader :: Text
+ , taMsg :: Text
+ , taLogTime :: Bool
+ } deriving (Show)
+
+
+
+announceForm :: Day -> TripID -> Html -> MForm Handler (FormResult Announcement, Widget)
+announceForm day tripId = renderDivs $ Announcement
+ <$> pure tripId
+ <*> areq textField "Header" Nothing
+ <*> areq textField "Text" Nothing
+ <*> pure day
+ <*> aopt urlField "Link" Nothing
+ <*> lift (liftIO getCurrentTime <&> Just)
+
+
+
diff --git a/lib/Server/Util.hs b/lib/Server/Util.hs
index 1c62663..a7a358e 100644
--- a/lib/Server/Util.hs
+++ b/lib/Server/Util.hs
@@ -1,7 +1,7 @@
{-# LANGUAGE FlexibleContexts #-}
-- | mostly the monad the service runs in
-module Server.Util (Service, ServiceM, runService, redirect) where
+module Server.Util (Service, ServiceM, runService) where
import Control.Monad.Logger (LoggingT, runStderrLoggingT)
import Data.ByteString (ByteString)
@@ -13,6 +13,3 @@ type Service api = ServerT api ServiceM
runService :: ServiceM a -> Handler a
runService = runStderrLoggingT
-
-redirect :: ByteString -> ServiceM a
-redirect path = throwError $ err302 { errHeaders = [("Location", path)] }
diff --git a/messages/de.msg b/messages/de.msg
new file mode 100644
index 0000000..bf5ba92
--- /dev/null
+++ b/messages/de.msg
@@ -0,0 +1,11 @@
+Announcements: Informationen
+None: nichts
+NewAnnouncement: Neue Information
+Header: Kurz
+Text: Lang
+Link: Link
+TripOnDay tripId day: #{tripId} an #{day}
+on: an
+SwitchLanguage: Sprache wechseln
+Switch: wechseln
+Stops: Stationen
diff --git a/messages/en.msg b/messages/en.msg
new file mode 100644
index 0000000..f9c5c8c
--- /dev/null
+++ b/messages/en.msg
@@ -0,0 +1,11 @@
+Announcements: Announcements
+None: none
+NewAnnouncement: New Announcement
+Header: Header
+Text: Text
+Link: Link
+TripOnDay tripId@Text day@String: #{tripId} on #{day}
+on: on
+SwitchLanguage: Switch language to:
+Switch: Switch
+Stops: Stops
diff --git a/tracktrain.cabal b/tracktrain.cabal
index ef3a1f2..4484247 100644
--- a/tracktrain.cabal
+++ b/tracktrain.cabal
@@ -93,6 +93,7 @@ library
, extra
, vector-algorithms
, yesod
+ , yesod-form
, blaze-html
hs-source-dirs: lib
exposed-modules: GTFS