beam database (unfinished), overloadedRecordDot, fourmolu, relude

also one fix where the "skip voting and see results" link is broken.
This commit is contained in:
Jack Wines 2025-08-25 07:54:36 -07:00
commit cee339a8ed
Signed by: Jack
SSH key fingerprint: SHA256:AaP2Hr/e3mEjeY+s9XJmQqAesqEms8ENRhwRkpO0WUk
11 changed files with 438 additions and 102 deletions

1
.envrc
View file

@ -1 +1,2 @@
use flake; use flake;
watch_file flake.nix *.cabal *.lock;

View file

@ -1,3 +0,0 @@
packages:
./
allow-newer: servant, servant-server, *:servant-server, *:base, lucid-htmx:*, beam:*

118
flake.lock generated
View file

@ -1,42 +1,108 @@
{ {
"nodes": { "nodes": {
"flake-utils": { "flake-parts": {
"inputs": { "inputs": {
"systems": "systems" "nixpkgs-lib": "nixpkgs-lib"
}, },
"locked": { "locked": {
"lastModified": 1689068808, "lastModified": 1754487366,
"narHash": "sha256-6ixXo3wt24N/melDWjq70UuHQLxGV8jZvooRanIHXw0=", "narHash": "sha256-pHYj8gUBapuUzKV/kN/tR3Zvqc7o6gdFB9XKXIp1SQ8=",
"owner": "numtide", "owner": "hercules-ci",
"repo": "flake-utils", "repo": "flake-parts",
"rev": "919d646de7be200f3bf08cb76ae1f09402b6f9b4", "rev": "af66ad14b28a127c5c0f3bbb298218fc63528a18",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "numtide", "owner": "hercules-ci",
"repo": "flake-utils", "repo": "flake-parts",
"type": "github"
}
},
"flake-root": {
"locked": {
"lastModified": 1723604017,
"narHash": "sha256-rBtQ8gg+Dn4Sx/s+pvjdq3CB2wQNzx9XGFq/JVGCB6k=",
"owner": "srid",
"repo": "flake-root",
"rev": "b759a56851e10cb13f6b8e5698af7b59c44be26e",
"type": "github"
},
"original": {
"owner": "srid",
"repo": "flake-root",
"type": "github"
}
},
"haskell-flake": {
"locked": {
"lastModified": 1756071733,
"narHash": "sha256-hRlG8+m5oOBb6/a8DQAzrt0ApLYkbNfActj7b3OzeLk=",
"owner": "srid",
"repo": "haskell-flake",
"rev": "99200161a88c1cb83bb114ab237e66d3fe327692",
"type": "github"
},
"original": {
"owner": "srid",
"repo": "haskell-flake",
"type": "github"
}
},
"mission-control": {
"locked": {
"lastModified": 1733438716,
"narHash": "sha256-1tt43rwHk0N5fwEhbpsHWO4nBVFCQN0w1KM427DNycM=",
"owner": "Platonic-Systems",
"repo": "mission-control",
"rev": "65d04c4ab9db076eff09824d2936a5c215c21f36",
"type": "github"
},
"original": {
"owner": "Platonic-Systems",
"repo": "mission-control",
"type": "github" "type": "github"
} }
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1692665005, "lastModified": 1755615617,
"narHash": "sha256-wJ2OF51EYNbTGwuI3EmJWAJV9K5pNuP1aBpD9DXeNb4=", "narHash": "sha256-HMwfAJBdrr8wXAkbGhtcby1zGFvs+StOp19xNsbqdOg=",
"owner": "NixOS", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "50097d75fa0dcc6be7271bc390e612fa0363a38d", "rev": "20075955deac2583bb12f07151c2df830ef346b4",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "NixOS", "owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs", "repo": "nixpkgs",
"type": "github" "type": "github"
} }
}, },
"nixpkgs-lib": {
"locked": {
"lastModified": 1753579242,
"narHash": "sha256-zvaMGVn14/Zz8hnp4VWT9xVnhc8vuL3TStRqwk22biA=",
"owner": "nix-community",
"repo": "nixpkgs.lib",
"rev": "0f36c44e01a6129be94e3ade315a5883f0228a6e",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "nixpkgs.lib",
"type": "github"
}
},
"root": { "root": {
"inputs": { "inputs": {
"flake-utils": "flake-utils", "flake-parts": "flake-parts",
"nixpkgs": "nixpkgs" "flake-root": "flake-root",
"haskell-flake": "haskell-flake",
"mission-control": "mission-control",
"nixpkgs": "nixpkgs",
"systems": "systems",
"treefmt-nix": "treefmt-nix"
} }
}, },
"systems": { "systems": {
@ -53,6 +119,26 @@
"repo": "default", "repo": "default",
"type": "github" "type": "github"
} }
},
"treefmt-nix": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1755934250,
"narHash": "sha256-CsDojnMgYsfshQw3t4zjRUkmMmUdZGthl16bXVWgRYU=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "74e1a52d5bd9430312f8d1b8b0354c92c17453e5",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "treefmt-nix",
"type": "github"
}
} }
}, },
"root": "root", "root": "root",

