1977 lines
87 KiB
Haskell
1977 lines
87 KiB
Haskell
-- SPDX-FileCopyrightText: 2022-24 Felix Hamann <felix.hamann@campus.lmu.de>,Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Sarah Vaupel <vaupel.sarah@campus.lmu.de>,Steffen Jost <jost@cip.ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Winnie Ros <winnie.ros@campus.lmu.de>,Steffen Jost <s.jost@fraport.de>
|
|
--
|
|
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
{-# LANGUAGE UndecidableInstances #-}
|
|
{-# OPTIONS_GHC -fno-warn-redundant-constraints #-}
|
|
|
|
|
|
{- FOP - Frequently occurring problems using dbTable:
|
|
|
|
- When changing a dbTable to a form, eg. using `dbSelect` then change the colonnade defnition from `dbColonnade` to `formColonnade`!
|
|
Both functions are equal to id, but the types are quite different.
|
|
|
|
- Don't mix up the row type alias traditionally ending with ...Data and the Action-Result-Type also ending with ...Data
|
|
|
|
-}
|
|
|
|
module Handler.Utils.Table.Pagination
|
|
( module Handler.Utils.Table.Pagination.Types
|
|
, dbFilterKey
|
|
, SomeExprValue(..)
|
|
, SortColumn(..), SortDirection(..)
|
|
, SortingSetting(..)
|
|
, pattern SortAscBy, pattern SortDescBy
|
|
, FilterColumn(..), IsFilterColumn, IsFilterColumnHandler, IsFilterProjected
|
|
, mkFilterProjectedPost
|
|
, DBTProjFilterPost(..)
|
|
, DBRow(..), _dbrOutput, _dbrCount
|
|
, DBStyle(..), defaultDBSFilterLayout, DBEmptyStyle(..)
|
|
, module Handler.Utils.Table.Pagination.CsvColumnExplanations
|
|
, DBCsvActionMode(..)
|
|
, DBCsvDiff(..), _DBCsvDiffNew, _DBCsvDiffExisting, _DBCsvDiffMissing, _dbCsvOldKey, _dbCsvOld, _dbCsvNewKey, _dbCsvNew
|
|
, DBTCsvEncode(..), DBTCsvDecode(..), DBTExtraRep(..)
|
|
, DBTProjCtx(..), _dbtProjFilter, _dbtProjRow, _dbtProjRow'
|
|
, DBTable(..), DBFilterUI, IsDBTable(..), DBCell(..)
|
|
, dbtProjId, dbtProjSimple
|
|
, dbtProjFilteredPostId, dbtProjFilteredPostSimple
|
|
, noCsvEncode, simpleCsvEncode, simpleCsvEncodeM
|
|
, withCsvExtraRep
|
|
, singletonFilter, multiFilter
|
|
, DBParams(..)
|
|
, cellAttrs, cellContents
|
|
, addCellClass
|
|
, PagesizeLimit(..)
|
|
, PaginationSettings(..), PaginationInput(..), piIsUnset
|
|
, PSValidator(..)
|
|
, defaultPagesize
|
|
, defaultFilter, defaultSorting
|
|
, restrictFilter, restrictSorting
|
|
, forceFilter
|
|
, ToSortable(..), Sortable(..)
|
|
, dbTable
|
|
, dbTableWidget, dbTableWidget'
|
|
, dbTableDB, dbTableDB'
|
|
, widgetColonnade, formColonnade, dbColonnade
|
|
, cell, wgtCell, textCell, stringCell, i18nCell
|
|
, anchorCell, anchorCell', anchorCellM, anchorCellM'
|
|
, linkEitherCell, linkEitherCellM, linkEitherCellM'
|
|
, maybeAnchorCellM, maybeAnchorCellM', maybeLinkEitherCellM'
|
|
, anchorCellC, anchorCellCM, anchorCellCM', linkEitherCellCM', maybeLinkEitherCellCM'
|
|
, cellTooltip, cellTooltips, cellTooltipIcon, cellTooltipWgt
|
|
, listCell, listCell', listCellOf, listCellOf'
|
|
, ilistCell, ilistCell', ilistCellOf, ilistCellOf'
|
|
, formCell, DBFormResult(..), getDBFormResult
|
|
, dbSelect, dbSelectIf
|
|
, (&)
|
|
, cap'
|
|
, module Control.Monad.Trans.Maybe
|
|
, module Colonnade
|
|
, DBSTemplateMode(..)
|
|
) where
|
|
|
|
import Handler.Utils.Table.Pagination.Types
|
|
import Handler.Utils.Table.Pagination.CsvColumnExplanations
|
|
import Handler.Utils.Form
|
|
import Handler.Utils.Csv
|
|
import Handler.Utils.I18n
|
|
import Utils
|
|
import Utils.Lens
|
|
|
|
import Import hiding (pi)
|
|
|
|
import Data.Ratio ((%))
|
|
import qualified Data.Foldable as Foldable
|
|
|
|
import qualified Yesod.Form.Functions as Yesod
|
|
|
|
import qualified Database.Esqueleto.Legacy as E
|
|
import qualified Database.Esqueleto.Utils as E
|
|
import qualified Database.Esqueleto.Internal.Internal as E (SqlSelect,unsafeSqlValue)
|
|
|
|
import qualified Network.Wai as Wai
|
|
|
|
import Control.Monad.RWS (RWST(..), execRWS, execRWST)
|
|
import Control.Monad.State (evalStateT, execStateT)
|
|
import Control.Monad.Trans.Maybe
|
|
import Control.Monad.State.Class (modify)
|
|
import qualified Control.Monad.State.Class as State
|
|
|
|
import Data.Map ((!))
|
|
import qualified Data.Map as Map
|
|
|
|
import qualified Data.Set as Set
|
|
|
|
import qualified Data.CaseInsensitive as CI
|
|
|
|
import Data.Csv (NamedRecord)
|
|
|
|
import Colonnade hiding (bool, fromMaybe, singleton)
|
|
import qualified Colonnade (singleton)
|
|
import Colonnade.Encode hiding (row)
|
|
|
|
import Text.Hamlet (hamletFile)
|
|
|
|
import Data.List (inits)
|
|
|
|
import Data.Maybe (fromJust)
|
|
|
|
import Data.Aeson.Text
|
|
|
|
import qualified Data.Text as Text
|
|
|
|
import qualified Data.Binary as B
|
|
import qualified Data.ByteArray as BA (convert)
|
|
import Crypto.MAC.HMAC (hmac, HMAC)
|
|
import Crypto.Hash.Algorithms (SHAKE256)
|
|
|
|
import qualified Data.ByteString.Base64.URL as Base64 (encode)
|
|
import qualified Data.ByteString.Lazy as LBS
|
|
|
|
import Data.Semigroup as Sem (Semigroup(..))
|
|
|
|
import qualified Data.Conduit.List as C (sourceList)
|
|
import qualified Data.Conduit.Combinators as C
|
|
|
|
import Handler.Utils.DateTime (formatTimeRangeW)
|
|
import qualified Control.Monad.Catch as Catch
|
|
|
|
import Data.Dynamic
|
|
|
|
import qualified Data.Csv as Csv
|
|
|
|
import Jobs.Queue
|
|
|
|
import Data.Typeable (eqT)
|
|
|
|
|
|
#if MIN_VERSION_base(4,11,0)
|
|
type Monoid' = Monoid
|
|
#else
|
|
type Monoid' x = (Sem.Semigroup x, Monoid x)
|
|
#endif
|
|
|
|
|
|
data WithIdent x = forall ident. PathPiece ident => WithIdent { _ident :: ident, _withoutIdent :: x }
|
|
|
|
instance PathPiece x => PathPiece (WithIdent x) where
|
|
toPathPiece (WithIdent ident x)
|
|
| not . null $ toPathPiece ident = toPathPiece ident <> "-" <> toPathPiece x
|
|
| otherwise = toPathPiece x
|
|
fromPathPiece txt = do
|
|
let sep :: Text
|
|
sep = "-"
|
|
(ident, (Text.stripSuffix sep -> Just rest)) <- return $ Text.breakOn sep txt
|
|
WithIdent <$> pure ident <*> fromPathPiece rest
|
|
|
|
|
|
dbFilterKey :: PathPiece dbtIdent => dbtIdent -> FilterKey -> Text
|
|
dbFilterKey ident = toPathPiece . WithIdent ident
|
|
|
|
|
|
data SomeExprValue = forall a. PersistField a => SomeExprValue { getSomeExprValue :: E.SqlExpr (E.Value a) }
|
|
|
|
data SortColumn t r' = forall a. PersistField a => SortColumn { getSortColumn :: t -> E.SqlExpr (E.Value a) }
|
|
| forall a. PersistField a => SortColumnNullsInv { getSortColumn :: t -> E.SqlExpr (E.Value a) }
|
|
| forall a. PersistField a => SortColumnNeverNull { getSortColumn :: t -> E.SqlExpr (E.Value a) }
|
|
| SortColumns { getSortColumns :: t -> [SomeExprValue] }
|
|
| SortProjected { sortProjected :: r' -> r' -> Ordering }
|
|
|
|
data SortDirection = SortAsc | SortDesc
|
|
deriving (Eq, Ord, Enum, Bounded, Show, Read, Generic)
|
|
|
|
instance Universe SortDirection
|
|
instance Finite SortDirection
|
|
|
|
nullaryPathPiece ''SortDirection $ camelToPathPiece' 1
|
|
pathPieceJSON ''SortDirection
|
|
|
|
sqlSortDirection :: SortColumn t r' -> Maybe (SortDirection -> t -> [E.SqlExpr E.OrderBy])
|
|
sqlSortDirection (SortColumn e ) = Just $ \case
|
|
SortAsc -> pure . E.asc . e
|
|
SortDesc -> pure . E.desc . e
|
|
sqlSortDirection (SortColumnNullsInv e ) = Just $ \case
|
|
SortAsc -> pure . E.ascNullsFirst . e
|
|
SortDesc -> pure . E.descNullsLast . e
|
|
sqlSortDirection (SortColumnNeverNull e ) = Just $ \case
|
|
SortAsc -> pure . E.asc . e
|
|
SortDesc -> pure . E.descNullsLast . e
|
|
sqlSortDirection (SortColumns es) = Just $ \case
|
|
SortAsc -> fmap (\(SomeExprValue v) -> E.asc v) . es
|
|
SortDesc -> fmap (\(SomeExprValue v) -> E.desc v) . es
|
|
sqlSortDirection _ = Nothing
|
|
|
|
sortDirectionProjected :: SortColumn t r' -> r' -> r' -> Ordering
|
|
sortDirectionProjected SortProjected{..} = sortProjected
|
|
sortDirectionProjected _ = \_ _ -> EQ
|
|
|
|
|
|
data SortingSetting = SortingSetting
|
|
{ sortKey :: SortingKey
|
|
, sortDir :: SortDirection
|
|
} deriving (Eq, Ord, Show, Read, Generic)
|
|
|
|
deriveJSON defaultOptions
|
|
{ fieldLabelModifier = camelToPathPiece' 1
|
|
} ''SortingSetting
|
|
|
|
instance PathPiece SortingSetting where
|
|
toPathPiece SortingSetting{..} = toPathPiece sortKey <> "-" <> toPathPiece sortDir
|
|
fromPathPiece str = do
|
|
let sep :: Text
|
|
sep = "-"
|
|
let (Text.dropEnd (Text.length sep) -> key, dir) = Text.breakOnEnd sep str
|
|
SortingSetting <$> fromPathPiece key <*> fromPathPiece dir
|
|
|
|
pattern SortAscBy :: SortingKey -> SortingSetting
|
|
pattern SortAscBy key = SortingSetting key SortAsc
|
|
|
|
pattern SortDescBy :: SortingKey -> SortingSetting
|
|
pattern SortDescBy key = SortingSetting key SortDesc
|
|
|
|
|
|
type DBTableKey k' = (Show k', ToJSON k', FromJSON k', Ord k', Binary k', Typeable k')
|
|
data DBRow r = forall k'. DBTableKey k' => DBRow
|
|
{ dbrKey :: k'
|
|
, dbrOutput :: r
|
|
, dbrCount :: Int64
|
|
}
|
|
|
|
makeLenses_ ''DBRow
|
|
|
|
instance Functor DBRow where
|
|
fmap f DBRow{..} = DBRow{ dbrOutput = f dbrOutput, .. }
|
|
|
|
instance Foldable DBRow where
|
|
foldMap f DBRow{..} = f dbrOutput
|
|
|
|
instance Traversable DBRow where
|
|
traverse f DBRow{..} = DBRow <$> pure dbrKey <*> f dbrOutput <*> pure dbrCount
|
|
|
|
|
|
newtype DBTProjFilterPost r' = DBTProjFilterPost { unDBTProjFilterPost :: r' -> DB Bool }
|
|
|
|
instance Default (DBTProjFilterPost r') where
|
|
def = mempty
|
|
|
|
instance Semigroup (DBTProjFilterPost r') where
|
|
DBTProjFilterPost f <> DBTProjFilterPost g = DBTProjFilterPost $ \r' -> f r' `and2M` g r'
|
|
|
|
instance Monoid (DBTProjFilterPost r') where
|
|
mempty = DBTProjFilterPost . const $ return True
|
|
|
|
|
|
data FilterColumn t fs = forall a. IsFilterColumn t a => FilterColumn a
|
|
| forall a. IsFilterColumnHandler t a => FilterColumnHandler a
|
|
| forall a. IsFilterProjected fs a => FilterProjected a
|
|
|
|
|
|
filterColumn :: FilterColumn t fs -> Maybe ([Text] -> t -> E.SqlExpr (E.Value Bool))
|
|
filterColumn (FilterColumn f) = Just $ filterColumn' f
|
|
filterColumn _ = Nothing
|
|
|
|
filterColumnHandler :: FilterColumn t fs -> Maybe ([Text] -> Handler (t -> E.SqlExpr (E.Value Bool)))
|
|
filterColumnHandler (FilterColumnHandler f) = Just $ filterColumnHandler' f
|
|
filterColumnHandler _ = Nothing
|
|
|
|
filterProjected :: FilterColumn t fs -> [Text] -> (fs -> fs)
|
|
filterProjected (FilterProjected f) = filterProjected' f
|
|
filterProjected _ = const id
|
|
|
|
mkFilterProjectedPost :: forall r' a t. IsFilterProjectedPost r' a => a -> FilterColumn t (DBTProjFilterPost r')
|
|
mkFilterProjectedPost fin = FilterProjected $ \(ts :: [Text]) -> (<> filterProjectedPost' @r' fin ts)
|
|
|
|
class IsFilterColumn t a where
|
|
filterColumn' :: a -> [Text] -> t -> E.SqlExpr (E.Value Bool)
|
|
|
|
instance IsFilterColumn t (E.SqlExpr (E.Value Bool)) where
|
|
filterColumn' fin _ _ = fin
|
|
|
|
instance IsFilterColumn t cont => IsFilterColumn t (t -> cont) where
|
|
filterColumn' cont is' t = filterColumn' (cont t) is' t
|
|
|
|
instance {-# OVERLAPPABLE #-} (PathPiece (Element l), IsFilterColumn t cont, MonoPointed l, Monoid l) => IsFilterColumn t (l -> cont) where
|
|
filterColumn' cont is' = filterColumn' (cont $ is' ^. mono' _PathPiece) is'
|
|
|
|
class IsFilterColumnHandler t a where
|
|
filterColumnHandler' :: a -> [Text] -> Handler (t -> E.SqlExpr (E.Value Bool))
|
|
|
|
instance IsFilterColumnHandler t ([Text] -> Handler (t -> E.SqlExpr (E.Value Bool))) where
|
|
filterColumnHandler' fin args = fin args
|
|
|
|
class IsFilterProjected fs a where
|
|
filterProjected' :: a -> [Text] -> (fs -> fs)
|
|
|
|
instance IsFilterProjected fs (fs -> fs) where
|
|
filterProjected' fin _ = fin
|
|
|
|
instance {-# OVERLAPPABLE #-} (PathPiece (Element l), IsFilterProjected fs cont, MonoPointed l, Monoid l) => IsFilterProjected fs (l -> cont) where
|
|
filterProjected' cont is' = filterProjected' (cont $ is' ^. mono' _PathPiece) is'
|
|
|
|
class IsFilterProjectedPost r' a where
|
|
filterProjectedPost' :: a -> [Text] -> DBTProjFilterPost r'
|
|
|
|
instance IsFilterProjectedPost r' Bool where
|
|
filterProjectedPost' fin _ = DBTProjFilterPost . const $ return fin
|
|
|
|
instance IsFilterProjectedPost r' (ReaderT SqlBackend (HandlerFor UniWorX) Bool) where
|
|
filterProjectedPost' fin _ = DBTProjFilterPost $ const fin
|
|
|
|
instance IsFilterProjectedPost r' (DBTProjFilterPost r') where
|
|
filterProjectedPost' fin _ = fin
|
|
|
|
instance IsFilterProjectedPost r' cont => IsFilterProjectedPost r' (r' -> cont) where
|
|
filterProjectedPost' cont is' = DBTProjFilterPost $ \r' -> let DBTProjFilterPost cont' = filterProjectedPost' (cont r') is' in cont' r'
|
|
|
|
instance {-# OVERLAPPABLE #-} (PathPiece (Element l), IsFilterProjectedPost r' cont, MonoPointed l, Monoid l) => IsFilterProjectedPost r' (l -> cont) where
|
|
filterProjectedPost' cont is' = filterProjectedPost' (cont $ is' ^. mono' _PathPiece) is'
|
|
|
|
|
|
data PagesizeLimit = PagesizeLimit !Int64 | PagesizeAll
|
|
deriving (Eq, Ord, Read, Show, Generic)
|
|
|
|
instance Bounded PagesizeLimit where
|
|
minBound = PagesizeLimit minBound
|
|
maxBound = PagesizeAll
|
|
|
|
instance Enum PagesizeLimit where
|
|
toEnum i
|
|
| toInteger i >= fromIntegral (minBound :: Int64)
|
|
, toInteger i <= fromIntegral (maxBound :: Int64)
|
|
= PagesizeLimit $ fromIntegral i
|
|
| toInteger i > fromIntegral (maxBound :: Int64)
|
|
= PagesizeAll
|
|
| otherwise
|
|
= error "toEnum PagesizeLimit: out of bounds"
|
|
fromEnum (PagesizeLimit i)
|
|
| toInteger i >= fromIntegral (minBound :: Int)
|
|
, toInteger i <= fromIntegral (maxBound :: Int)
|
|
= fromIntegral i
|
|
| otherwise
|
|
= error "fromEnum PagesizeLimit: out of bounds"
|
|
fromEnum PagesizeAll
|
|
= error "fromEnum PagesizeLimit: infinite"
|
|
|
|
succ (PagesizeLimit i)
|
|
| i == maxBound = PagesizeAll
|
|
| otherwise = PagesizeLimit $ succ i
|
|
succ PagesizeAll = error "succ PagesizeLimit: out of bounds"
|
|
pred (PagesizeLimit i)
|
|
| i == minBound = error "pred PagesizeLimit: out of bounds"
|
|
| otherwise = PagesizeLimit $ pred i
|
|
pred PagesizeAll = PagesizeLimit maxBound
|
|
|
|
instance PathPiece PagesizeLimit where
|
|
toPathPiece PagesizeAll = "all"
|
|
toPathPiece (PagesizeLimit n) = toPathPiece n
|
|
fromPathPiece str
|
|
| CI.mk str == "all" = Just PagesizeAll
|
|
| otherwise = PagesizeLimit <$> fromPathPiece str
|
|
|
|
deriveJSON defaultOptions
|
|
{ constructorTagModifier = camelToPathPiece' 1
|
|
, sumEncoding = UntaggedValue
|
|
} ''PagesizeLimit
|
|
|
|
|
|
data PaginationSettings = PaginationSettings
|
|
{ psSorting :: [SortingSetting]
|
|
, psFilter :: Map FilterKey [Text]
|
|
, psLimit :: PagesizeLimit
|
|
, psPage :: Int64
|
|
}
|
|
|
|
makeLenses_ ''PaginationSettings
|
|
|
|
instance Default PaginationSettings where
|
|
def = PaginationSettings
|
|
{ psSorting = []
|
|
, psFilter = Map.empty
|
|
, psLimit = PagesizeLimit 50
|
|
, psPage = 0
|
|
}
|
|
|
|
deriveJSON defaultOptions
|
|
{ fieldLabelModifier = camelToPathPiece' 1
|
|
} ''PaginationSettings
|
|
|
|
data PaginationInput = PaginationInput
|
|
{ piSorting :: Maybe [SortingSetting]
|
|
, piFilter :: Maybe (Map FilterKey [Text])
|
|
, piLimit :: Maybe PagesizeLimit
|
|
, piPage :: Maybe Int64
|
|
} deriving (Eq, Ord, Show, Read, Generic)
|
|
|
|
instance Default PaginationInput where
|
|
def = PaginationInput
|
|
{ piSorting = Nothing
|
|
, piFilter = Nothing
|
|
, piLimit = Nothing
|
|
, piPage = Nothing
|
|
}
|
|
|
|
makeLenses_ ''PaginationInput
|
|
deriveJSON defaultOptions
|
|
{ fieldLabelModifier = camelToPathPiece' 1
|
|
, omitNothingFields = True
|
|
} ''PaginationInput
|
|
|
|
piIsUnset :: PaginationInput -> Bool
|
|
piIsUnset PaginationInput{..} = and
|
|
[ isNothing piSorting
|
|
, isNothing piFilter
|
|
, isNothing piLimit
|
|
, isNothing piPage
|
|
]
|
|
|
|
psToPi :: PaginationSettings -> PaginationInput
|
|
psToPi PaginationSettings{..} = PaginationInput
|
|
{ piSorting = Just psSorting
|
|
, piFilter = Just psFilter
|
|
, piLimit = Just psLimit
|
|
, piPage = Just psPage
|
|
}
|
|
|
|
|
|
data DBCsvActionMode = DBCsvActionNew | DBCsvActionExisting | DBCsvActionMissing
|
|
deriving (Read, Show, Eq, Ord, Enum, Bounded, Generic)
|
|
instance Universe DBCsvActionMode
|
|
instance Finite DBCsvActionMode
|
|
|
|
nullaryPathPiece ''DBCsvActionMode $ camelToPathPiece' 3
|
|
deriveJSON defaultOptions
|
|
{ constructorTagModifier = camelToPathPiece' 3
|
|
} ''DBCsvActionMode
|
|
|
|
|
|
data ButtonCsvMode = BtnCsvExport | BtnCsvImport | BtnCsvImportConfirm | BtnCsvImportAbort
|
|
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic)
|
|
instance Universe ButtonCsvMode
|
|
instance Finite ButtonCsvMode
|
|
|
|
embedRenderMessage ''UniWorX ''ButtonCsvMode id
|
|
|
|
nullaryPathPiece ''ButtonCsvMode $ camelToPathPiece' 1
|
|
|
|
instance Button UniWorX ButtonCsvMode where
|
|
btnLabel BtnCsvExport
|
|
= [whamlet|
|
|
$newline never
|
|
#{iconFileCSV}
|
|
\ _{BtnCsvExport}
|
|
|]
|
|
btnLabel x = [whamlet|_{x}|]
|
|
|
|
btnClasses BtnCsvImportAbort = [BCIsButton, BCDanger]
|
|
btnClasses BtnCsvImportConfirm = [BCIsButton, BCPrimary]
|
|
btnClasses _ = [BCIsButton]
|
|
|
|
btnValidate _ BtnCsvImportAbort = False
|
|
btnValidate _ _ = True
|
|
|
|
|
|
data DBCsvMode
|
|
= DBCsvNormal
|
|
| DBCsvExport
|
|
{ dbCsvExportData :: Dynamic
|
|
}
|
|
| DBCsvImport
|
|
{ dbCsvFiles :: FileUploads
|
|
}
|
|
| DBCsvExportExample
|
|
| DBCsvAbort
|
|
|
|
makePrisms ''DBCsvMode
|
|
|
|
data DBCsvDiff r' csv k'
|
|
= DBCsvDiffNew
|
|
{ dbCsvNewKey :: Maybe k'
|
|
, dbCsvNew :: csv
|
|
}
|
|
| DBCsvDiffExisting
|
|
{ dbCsvOldKey :: k'
|
|
, dbCsvOld :: r'
|
|
, dbCsvNew :: csv
|
|
}
|
|
| DBCsvDiffMissing
|
|
{ dbCsvOldKey :: k'
|
|
, dbCsvOld :: r'
|
|
}
|
|
|
|
makeLenses_ ''DBCsvDiff
|
|
makePrisms ''DBCsvDiff
|
|
|
|
data DBCsvException k'
|
|
= DBCsvDuplicateKey
|
|
{ dbCsvDuplicateKey :: k'
|
|
, dbCsvDuplicateKeyRowA, dbCsvDuplicateKeyRowB :: NamedRecord
|
|
}
|
|
| DBCsvException
|
|
{ dbCsvExceptionRow :: NamedRecord
|
|
, dbCsvException :: Text
|
|
}
|
|
| DBCsvUnavailableActionRequested
|
|
{ dbCsvActions :: Set Value
|
|
}
|
|
deriving (Show)
|
|
|
|
makeLenses_ ''DBCsvException
|
|
|
|
instance (Typeable k', Show k') => Exception (DBCsvException k')
|
|
|
|
|
|
data DBTProjCtx fs r = DBTProjCtx
|
|
{ dbtProjFilter :: fs
|
|
, dbtProjRow :: DBRow r
|
|
}
|
|
|
|
makeLenses_ ''DBTProjCtx
|
|
|
|
_dbtProjRow' :: Lens' (DBTProjCtx () r) (DBRow r)
|
|
_dbtProjRow' = _dbtProjRow
|
|
|
|
|
|
newtype PSValidator m x = PSValidator { runPSValidator :: DBTable m x -> Maybe PaginationInput -> ([SomeMessage UniWorX], PaginationSettings) }
|
|
|
|
instance Default (PSValidator m x) where
|
|
def = PSValidator $ \DBTable{} -> \case
|
|
Nothing -> def
|
|
Just pi -> swap . (\act -> execRWS act pi def) $ do
|
|
asks piSorting >>= maybe (return ()) (\s -> modify $ \ps -> ps { psSorting = s })
|
|
asks piFilter >>= maybe (return ()) (\f -> modify $ \ps -> ps { psFilter = f })
|
|
|
|
l <- asks piLimit
|
|
case l of
|
|
Just (PagesizeLimit l')
|
|
| l' <= 0 -> tell . pure $ SomeMessage MsgPSLimitNonPositive
|
|
| otherwise -> modify $ \ps -> ps { psLimit = PagesizeLimit l' }
|
|
Just PagesizeAll
|
|
-> modify $ \ps -> ps { psLimit = PagesizeAll }
|
|
Nothing -> return ()
|
|
|
|
asks piPage >>= maybe (return ()) (\p -> modify $ \ps -> ps { psPage = p })
|
|
|
|
defaultFilter :: Map FilterKey [Text] -> PSValidator m x -> PSValidator m x
|
|
defaultFilter psFilter (runPSValidator -> f) = PSValidator $ \dbTable' -> injectDefault <*> f dbTable'
|
|
where
|
|
injectDefault x = case x >>= piFilter of
|
|
Just _ -> id
|
|
Nothing -> set (_2._psFilter) psFilter
|
|
|
|
defaultSorting :: [SortingSetting] -> PSValidator m x -> PSValidator m x
|
|
defaultSorting psSorting (runPSValidator -> f) = PSValidator $ \dbTable' -> injectDefault <*> f dbTable'
|
|
where
|
|
injectDefault x = case x >>= piSorting of
|
|
Just prev -> _2 . _psSorting <>~ filter (\ss -> none (((==) `on` sortKey) ss) prev) psSorting
|
|
Nothing -> set (_2 . _psSorting) psSorting
|
|
|
|
defaultPagesize :: PagesizeLimit -> PSValidator m x -> PSValidator m x
|
|
defaultPagesize psLimit (runPSValidator -> f) = PSValidator $ \dbTable' -> injectDefault <*> f dbTable'
|
|
where
|
|
injectDefault x = case x >>= piLimit of
|
|
Just _ -> id
|
|
Nothing -> set (_2._psLimit) psLimit
|
|
|
|
restrictFilter :: (FilterKey -> [Text] -> Bool) -> PSValidator m x -> PSValidator m x
|
|
restrictFilter restrict (runPSValidator -> f) = PSValidator $ \dbTable' ps -> over _2 restrict' $ f dbTable' ps
|
|
where
|
|
restrict' p = p { psFilter = Map.filterWithKey restrict $ psFilter p }
|
|
|
|
forceFilter :: ( MonoFoldable mono
|
|
, MonoPointed mono
|
|
, Monoid mono
|
|
, PathPiece (Element mono)
|
|
)
|
|
=> FilterKey -> mono -> PSValidator m x -> PSValidator m x
|
|
forceFilter key args (runPSValidator -> f) = PSValidator $ \dbTable' ps -> over _2 inject $ f dbTable' ps
|
|
where
|
|
inject p = p { psFilter = psFilter p <> Map.singleton key (review monoPathPieces args) }
|
|
|
|
restrictSorting :: (SortingKey -> SortDirection -> Bool) -> PSValidator m x -> PSValidator m x
|
|
restrictSorting restrict (runPSValidator -> f) = PSValidator $ \dbTable' ps -> over _2 restrict' $ f dbTable' ps
|
|
where
|
|
restrict' p = p { psSorting = filter (\SortingSetting{..} -> restrict sortKey sortDir) $ psSorting p }
|
|
|
|
|
|
data DBEmptyStyle = DBESNoHeading | DBESHeading
|
|
deriving (Enum, Bounded, Ord, Eq, Show, Read)
|
|
|
|
instance Default DBEmptyStyle where
|
|
def = DBESHeading
|
|
|
|
data DBStyle r = DBStyle
|
|
{ dbsEmptyStyle :: DBEmptyStyle
|
|
, dbsEmptyMessage :: (SomeMessage UniWorX)
|
|
, dbsAttrs :: [(Text, Text)]
|
|
, dbsFilterLayout :: Widget
|
|
-> Enctype
|
|
-> SomeRoute UniWorX
|
|
-> Widget
|
|
-> Widget
|
|
-- ^ Filter UI, Filter Encoding, Filter action, table
|
|
, dbsTemplate :: DBSTemplateMode r
|
|
}
|
|
|
|
data DBSTemplateMode r = DBSTDefault { dbstmNumber :: Int64 -> Bool, dbstmShowNumber :: Int64 -> Bool }
|
|
| DBSTCourse
|
|
(Lens' r (Entity Course)) -- course
|
|
(Traversal' r (Entity User)) -- lecturers
|
|
(Lens' r Bool) -- isRegistered
|
|
(Lens' r (Entity School)) -- school
|
|
(Lens' r Bool) -- mayEditCourse
|
|
|
|
instance Default (DBStyle r) where
|
|
def = DBStyle
|
|
{ dbsEmptyStyle = def
|
|
, dbsEmptyMessage = (SomeMessage MsgNoTableContent)
|
|
, dbsAttrs = [ ("class", "table table--striped table--hover table--sortable") ]
|
|
, dbsFilterLayout = \_filterWgdt _filterEnctype _filterAction scrolltable ->
|
|
[whamlet|
|
|
$newline never
|
|
<!-- No Filter UI -->
|
|
^{scrolltable}
|
|
|]
|
|
, dbsTemplate = DBSTDefault (>= 10) (\n -> n `mod` 5 == 0)
|
|
}
|
|
|
|
defaultDBSFilterLayout :: Widget -- ^ Filter UI
|
|
-> Enctype
|
|
-> SomeRoute UniWorX -- ^ Filter action (target uri)
|
|
-> Widget -- ^ Table
|
|
-> Widget
|
|
defaultDBSFilterLayout filterWdgt filterEnctype filterAction scrolltable
|
|
= $(widgetFile "table/layout-filter-default")
|
|
where
|
|
filterForm = wrapForm filterWdgt FormSettings
|
|
{ formMethod = GET
|
|
, formAction = Just filterAction
|
|
, formEncoding = filterEnctype
|
|
, formAttrs = [("class", "table-filter-form"), ("autocomplete", "off")]
|
|
, formSubmit = FormAutoSubmit
|
|
, formAnchor = Nothing :: Maybe Text
|
|
}
|
|
|
|
|
|
singletonFilter :: Ord k => k -> Prism' (Map k [v]) (Maybe v)
|
|
-- ^ for use with @prismAForm@
|
|
singletonFilter key = prism' fromInner (fmap Just . fromOuter)
|
|
where
|
|
fromInner = maybe Map.empty $ Map.singleton key . pure
|
|
fromOuter = Map.lookup key >=> listToMaybe
|
|
|
|
multiFilter :: Ord k => k -> Prism' (Map k [v]) (Maybe [v])
|
|
-- ^ for use with @prismAForm@
|
|
multiFilter key = prism' fromInner fromOuter
|
|
where
|
|
-- prism' :: (Maybe [v] -> (Map k [v])) -> ((Map k [v]) -> Maybe (Maybe [v])) -> Prism' (Map k [v]) (Maybe [v])
|
|
fromInner = maybe Map.empty (Map.singleton key)
|
|
fromOuter = Just . Map.lookup key
|
|
|
|
data DBTCsvEncode r' k' csv = forall exportData filename sheetName.
|
|
( ToNamedRecord csv, CsvColumnsExplained csv
|
|
, DBTableKey k'
|
|
, Typeable exportData
|
|
, RenderMessage UniWorX filename, RenderMessage UniWorX sheetName
|
|
) => DBTCsvEncode
|
|
{ dbtCsvExportForm :: AForm DB exportData
|
|
, dbtCsvHeader :: Maybe exportData -> DB Csv.Header -- ^ @exportData@ is @Nothing@, if we're reporting an error or exporting example data
|
|
, dbtCsvExampleData :: Maybe [csv]
|
|
, dbtCsvDoEncode :: exportData -> ConduitT (k', r') csv DB ()
|
|
, dbtCsvName :: filename
|
|
, dbtCsvSheetName :: sheetName
|
|
, dbtCsvNoExportData :: Maybe (AnIso' exportData ())
|
|
}
|
|
data DBTExtraRep r' k'
|
|
= forall rep.
|
|
( HasContentType rep
|
|
, DBTableKey k'
|
|
) => DBTExtraRep
|
|
{ dbtERepDoEncode :: ConduitT (k', r') Void DB rep
|
|
}
|
|
| forall rep.
|
|
( ToContent rep
|
|
, DBTableKey k'
|
|
) => DBTExtraRepFor
|
|
{ dbtERepContentType :: ContentType
|
|
, dbtERepDoEncode :: ConduitT (k', r') Void DB rep
|
|
}
|
|
data DBTCsvDecode r' k' csv = forall route csvAction csvActionClass csvException.
|
|
( FromNamedRecord csv, ToNamedRecord csv
|
|
, DBTableKey k'
|
|
, RedirectUrl UniWorX route
|
|
, Typeable csv
|
|
, Ord csvAction, FromJSON csvAction, ToJSON csvAction
|
|
, Ord csvActionClass
|
|
, Exception csvException
|
|
) => DBTCsvDecode
|
|
{ dbtCsvRowKey :: csv -> MaybeT DB k'
|
|
, dbtCsvComputeActions :: DBCsvDiff r' csv k' -> ConduitT () csvAction DB ()
|
|
, dbtCsvValidateActions :: RWST (Set csvAction) [Message] [csvAction] DB ()
|
|
, dbtCsvClassifyAction :: csvAction -> csvActionClass
|
|
, dbtCsvCoarsenActionClass :: csvActionClass -> DBCsvActionMode
|
|
, dbtCsvExecuteActions :: ConduitT csvAction Void (YesodJobDB UniWorX) route
|
|
, dbtCsvRenderKey :: Map k' r' -> csvAction -> Widget
|
|
, dbtCsvRenderActionClass :: csvActionClass -> Widget
|
|
, dbtCsvRenderException :: csvException -> DB Text
|
|
}
|
|
|
|
data DBTable m x = forall a r r' h i t k k' csv colonnade (p :: Pillar) fs.
|
|
( ToSortable h, Functor h
|
|
, E.SqlSelect a r, E.SqlIn k k', DBTableKey k'
|
|
, PathPiece i, Eq i
|
|
, E.From t
|
|
, AsCornice h p r' (DBCell m x) colonnade
|
|
, Default fs
|
|
) => DBTable
|
|
{ dbtSQLQuery :: t -> E.SqlQuery a
|
|
, dbtRowKey :: t -> k -- ^ required for table forms; always same key for repeated requests. For joins: return unique tuples.
|
|
, dbtProj :: ReaderT (DBTProjCtx fs r) (MaybeT (ReaderT SqlBackend (HandlerFor UniWorX))) r'
|
|
, dbtColonnade :: colonnade
|
|
, dbtSorting :: Map SortingKey (SortColumn t r')
|
|
, dbtFilter :: Map FilterKey (FilterColumn t fs)
|
|
, dbtFilterUI :: DBFilterUI
|
|
, dbtStyle :: DBStyle r'
|
|
, dbtParams :: DBParams m x
|
|
, dbtCsvEncode :: Maybe (DBTCsvEncode r' k' csv)
|
|
, dbtCsvDecode :: Maybe (DBTCsvDecode r' k' csv)
|
|
, dbtExtraReps :: [DBTExtraRep r' k']
|
|
, dbtIdent :: i
|
|
}
|
|
|
|
type DBFilterUI = Maybe (Map FilterKey [Text]) -> AForm DB (Map FilterKey [Text])
|
|
|
|
dbtProjId' :: forall fs r r'.
|
|
DBRow r ~ r'
|
|
=> ReaderT (DBTProjCtx fs r) (MaybeT (ReaderT SqlBackend (HandlerFor UniWorX))) r'
|
|
dbtProjId' = view _dbtProjRow
|
|
|
|
-- | Reicht das Ergebnis der SQL-Abfrage direkt durch an colonnade und csv
|
|
dbtProjId :: forall fs r r'.
|
|
( fs ~ (), DBRow r ~ r' )
|
|
=> ReaderT (DBTProjCtx fs r) (MaybeT (ReaderT SqlBackend (HandlerFor UniWorX))) r'
|
|
dbtProjId = dbtProjId'
|
|
|
|
dbtProjSimple' :: forall fs r r' r''.
|
|
DBRow r'' ~ r'
|
|
=> (r -> DB r'')
|
|
-> ReaderT (DBTProjCtx fs r) (MaybeT (ReaderT SqlBackend (HandlerFor UniWorX))) r'
|
|
dbtProjSimple' cont = (views _dbtProjRow . set _dbrOutput) <=< (hoist lift . magnify (_dbtProjRow . _dbrOutput)) $ lift . cont =<< ask
|
|
|
|
-- | Transformation des SQL Ergbnistyp vor dem Weiterreichen an colonnade oder csv durch eine einfache monadische Funktion
|
|
dbtProjSimple :: forall fs r r' r''.
|
|
( fs ~ (), DBRow r'' ~ r' )
|
|
=> (r -> DB r'')
|
|
-> ReaderT (DBTProjCtx fs r) (MaybeT (ReaderT SqlBackend (HandlerFor UniWorX))) r'
|
|
dbtProjSimple = dbtProjSimple'
|
|
|
|
withFilteredPost :: forall fs r r'.
|
|
fs ~ DBTProjFilterPost r'
|
|
=> ReaderT (DBTProjCtx fs r) (MaybeT (ReaderT SqlBackend (HandlerFor UniWorX))) r'
|
|
-> ReaderT (DBTProjCtx fs r) (MaybeT (ReaderT SqlBackend (HandlerFor UniWorX))) r'
|
|
withFilteredPost proj = do
|
|
r' <- proj
|
|
p <- views _dbtProjFilter unDBTProjFilterPost
|
|
guardM . lift . lift $ p r'
|
|
return r'
|
|
|
|
-- | Wie `dbtProjId` plus zusätzliches Filtern der SQL-Abfrage in Haskell
|
|
-- Nur zu Verwenden, wenn Filter mit mkFilterProjectedPost verwendet werden; ein Typfehler weist daraufhin, wenn dies nötig ist!
|
|
dbtProjFilteredPostId :: forall fs r r'.
|
|
( fs ~ DBTProjFilterPost r', DBRow r ~ r' )
|
|
=> ReaderT (DBTProjCtx fs r) (MaybeT (ReaderT SqlBackend (HandlerFor UniWorX))) r'
|
|
dbtProjFilteredPostId = withFilteredPost dbtProjId'
|
|
|
|
-- | Kombination aus `dbtProjFilteredPostId` und `dbtProjSimple`, d.h. Ergebniszeilen in Haskell transformieren und filtern
|
|
dbtProjFilteredPostSimple :: forall fs r r' r''.
|
|
( fs ~ DBTProjFilterPost r', DBRow r'' ~ r' )
|
|
=> (r -> DB r'')
|
|
-> ReaderT (DBTProjCtx fs r) (MaybeT (ReaderT SqlBackend (HandlerFor UniWorX))) r'
|
|
dbtProjFilteredPostSimple = withFilteredPost . dbtProjSimple'
|
|
|
|
|
|
noCsvEncode :: Maybe (DBTCsvEncode r' k' Void)
|
|
noCsvEncode = Nothing
|
|
|
|
simpleCsvEncode :: forall filename sheetName r' k' csv.
|
|
( ToNamedRecord csv, DefaultOrdered csv, CsvColumnsExplained csv
|
|
, DBTableKey k'
|
|
, RenderMessage UniWorX filename, RenderMessage UniWorX sheetName
|
|
)
|
|
=> filename -> sheetName -> (r' -> csv) -> Maybe (DBTCsvEncode r' k' csv)
|
|
simpleCsvEncode fName sName f = Just DBTCsvEncode
|
|
{ dbtCsvExportForm = pure ()
|
|
, dbtCsvDoEncode = \() -> C.map (f . view _2)
|
|
, dbtCsvName = fName
|
|
, dbtCsvSheetName = sName
|
|
, dbtCsvNoExportData = Just id
|
|
, dbtCsvHeader = const . return $ headerOrder (error "headerOrder" :: csv)
|
|
, dbtCsvExampleData = Nothing
|
|
}
|
|
|
|
simpleCsvEncodeM :: forall filename sheetName r' k' csv.
|
|
( ToNamedRecord csv, DefaultOrdered csv, CsvColumnsExplained csv
|
|
, DBTableKey k'
|
|
, RenderMessage UniWorX filename, RenderMessage UniWorX sheetName
|
|
)
|
|
=> filename -> sheetName -> ReaderT r' DB csv -> Maybe (DBTCsvEncode r' k' csv)
|
|
simpleCsvEncodeM fName sName f = Just DBTCsvEncode
|
|
{ dbtCsvExportForm = pure ()
|
|
, dbtCsvDoEncode = \() -> C.mapM (runReaderT f . view _2)
|
|
, dbtCsvName = fName
|
|
, dbtCsvSheetName = sName
|
|
, dbtCsvNoExportData = Just id
|
|
, dbtCsvHeader = const . return $ headerOrder (error "headerOrder" :: csv)
|
|
, dbtCsvExampleData = Nothing
|
|
}
|
|
|
|
withCsvExtraRep :: forall exportData csv sheetName r' k'.
|
|
( Typeable exportData
|
|
, RenderMessage UniWorX sheetName
|
|
)
|
|
=> sheetName
|
|
-> exportData
|
|
-> Maybe (DBTCsvEncode r' k' csv)
|
|
-> [DBTExtraRep r' k'] -> [DBTExtraRep r' k']
|
|
withCsvExtraRep sheetName exportData mEncode = maybe id (flip snoc) (csvExtraRep FormatCsv)
|
|
. maybe id (flip snoc) (csvExtraRep FormatXlsx)
|
|
where
|
|
csvExtraRep fmt = do
|
|
DBTCsvEncode{ dbtCsvNoExportData = (_ :: Maybe (AnIso' exportData' ())), .. } <- mEncode
|
|
Refl <- eqT @exportData @exportData'
|
|
return DBTExtraRepFor
|
|
{ dbtERepContentType = case fmt of
|
|
FormatCsv -> typeCsv'
|
|
FormatXlsx -> typeXlsx
|
|
, dbtERepDoEncode = do
|
|
csvRendered <- toCsvRendered <$> lift (dbtCsvHeader $ Just exportData) <*> (dbtCsvDoEncode exportData .| C.foldMap (pure @[]))
|
|
encOpts <- csvOptionsForFormat fmt
|
|
csvRenderedToTypedContentWith encOpts sheetName csvRendered
|
|
}
|
|
|
|
|
|
class (MonadHandler m, HandlerSite m ~ UniWorX, Monoid' x, Monoid' (DBCell m x), Default (DBParams m x)) => IsDBTable (m :: Type -> Type) (x :: Type) where
|
|
data DBParams m x :: Type
|
|
type DBResult m x :: Type
|
|
-- type DBResult' m x :: Type
|
|
|
|
data DBCell m x :: Type
|
|
dbCell :: Iso' (DBCell m x) ([(Text, Text)], WriterT x m Widget)
|
|
|
|
-- dbWidget :: Proxy m -> Proxy x -> Iso' (DBResult m x) (Widget, DBResult' m x)
|
|
-- | Format @DBTable@ when sort-circuiting
|
|
dbWidget :: forall m' p p'. (MonadHandler m', HandlerSite m' ~ UniWorX) => p m -> p' x -> DBResult m x -> m' Widget
|
|
-- | Format @DBTable@ when not short-circuiting
|
|
dbHandler :: forall m' p p'. (MonadHandler m', HandlerSite m' ~ UniWorX) => p m -> p' x -> (Widget -> Widget) -> DBResult m x -> m' (DBResult m x)
|
|
runDBTable :: forall m' k'. (MonadHandler m', HandlerSite m' ~ UniWorX, ToJSON k') => DBTable m x -> PaginationInput -> [k'] -> m (x, Widget) -> ReaderT SqlBackend m' (DBResult m x)
|
|
|
|
dbInvalidateResult :: forall m'. (MonadHandler m', HandlerSite m' ~ UniWorX) => DBParams m x -> DBTableInvalid -> DBResult m x -> m' (DBResult m x)
|
|
dbInvalidateResult _ _ = return
|
|
|
|
cellAttrs :: IsDBTable m x => Lens' (DBCell m x) [(Text, Text)]
|
|
cellAttrs = dbCell . _1
|
|
|
|
cellContents :: IsDBTable m x => Lens' (DBCell m x) (WriterT x m Widget)
|
|
cellContents = dbCell . _2
|
|
|
|
addCellClass :: (IsDBTable m x, PathPiece t) => t -> DBCell m x -> DBCell m x
|
|
addCellClass = over cellAttrs . Yesod.addClass . toPathPiece
|
|
|
|
instance Monoid' x => IsDBTable (HandlerFor UniWorX) x where
|
|
data DBParams (HandlerFor UniWorX) x = DBParamsWidget
|
|
type DBResult (HandlerFor UniWorX) x = (x, Widget)
|
|
-- type DBResult' (WidgetFor UniWorX) () = ()
|
|
|
|
data DBCell (HandlerFor UniWorX) x = WidgetCell
|
|
{ wgtCellAttrs :: [(Text, Text)]
|
|
, wgtCellContents :: WriterT x (HandlerFor UniWorX) Widget
|
|
}
|
|
|
|
dbCell = iso
|
|
(\WidgetCell{..} -> (wgtCellAttrs, wgtCellContents))
|
|
(uncurry WidgetCell)
|
|
|
|
-- dbWidget Proxy Proxy = iso (, ()) $ view _1
|
|
dbWidget _ _ = return . snd
|
|
dbHandler _ _ f = return . over _2 f
|
|
runDBTable _ _ _ = liftHandler
|
|
|
|
instance Monoid' x => Sem.Semigroup (DBCell (HandlerFor UniWorX) x) where
|
|
(WidgetCell a c) <> (WidgetCell a' c') = WidgetCell (a <> a') ((<>) <$> c <*> c')
|
|
|
|
instance Monoid' x => Monoid (DBCell (HandlerFor UniWorX) x) where
|
|
mempty = WidgetCell mempty $ return mempty
|
|
mappend = (<>)
|
|
|
|
instance Default (DBParams (HandlerFor UniWorX) x) where
|
|
def = DBParamsWidget
|
|
|
|
instance Monoid' x => IsDBTable (ReaderT SqlBackend (HandlerFor UniWorX)) x where
|
|
data DBParams (ReaderT SqlBackend (HandlerFor UniWorX)) x = DBParamsDB
|
|
type DBResult (ReaderT SqlBackend (HandlerFor UniWorX)) x = (x, Widget)
|
|
|
|
data DBCell (ReaderT SqlBackend (HandlerFor UniWorX)) x = DBCell
|
|
{ dbCellAttrs :: [(Text, Text)]
|
|
, dbCellContents :: WriterT x (ReaderT SqlBackend (HandlerFor UniWorX)) Widget
|
|
}
|
|
|
|
dbCell = iso
|
|
(\DBCell{..} -> (dbCellAttrs, dbCellContents))
|
|
(uncurry DBCell)
|
|
|
|
dbWidget _ _ = return . snd
|
|
dbHandler _ _ f = return . over _2 f
|
|
-- runDBTable :: forall m. (MonadHandler m, HandlerSite m ~ UniWorX) => ReaderT SqlBackend (HandlerFor UniWorX) ((), Widget) -> m (Widget)
|
|
runDBTable _ _ _ = mapReaderT liftHandler
|
|
|
|
instance Monoid' x => Sem.Semigroup (DBCell (ReaderT SqlBackend (HandlerFor UniWorX)) x) where
|
|
(DBCell a c) <> (DBCell a' c') = DBCell (a <> a') ((<>) <$> c <*> c')
|
|
|
|
instance Monoid' x => Monoid (DBCell (ReaderT SqlBackend (HandlerFor UniWorX)) x) where
|
|
mempty = DBCell mempty $ return mempty
|
|
mappend = (<>)
|
|
|
|
instance Default (DBParams (ReaderT SqlBackend (HandlerFor UniWorX)) x) where
|
|
def = DBParamsDB
|
|
|
|
data DBParamsFormIdent where
|
|
DBParamsFormTableIdent :: DBParamsFormIdent
|
|
DBParamsFormOverrideIdent :: forall t. PathPiece t => t -> DBParamsFormIdent
|
|
DBParamsFormNoIdent :: DBParamsFormIdent
|
|
|
|
instance Default DBParamsFormIdent where
|
|
def = DBParamsFormTableIdent
|
|
|
|
unDBParamsFormIdent :: DBTable m x -> DBParamsFormIdent -> Maybe Text
|
|
unDBParamsFormIdent DBTable{dbtIdent} DBParamsFormTableIdent = Just $ toPathPiece dbtIdent
|
|
unDBParamsFormIdent _ (DBParamsFormOverrideIdent x) = Just $ toPathPiece x
|
|
unDBParamsFormIdent _ DBParamsFormNoIdent = Nothing
|
|
|
|
instance Monoid' x => IsDBTable (RWST (Maybe (Env, FileEnv), UniWorX, [Lang]) Enctype Ints (HandlerFor UniWorX)) x where
|
|
data DBParams (RWST (Maybe (Env, FileEnv), UniWorX, [Lang]) Enctype Ints (HandlerFor UniWorX)) x = forall a. DBParamsForm
|
|
{ dbParamsFormMethod :: StdMethod
|
|
, dbParamsFormAction :: Maybe (SomeRoute UniWorX)
|
|
, dbParamsFormAttrs :: [(Text, Text)]
|
|
, dbParamsFormSubmit :: FormSubmitType
|
|
, dbParamsFormAdditional :: Form a
|
|
, dbParamsFormEvaluate :: forall m' a' x'. (MonadHandler m', HandlerSite m' ~ UniWorX, MonadResource m') => (Html -> MForm (HandlerFor UniWorX) (FormResult a', x')) -> m' ((FormResult a', x'), Enctype)
|
|
, dbParamsFormResult :: Lens' x (FormResult a)
|
|
, dbParamsFormIdent :: DBParamsFormIdent
|
|
}
|
|
type DBResult (RWST (Maybe (Env, FileEnv), UniWorX, [Lang]) Enctype Ints (HandlerFor UniWorX)) x = (x, Widget)
|
|
-- type DBResult' (RWST (Maybe (Env, FileEnv), UniWorX, [Lang]) Enctype Ints (HandlerFor UniWorX)) (FormResult a) = (FormResult a, Enctype)
|
|
|
|
data DBCell (RWST (Maybe (Env, FileEnv), UniWorX, [Lang]) Enctype Ints (HandlerFor UniWorX)) x = forall a. FormCell
|
|
{ formCellAttrs :: [(Text, Text)]
|
|
, formCellContents :: WriterT x (MForm (HandlerFor UniWorX)) (FormResult a, Widget)
|
|
, formCellLens :: Lens' x (FormResult a)
|
|
}
|
|
|
|
-- dbCell :: Iso'
|
|
-- (DBCell (RWST ... ... ... (HandlerFor UniWorX)) x)
|
|
-- ([(Text, Text)], WriterT x (RWST ... ... ... (HandlerFor UniWorX)) Widget)
|
|
dbCell = iso
|
|
(\FormCell{..} -> (formCellAttrs, formCellContents >>= uncurry ($>) . over _1 (tell . (flip $ set formCellLens) mempty)))
|
|
(\(attrs, mkWidget) -> FormCell attrs ((pure (), ) <$> mkWidget) $ lens (\_ -> pure ()) (\s _ -> s))
|
|
|
|
-- dbWidget Proxy Proxy = iso ((,) <$> view (_1._2) <*> ((,) <$> view (_1._1) <*> view _2))
|
|
-- ((,) <$> ((,) <$> view (_2._1) <*> view _1) <*> view (_2._2))
|
|
dbWidget _ _ = return . snd
|
|
dbHandler _ _ f = return . over _2 f
|
|
-- runDBTable :: forall m' k'. (MonadHandler m', HandlerSite m' ~ UniWorX, ToJSON k') => DBTable (MForm (HandlerFor UniWorX)) x -> PaginationInput -> [k'] -> (MForm (HandlerFor UniWorX)) (x, Widget) -> ReaderT SqlBackend m' (x, Widget)
|
|
runDBTable dbtable@(DBTable{ dbtParams = dbtParams@DBParamsForm{..} }) pi pKeys
|
|
= fmap ((\(res, (wdgt, x)) -> (x & dbParamsFormResult .~ res, wdgt)) . view _1)
|
|
. dbParamsFormEvaluate
|
|
. fmap (fmap $ \(x, wdgt) -> (x ^. dbParamsFormResult, (wdgt, x)))
|
|
. maybe id (identifyForm' dbParamsFormResult) (unDBParamsFormIdent dbtable dbParamsFormIdent)
|
|
. dbParamsFormWrap dbtable dbtParams
|
|
. addPIHiddenField dbtable pi
|
|
. addPreviousHiddenField dbtable pKeys
|
|
. withFragment
|
|
|
|
dbInvalidateResult DBParamsForm{..} reason result = do
|
|
reasonTxt <- getMessageRender <*> pure reason
|
|
let
|
|
adjResult (FormFailure errs) = FormFailure $ reasonTxt : errs
|
|
adjResult _ = FormFailure $ pure reasonTxt
|
|
return $ over (_1 . dbParamsFormResult) adjResult result
|
|
|
|
instance Default (DBParams (RWST (Maybe (Env, FileEnv), UniWorX, [Lang]) Enctype Ints (HandlerFor UniWorX)) x) where
|
|
def = DBParamsForm
|
|
{ dbParamsFormMethod = POST
|
|
, dbParamsFormAction = Nothing -- Recall: Nothing preserves GET Parameters
|
|
, dbParamsFormAttrs = []
|
|
, dbParamsFormSubmit = FormSubmit
|
|
, dbParamsFormAdditional = \_ -> return (pure (), mempty)
|
|
, dbParamsFormEvaluate = liftHandler . runFormPost
|
|
, dbParamsFormResult = lens (\_ -> pure ()) (\s _ -> s)
|
|
, dbParamsFormIdent = def
|
|
}
|
|
|
|
dbParamsFormWrap :: Monoid' x => DBTable (MForm (HandlerFor UniWorX)) x -> DBParams (MForm (HandlerFor UniWorX)) x -> (Html -> MForm (HandlerFor UniWorX) (x, Widget)) -> (Html -> MForm (HandlerFor UniWorX) (x, Widget))
|
|
dbParamsFormWrap DBTable{ dbtIdent } DBParamsForm{..} tableForm frag = do
|
|
let form = mappend <$> tableForm frag <*> (fmap (over _1 $ (flip $ set dbParamsFormResult) mempty) $ dbParamsFormAdditional mempty)
|
|
((res, fWidget), enctype) <- listen form
|
|
return . (res,) $ wrapForm fWidget FormSettings
|
|
{ formMethod = dbParamsFormMethod
|
|
, formAction = dbParamsFormAction
|
|
, formEncoding = enctype
|
|
, formAttrs = dbParamsFormAttrs
|
|
, formSubmit = dbParamsFormSubmit
|
|
, formAnchor = Just $ WithIdent dbtIdent ("form" :: Text)
|
|
}
|
|
|
|
|
|
addPIHiddenField :: DBTable m' x -> PaginationInput -> (Html -> MForm m a) -> (Html -> MForm m a)
|
|
addPIHiddenField DBTable{ dbtIdent } pi form fragment
|
|
= form $ fragment <> [shamlet|
|
|
$newline never
|
|
<input type=hidden name=#{wIdent "pagination"} value=#{encodeToTextBuilder pi}>
|
|
|]
|
|
where
|
|
wIdent :: Text -> Text
|
|
wIdent = toPathPiece . WithIdent dbtIdent
|
|
|
|
addPreviousHiddenField :: (ToJSON k', MonadHandler m, HandlerSite m ~ UniWorX) => DBTable m' x -> [k'] -> (Html -> MForm m a) -> (Html -> MForm m a)
|
|
addPreviousHiddenField DBTable{ dbtIdent } pKeys form fragment = do
|
|
encrypted <- liftHandler $ encodedSecretBox SecretBoxShort pKeys
|
|
form $ fragment <> [shamlet|
|
|
$newline never
|
|
<input type=hidden name=#{wIdent "previous"} value=#{encrypted}>
|
|
|]
|
|
where
|
|
wIdent :: Text -> Text
|
|
wIdent = toPathPiece . WithIdent dbtIdent
|
|
|
|
instance Monoid' x => Sem.Semigroup (DBCell (RWST (Maybe (Env, FileEnv), UniWorX, [Lang]) Enctype Ints (HandlerFor UniWorX)) x) where
|
|
(FormCell attrs c l) <> (FormCell attrs' c' l') = FormCell (attrs <> attrs') ((\(a, w) (a', w') -> ((,) <$> a <*> a', w <> w')) <$> c <*> c') (lens (liftA2 (,) <$> view l <*> view l') (\s as -> s & l .~ (fst <$> as) & l' .~ (snd <$> as)))
|
|
|
|
instance Monoid' x => Monoid (DBCell (RWST (Maybe (Env, FileEnv), UniWorX, [Lang]) Enctype Ints (HandlerFor UniWorX)) x) where
|
|
mempty = FormCell mempty (return mempty) $ lens (\_ -> pure ()) (\s _ -> s)
|
|
mappend = (<>)
|
|
|
|
instance IsDBTable m a => IsString (DBCell m a) where
|
|
fromString = cell . fromString
|
|
|
|
|
|
-- | DB-backed tables with pagination, may short-circuit a handler if the frontend only asks for the table content, i.e. handler actions after calls to dbTable may not happen at all.
|
|
dbTable :: forall m x. IsDBTable m x => PSValidator m x -> DBTable m x -> DB (DBResult m x)
|
|
dbTable PSValidator{..} dbtable@DBTable{ dbtIdent = dbtIdent'@(toPathPiece -> dbtIdent), dbtStyle = DBStyle{..}, .. } = do
|
|
doSorting <- or2M
|
|
(getsYesod . views _appBotMitigations $ Set.notMember SettingBotMitigationOnlyLoggedInTableSorting)
|
|
(is _Just <$> maybeAuthId)
|
|
|
|
let
|
|
sortingOptions = mkOptionList
|
|
[ Option t' (SortingSetting t d) t'
|
|
| (t, _) <- mapToList dbtSorting
|
|
, d <- [SortAsc, SortDesc]
|
|
, let t' = toPathPiece $ SortingSetting t d
|
|
]
|
|
wIdent :: Text -> Text
|
|
wIdent = toPathPiece . WithIdent dbtIdent
|
|
dbsAttrs'
|
|
| not $ null dbtIdent = ("id", dbtIdent) : dbsAttrs
|
|
| otherwise = dbsAttrs
|
|
multiTextField :: forall m'. Applicative m' => Field m' [Text]
|
|
multiTextField = Field
|
|
{ fieldParse = \ts _ -> pure . Right $ Just ts
|
|
, fieldView = error "multiTextField: should not be rendered"
|
|
, fieldEnctype = UrlEncoded
|
|
}
|
|
|
|
piPreviousPost <- lift . runInputPost $ iopt (jsonField JsonFieldHidden) (wIdent "pagination")
|
|
piPreviousGet <- lift . runInputGet $ iopt (jsonField JsonFieldHidden) (wIdent "pagination")
|
|
let
|
|
piPreviousRes = maybe FormMissing FormSuccess $ piPreviousPost <|> piPreviousGet
|
|
$logDebugS "dbTable" [st|#{wIdent "pagination"}: #{tshow piPreviousRes}|]
|
|
|
|
previousKeys <- throwExceptT . runMaybeT $ encodedSecretBoxOpen =<< MaybeT (lift . lookupPostParam $ wIdent "previous")
|
|
|
|
piInput <- lift . runInputGetResult $ PaginationInput
|
|
<$> iopt (multiSelectField $ return sortingOptions) (wIdent "sorting")
|
|
<*> (assertM' (not . Map.null) . Map.mapMaybe (assertM $ not . null) <$> Map.traverseWithKey (\k _ -> iopt multiTextField $ dbFilterKey dbtIdent' k) dbtFilter)
|
|
<*> iopt pathPieceField (wIdent "pagesize")
|
|
<*> iopt intField (wIdent "page")
|
|
|
|
let prevPi
|
|
= views _2 psToPi . runPSValidator dbtable . formResultToMaybe $ piPreviousRes <|> piInput
|
|
|
|
referencePagesize = psLimit . snd . runPSValidator dbtable $ Just prevPi
|
|
|
|
(((filterRes, filterWdgt), filterEnc), ((pagesizeRes, pagesizeWdgt), pagesizeEnc)) <- mdo
|
|
(filterRes'@((filterRes, _), _)) <- runFormGet . identifyForm (FIDDBTableFilter dbtIdent) . addPIHiddenField dbtable (prevPi & _piFilter .~ Nothing & _piPage .~ Nothing & _piLimit .~ (formResult' pagesizeRes <|> piLimit prevPi)) . renderAForm FormDBTableFilter $ dbtFilterUI (piFilter prevPi)
|
|
|
|
(pagesizeRes'@((pagesizeRes, _), _)) <- lift . runFormGet . identifyForm (FIDDBTablePagesize dbtIdent) . addPIHiddenField dbtable (prevPi & _piPage .~ Nothing & _piLimit .~ Nothing & _piFilter .~ (formResult' filterRes <|> piFilter prevPi)) . renderAForm FormDBTablePagesize $
|
|
areq (pagesizeField referencePagesize) (fslI MsgDBTablePagesize & addName (wIdent "pagesize") & addClass ("select--pagesize" :: Text)) (Just referencePagesize)
|
|
return (filterRes', pagesizeRes')
|
|
|
|
let
|
|
piResult = (prevPi &) . (_piFilter ?~) <$> filterRes
|
|
<|> (prevPi &) . (_piLimit ?~) <$> pagesizeRes
|
|
<|> piPreviousRes
|
|
<|> piInput
|
|
|
|
psShortcircuit <- (== Just dbtIdent') <$> lookupCustomHeader HeaderDBTableShortcircuit
|
|
|
|
let
|
|
-- adjustPI = over _piSorting $ guardOnM doSorting -- probably not neccessary; not displaying the links should be enough for now
|
|
((errs, PaginationSettings{..}), paginationInput@PaginationInput{..})
|
|
| FormSuccess pi <- piResult
|
|
, not $ piIsUnset pi
|
|
= (, pi) . runPSValidator dbtable $ Just pi
|
|
| FormFailure errs' <- piResult
|
|
= (, def) . first (map SomeMessage errs' <>) $ runPSValidator dbtable Nothing
|
|
| otherwise
|
|
= (, def) $ runPSValidator dbtable Nothing
|
|
psSorting' = map (\SortingSetting{..} -> (Map.findWithDefault (error $ "Invalid sorting key: " <> show sortKey) sortKey dbtSorting, sortDir)) psSorting
|
|
|
|
forM_ errs $ \err -> do
|
|
mr <- getMessageRender
|
|
$logDebugS "dbTable paginationSettings" $ mr err
|
|
addMessageI Warning err
|
|
|
|
currentRoute <- fromMaybe (error "dbTable called from 404-handler") <$> getCurrentRoute
|
|
getParams <- liftHandler $ queryToQueryText . Wai.queryString . reqWaiRequest <$> getRequest
|
|
let
|
|
tblLink :: (QueryText -> QueryText) -> SomeRoute UniWorX
|
|
tblLink f = SomeRoute . (currentRoute, ) . over (mapped . _2) (fromMaybe Text.empty) $ (f . substPi . setParam "_hasdata" Nothing . setParam (toPathPiece PostFormIdentifier) Nothing) getParams
|
|
substPi = foldr (.) id
|
|
[ setParams (wIdent "sorting") . map toPathPiece $ fromMaybe [] piSorting
|
|
, foldr (.) id . map (\k -> setParams (dbFilterKey dbtIdent' k) . fromMaybe [] . join $ traverse (Map.lookup k) piFilter) $ Map.keys dbtFilter
|
|
, setParam (wIdent "pagesize") $ fmap toPathPiece piLimit
|
|
, setParam (wIdent "page") $ fmap toPathPiece piPage
|
|
, setParam (wIdent "pagination") Nothing
|
|
]
|
|
tblLink' :: (QueryText -> QueryText) -> Widget
|
|
tblLink' = toWidget <=< toTextUrl . tblLink
|
|
|
|
let noExportData
|
|
| Just DBTCsvEncode{..} <- dbtCsvEncode
|
|
= is _Just dbtCsvNoExportData
|
|
| otherwise
|
|
= True
|
|
|
|
((csvExportRes, csvExportWdgt), csvExportEnctype) <- bool runFormPost runFormGet noExportData . addPIHiddenField dbtable paginationInput . identifyForm (FIDDBTableCsvExport dbtIdent) . renderAForm FormDBTableCsvExport . fmap DBCsvExport $ case dbtCsvEncode of
|
|
Just DBTCsvEncode{..}
|
|
| Just (cloneIso -> noExportData') <- dbtCsvNoExportData
|
|
-> toDyn . view (noExportData' . from noExportData') <$> dbtCsvExportForm
|
|
| otherwise
|
|
-> toDyn <$> dbtCsvExportForm
|
|
Nothing
|
|
-> pure $ toDyn ()
|
|
let importButtons prevRes = do
|
|
isReImport <- hasGlobalPostParamForm PostDBCsvReImport
|
|
if | is _FormSuccess prevRes || isReImport
|
|
-> return [BtnCsvImport, BtnCsvImportAbort]
|
|
| otherwise
|
|
-> return [BtnCsvImport]
|
|
handleBtnAbort _ (FormSuccess BtnCsvImportAbort) = pure DBCsvAbort
|
|
handleBtnAbort x btn = x <* btn
|
|
((csvImportRes, csvImportWdgt), csvImportEnctype) <- lift . runFormPost . withGlobalPostParam PostDBCsvReImport () . withButtonFormCombM' handleBtnAbort importButtons . identifyForm (FIDDBTableCsvImport dbtIdent) . renderAForm FormDBTableCsvImport $ DBCsvImport
|
|
<$> areq fileFieldMultiple (fslI MsgCsvFile) Nothing
|
|
exportExampleRes <- guardOn <$> hasGlobalGetParam GetCsvExampleData <*> pure DBCsvExportExample
|
|
|
|
let
|
|
csvMode = asum
|
|
[ maybe FormMissing FormSuccess exportExampleRes
|
|
, csvExportRes <* guard (is _Just dbtCsvEncode)
|
|
, csvImportRes <* guard (is _Just dbtCsvDecode)
|
|
, FormSuccess DBCsvNormal
|
|
]
|
|
csvExportWdgt' = wrapForm' BtnCsvExport csvExportWdgt FormSettings
|
|
{ formMethod = bool POST GET noExportData
|
|
, formAction = Just $ tblLink id
|
|
, formEncoding = csvExportEnctype
|
|
, formAttrs = []
|
|
, formSubmit = FormSubmit
|
|
, formAnchor = Just $ wIdent "csv-export"
|
|
}
|
|
csvImportWdgt' = wrapForm csvImportWdgt FormSettings
|
|
{ formMethod = POST
|
|
, formAction = Just $ tblLink id
|
|
, formEncoding = csvImportEnctype
|
|
, formAttrs = []
|
|
, formSubmit = FormNoSubmit
|
|
, formAnchor = Just $ wIdent "csv-import"
|
|
}
|
|
csvImportExplanation :: Widget
|
|
csvImportExplanation = modal [whamlet|_{MsgCsvImportExplanationLabel}|] $ Right $(i18nWidgetFile "table/csv-import-explanation")
|
|
csvColExplanations = case dbtCsvEncode of
|
|
Just (DBTCsvEncode{} :: DBTCsvEncode r' k' csv) -> assertM' (not . null) . Map.toList . csvColumnsExplanations $ Proxy @csv
|
|
Nothing -> Nothing
|
|
csvColExplanations' = case csvColExplanations of
|
|
Just csvColExplanations'' -> modal [whamlet|_{MsgCsvColumnsExplanationsLabel}|] $ Right $(widgetFile "table/csv-column-explanations")
|
|
Nothing -> mempty
|
|
|
|
psFilter' = imap (\key args -> (, args) $ Map.findWithDefault (error $ "Invalid filter key: " <> show key) key dbtFilter) psFilter
|
|
|
|
primarySortSql = flip has psSorting' $ _head . _1 . to sqlSortDirection . _Just
|
|
|
|
sortSql :: _ -> [E.SqlExpr E.OrderBy]
|
|
sortSql t = concatMap (\(f, d) -> f d t) $ mapMaybe (\(c, d) -> (, d) <$> sqlSortDirection c) psSorting'
|
|
|
|
filterSql :: Map FilterKey (Maybe (_ -> E.SqlExpr (E.Value Bool))) -- could there be any reason not to remove Nothing values from the map already here?
|
|
filterSql = map (\(fc, args) -> ($ args) <$> filterColumn fc) $ psFilter'
|
|
|
|
-- selectPagesize = primarySortSql
|
|
-- && all (is _Just) filterSql
|
|
|
|
-- psLimit' = bool PagesizeAll psLimit selectPagesize
|
|
|
|
filterHandler <- case csvMode of
|
|
FormSuccess DBCsvImport{} -> return mempty -- don't execute Handler actions for unneeded filters upon csv _import_
|
|
_other -> liftHandler $ forM psFilter' $ \(fc,args) -> mapM ($ args) $ filterColumnHandler fc
|
|
|
|
rows' <- E.select . E.from $ \t -> do
|
|
res <- dbtSQLQuery t
|
|
E.orderBy $ sortSql t
|
|
case csvMode of
|
|
-- FormSuccess DBCsvExport{} -> return ()
|
|
FormSuccess DBCsvImport{} -> return () -- don't apply filter and sorting for csv _import_; we expect all rows to be available for matching with provided csv
|
|
_other -> do
|
|
case previousKeys of
|
|
Nothing
|
|
| PagesizeLimit l <- psLimit
|
|
-- , selectPagesize
|
|
, hasn't (_FormSuccess . _DBCsvExport) csvMode
|
|
-> do
|
|
E.limit l
|
|
E.offset $ psPage * l
|
|
Just ps -> E.where_ $ dbtRowKey t `E.sqlIn` ps -- Note that multiple where_ are indeed concatenated
|
|
_other -> return ()
|
|
let filterAppT = Map.foldr (\fc expr -> maybe expr ((: expr) . ($ t)) fc) []
|
|
sqlFilters = filterAppT filterHandler <> filterAppT filterSql -- Note that <> on the maps won't work as intended, since keys are present in both
|
|
unless (null sqlFilters) $ E.where_ $ E.and sqlFilters
|
|
return (E.unsafeSqlValue "count(*) OVER ()" :: E.SqlExpr (E.Value Int64), dbtRowKey t, res)
|
|
|
|
let mapMaybeM' f = mapMaybeM $ \(k, v) -> (,) <$> pure k <*> f v
|
|
firstRow :: Int64
|
|
firstRow
|
|
| PagesizeLimit l <- psLimit
|
|
= succ (psPage * l)
|
|
| otherwise
|
|
= 1
|
|
reproduceSorting
|
|
| Just ps <- previousKeys
|
|
= sortOn $ \(_, dbrKey, _) -> elemIndex dbrKey ps
|
|
| otherwise
|
|
= id
|
|
|
|
dbtProjFilter = ala Endo foldMap (psFilter' <&> \(f, args) -> filterProjected f args) def
|
|
|
|
sortProjected
|
|
| is _Just previousKeys
|
|
= id
|
|
| primarySortSql
|
|
= id
|
|
| otherwise
|
|
= sortBy $ concatMap (\(c, d) (_, r) (_, r') -> adjustOrder d $ sortDirectionProjected c r r') psSorting'
|
|
where
|
|
adjustOrder SortAsc x = x
|
|
adjustOrder SortDesc LT = GT
|
|
adjustOrder SortDesc EQ = EQ
|
|
adjustOrder SortDesc GT = LT
|
|
|
|
(currentKeys, rows) <- fmap (unzip . sortProjected) . mapMaybeM' (\dbtProjRow -> runReaderT dbtProj DBTProjCtx{..}) . map (\(E.Value dbrCount, dbrKey, dbrOutput) -> (dbrKey, DBRow{..})) $ reproduceSorting rows'
|
|
|
|
csvExample <- runMaybeT $ do
|
|
DBTCsvEncode{..} <- hoistMaybe dbtCsvEncode
|
|
exData <- hoistMaybe dbtCsvExampleData
|
|
hdr <- lift $ dbtCsvHeader Nothing
|
|
exportUrl <- toTextUrl (currentRoute, [(toPathPiece GetCsvExampleData, "")])
|
|
return $(widgetFile "table/csv-example")
|
|
|
|
formResult csvMode $ \case
|
|
DBCsvAbort{} -> do
|
|
addMessageI Info MsgCsvImportAborted
|
|
redirect $ tblLink id
|
|
DBCsvExportExample{}
|
|
| Just DBTCsvEncode{..} <- dbtCsvEncode
|
|
, Just exData <- dbtCsvExampleData -> do
|
|
hdr <- dbtCsvHeader Nothing
|
|
setContentDispositionCsv dbtCsvName
|
|
sendResponse <=< liftHandler . respondCsv dbtCsvSheetName hdr $ C.sourceList exData
|
|
DBCsvExport{..}
|
|
| Just DBTCsvEncode{..} <- dbtCsvEncode
|
|
, Just exportData <- fromDynamic dbCsvExportData -> do
|
|
hdr <- dbtCsvHeader $ Just exportData
|
|
setContentDispositionCsv dbtCsvName
|
|
sendResponse <=< liftHandler . respondCsvDB dbtCsvSheetName hdr $ C.sourceList (zip currentKeys rows) .| dbtCsvDoEncode exportData >> lift E.transactionSave
|
|
DBCsvImport{..}
|
|
| Just DBTCsvEncode{..} <- dbtCsvEncode
|
|
, Just (DBTCsvDecode{ dbtCsvClassifyAction = dbtCsvClassifyAction :: csvAction -> csvActionClass
|
|
, ..
|
|
} :: DBTCsvDecode r' k' csv) <- dbtCsvDecode -> do
|
|
let existing = Map.fromList $ zip currentKeys rows
|
|
sourceDiff :: ConduitT () (DBCsvDiff r' csv k') (StateT (Map k' csv) DB) ()
|
|
sourceDiff = do
|
|
let
|
|
toDiff :: csv -> StateT (Map k' csv) DB (DBCsvDiff r' csv k')
|
|
toDiff row = do
|
|
rowKey <- lift $
|
|
handle (throwM . (DBCsvException (toNamedRecord row) :: Text -> DBCsvException k') <=< dbtCsvRenderException) . runMaybeT $ dbtCsvRowKey row
|
|
seenKeys <- State.get
|
|
(<* modify (maybe id (flip Map.insert row) rowKey)) $ if
|
|
| Just rowKey' <- rowKey
|
|
, Just oldRow <- Map.lookup rowKey' seenKeys
|
|
-> throwM $ DBCsvDuplicateKey rowKey' (toNamedRecord oldRow) (toNamedRecord row)
|
|
| Just rowKey' <- rowKey
|
|
, Just oldRow <- Map.lookup rowKey' existing
|
|
-> return $ DBCsvDiffExisting rowKey' oldRow row
|
|
| otherwise
|
|
-> return $ DBCsvDiffNew rowKey row
|
|
transPipe liftHandler dbCsvFiles .| fileSourceCsv .| C.mapM toDiff
|
|
|
|
seen <- State.get
|
|
forM_ (Map.toList existing) $ \(rowKey, oldRow) -> if
|
|
| Map.member rowKey seen -> return ()
|
|
| otherwise -> yield $ DBCsvDiffMissing rowKey oldRow
|
|
|
|
accActionMap :: Map csvActionClass (Set csvAction) -> csvAction -> Map csvActionClass (Set csvAction)
|
|
accActionMap acc csvAct = Map.insertWith Set.union (dbtCsvClassifyAction csvAct) (Set.singleton csvAct) acc
|
|
|
|
importCsv = do
|
|
let
|
|
dbtCsvComputeActions' :: ConduitT (DBCsvDiff r' csv k') Void DB (Map csvActionClass (Set csvAction))
|
|
dbtCsvComputeActions' = do
|
|
let innerAct = awaitForever $ \x
|
|
-> let doHandle
|
|
| Just inpCsv <- x ^? _dbCsvNew
|
|
= handle $ throwM . (DBCsvException (toNamedRecord inpCsv) :: Text -> DBCsvException k') <=< dbtCsvRenderException
|
|
| otherwise
|
|
= id
|
|
in C.sourceList <=< lift . doHandle . runConduit $ dbtCsvComputeActions x .| C.foldMap pure
|
|
innerAct .| C.foldl accActionMap Map.empty
|
|
actionMap <- flip evalStateT Map.empty . runConduit $ sourceDiff .| transPipe lift dbtCsvComputeActions'
|
|
|
|
when (Map.null actionMap) $ do
|
|
addMessageI Info MsgCsvImportUnnecessary
|
|
redirect $ tblLink id
|
|
|
|
E.transactionSave -- If dbtCsvComputeActions has side-effects, commit those
|
|
|
|
liftHandler . (>>= sendResponse) $
|
|
siteLayoutMsg MsgCsvImportConfirmationHeading $ do
|
|
setTitleI MsgCsvImportConfirmationHeading
|
|
|
|
let
|
|
precomputeIdents :: forall f m'. (Eq (Element f), MonoFoldable f, MonadHandler m') => f -> m' (Element f -> Text)
|
|
precomputeIdents = foldM (\f act -> (\id' x -> bool (f x) id' $ act == x) <$> newIdent) (\_ -> error "No id precomputed")
|
|
actionClassIdent <- precomputeIdents $ Map.keys actionMap
|
|
actionIdent <- precomputeIdents . Set.unions $ Map.elems actionMap
|
|
|
|
let defaultChecked actClass = case dbtCsvCoarsenActionClass actClass of
|
|
DBCsvActionMissing -> False
|
|
_other -> True
|
|
csvActionCheckBox :: [(Text, Text)] -> csvAction -> Widget
|
|
csvActionCheckBox vAttrs act = do
|
|
let
|
|
sJsonField :: Field (HandlerFor UniWorX) csvAction
|
|
sJsonField = secretJsonField' $ \theId name attrs val _isReq ->
|
|
[whamlet|
|
|
$newline never
|
|
<input id=#{theId} *{attrs} type=checkbox name=#{name} value=#{either id id val} :defaultChecked (dbtCsvClassifyAction act):checked>
|
|
|]
|
|
fieldView sJsonField (actionIdent act) (toPathPiece PostDBCsvImportAction) vAttrs (Right act) False
|
|
availableActs :: Widget
|
|
availableActs = fieldView (secretJsonField :: Field Handler (Set csvAction)) "" (toPathPiece PostDBCsvImportAvailableActions) [] (Right . Set.unions $ Map.elems actionMap) False
|
|
(csvImportConfirmForm', csvImportConfirmEnctype) <- liftHandler . generateFormPost . withButtonForm' [BtnCsvImportConfirm, BtnCsvImportAbort] . identifyForm (FIDDBTableCsvImportConfirm dbtIdent) $ \csrf -> return (error "No meaningful FormResult", $(widgetFile "csv-import-confirmation"))
|
|
let csvImportConfirmForm = wrapForm csvImportConfirmForm' FormSettings
|
|
{ formMethod = POST
|
|
, formAction = Just $ tblLink id
|
|
, formEncoding = csvImportConfirmEnctype
|
|
, formAttrs = []
|
|
, formSubmit = FormNoSubmit
|
|
, formAnchor = Nothing :: Maybe Text
|
|
}
|
|
|
|
$(widgetFile "csv-import-confirmation-wrapper")
|
|
|
|
csvReImport = $(widgetFile "table/csv-reimport")
|
|
|
|
hdr <- dbtCsvHeader Nothing
|
|
catches importCsv
|
|
[ Catch.Handler $ \case
|
|
(DBCsvDuplicateKey{..} :: DBCsvException k')
|
|
-> liftHandler $ sendResponseStatus badRequest400 =<< do
|
|
mr <- getMessageRender
|
|
|
|
let offendingCsv = CsvRendered hdr [ dbCsvDuplicateKeyRowA, dbCsvDuplicateKeyRowB ]
|
|
heading = ErrorResponseTitle $ InvalidArgs [mr MsgDBCsvDuplicateKey]
|
|
|
|
siteLayoutMsg heading $ do
|
|
setTitleI heading
|
|
[whamlet|
|
|
$newline never
|
|
<section>
|
|
<p>_{MsgDBCsvDuplicateKey}
|
|
<p>_{MsgDBCsvDuplicateKeyTip}
|
|
^{offendingCsv}
|
|
<section>
|
|
^{csvReImport}
|
|
|]
|
|
(DBCsvException{..} :: DBCsvException k')
|
|
-> liftHandler $ sendResponseStatus badRequest400 =<< do
|
|
mr <- getMessageRender
|
|
|
|
let offendingCsv = CsvRendered hdr [ dbCsvExceptionRow ]
|
|
heading = ErrorResponseTitle $ InvalidArgs [mr MsgDBCsvException]
|
|
|
|
siteLayoutMsg heading $ do
|
|
setTitleI heading
|
|
[whamlet|
|
|
$newline never
|
|
<section>
|
|
<p>_{MsgDBCsvException}
|
|
$if not (Text.null dbCsvException)
|
|
<p>#{dbCsvException}
|
|
^{offendingCsv}
|
|
<section>
|
|
^{csvReImport}
|
|
|]
|
|
other -> throwM other
|
|
, Catch.Handler $ \(csvParseError :: CsvParseError)
|
|
-> liftHandler $ sendResponseStatus badRequest400 =<< do
|
|
mr <- getMessageRender
|
|
|
|
let heading = ErrorResponseTitle $ InvalidArgs [mr MsgDBCsvParseError]
|
|
|
|
siteLayoutMsg heading $ do
|
|
setTitleI heading
|
|
[whamlet|
|
|
$newline never
|
|
<section>
|
|
<p>_{MsgDBCsvParseErrorTip}
|
|
<pre .csv-parse-error>
|
|
$case csvParseError
|
|
$of CsvParseError _ errMsg
|
|
#{errMsg}
|
|
$of IncrementalError errMsg
|
|
#{errMsg}
|
|
<section>
|
|
^{csvReImport}
|
|
|]
|
|
]
|
|
_other -> return ()
|
|
|
|
let extraReps = maybe id ($) addCSVReps dbtExtraReps
|
|
where addCSVReps = do
|
|
DBTCsvEncode{..} <- dbtCsvEncode
|
|
noExportData' <- cloneIso <$> dbtCsvNoExportData
|
|
let exportData = noExportData' # ()
|
|
return $ withCsvExtraRep dbtCsvSheetName exportData dbtCsvEncode
|
|
extraRepContentType = \case
|
|
DBTExtraRep{..} -> getContentType dbtERepDoEncode
|
|
DBTExtraRepFor{..} -> dbtERepContentType
|
|
extraReps' = (typeHtml, Nothing) : map ((,) <$> extraRepContentType <*> Just) extraReps
|
|
doAltRep = maybe True (== dbtIdent) <$> lookupGlobalGetParam GetSelectTable
|
|
|
|
maybeT (return ()) $ do
|
|
guardM doAltRep
|
|
|
|
cts <- reqAccept <$> getRequest
|
|
|
|
altRep <- hoistMaybe <=< asum $ do
|
|
mRep <- hoistMaybe . selectRep' extraReps' =<< cts
|
|
return . return $ mRep <&> \case
|
|
DBTExtraRep{..} -> fmap toTypedContent . runConduit $ C.sourceList (zip currentKeys rows) .| dbtERepDoEncode
|
|
DBTExtraRepFor{..} -> fmap (TypedContent dbtERepContentType . toContent) . runConduit $ C.sourceList (zip currentKeys rows) .| dbtERepDoEncode
|
|
|
|
lift $ sendResponse =<< altRep
|
|
|
|
let
|
|
rowCount = fromMaybe 0 $ rows' ^? _head . _1 . _Value
|
|
-- | selectPagesize = fromMaybe 0 $ rows' ^? _head . _1 . _Value
|
|
-- | otherwise = olength64 rows
|
|
|
|
rawAction = tblLink
|
|
$ setParam (wIdent "sorting") Nothing
|
|
. setParam (wIdent "pagesize") Nothing
|
|
. setParam (wIdent "page") Nothing
|
|
. setParam (wIdent "pagination") Nothing
|
|
|
|
table' :: WriterT x m Widget
|
|
table' = let
|
|
columnCount :: Int64
|
|
columnCount = olength64 . getColonnade . discard $ dbtColonnade ^. _Cornice
|
|
|
|
numberColumn = case dbsTemplate of
|
|
DBSTDefault{..} -> dbstmNumber rowCount
|
|
_other -> False
|
|
|
|
genHeaders :: forall h. Cornice h _ _ (DBCell m x) -> SortableP h -> WriterT x m Widget
|
|
genHeaders cornice SortableP{..} = fmap wrap' . execWriterT . go mempty $ annotate cornice
|
|
where
|
|
go :: forall (p' :: Pillar) r'.
|
|
[(Int, Int, Int)]
|
|
-> AnnotatedCornice (Maybe Int) h p' r' (DBCell m x)
|
|
-> WriterT (Seq (Seq (Widget, Int))) (WriterT x m) ()
|
|
go rowspanAcc (AnnotatedCorniceBase _ (Colonnade (toList -> v))) = mapWriterT (over (mapped . _2) pure) . forM_ (zip (inits v) v) $ \(before, OneColonnade Sized{..} _) -> do
|
|
let (_, cellSize') = compCellSize rowspanAcc (map oneColonnadeHead before) Sized{..}
|
|
whenIsJust cellSize' $ \cellSize -> tellM . fmap pure $ fromContent Sized { sizedSize = cellSize, sizedContent }
|
|
go rowspanAcc (AnnotatedCorniceCap _ v@(toList -> oneCornices)) = do
|
|
rowspanAcc' <- (execStateT ?? rowspanAcc) . hoist (mapWriterT $ over (mapped . _2) pure) . forM_ (zip (inits oneCornices) oneCornices) $ \(before, OneCornice h (size -> sz')) -> do
|
|
let sz = Sized sz' h
|
|
let (beforeSize, cellSize') = compCellSize rowspanAcc (concatMap (map oneColonnadeHead . toList . getColonnade . uncapAnnotated . oneCorniceBody) before) sz
|
|
whenIsJust cellSize' $ \cellSize -> do
|
|
let Sized{..} = sz
|
|
lift . tellM . fmap pure $ fromContent Sized { sizedSize = cellSize, sizedContent }
|
|
if | [n] <- mapMaybe (\(key, val) -> guardOnM (is _Rowspan key) $ readMay val) (toSortable sizedContent ^. _sortableContent . cellAttrs)
|
|
-> State.modify $ (:) (n, beforeSize, cellSize)
|
|
| otherwise -> return ()
|
|
let rowspanAcc'' = rowspanAcc'
|
|
& traverse . _1 %~ pred
|
|
whenIsJust (flattenAnnotated v) $ go rowspanAcc''
|
|
|
|
compCellSize :: forall h' c. [(Int, Int, Int)] -> [Sized (Maybe Int) h' c] -> Sized (Maybe Int) h' c -> (Int, Maybe Int)
|
|
compCellSize rowspanAcc before Sized{..} = (beforeSize,) . assertM' (> 0) $ fromMaybe 1 sizedSize - shadowed
|
|
where Sum beforeSize = foldMap (\(Sized sz _) -> Sum $ fromMaybe 1 sz) before
|
|
Sum shadowed = flip foldMap rowspanAcc $ \(rowsRem, firstCol, sz) -> fromMaybe mempty $ do
|
|
guard $ rowsRem > 0
|
|
guard $ firstCol <= beforeSize
|
|
guard $ beforeSize < firstCol + sz
|
|
return . Sum $ sz - (beforeSize - firstCol)
|
|
|
|
wrap' :: Seq (Seq (Widget, Int)) -> Widget
|
|
wrap' wRows = view _2 $ Foldable.foldl (\(stackHeight', acc) row -> (Nothing, (acc <>) . wrap stackHeight' $ foldOf (folded . _1) row)) (stackHeight, mempty) wRows
|
|
where stackHeight = Just $ length wRows
|
|
wrap :: Maybe Int -> Widget -> Widget
|
|
wrap stackHeight row = case dbsTemplate of
|
|
DBSTCourse{} -> row
|
|
DBSTDefault{} -> $(widgetFile "table/header")
|
|
fromContent :: Sized Int h (DBCell m x) -> WriterT x m (Widget, Int)
|
|
fromContent Sized{ sizedSize = cellSize, sizedContent = toSortable -> Sortable{..} } = do
|
|
widget <- sortableContent ^. cellContents
|
|
let
|
|
directions = [dir | SortingSetting k dir <- psSorting, Just k == sortableKey ]
|
|
isSortable = isJust sortableKey
|
|
isSorted dir = fromMaybe False $ (==) <$> (SortingSetting <$> sortableKey <*> pure dir) <*> listToMaybe psSorting
|
|
attrs = sortableContent ^. cellAttrs
|
|
piSorting' = [ sSet | sSet <- fromMaybe [] piSorting, Just (sortKey sSet) /= sortableKey ]
|
|
rowspan = preview _head $ do
|
|
(key, val) <- attrs
|
|
guard $ is _Rowspan key
|
|
hoistMaybe $ readMay val
|
|
return . (, fromMaybe 1 rowspan) $ case dbsTemplate of
|
|
DBSTCourse{} -> $(widgetFile "table/course/header")
|
|
DBSTDefault{} -> $(widgetFile "table/cell/header")
|
|
in do
|
|
wHeaders <- maybe (return Nothing) (fmap Just . genHeaders (dbtColonnade ^. _Cornice)) pSortable
|
|
now <- liftIO getCurrentTime
|
|
case dbsTemplate of
|
|
DBSTCourse c l r s e -> do
|
|
wRows <- forM rows $ \row' -> let
|
|
Course{..} = row' ^. c . _entityVal
|
|
lecturerUsers = row' ^.. l
|
|
courseLecturers = userSurname . entityVal <$> lecturerUsers
|
|
isRegistered = row' ^. r
|
|
mayEdit = row' ^. e
|
|
nmnow = NTop $ Just now
|
|
courseIsVisible = NTop courseVisibleFrom <= nmnow && nmnow <= NTop courseVisibleTo
|
|
courseSchoolName = schoolName $ row' ^. s . _entityVal
|
|
courseSemester = (termToText . unTermKey) courseTerm
|
|
in return $(widgetFile "table/course/course-teaser")
|
|
return $(widgetFile "table/course/colonnade")
|
|
DBSTDefault{..} -> do
|
|
let colonnade = discard $ dbtColonnade ^. _Cornice
|
|
wRows' <- forM rows $ \row' -> forM (oneColonnadeEncode <$> getColonnade colonnade) $ \(($ row') -> cell') -> do
|
|
widget <- cell' ^. cellContents
|
|
let attrs = cell' ^. cellAttrs
|
|
return $(widgetFile "table/cell/body")
|
|
let wRows = zip [firstRow..] wRows'
|
|
return $(widgetFile "table/colonnade")
|
|
|
|
pageCount
|
|
| PagesizeLimit l <- psLimit
|
|
= max 1 . ceiling $ rowCount % l
|
|
| otherwise
|
|
= 1
|
|
pageNumbers = [0..pred pageCount]
|
|
|
|
pagesizeWdgt' = wrapForm pagesizeWdgt FormSettings
|
|
{ formMethod = GET
|
|
, formAction = Just . SomeRoute $ rawAction :#: wIdent "table-wrapper"
|
|
, formEncoding = pagesizeEnc
|
|
, formAttrs = [("class", "pagesize"), ("autocomplete", "off")]
|
|
, formSubmit = FormAutoSubmit
|
|
, formAnchor = Just $ wIdent "pagesize-form"
|
|
}
|
|
showPagesizeWdgt = toEnum (fromIntegral rowCount) > minimum (pagesizeOptions referencePagesize)
|
|
-- && selectPagesize
|
|
|
|
csvWdgt = $(widgetFile "table/csv-transcode")
|
|
|
|
uiLayout :: Widget -> Widget
|
|
uiLayout table = dbsFilterLayout filterWdgt filterEnc (SomeRoute $ rawAction :#: wIdent "table-wrapper") $(widgetFile "table/layout")
|
|
|
|
dbInvalidateResult' = foldr (<=<) return . catMaybes $
|
|
[ do
|
|
pKeys <- previousKeys
|
|
guard $ pKeys /= currentKeys
|
|
return . dbInvalidateResult dbtParams . DBTIRowsMissing $ length previousKeys - length currentKeys
|
|
]
|
|
|
|
((csvImportConfirmRes, _confirmView), _enctype) <- case dbtCsvDecode of
|
|
Just (DBTCsvDecode{dbtCsvExecuteActions, dbtCsvValidateActions} :: DBTCsvDecode r' k' csv) -> do
|
|
lift . runFormPost . withButtonForm' [BtnCsvImportConfirm, BtnCsvImportAbort] . identifyForm (FIDDBTableCsvImportConfirm dbtIdent) $ \_csrf -> do
|
|
availableActs <- fromMaybe Set.empty <$> globalPostParamField PostDBCsvImportAvailableActions secretJsonField
|
|
acts <- globalPostParamFields PostDBCsvImportAction secretJsonField
|
|
return . (, mempty) . FormSuccess $ if
|
|
| unavailableActs <- filter (`Set.notMember` availableActs) acts
|
|
, not $ null unavailableActs -> do
|
|
throwM . DBCsvUnavailableActionRequested @k' . Set.fromList $ map toJSON unavailableActs
|
|
| otherwise -> do
|
|
(acts', validationMsgs) <- execRWST dbtCsvValidateActions availableActs acts
|
|
if | not $ null validationMsgs -> do
|
|
mapM_ addMessage' validationMsgs
|
|
E.transactionUndo
|
|
redirect $ tblLink id
|
|
| null acts' -> do
|
|
addMessageI Info MsgCsvImportAborted
|
|
redirect $ tblLink id
|
|
| otherwise -> do
|
|
finalDest <- runDBJobs' . runConduit $ C.sourceList acts' .| dbtCsvExecuteActions
|
|
addMessageI Success . MsgCsvImportSuccessful $ length acts'
|
|
E.transactionSave
|
|
redirect finalDest
|
|
|
|
_other -> return ((FormMissing, mempty), mempty)
|
|
formResult csvImportConfirmRes $ \case
|
|
(_, BtnCsvImportAbort) -> do
|
|
addMessageI Info MsgCsvImportAborted
|
|
redirect $ tblLink id
|
|
(act, _) -> act
|
|
|
|
let
|
|
wrapLayout :: DBResult m x -> DB (DBResult m x)
|
|
wrapLayout = dbHandler (Proxy @m) (Proxy @x) $ (\table -> $(widgetFile "table/layout-wrapper")) . uiLayout
|
|
shortcircuit :: forall void. DBResult m x -> DB void
|
|
shortcircuit res = do
|
|
addCustomHeader HeaderDBTableCanonicalURL =<< toTextUrl (tblLink substPi)
|
|
sendResponse =<< tblLayout . uiLayout =<< dbWidget (Proxy @m) (Proxy @x) res
|
|
dbInvalidateResult' <=< bool wrapLayout shortcircuit psShortcircuit <=< runDBTable dbtable paginationInput currentKeys . fmap swap $ runWriterT table'
|
|
where
|
|
tblLayout :: forall m'. (MonadHandler m', HandlerSite m' ~ UniWorX) => Widget -> m' Html
|
|
tblLayout tbl' = do
|
|
tbl <- liftHandler $ widgetToPageContent tbl'
|
|
withUrlRenderer $(hamletFile "templates/table/layout-standalone.hamlet")
|
|
|
|
setParams :: Text -> [Text] -> QueryText -> QueryText
|
|
setParams key vs qt = map ((key, ) . Just) vs ++ [ i | i@(key', _) <- qt, key' /= key ]
|
|
|
|
setParam :: Text -> Maybe Text -> QueryText -> QueryText
|
|
setParam key = setParams key . maybeToList
|
|
|
|
|
|
dbTableWidget :: Monoid x
|
|
=> PSValidator (HandlerFor UniWorX) x
|
|
-> DBTable (HandlerFor UniWorX) x
|
|
-> DB (DBResult (HandlerFor UniWorX) x)
|
|
dbTableWidget = dbTable
|
|
|
|
dbTableWidget' :: PSValidator (HandlerFor UniWorX) ()
|
|
-> DBTable (HandlerFor UniWorX) ()
|
|
-> DB Widget
|
|
dbTableWidget' = fmap (fmap snd) . dbTable
|
|
|
|
dbTableDB :: Monoid x
|
|
=> PSValidator DB x
|
|
-> DBTable DB x
|
|
-> DB (DBResult DB x)
|
|
dbTableDB = dbTable
|
|
|
|
dbTableDB' :: PSValidator DB ()
|
|
-> DBTable DB ()
|
|
-> DB Widget
|
|
dbTableDB' = fmap (fmap snd) . dbTable
|
|
|
|
widgetColonnade :: Colonnade h r (DBCell (HandlerFor UniWorX) x)
|
|
-> Colonnade h r (DBCell (HandlerFor UniWorX) x)
|
|
widgetColonnade = id
|
|
|
|
-- | force the column list type for tables that contain forms, especially those constructed with dbSelect, avoids explicit type signatures
|
|
formColonnade :: Colonnade h r (DBCell (RWST (Maybe (Env, FileEnv), UniWorX, [Lang]) Enctype Ints (HandlerFor UniWorX)) (FormResult a))
|
|
-> Colonnade h r (DBCell (RWST (Maybe (Env, FileEnv), UniWorX, [Lang]) Enctype Ints (HandlerFor UniWorX)) (FormResult a))
|
|
formColonnade = id
|
|
|
|
-- | force the column list type for simple tables that do not contain forms, and especially no dbSelect, avoids explicit type signatures
|
|
dbColonnade :: Colonnade h r (DBCell DB x)
|
|
-> Colonnade h r (DBCell DB x)
|
|
dbColonnade = id
|
|
|
|
pagesizeOptions :: PagesizeLimit -- ^ Current/previous value
|
|
-> NonNull [PagesizeLimit]
|
|
pagesizeOptions psLim = impureNonNull . Set.toAscList . Set.fromList $ psLim : PagesizeAll : map PagesizeLimit opts
|
|
where
|
|
opts :: [Int64]
|
|
opts = filter (> 0) $ opts' <> map (`div` 2) opts'
|
|
|
|
opts' :: [Int64]
|
|
opts' = [ 10^n | n <- [1..3]]
|
|
|
|
pagesizeField :: PagesizeLimit -> Field Handler PagesizeLimit
|
|
pagesizeField psLim = selectField $ do
|
|
MsgRenderer mr <- getMsgRenderer
|
|
let
|
|
optText (PagesizeLimit l) = tshow l
|
|
optText PagesizeAll = mr MsgDBTablePagesizeAll
|
|
|
|
toOptionList = flip OptionList fromPathPiece . map (\o -> Option (optText o) o $ toPathPiece o)
|
|
return . toOptionList . toNullable $ pagesizeOptions psLim
|
|
|
|
|
|
---------------------------------------------------------------
|
|
--- DBCell utility functions, more in Handler.Utils.Table.Cells
|
|
|
|
cell :: IsDBTable m a => Widget -> DBCell m a
|
|
cell wgt = dbCell # ([], return wgt)
|
|
|
|
wgtCell :: (IsDBTable m a, ToWidget UniWorX wgt) => wgt -> DBCell m a
|
|
wgtCell = cell . toWidget
|
|
|
|
textCell :: (IsDBTable m a) => Text -> DBCell m a
|
|
textCell = wgtCell
|
|
|
|
stringCell :: (MonoFoldable msg, Element msg ~ Char, IsDBTable m a) => msg -> DBCell m a
|
|
stringCell = wgtCell . (pack :: String -> Text) . otoList
|
|
|
|
i18nCell :: (RenderMessage UniWorX msg, IsDBTable m a) => msg -> DBCell m a
|
|
i18nCell msg = cell $ do
|
|
mr <- getMessageRender
|
|
toWidget $ mr msg
|
|
|
|
cellTooltip :: (RenderMessage UniWorX msg, IsDBTable m a) => msg -> DBCell m a -> DBCell m a
|
|
cellTooltip = cellTooltipIcon Nothing
|
|
|
|
-- note that you can also use `cellTooltip` with `SomeMessages`, which uses ' ' for separation only
|
|
cellTooltips :: (RenderMessage UniWorX msg, IsDBTable m a) => [msg] -> DBCell m a -> DBCell m a
|
|
cellTooltips msgs = cellTooltipWgt Nothing [whamlet|
|
|
$forall msg <- msgs
|
|
<p>
|
|
_{msg}
|
|
|]
|
|
|
|
cellTooltipIcon :: (RenderMessage UniWorX msg, IsDBTable m a) => Maybe Icon -> msg -> DBCell m a -> DBCell m a
|
|
cellTooltipIcon icn = cellTooltipWgt icn . msg2widget
|
|
|
|
cellTooltipWgt :: (IsDBTable m a) => Maybe Icon -> Widget-> DBCell m a -> DBCell m a
|
|
cellTooltipWgt icn wgt = cellContents.mapped %~ (<> tipWdgt)
|
|
where
|
|
tipWdgt = iconTooltip wgt icn True
|
|
|
|
-- | Always display widget; maybe a link if user is Authorized.
|
|
-- Also see variant `linkEmptyCell`
|
|
anchorCell :: (HasRoute UniWorX url, ToWidget UniWorX wgt, IsDBTable m a) => url -> wgt -> DBCell m a
|
|
anchorCell = anchorCellM . return
|
|
|
|
anchorCellC :: (HasRoute UniWorX url, ToWidget UniWorX wgt, IsDBTable m a, Binary cache) => cache -> url -> wgt -> DBCell m a
|
|
anchorCellC cache = anchorCellCM cache . return
|
|
|
|
anchorCell' :: ( IsDBTable m a
|
|
, ToWidget UniWorX wgt
|
|
, HasRoute UniWorX url
|
|
)
|
|
=> (r -> url)
|
|
-> (r -> wgt)
|
|
-> (r -> DBCell m a)
|
|
anchorCell' mkRoute mkWidget val = anchorCell (mkRoute val) (mkWidget val)
|
|
|
|
anchorCellM :: (HasRoute UniWorX url, ToWidget UniWorX wgt, IsDBTable m a) => WidgetFor UniWorX url -> wgt -> DBCell m a
|
|
anchorCellM routeM widget = anchorCellM' routeM id (const widget)
|
|
|
|
anchorCellCM :: (HasRoute UniWorX url, ToWidget UniWorX wgt, IsDBTable m a, Binary cache) => cache -> WidgetFor UniWorX url -> wgt -> DBCell m a
|
|
anchorCellCM cache routeM widget = anchorCellCM' cache routeM id (const widget)
|
|
|
|
anchorCellM' :: (HasRoute UniWorX url, ToWidget UniWorX wgt, IsDBTable m a) => WidgetFor UniWorX x -> (x -> url) -> (x -> wgt) -> DBCell m a
|
|
anchorCellM' xM x2route x2widget = linkEitherCellM' xM x2route (x2widget, x2widget)
|
|
|
|
anchorCellCM' :: (HasRoute UniWorX url, ToWidget UniWorX wgt, IsDBTable m a, Binary cache) => cache -> WidgetFor UniWorX x -> (x -> url) -> (x -> wgt) -> DBCell m a
|
|
anchorCellCM' cache xM x2route x2widget = linkEitherCellCM' cache xM x2route (x2widget, x2widget)
|
|
|
|
maybeAnchorCellM :: (HasRoute UniWorX url, ToWidget UniWorX wgt, IsDBTable m a) => MaybeT (WidgetFor UniWorX) url -> wgt -> DBCell m a
|
|
maybeAnchorCellM routeM widget = maybeAnchorCellM' routeM id (const widget)
|
|
|
|
maybeAnchorCellM' :: (HasRoute UniWorX url, ToWidget UniWorX wgt, IsDBTable m a) => MaybeT (WidgetFor UniWorX) x -> (x -> url) -> (Maybe x -> wgt) -> DBCell m a
|
|
maybeAnchorCellM' xM x2route x2widget = maybeLinkEitherCellM' xM x2route (x2widget . Just, x2widget)
|
|
|
|
-- | Variant of `anchorCell` that displays different widgets depending whether the route is authorized for current user
|
|
linkEitherCell :: (HasRoute UniWorX url, ToWidget UniWorX wgt, ToWidget UniWorX wgt', IsDBTable m a) => url -> (wgt, wgt') -> DBCell m a
|
|
linkEitherCell = linkEitherCellM . return
|
|
|
|
linkEitherCellM :: (HasRoute UniWorX url, ToWidget UniWorX wgt, ToWidget UniWorX wgt', IsDBTable m a) => WidgetFor UniWorX url -> (wgt, wgt') -> DBCell m a
|
|
linkEitherCellM routeM (widgetAuth,widgetUnauth) = linkEitherCellM' routeM id (const widgetAuth, const widgetUnauth)
|
|
|
|
linkEitherCellM' :: forall m url wgt wgt' a x.
|
|
( HasRoute UniWorX url
|
|
, ToWidget UniWorX wgt
|
|
, ToWidget UniWorX wgt'
|
|
, IsDBTable m a
|
|
)
|
|
=> WidgetFor UniWorX x -> (x -> url) -> (x -> wgt, x -> wgt') -> DBCell m a
|
|
linkEitherCellM' xM x2route (x2widgetAuth,x2widgetUnauth) = maybeLinkEitherCellM' (lift xM) x2route (x2widgetAuth, x2widgetUnauth . fromJust)
|
|
|
|
linkEitherCellCM' :: forall m url wgt wgt' a x cache.
|
|
( HasRoute UniWorX url
|
|
, ToWidget UniWorX wgt
|
|
, ToWidget UniWorX wgt'
|
|
, IsDBTable m a
|
|
, Binary cache
|
|
)
|
|
=> cache -> WidgetFor UniWorX x -> (x -> url) -> (x -> wgt, x -> wgt') -> DBCell m a
|
|
linkEitherCellCM' cache xM x2route (x2widgetAuth,x2widgetUnauth) = maybeLinkEitherCellCM' (Just . toStrict $ B.encode cache) (lift xM) x2route (x2widgetAuth, x2widgetUnauth . fromJust)
|
|
|
|
maybeLinkEitherCellM' :: forall m url wgt wgt' a x.
|
|
( HasRoute UniWorX url
|
|
, ToWidget UniWorX wgt
|
|
, ToWidget UniWorX wgt'
|
|
, IsDBTable m a
|
|
)
|
|
=> MaybeT (WidgetFor UniWorX) x -> (x -> url) -> (x -> wgt, Maybe x -> wgt') -> DBCell m a
|
|
maybeLinkEitherCellM' = maybeLinkEitherCellCM' Nothing
|
|
|
|
maybeLinkEitherCellCM' :: forall m url wgt wgt' a x.
|
|
( HasRoute UniWorX url
|
|
, ToWidget UniWorX wgt
|
|
, ToWidget UniWorX wgt'
|
|
, IsDBTable m a
|
|
)
|
|
=> Maybe ByteString -> MaybeT (WidgetFor UniWorX) x -> (x -> url) -> (x -> wgt, Maybe x -> wgt') -> DBCell m a
|
|
maybeLinkEitherCellCM' mCache xM x2route (x2widgetAuth,x2widgetUnauth) = cell $ do
|
|
x' <- runMaybeT xM
|
|
case x' of
|
|
Just x -> do
|
|
let route = x2route x
|
|
widget, widgetUnauth :: Widget
|
|
widget = toWidget $ x2widgetAuth x
|
|
widgetUnauth = toWidget . x2widgetUnauth $ Just x
|
|
authResult <- liftHandler . maybe id $cachedHereBinary mCache . hasReadAccessTo $ urlRoute route
|
|
linkUrl <- toTextUrl route
|
|
if
|
|
| authResult -> $(widgetFile "table/cell/link") -- show allowed link
|
|
| otherwise -> widgetUnauth
|
|
_otherwise -> do
|
|
toWidget $ x2widgetUnauth Nothing
|
|
|
|
|
|
listCell :: (IsDBTable m a, MonoFoldable mono) => mono -> (Element mono -> DBCell m a) -> DBCell m a
|
|
listCell = listCell' . return
|
|
|
|
listCell' :: (IsDBTable m a, MonoFoldable mono) => WriterT a m mono -> (Element mono -> DBCell m a) -> DBCell m a
|
|
listCell' mkXS mkCell = ilistCell' (otoList <$> mkXS) $ const mkCell
|
|
|
|
ilistCell :: (IsDBTable m a, MonoFoldableWithKey mono) => mono -> (MonoKey mono -> Element mono -> DBCell m a) -> DBCell m a
|
|
ilistCell = ilistCell' . return
|
|
|
|
ilistCell' :: (IsDBTable m a, MonoFoldableWithKey mono) => WriterT a m mono -> (MonoKey mono -> Element mono -> DBCell m a) -> DBCell m a
|
|
ilistCell' mkXS mkCell = review dbCell . ([], ) $ do
|
|
xs <- mkXS
|
|
cells <- forM (otoKeyedList xs) $
|
|
\(view dbCell . uncurry mkCell -> (attrs, mkWidget)) -> (attrs, ) <$> mkWidget
|
|
return $(widgetFile "table/cell/list")
|
|
|
|
listCellOf :: IsDBTable m a' => Getting (Endo [a]) s a -> s -> (a -> DBCell m a') -> DBCell m a'
|
|
listCellOf l x = listCell (x ^.. l)
|
|
|
|
listCellOf' :: IsDBTable m a' => Getting (Endo [a]) s a -> WriterT a' m s -> (a -> DBCell m a') -> DBCell m a'
|
|
listCellOf' l mkX = listCell' (toListOf l <$> mkX)
|
|
|
|
ilistCellOf :: IsDBTable m a' => IndexedGetting i (Endo [(i, a)]) s a -> s -> (i -> a -> DBCell m a') -> DBCell m a'
|
|
ilistCellOf l x = listCell (itoListOf l x) . uncurry
|
|
|
|
ilistCellOf' :: IsDBTable m a' => IndexedGetting i (Endo [(i, a)]) s a -> WriterT a' m s -> (i -> a -> DBCell m a') -> DBCell m a'
|
|
ilistCellOf' l mkX = listCell' (itoListOf l <$> mkX) . uncurry
|
|
|
|
newtype DBFormResult i a r = DBFormResult (Map i (r, a -> a))
|
|
|
|
instance Functor (DBFormResult i a) where
|
|
f `fmap` (DBFormResult resMap) = DBFormResult $ fmap (over _1 f) resMap
|
|
|
|
instance Ord i => Sem.Semigroup (DBFormResult i a r) where
|
|
(DBFormResult m1) <> (DBFormResult m2) = DBFormResult $ Map.unionWith (\(r, f1) (_, f2) -> (r, f2 . f1)) m1 m2
|
|
|
|
instance Ord i => Monoid (DBFormResult i a r) where
|
|
mempty = DBFormResult Map.empty
|
|
mappend = (<>)
|
|
|
|
getDBFormResult :: forall r i a. (r -> a) -> DBFormResult i a r -> Map i a
|
|
getDBFormResult initial (DBFormResult m) = Map.map (\(r, f) -> f $ initial r) m
|
|
|
|
formCell :: forall x r i a. Monoid x
|
|
=> Lens' x (FormResult (DBFormResult i a (DBRow r))) -- ^ lens focussing on the form result within the larger DBResult; @id@ iff the form delivers the only result of the table
|
|
-> (DBRow r -> MForm (HandlerFor UniWorX) i) -- ^ generate row identfifiers for use in form result
|
|
-> (DBRow r -> (forall p. PathPiece p => p -> Text) -> MForm (HandlerFor UniWorX) (FormResult (a -> a), Widget)) -- ^ Given the row data and a callback to make an input name suitably unique generate the `MForm`
|
|
-> (DBRow r -> DBCell (MForm (HandlerFor UniWorX)) x)
|
|
formCell formCellLens genIndex genForm input@(DBRow{dbrKey}) = FormCell
|
|
{ formCellAttrs = []
|
|
, formCellContents = do -- MForm (HandlerFor UniWorX) (FormResult (Map i (Endo a)), Widget)
|
|
i <- lift $ genIndex input
|
|
hashKey <- LBS.toStrict . B.encode <$> cryptoIDKey return
|
|
let
|
|
mkUnique :: PathPiece p => p -> Text
|
|
mkUnique (toPathPiece -> name) = name <> "-" <> decodeUtf8 (Base64.encode rowKeyHash)
|
|
where
|
|
rowKeyHash = (BA.convert :: HMAC (SHAKE256 264) -> ByteString) . hmac hashKey . LBS.toStrict $ B.encode dbrKey
|
|
(edit, w) <- lift $ genForm input mkUnique
|
|
return (DBFormResult . Map.singleton i . (input,) <$> edit, w)
|
|
, formCellLens
|
|
}
|
|
|
|
|
|
-- Predefined colonnades
|
|
|
|
dbSelect :: forall x h r i a. (Headedness h, Monoid' x)
|
|
=> Lens' x (FormResult (DBFormResult i a (DBRow r)))
|
|
-> Setter' a Bool
|
|
-> (DBRow r -> MForm (HandlerFor UniWorX) i)
|
|
-> Colonnade h (DBRow r) (DBCell (MForm (HandlerFor UniWorX)) x)
|
|
-- dbSelect resLens selLens genIndex = Colonnade.singleton (headednessPure $ i18nCell MsgSelectColumn) $ formCell resLens genIndex genForm
|
|
dbSelect resLens selLens genIndex = Colonnade.singleton (headednessPure $ mempty & cellAttrs <>~ pure ("uw-hide-columns--no-hide","")) $ formCell resLens genIndex genForm
|
|
where
|
|
genForm _ mkUnique = do
|
|
(selResult, selWidget) <- mreq checkBoxField (fsUniq mkUnique "select") (Just False)
|
|
return (set selLens <$> selResult, [whamlet|^{fvWidget selWidget}|])
|
|
|
|
-- conditional version of dbSelect producing disabled checkboxes if the condition is not met
|
|
dbSelectIf :: forall x h r i a. (Headedness h, Monoid' x)
|
|
=> Lens' x (FormResult (DBFormResult i a (DBRow r)))
|
|
-> Setter' a Bool
|
|
-> (DBRow r -> MForm (HandlerFor UniWorX) i)
|
|
-> (DBRow r -> Bool)
|
|
-> Colonnade h (DBRow r) (DBCell (MForm (HandlerFor UniWorX)) x)
|
|
dbSelectIf resLens selLens genIndex condition = Colonnade.singleton (headednessPure $ mempty & cellAttrs <>~ [("uw-hide-columns--no-hide", mempty)] ) fCell
|
|
where
|
|
fCell = formCell resLens genIndex genForm
|
|
genForm row mkUnique = do
|
|
(selResult, selWidget) <- mreq checkBoxField ((bool inputDisabled id $ condition row) $ fsUniq mkUnique "select") (Just False) -- produces disabled field, but still checked by master checkbox from header
|
|
--(selResult, selWidget) <- mreq (bool noField checkBoxField $ condition row) (fsUniq mkUnique "select") (Just False) -- omits field entirely, but also removes master checkbox from header
|
|
{- Similar to previous: omits field entirely, but also removes master checkbox from header
|
|
(selResult, selWidget) <- if condition row
|
|
then mreq checkBoxField (fsUniq mkUnique "select") (Just False)
|
|
else return (FormMissing, FieldView "" Nothing "" mempty Nothing False)
|
|
-}
|
|
return (set selLens <$> selResult, [whamlet|^{fvWidget selWidget}|])
|
|
|
|
|
|
cap' :: ( AsCornice Sortable p r' (DBCell m x) colonnade
|
|
, IsDBTable m x
|
|
)
|
|
=> colonnade
|
|
-> Cornice Sortable ('Cap p) r' (DBCell m x)
|
|
cap' (view _Cornice -> cornice) = case cornice of
|
|
CorniceBase Colonnade{..}
|
|
| [OneColonnade{..}] <- toList getColonnade
|
|
-> recap (oneColonnadeHead & _sortableContent . cellAttrs %~ incRowspan) cornice
|
|
CorniceCap cornices
|
|
-> CorniceCap $ fmap (\OneCornice{..} -> OneCornice { oneCorniceHead = oneCorniceHead & _sortableContent . cellAttrs %~ incRowspan, oneCorniceBody = cap' oneCorniceBody }) cornices
|
|
other
|
|
-> recap (fromSortable . Sortable Nothing $ cell mempty) other
|
|
where
|
|
incRowspan :: [(Text, Text)] -> [(Text, Text)]
|
|
incRowspan attrs
|
|
| [n] <- mapMaybe (\(key, val) -> guardOnM (is _Rowspan key) $ readMay val) attrs
|
|
= (_Rowspan # (), tshow (succ n :: Natural)) : filter (hasn't $ _1 . _Rowspan) attrs
|
|
| otherwise = (_Rowspan # (), "2") : filter (hasn't $ _1 . _Rowspan) attrs
|
|
|
|
_Rowspan :: Prism' Text ()
|
|
_Rowspan = nearly <$> id <*> ((==) `on` CI.mk) $ "rowspan"
|