151
flake.nix
View file

@ -1,43 +1,128 @@
# SPDX-FileCopyrightText: 2021 Serokell <https://serokell.io/>
#
# SPDX-License-Identifier: CC0-1.0
{ {
description = "My haskell application"; description = "srid/haskell-template: Nix template for Haskell projects";
inputs = { inputs = {
nixpkgs.url = "github:NixOS/nixpkgs"; nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils"; systems.url = "github:nix-systems/default";
flake-parts.url = "github:hercules-ci/flake-parts";
haskell-flake.url = "github:srid/haskell-flake";
treefmt-nix.url = "github:numtide/treefmt-nix";
treefmt-nix.inputs.nixpkgs.follows = "nixpkgs";
# tmp-postgres.url = "github:jfischoff/tmp-postgres";
# tmp-postgres.flake = false;
mission-control.url = "github:Platonic-Systems/mission-control";
flake-root.url = "github:srid/flake-root";
}; };
outputs = { self, nixpkgs, flake-utils }: outputs = inputs:
flake-utils.lib.eachDefaultSystem (system: inputs.flake-parts.lib.mkFlake { inherit inputs; } {
let systems = import inputs.systems;
pkgs = nixpkgs.legacyPackages.${system}; imports = [
inputs.haskell-flake.flakeModule
inputs.treefmt-nix.flakeModule
inputs.flake-root.flakeModule
inputs.mission-control.flakeModule
];
perSystem = { self', system, lib, config, pkgs, ... }: {
# Our only Haskell project. You can have multiple projects, but this template
# has only one.
# See https://github.com/srid/haskell-flake/blob/master/example/flake.nix
haskellProjects.default = {
# The base package set (this value is the default)
# basePackages = pkgs.haskellPackages.ghc_9_12_1;
haskellPackages = pkgs.haskellPackages; # Packages to add on top of `basePackages`
packages = {
jailbreakUnbreak = pkg: # Add source or Hackage overrides here
pkgs.haskell.lib.doJailbreak (pkg.overrideAttrs (_: { meta = { }; })); # (Local packages are added automatically)
# https://github.com/lehins/hip.git
# DON'T FORGET TO PUT YOUR PACKAGE NAME HERE, REMOVING `throw` # hip.source = inputs.hip + "/hip";
packageName = "rcv-site"; # tmp-postgres.source = inputs.tmp-postgres;
in {
packages.${packageName} =
haskellPackages.callCabal2nix packageName self rec {
# Dependency overrides go here
}; };
packages.default = self.packages.${system}.${packageName}; # Add your package overrides here
defaultPackage = self.packages.${system}.default; settings = {
devShells.default = pkgs.mkShell {
buildInputs = with pkgs; [ # tmp-postgres.check = false;
haskellPackages.haskell-language-server # you must build it with your ghc to work
cabal-install # beam-migrate = {
]; # broken = false;
inputsFrom = map (__getAttr "env") (__attrValues self.packages.${system}); # jailbreak = true;
# };
# beam-postgres = {
# broken = false;
# jailbreak = true;
# };
# barbies-th = {
# broken = false;
# jailbreak = true;
# };
};
# Development shell configuration
devShell = {
# hlsCheck.enable = false;
};
# What should haskell-flake add to flake outputs?
autoWire = [ "packages" "apps" "checks" ]; # Wire all but the devShell
}; };
devShell = self.devShells.${system}.default;
}); # Auto formatters. This also adds a flake check to ensure that the
# source tree was auto formatted.
treefmt.config = {
projectRootFile = "flake.nix";
programs.ormolu.enable = true;
programs.nixpkgs-fmt.enable = true;
programs.cabal-fmt.enable = true;
programs.hlint.enable = true;
# We use fourmolu
programs.ormolu.package = pkgs.haskellPackages.fourmolu;
};
mission-control.scripts = {
hoogle = {
description = "Start Hoogle server for project dependencies";
exec = ''
hoogle serve -p 8888 --local;
'';
category = "Dev Tools";
};
haddocks = {
description = "make docs & serve them";
exec = ''
echo http://127.0.0.1:8887;
cabal haddock-project --executables --internal --hoogle || true;
python3 -m http.server -d haddocks 8887;
'';
category = "Dev Tools";
};
};
# Default package & app.
packages.default = self'.packages.rcv-site;
apps.default = self'.apps.rcv-site;
# Default shell.
devShells.default = pkgs.mkShell {
name = "haskell-template";
meta.description = "Haskell development environment";
# See https://zero-to-flakes.com/haskell-flake/devshell#composing-devshells
inputsFrom = [
config.haskellProjects.default.outputs.devShell
config.treefmt.build.devShell
config.mission-control.devShell
];
nativeBuildInputs = with pkgs; [
];
};
};
};
} }

17
fourmolu.yaml Normal file
View file

@ -0,0 +1,17 @@
# Generated from web app, for more information, see: https://fourmolu.github.io/config/
indentation: 4
column-limit: none
function-arrows: trailing
comma-style: leading
import-export-style: diff-friendly
indent-wheres: false
record-brace-space: false
newlines-between-decls: 1
haddock-style: multi-line
haddock-style-module: null
let-style: auto
in-style: right-align
single-constraint-parens: always
unicode: never
respectful: true
single-deriving-parens: always

View file

@ -12,13 +12,18 @@ executable rcv-site
DeriveAnyClass, DeriveAnyClass,
DeriveGeneric, DeriveGeneric,
DuplicateRecordFields, DuplicateRecordFields,
ExtendedDefaultRules,
FlexibleContexts, FlexibleContexts,
FlexibleInstances, FlexibleInstances,
ImpredicativeTypes, ImpredicativeTypes,
ExtendedDefaultRules, InstanceSigs,
MultiParamTypeClasses, MultiParamTypeClasses,
NoFieldSelectors,
DerivingStrategies,
DerivingVia,
NamedFieldPuns, NamedFieldPuns,
OverloadedLabels, OverloadedLabels,
OverloadedRecordDot,
OverloadedLists, OverloadedLists,
OverloadedStrings, OverloadedStrings,
PartialTypeSignatures, PartialTypeSignatures,
@ -27,6 +32,7 @@ executable rcv-site
RecursiveDo, RecursiveDo,
ScopedTypeVariables, ScopedTypeVariables,
StandaloneDeriving, StandaloneDeriving,
StrictData,
TemplateHaskell, TemplateHaskell,
TupleSections, TupleSections,
TypeApplications, TypeApplications,
@ -36,15 +42,25 @@ executable rcv-site
UndecidableInstances, UndecidableInstances,
hs-source-dirs: hs-source-dirs:
src src
mixins:
base hiding (Prelude),
relude (Relude as Prelude),
relude
build-depends: build-depends:
acid-state, acid-state,
aeson, aeson,
-- beam-sqlite,
-- beam-core,
-- sqlite-simple,
async, async,
relude,
base, base,
beam-core,
beam-migrate,
beam-postgres,
bytestring, bytestring,
cereal,
commonmark,
containers, containers,
deepseq, deepseq,
hashable, hashable,
@ -53,7 +69,6 @@ executable rcv-site
lucid-htmx, lucid-htmx,
mtl, mtl,
network-uri, network-uri,
commonmark,
random, random,
safecopy, safecopy,
servant, servant,
@ -69,11 +84,11 @@ executable rcv-site
warp warp
-- warp-tls -- warp-tls
default-language: default-language:
Haskell2010 GHC2021
other-modules: other-modules:
API API
Database Database
-- BeamDatabase BeamDatabase
InstantRunoff InstantRunoff
Error Error
Poll Poll
@ -81,12 +96,12 @@ executable rcv-site
Ballot Ballot
AppM AppM
ghc-options: ghc-options:
-Wall -- -Wall
-Wcompat -- -Wcompat
-fwarn-redundant-constraints -- -fwarn-redundant-constraints
-fwarn-incomplete-uni-patterns -- -fwarn-incomplete-uni-patterns
-fwarn-tabs -- -fwarn-tabs
-fwarn-incomplete-record-updates -- -fwarn-incomplete-record-updates
-fwarn-identities -- -fwarn-identities
-threaded -- -threaded
"-with-rtsopts=-I0 -N" "-with-rtsopts=-I0 -N"

134
src/BeamDatabase.hs Normal file
View file

@ -0,0 +1,134 @@
module BeamDatabase where
import Data.Word
import Data.Int
import GHC.Generics (Generic)
import qualified Data.List as L
import qualified Data.Vector as V
import qualified Data.List.NonEmpty as LN
import qualified Data.Text as T
import qualified Database.Beam as B
import qualified Database.Beam.Migrate as BM
import qualified Database.Beam.Migrate.Simple as BMS
import qualified API as A
import qualified Database.Beam.Postgres as BP
import qualified Database.Beam.Postgres.Migrate as BPM
import qualified Data.ByteString as By
-- import qualified Database.Sqlite.Simple as PS
import qualified Data.String as S
import Database.Beam.Backend.SQL (SqlSerial, IsSql92DataTypeSyntax (domainType))
-- import Database.Beam.Sqlite as B ( connect )
import qualified Database.Beam as BM
import qualified Database.Beam.Backend.SQL.BeamExtensions as BSQLE
import qualified Database.Beam.Backend.SQL.BeamExtensions as B
-- deriving instance Show Poll
-- deriving instance Eq Poll
-- deriving instance Ord Poll
type SerialInt = SqlSerial Int32
data OptionT f = OptionT {
identity:: B.C f SerialInt,
forPoll :: B.PrimaryKey PollT f,
name :: B.C f T.Text
} deriving (Generic, B.Beamable)
type Option = OptionT B.Identity
data BallotT f = BallotT {
identity :: B.C f SerialInt,
forPoll :: B.PrimaryKey PollT f,
votes :: B.C f By.ByteString-- [Int32]
} deriving (Generic, B.Beamable)
type Ballot = BallotT B.Identity
type Poll = PollT B.Identity
data PollT f = PollT {
identity :: B.C f SerialInt,
title :: B.C f (Maybe T.Text),
question :: B.C f T.Text
} deriving (Generic, B.Beamable)
-- deriving instance Show Ballot
-- deriving instance Eq Ballot
-- deriving instance Ord Ballot
instance B.Table PollT where
data PrimaryKey PollT f = PollId (B.C f SerialInt) deriving (Generic, B.Beamable)
primaryKey :: PollT f -> BM.PrimaryKey PollT f
primaryKey = PollId . (.identity)
instance B.Table OptionT where
data PrimaryKey OptionT f = OptionId (B.Columnar f SerialInt) deriving (Generic, B.Beamable)
primaryKey :: OptionT f -> BM.PrimaryKey OptionT f
primaryKey (OptionT {..})= OptionId identity
instance B.Table BallotT where
data PrimaryKey BallotT f = BallotId (B.Columnar f SerialInt) deriving (Generic, B.Beamable)
primaryKey :: BallotT f -> BM.PrimaryKey BallotT f
primaryKey (BallotT {..}) = BallotId identity
type PollId = B.PrimaryKey PollT B.Identity
-- the actual database
data PollDatabase f = PollDatabase {
polls :: f (B.TableEntity PollT),
options :: f (B.TableEntity OptionT),
ballots :: f (B.TableEntity BallotT)
} deriving (Generic, B.Database be)
-- forPollOptions :: OptionT f -> PollT f
forPollOptions (OptionT {..})= forPoll
getPoll :: Int32 -> BM.Q BP.Postgres PollDatabase s (PollT (BM.QExpr BP.Postgres s))
getPoll id = do
poll' <- B.all_ $ (.polls) pollDb
B.guard_ (B.primaryKey poll' B.==. (PollId . fromIntegral $ id))
pure poll'
getOptionsForPoll :: BM.MonadBeam BP.Postgres m => Int32 -> m [Option]
getOptionsForPoll = B.runSelectReturningList . B.select . getOptionsForPoll'
getOptionsForPoll' :: Int32 -> BM.Q BP.Postgres PollDatabase s (OptionT (BM.QExpr BP.Postgres s))
getOptionsForPoll' id = do
(options' :: OptionT f) <- B.all_ $ (.options) pollDb
B.guard_ ((forPollOptions options') B.==. (PollId . fromIntegral $ id))
pure options'
-- insertPoll :: MonadBeamInsertReturning BP.Postgres m => A.CreatePollReq -> m (PollT BM.Identity, [OptionT BM.Identity])
-- insertPoll :: MonadBeamInsertReturning BP.Postgres m => A.CreatePollReq -> m [PollT BM.Identity]
-- insertPoll A.CreatePollReq{..} = BSQLE.runInsertReturningList . B.insert (polls pollDb) $ B.insertExpressions [PollT B.default_ (B.val_ question)]
-- -- insertOptions :: MonadBeamInsertReturning BP.Postgres m => m [OptionT BM.Identity]
-- insertOptions pollId' optionNames = BSQLE.runInsertReturningList . B.insert (options pollDb) $ B.insertExpressions $ map toOption optionNames
-- where
-- toOption optionName = OptionT B.default_ (B.val_ $ B.SqlSerial $ pollId') (B.val_ optionName)
-- postBallot :: MonadBeamInsertReturning BP.Postgres m => Int32 -> [Int32] -> m ()
postBallot pollKey optionKeys = BSQLE.runInsertReturningList . B.insert ((.ballots) pollDb) $ B.insertExpressions [BallotT B.default_ (B.val_ pollKey) (B.val_ optionKeys)]
pollDb :: B.DatabaseSettings BP.Postgres PollDatabase
pollDb = BM.unCheckDatabase checkedSettings
checkedSettings :: BM.CheckedDatabaseSettings BP.Postgres PollDatabase
checkedSettings = BM.defaultMigratableDbSettings
connection :: IO BP.Connection
connection = BP.connect $ BP.defaultConnectInfo {BP.connectUser = "jackoe", BP.connectDatabase = "postgres"}
makeDB = BMS.createSchema BPM.migrationBackend checkedSettings

View file

@ -28,16 +28,16 @@ createPoll createInfo pollId = MS.modify go
} }
getPollForBallot :: P.PollId -> Ac.Query DB (Maybe P.CreatePollInfo) getPollForBallot :: P.PollId -> Ac.Query DB (Maybe P.CreatePollInfo)
getPollForBallot pollId = MR.asks (fmap P.createInfo . M.lookup pollId . polls) getPollForBallot pollId = MR.asks (fmap (.createInfo) . M.lookup pollId . (.polls))
getPollIds :: Ac.Query DB [P.PollId] getPollIds :: Ac.Query DB [P.PollId]
getPollIds = MR.asks (M.keys . polls) getPollIds = MR.asks (M.keys . (.polls))
getDB :: Ac.Query DB DB getDB :: Ac.Query DB DB
getDB = MR.ask getDB = MR.ask
getPoll :: P.PollId -> Ac.Query DB (Maybe P.Poll) getPoll :: P.PollId -> Ac.Query DB (Maybe P.Poll)
getPoll pollId = MR.asks $ M.lookup pollId . polls getPoll pollId = MR.asks $ M.lookup pollId . (.polls)
postBallot :: P.PollId -> B.Ballot -> Ac.Update DB () postBallot :: P.PollId -> B.Ballot -> Ac.Update DB ()
postBallot pollId ballot = MS.modify go postBallot pollId ballot = MS.modify go

View file

@ -62,7 +62,7 @@ rank votes =
candidates = S.unions . LN.map (S.fromList . LN.toList) $ votes candidates = S.unions . LN.map (S.fromList . LN.toList) $ votes
filterVotes :: (a -> Bool) -> LN.NonEmpty (LN.NonEmpty a) -> Maybe (LN.NonEmpty (LN.NonEmpty a)) filterVotes :: (a -> Bool) -> LN.NonEmpty (LN.NonEmpty a) -> Maybe (LN.NonEmpty (LN.NonEmpty a))
filterVotes f = catMaybes . LN.map (LN.nonEmpty . LN.filter f) filterVotes f = InstantRunoff.catMaybes . LN.map (LN.nonEmpty . LN.filter f)
catMaybes :: LN.NonEmpty (Maybe a) -> Maybe (LN.NonEmpty a) catMaybes :: LN.NonEmpty (Maybe a) -> Maybe (LN.NonEmpty a)
catMaybes = LN.nonEmpty . M.catMaybes . LN.toList catMaybes = LN.nonEmpty . M.catMaybes . LN.toList

View file

@ -6,7 +6,7 @@ import qualified Control.Monad.Reader as Rd
pageHead :: AppM (L.Html ()) pageHead :: AppM (L.Html ())
pageHead = do pageHead = do
script <- Rd.asks script script <- Rd.asks (.script)
pure . head_ $ do pure . head_ $ do
link_ [href_ "/static/style.css", rel_ "stylesheet"] link_ [href_ "/static/style.css", rel_ "stylesheet"]
link_ [href_ "/static/paper.min.css", rel_ "stylesheet"] link_ [href_ "/static/paper.min.css", rel_ "stylesheet"]

View file

@ -32,10 +32,11 @@ import qualified Network.Wai.Middleware.ForceSSL as TLS
import qualified Network.HTTP.Types.Status as TS import qualified Network.HTTP.Types.Status as TS
import LucidUtils import LucidUtils
import qualified Data.Text.Lazy as TL import qualified Data.Text.Lazy as TL
import Prelude hiding (for_)
checkLength :: T.Text -> AppM () checkLength :: T.Text -> AppM ()
checkLength txt checkLength txt
| (T.length txt) <= 100 = pure () | T.length txt <= 100 = pure ()
| otherwise = Er.nameTooLong | otherwise = Er.nameTooLong
throwOrLift :: AppM a -> Maybe a -> AppM a throwOrLift :: AppM a -> Maybe a -> AppM a
@ -49,21 +50,21 @@ getFromPollId pollId query = do
results :: P.PollId -> AppM (L.Html ()) results :: P.PollId -> AppM (L.Html ())
results pollId = do results pollId = do
db <- Rd.asks db db <- Rd.asks (.db)
poll :: P.Poll <- getFromPollId pollId (liftIO . Ac.query db . DB.GetPoll) poll :: P.Poll <- getFromPollId pollId (liftIO . Ac.query db . DB.GetPoll)
case toNonEmptyList . P.votes $ poll of case toNonEmptyList . (.votes) $ poll of
Nothing -> fullPage "poll doesn't have any votes" Nothing -> fullPage "poll doesn't have any votes"
Just votesList -> do Just votesList -> do
let results' = IR.solve (LN.filter (not . T.null) . P.options . P.createInfo $ poll) (votesList) let results' = IR.solve (LN.filter (not . T.null) . (.options) . (.createInfo) $ poll) votesList
fullPage $ do fullPage $ do
h2_ "results" h2_ "results"
toHtml $ T.append (T.pack . show . length . P.votes $ poll) " ballots submitted" toHtml $ T.append (T.pack . show . length . (.votes) $ poll) " ballots submitted"
maybe (pure ()) (h3_ . toHtml) . P.title . P.createInfo $ poll maybe (pure ()) (h3_ . toHtml) . (.title) . (.createInfo) $ poll
h3_ . toHtml . P.question . P.createInfo $ poll h3_ . toHtml . (.question) . (.createInfo) $ poll
with div_ [id_ "results"] . mconcat $ zipWith nthPlaceFor results' nthPlaces with div_ [id_ "results"] . mconcat $ zipWith nthPlaceFor results' nthPlaces
where where
toNonEmptyList :: [B.Ballot] -> Maybe (LN.NonEmpty (LN.NonEmpty T.Text)) toNonEmptyList :: [B.Ballot] -> Maybe (LN.NonEmpty (LN.NonEmpty T.Text))
toNonEmptyList = LN.nonEmpty . My.mapMaybe (LN.nonEmpty . filter (not . T.null) . B.options) toNonEmptyList = LN.nonEmpty . My.mapMaybe (LN.nonEmpty . filter (not . T.null) . (.options))
nthPlaceFor :: S.Set T.Text -> L.Html () -> L.Html () nthPlaceFor :: S.Set T.Text -> L.Html () -> L.Html ()
nthPlaceFor options place = do nthPlaceFor options place = do
@ -83,8 +84,8 @@ nthPlaces =
makePoll :: P.CreatePollInfo -> AppM (L.Html ()) makePoll :: P.CreatePollInfo -> AppM (L.Html ())
makePoll pollReq = do makePoll pollReq = do
checkTextLengths checkTextLengths
db <- Rd.asks db db <- Rd.asks (.db)
gen <- Rd.asks gen gen <- Rd.asks (.gen)
-- TODO: handle rare case of poll id collision -- TODO: handle rare case of poll id collision
pollId <- P.PollId <$> R.uniformWord64 gen pollId <- P.PollId <$> R.uniformWord64 gen
liftIO $ Ac.update db (DB.CreatePoll pollReq pollId) liftIO $ Ac.update db (DB.CreatePoll pollReq pollId)
@ -94,9 +95,9 @@ makePoll pollReq = do
with a_ [href_ fillOutLink] (toHtml fillOutLink) with a_ [href_ fillOutLink] (toHtml fillOutLink)
where where
checkTextLengths = do checkTextLengths = do
M.mapM_ checkLength . LN.toList . P.options $ pollReq M.mapM_ checkLength . LN.toList . (.options) $ pollReq
checkLength . P.question $ pollReq checkLength . (.question) $ pollReq
maybe (pure ()) checkLength . P.title $ pollReq maybe (pure ()) checkLength . (.title) $ pollReq
-- TODO: lift current domain into ENV -- TODO: lift current domain into ENV
toPollIdLink :: P.PollId -> T.Text toPollIdLink :: P.PollId -> T.Text
@ -104,14 +105,14 @@ toPollIdLink (P.PollId pollId) = T.append "https://rankedchoice.net/poll/" (T.pa
vote :: P.PollId -> B.Ballot -> AppM (L.Html ()) vote :: P.PollId -> B.Ballot -> AppM (L.Html ())
vote pollId ballot = do vote pollId ballot = do
M.mapM_ checkLength . B.options $ ballot M.mapM_ checkLength . (.options) $ ballot
db <- Rd.asks db db <- Rd.asks (.db)
liftIO $ Ac.update db (DB.PostBallot pollId ballot') liftIO $ Ac.update db (DB.PostBallot pollId ballot')
pure $ with div_ [id_ "resultLink"] $ do pure $ with div_ [id_ "resultLink"] $ do
"success! Here are the " "success! Here are the "
with a_ [href_ (toPollIdLink pollId `T.append` "/results")] "results" with a_ [href_ (toPollIdLink pollId `T.append` "/results")] "results"
where where
ballot' = B.Ballot . filter (not . T.null) . B.options $ ballot ballot' = B.Ballot . filter (not . T.null) . (.options) $ ballot
server :: ServerT A.RCVAPI AppM server :: ServerT A.RCVAPI AppM
server = createPage server = createPage
@ -130,23 +131,23 @@ emptyHiddenInput = input_ [hidden_ "", name_ "options", value_ ""]
getPollForBallot :: P.PollId -> AppM (L.Html ()) getPollForBallot :: P.PollId -> AppM (L.Html ())
getPollForBallot pollId = do getPollForBallot pollId = do
db <- Rd.asks db db <- Rd.asks (.db)
createInfo <- getFromPollId pollId (liftIO . Ac.query db . DB.GetPollForBallot) createInfo <- getFromPollId pollId (liftIO . Ac.query db . DB.GetPollForBallot)
fullPage $ do fullPage $ do
My.maybe (pure ()) (h3_ . toHtml) (P.title createInfo) My.maybe (pure ()) (h3_ . toHtml) ((.title) createInfo)
h3_ . toHtml . P.question $ createInfo h3_ . toHtml . (.question) $ createInfo
with div_ [id_ "drag-boxes-container"] $ do with div_ [id_ "drag-boxes-container"] $ do
div_ $ do -- TODO: check accessibility on this div_ $ do -- TODO: check accessibility on this
"drag from here" "drag from here"
with div_ [classes_ ["draggable-options","sortable-from", "options", "child-borders", "border-primary", "background-primary"]] with div_ [classes_ ["draggable-options","sortable-from", "options", "child-borders", "border-primary", "background-primary"]]
. mconcat . map toFormInput . LN.toList . P.options $ createInfo . mconcat . map toFormInput . LN.toList . (.options) $ createInfo
with form_ [hxPost_ "", id_ "drag-into-vote", hxTarget_ "closest body"] $ do with form_ [hxPost_ "", id_ "drag-into-vote", hxTarget_ "closest body"] $ do
div_$ do div_$ do
"to here in order of preference" "to here in order of preference"
with div_ [classes_ ["draggable-options", "sortable", "options", "child-borders", "border-primary", "background-primary"]] with div_ [classes_ ["draggable-options", "sortable", "options", "child-borders", "border-primary", "background-primary"]]
(emptyHiddenInput <> emptyHiddenInput) (emptyHiddenInput <> emptyHiddenInput)
input_ [id_ "ballot-submit", type_ "submit", classes_ ["paper-btn", "btn-primary"], value_ "submit", form_ "drag-into-vote"] input_ [id_ "ballot-submit", type_ "submit", classes_ ["paper-btn", "btn-primary"], value_ "submit", form_ "drag-into-vote"]
a_ [href_ (T.concat [T.pack . show . P.asWord $ pollId, "/results"])] "skip voting and see results" a_ [href_ "results"] "skip voting and see results"
where where
toFormInput :: T.Text -> L.Html () toFormInput :: T.Text -> L.Html ()
toFormInput option = with div_ [classes_ []] $ input_ [type_ "hidden", value_ option, name_ "options"] <> toHtml option toFormInput option = with div_ [classes_ []] $ input_ [type_ "hidden", value_ option, name_ "options"] <> toHtml option
@ -184,7 +185,7 @@ createPage = do
input_ [type_ "submit", classes_ ["paper-btn", "btn-primary"], value_ "submit"] input_ [type_ "submit", classes_ ["paper-btn", "btn-primary"], value_ "submit"]
indexPage :: AppM (L.Html ()) indexPage :: AppM (L.Html ())
indexPage = fullPage . toHtmlRaw =<< Rd.asks index indexPage = fullPage . toHtmlRaw =<< Rd.asks (.index)
api :: Proxy A.RCVAPI api :: Proxy A.RCVAPI
api = Proxy api = Proxy
@ -193,7 +194,7 @@ getEnv :: IO Env
getEnv = do getEnv = do
db <- DB.openLocalDB db <- DB.openLocalDB
-- this needs to be in a <script> tag in the header, -- this needs to be in a <script> tag in the header,
-- so we need it in the ENv -- so we need it in the Env
script <- TIO.readFile "public/static/script.js" script <- TIO.readFile "public/static/script.js"
let gen = R.globalStdGen let gen = R.globalStdGen
index <- convertMarkdown "public/static/index.md" index <- convertMarkdown "public/static/index.md"
@ -225,11 +226,11 @@ emptyApp _ respondf = respondf $ NW.responseLBS TS.status200 [] "redirecting to
main :: IO () main :: IO ()
main = do main = do
env <- getEnv (Env {..}) <- getEnv
M.void . liftIO $ Ac.update (db env) (DB.CreatePoll examplePoll (P.PollId 7)) M.void . liftIO $ Ac.update db (DB.CreatePoll examplePoll (P.PollId 7))
M.void . liftIO $ Ac.update (db env) (DB.PostBallot (P.PollId 7) (B.Ballot ["blue", "green", "yellow", "orange", "pink"])) M.void . liftIO $ Ac.update db (DB.PostBallot (P.PollId 7) (B.Ballot ["blue", "green", "yellow", "orange", "pink"]))
M.void . liftIO $ Ac.update (db env) (DB.PostBallot (P.PollId 7) (B.Ballot ["blue", "red", "pink", "purple", "green"])) M.void . liftIO $ Ac.update db (DB.PostBallot (P.PollId 7) (B.Ballot ["blue", "red", "pink", "purple", "green"]))
M.void . liftIO $ Ac.update (db env) (DB.PostBallot (P.PollId 7) (B.Ballot ["purple", "black", "yellow", "orange", "blue"])) M.void . liftIO $ Ac.update db (DB.PostBallot (P.PollId 7) (B.Ballot ["purple", "black", "yellow", "orange", "blue"]))
mapM_ print =<< (liftIO . Ac.query (db env) $ DB.GetPollIds) mapM_ print =<< (liftIO . Ac.query db $ DB.GetPollIds)
let application = serve api . hoistServer api (runWithEnv env) $ server let application = serve api . hoistServer api (runWithEnv (Env {..})) $ server
W.run 8081 application W.run 8081 application