feat(csv): introduce csv export

This commit is contained in:
Gregor Kleen 2019-07-10 19:24:10 +02:00
parent caff343265
commit 631bbef0b8
25 changed files with 387 additions and 49 deletions

View File

@ -1171,3 +1171,10 @@ VersionHistory: Versionsgeschichte
KnownBugs: Bekannte Bugs KnownBugs: Bekannte Bugs
ExamUsersHeading: Klausurteilnehmer ExamUsersHeading: Klausurteilnehmer
CsvFile: CSV-Datei
CsvModifyExisting: Existierende Einträge angleichen
CsvAddNew: Neue Einträge einfügen
CsvDeleteMissing: Fehlende Einträge entfernen
BtnCsvExport: CSV-Datei exportieren
BtnCsvImport: CSV-Datei importieren

View File

@ -32,6 +32,7 @@ dependencies:
- data-default - data-default
- aeson >=0.6 && <1.3 - aeson >=0.6 && <1.3
- conduit >=1.0 && <2.0 - conduit >=1.0 && <2.0
- conduit-combinators
- monad-logger >=0.3 && <0.4 - monad-logger >=0.3 && <0.4
- fast-logger >=2.2 && <2.5 - fast-logger >=2.2 && <2.5
- wai-logger >=2.2 && <2.4 - wai-logger >=2.2 && <2.4
@ -129,6 +130,9 @@ dependencies:
- hourglass - hourglass
- unix - unix
- stm-delay - stm-delay
- cassava
- cassava-conduit
- constraints
other-extensions: other-extensions:
- GeneralizedNewtypeDeriving - GeneralizedNewtypeDeriving

View File

@ -388,6 +388,8 @@ postAdminFeaturesR = do
} }
psValidator = def -- & defaultSorting [SortAscBy "name", SortAscBy "short", SortAscBy "key"] psValidator = def -- & defaultSorting [SortAscBy "name", SortAscBy "short", SortAscBy "key"]
& defaultSorting [SortAscBy "key"] & defaultSorting [SortAscBy "key"]
dbtCsvEncode = noCsvEncode
dbtCsvDecode = Nothing
in dbTable psValidator DBTable{..} in dbTable psValidator DBTable{..}
mkStudytermsTable :: Set (Key StudyTerms) -> Set (Key StudyTerms) -> DB (FormResult (DBFormResult (Key StudyTerms) (Maybe Text, Maybe Text) (DBRow (Entity StudyTerms))), Widget) mkStudytermsTable :: Set (Key StudyTerms) -> Set (Key StudyTerms) -> DB (FormResult (DBFormResult (Key StudyTerms) (Maybe Text, Maybe Text) (DBRow (Entity StudyTerms))), Widget)
@ -421,6 +423,8 @@ postAdminFeaturesR = do
psValidator = def psValidator = def
-- & defaultSorting [SortAscBy "name", SortAscBy "short", SortAscBy "key"] -- & defaultSorting [SortAscBy "name", SortAscBy "short", SortAscBy "key"]
& defaultSorting [SortDescBy "isnew", SortDescBy "isbad", SortAscBy "key"] & defaultSorting [SortDescBy "isnew", SortDescBy "isbad", SortAscBy "key"]
dbtCsvEncode = noCsvEncode
dbtCsvDecode = Nothing
in dbTable psValidator DBTable{..} in dbTable psValidator DBTable{..}
mkCandidateTable = mkCandidateTable =
@ -454,5 +458,7 @@ postAdminFeaturesR = do
] ]
dbtParams = def dbtParams = def
psValidator = def & defaultSorting [SortAscBy "incidence", SortAscBy "key", SortAscBy "name"] psValidator = def & defaultSorting [SortAscBy "incidence", SortAscBy "key", SortAscBy "name"]
dbtCsvEncode = noCsvEncode
dbtCsvDecode = Nothing
in dbTable psValidator DBTable{..} in dbTable psValidator DBTable{..}

View File

@ -380,6 +380,8 @@ makeCorrectionsTable whereClause dbtColonnade dbtFilterUI psValidator dbtProj' d
, dbtStyle = def { dbsFilterLayout = maybe (\_ _ _ -> id) (\_ -> defaultDBSFilterLayout) dbtFilterUI } , dbtStyle = def { dbsFilterLayout = maybe (\_ _ _ -> id) (\_ -> defaultDBSFilterLayout) dbtFilterUI }
, dbtParams , dbtParams
, dbtIdent = "corrections" :: Text , dbtIdent = "corrections" :: Text
, dbtCsvEncode = noCsvEncode
, dbtCsvDecode = Nothing
} }
data ActionCorrections = CorrDownload data ActionCorrections = CorrDownload

View File

@ -205,6 +205,8 @@ makeCourseTable whereClause colChoices psValidator = do
, dbtStyle = def { dbsFilterLayout = defaultDBSFilterLayout } , dbtStyle = def { dbsFilterLayout = defaultDBSFilterLayout }
, dbtParams = def , dbtParams = def
, dbtIdent = "courses" :: Text , dbtIdent = "courses" :: Text
, dbtCsvEncode = noCsvEncode
, dbtCsvDecode = Nothing
} }
getCourseListR :: Handler Html getCourseListR :: Handler Html
@ -402,6 +404,8 @@ getCShowR tid ssh csh = do
dbtParams = def dbtParams = def
dbtIdent :: Text dbtIdent :: Text
dbtIdent = "tutorials" dbtIdent = "tutorials"
dbtCsvEncode = noCsvEncode
dbtCsvDecode = Nothing
tutorialDBTableValidator = def tutorialDBTableValidator = def
& defaultSorting [SortAscBy "type", SortAscBy "name"] & defaultSorting [SortAscBy "type", SortAscBy "name"]
@ -459,6 +463,8 @@ getCShowR tid ssh csh = do
dbtParams = def dbtParams = def
dbtIdent :: Text dbtIdent :: Text
dbtIdent = "exams" dbtIdent = "exams"
dbtCsvEncode = noCsvEncode
dbtCsvDecode = Nothing
examDBTableValidator = def examDBTableValidator = def
& defaultSorting [SortAscBy "time"] & defaultSorting [SortAscBy "time"]
@ -1181,6 +1187,8 @@ makeCourseUserTable cid restrict colChoices psValidator = do
, dbParamsFormResult = id , dbParamsFormResult = id
, dbParamsFormIdent = def , dbParamsFormIdent = def
} }
dbtCsvEncode = noCsvEncode
dbtCsvDecode = Nothing
over _1 postprocess <$> dbTable psValidator DBTable{..} over _1 postprocess <$> dbTable psValidator DBTable{..}
where where
postprocess :: FormResult (First act, DBFormResult UserId Bool UserTableData) -> FormResult (act, Set UserId) postprocess :: FormResult (First act, DBFormResult UserId Bool UserTableData) -> FormResult (act, Set UserId)

View File

@ -9,6 +9,7 @@ import Handler.Utils.Exam
import Handler.Utils.Invitations import Handler.Utils.Invitations
import Handler.Utils.Table.Columns import Handler.Utils.Table.Columns
import Handler.Utils.Table.Cells import Handler.Utils.Table.Cells
import Handler.Utils.Csv
import Jobs.Queue import Jobs.Queue
import Utils.Lens hiding (parts) import Utils.Lens hiding (parts)
@ -29,6 +30,10 @@ import qualified Data.CaseInsensitive as CI
import qualified Control.Monad.State.Class as State import qualified Control.Monad.State.Class as State
import qualified Data.Csv as Csv
import qualified Data.Conduit.List as C
getCExamListR :: TermId -> SchoolId -> CourseShorthand -> Handler Html getCExamListR :: TermId -> SchoolId -> CourseShorthand -> Handler Html
getCExamListR tid ssh csh = do getCExamListR tid ssh csh = do
@ -74,6 +79,8 @@ getCExamListR tid ssh csh = do
dbtParams = def dbtParams = def
dbtIdent :: Text dbtIdent :: Text
dbtIdent = "exams" dbtIdent = "exams"
dbtCsvEncode = noCsvEncode
dbtCsvDecode = Nothing
examDBTableValidator = def examDBTableValidator = def
& defaultSorting [SortAscBy "time"] & defaultSorting [SortAscBy "time"]
@ -760,6 +767,9 @@ queryStudyDegree = $(sqlIJproj 3 2) . $(sqlLOJproj 2 2) . $(sqlLOJproj 3 3)
queryStudyField :: ExamUserTableExpr -> E.SqlExpr (Maybe (Entity StudyTerms)) queryStudyField :: ExamUserTableExpr -> E.SqlExpr (Maybe (Entity StudyTerms))
queryStudyField = $(sqlIJproj 3 3) . $(sqlLOJproj 2 2) . $(sqlLOJproj 3 3) queryStudyField = $(sqlIJproj 3 3) . $(sqlLOJproj 2 2) . $(sqlLOJproj 3 3)
resultUser :: Lens' ExamUserTableData (Entity User)
resultUser = _dbrOutput . _2
resultStudyFeatures :: Traversal' ExamUserTableData (Entity StudyFeatures) resultStudyFeatures :: Traversal' ExamUserTableData (Entity StudyFeatures)
resultStudyFeatures = _dbrOutput . _4 . _Just resultStudyFeatures = _dbrOutput . _4 . _Just
@ -769,6 +779,32 @@ resultStudyDegree = _dbrOutput . _5 . _Just
resultStudyField :: Traversal' ExamUserTableData (Entity StudyTerms) resultStudyField :: Traversal' ExamUserTableData (Entity StudyTerms)
resultStudyField = _dbrOutput . _6 . _Just resultStudyField = _dbrOutput . _6 . _Just
resultExamOccurrence :: Traversal' ExamUserTableData (Entity ExamOccurrence)
resultExamOccurrence = _dbrOutput . _3 . _Just
data ExamUserTableCsv = ExamUserTableCsv
{ csvUserSurname :: Text
, csvUserName :: Text
, csvUserMatriculation :: Maybe Text
, csvUserField :: Maybe Text
, csvUserDegree :: Maybe Text
, csvUserSemester :: Maybe Int
, csvUserRoom :: Maybe Text
}
deriving (Generic)
examUserTableCsvOptions :: Csv.Options
examUserTableCsvOptions = Csv.defaultOptions { Csv.fieldLabelModifier = camelToPathPiece' 1 }
instance ToNamedRecord ExamUserTableCsv where
toNamedRecord = Csv.genericToNamedRecord examUserTableCsvOptions
instance FromNamedRecord ExamUserTableCsv where
parseNamedRecord = Csv.genericParseNamedRecord examUserTableCsvOptions
instance DefaultOrdered ExamUserTableCsv where
headerOrder = Csv.genericHeaderOrder examUserTableCsvOptions
getEUsersR, postEUsersR :: TermId -> SchoolId -> CourseShorthand -> ExamName -> Handler Html getEUsersR, postEUsersR :: TermId -> SchoolId -> CourseShorthand -> ExamName -> Handler Html
getEUsersR = postEUsersR getEUsersR = postEUsersR
postEUsersR tid ssh csh examn = do postEUsersR tid ssh csh examn = do
@ -825,6 +861,16 @@ postEUsersR tid ssh csh examn = do
dbtParams = def dbtParams = def
dbtIdent :: Text dbtIdent :: Text
dbtIdent = "exam-users" dbtIdent = "exam-users"
dbtCsvEncode :: DBTCsvEncode ExamUserTableData ExamUserTableCsv
dbtCsvEncode = DictJust . C.map $ ExamUserTableCsv
<$> view (resultUser . _entityVal . _userSurname)
<*> view (resultUser . _entityVal . _userDisplayName)
<*> view (resultUser . _entityVal . _userMatrikelnummer)
<*> preview (resultStudyField . _entityVal . to (\StudyTerms{..} -> studyTermsName <|> studyTermsShorthand <|> Just (tshow studyTermsKey)) . _Just)
<*> preview (resultStudyDegree . _entityVal . to (\StudyDegree{..} -> studyDegreeName <|> studyDegreeShorthand <|> Just (tshow studyDegreeKey)) . _Just)
<*> preview (resultStudyFeatures . _entityVal . _studyFeaturesSemester)
<*> preview (resultExamOccurrence . _entityVal . _examOccurrenceRoom)
dbtCsvDecode = Nothing
examUsersDBTableValidator = def examUsersDBTableValidator = def
((), examUsersTable) <- runDB $ dbTable examUsersDBTableValidator examUsersDBTable ((), examUsersTable) <- runDB $ dbTable examUsersDBTableValidator examUsersDBTable

View File

@ -80,6 +80,8 @@ homeOpenCourses = do
, dbtStyle = def , dbtStyle = def
, dbtParams = def , dbtParams = def
, dbtIdent = "open-courses" :: Text , dbtIdent = "open-courses" :: Text
, dbtCsvEncode = noCsvEncode
, dbtCsvDecode = Nothing
} }
$(widgetFile "home/openCourses") $(widgetFile "home/openCourses")
@ -179,6 +181,8 @@ homeUpcomingSheets uid = do
, dbtStyle = def { dbsEmptyStyle = DBESNoHeading, dbsEmptyMessage = MsgNoUpcomingSheetDeadlines } , dbtStyle = def { dbsEmptyStyle = DBESNoHeading, dbsEmptyMessage = MsgNoUpcomingSheetDeadlines }
, dbtParams = def , dbtParams = def
, dbtIdent = "upcoming-sheets" :: Text , dbtIdent = "upcoming-sheets" :: Text
, dbtCsvEncode = noCsvEncode
, dbtCsvDecode = Nothing
} }
$(widgetFile "home/upcomingSheets") $(widgetFile "home/upcomingSheets")
@ -286,6 +290,8 @@ homeUpcomingExams uid = do
dbtParams = def dbtParams = def
dbtIdent :: Text dbtIdent :: Text
dbtIdent = "exams" dbtIdent = "exams"
dbtCsvEncode = noCsvEncode
dbtCsvDecode = Nothing
examDBTableValidator = def examDBTableValidator = def
& defaultSorting [SortAscBy "time"] & defaultSorting [SortAscBy "time"]

View File

@ -149,6 +149,8 @@ getMaterialListR tid ssh csh = do
] ]
, dbtFilter = mempty , dbtFilter = mempty
, dbtFilterUI = mempty , dbtFilterUI = mempty
, dbtCsvEncode = noCsvEncode
, dbtCsvDecode = Nothing
} }
let headingLong = prependCourseTitle tid ssh csh MsgMaterialListHeading let headingLong = prependCourseTitle tid ssh csh MsgMaterialListHeading
@ -219,6 +221,8 @@ getMShowR tid ssh csh mnm = do
[ sortFilePath $(sqlIJproj 2 2) [ sortFilePath $(sqlIJproj 2 2)
, sortFileModification $(sqlIJproj 2 2) , sortFileModification $(sqlIJproj 2 2)
] ]
, dbtCsvEncode = noCsvEncode
, dbtCsvDecode = Nothing
} }
return (matEnt,fileTable') return (matEnt,fileTable')

View File

@ -258,6 +258,8 @@ mkOwnedCoursesTable =
] ]
dbtFilterUI = mempty dbtFilterUI = mempty
dbtParams = def dbtParams = def
dbtCsvEncode = noCsvEncode
dbtCsvDecode = Nothing
in \uid -> let dbtSQLQuery = dbtSQLQuery' uid in (_1 %~ getAny) <$> dbTableWidget validator DBTable{..} in \uid -> let dbtSQLQuery = dbtSQLQuery' uid in (_1 %~ getAny) <$> dbTableWidget validator DBTable{..}
@ -308,6 +310,8 @@ mkEnrolledCoursesTable =
, dbtFilterUI = mempty , dbtFilterUI = mempty
, dbtStyle = def , dbtStyle = def
, dbtParams = def , dbtParams = def
, dbtCsvEncode = noCsvEncode
, dbtCsvDecode = Nothing
} }
@ -387,6 +391,8 @@ mkSubmissionTable =
] ]
dbtFilterUI = mempty dbtFilterUI = mempty
dbtParams = def dbtParams = def
dbtCsvEncode = noCsvEncode
dbtCsvDecode = Nothing
in \uid -> let dbtSQLQuery = dbtSQLQuery' uid in \uid -> let dbtSQLQuery = dbtSQLQuery' uid
dbtSorting = dbtSorting' uid dbtSorting = dbtSorting' uid
in dbTableWidget' validator DBTable{..} in dbTableWidget' validator DBTable{..}
@ -459,6 +465,8 @@ mkSubmissionGroupTable =
] ]
dbtFilterUI = mempty dbtFilterUI = mempty
dbtParams = def dbtParams = def
dbtCsvEncode = noCsvEncode
dbtCsvDecode = Nothing
in \uid -> let dbtSQLQuery = dbtSQLQuery' uid in \uid -> let dbtSQLQuery = dbtSQLQuery' uid
in dbTableWidget' validator DBTable{..} in dbTableWidget' validator DBTable{..}
@ -535,6 +543,8 @@ mkCorrectionsTable =
] ]
dbtFilterUI = mempty dbtFilterUI = mempty
dbtParams = def dbtParams = def
dbtCsvEncode = noCsvEncode
dbtCsvDecode = Nothing
in \uid -> let dbtSQLQuery = dbtSQLQuery' uid in \uid -> let dbtSQLQuery = dbtSQLQuery' uid
in dbTableWidget' validator DBTable{..} in dbTableWidget' validator DBTable{..}

View File

@ -310,6 +310,8 @@ getSheetListR tid ssh csh = do
, dbtStyle = def , dbtStyle = def
, dbtParams = def , dbtParams = def
, dbtIdent = "sheets" :: Text , dbtIdent = "sheets" :: Text
, dbtCsvEncode = noCsvEncode
, dbtCsvDecode = Nothing
} }
-- ) ( -- !!!DEPRECTAED!!! Summary only over shown rows !!! -- ) ( -- !!!DEPRECTAED!!! Summary only over shown rows !!!
-- -- Collect summary over all Sheets, not just the ones shown due to pagination: -- -- Collect summary over all Sheets, not just the ones shown due to pagination:
@ -404,6 +406,8 @@ getSShowR tid ssh csh shn = do
) )
] ]
, dbtParams = def , dbtParams = def
, dbtCsvEncode = noCsvEncode
, dbtCsvDecode = Nothing
} }
(hasHints, hasSolution) <- runDB $ do (hasHints, hasSolution) <- runDB $ do
hasHints <- (/= 0) <$> count [ SheetFileSheet ==. sid, SheetFileType ==. SheetHint ] hasHints <- (/= 0) <$> count [ SheetFileSheet ==. sid, SheetFileType ==. SheetHint ]

View File

@ -520,6 +520,8 @@ submissionHelper tid ssh csh shn mcid = do
, dbtFilter = mempty , dbtFilter = mempty
, dbtFilterUI = mempty , dbtFilterUI = mempty
, dbtParams = def , dbtParams = def
, dbtCsvEncode = noCsvEncode
, dbtCsvDecode = Nothing
} }
mFileTable <- traverse (runDB . dbTableWidget' def) . fmap smid2ArchiveTable $ (,) <$> msmid <*> mcid mFileTable <- traverse (runDB . dbTableWidget' def) . fmap smid2ArchiveTable $ (,) <$> msmid <*> mcid

View File

@ -224,6 +224,8 @@ postMessageListR = do
, dbParamsFormIdent = def , dbParamsFormIdent = def
} }
, dbtIdent = "messages" :: Text , dbtIdent = "messages" :: Text
, dbtCsvEncode = noCsvEncode
, dbtCsvDecode = Nothing
} }
let tableRes = tableRes' & mapped._2 %~ Map.keysSet . Map.filter id . getDBFormResult (const False) let tableRes = tableRes' & mapped._2 %~ Map.keysSet . Map.filter id . getDBFormResult (const False)

View File

@ -145,6 +145,8 @@ getTermShowR = do
, dbtStyle = def , dbtStyle = def
, dbtParams = def , dbtParams = def
, dbtIdent = "terms" :: Text , dbtIdent = "terms" :: Text
, dbtCsvEncode = noCsvEncode
, dbtCsvDecode = Nothing
} }
defaultLayout $ do defaultLayout $ do
setTitleI MsgTermsHeading setTitleI MsgTermsHeading

View File

@ -93,6 +93,8 @@ getCTutorialListR tid ssh csh = do
dbtParams = def dbtParams = def
dbtIdent :: Text dbtIdent :: Text
dbtIdent = "tutorials" dbtIdent = "tutorials"
dbtCsvEncode = noCsvEncode
dbtCsvDecode = Nothing
tutorialDBTableValidator = def tutorialDBTableValidator = def
& defaultSorting [SortAscBy "type", SortAscBy "name"] & defaultSorting [SortAscBy "type", SortAscBy "name"]

View File

@ -140,6 +140,8 @@ getUsersR = do
, dbtStyle = def { dbsFilterLayout = defaultDBSFilterLayout } , dbtStyle = def { dbsFilterLayout = defaultDBSFilterLayout }
, dbtParams = def , dbtParams = def
, dbtIdent = "users" :: Text , dbtIdent = "users" :: Text
, dbtCsvEncode = noCsvEncode
, dbtCsvDecode = Nothing
} }
defaultLayout $ do defaultLayout $ do

View File

@ -31,6 +31,7 @@ import Handler.Utils.Rating as Handler.Utils hiding (extractRatings)
-- import Handler.Utils.Submission as Handler.Utils -- import Handler.Utils.Submission as Handler.Utils
import Handler.Utils.Sheet as Handler.Utils import Handler.Utils.Sheet as Handler.Utils
import Handler.Utils.Mail as Handler.Utils import Handler.Utils.Mail as Handler.Utils
import Handler.Utils.ContentDisposition as Handler.Utils
import System.Directory (listDirectory) import System.Directory (listDirectory)
import System.FilePath.Posix (takeBaseName, takeFileName) import System.FilePath.Posix (takeBaseName, takeFileName)
@ -41,21 +42,6 @@ import qualified Data.List.NonEmpty as NonEmpty
import Control.Monad.Logger import Control.Monad.Logger
-- | Check whether the user's preference for files is inline-viewing or downloading
downloadFiles :: (MonadHandler m, HandlerSite m ~ UniWorX) => m Bool
downloadFiles = do
mauth <- liftHandlerT maybeAuth
case mauth of
Just (Entity _ User{..}) -> return userDownloadFiles
Nothing -> do
UserDefaultConf{..} <- getsYesod $ view _appUserDefaults
return userDefaultDownloadFiles
setContentDisposition' :: (MonadHandler m, HandlerSite m ~ UniWorX) => Maybe FilePath -> m ()
setContentDisposition' mFileName = do
wantsDownload <- downloadFiles
setContentDisposition (bool ContentInline ContentAttachment wantsDownload) mFileName
-- | Simply send a `File`-Value -- | Simply send a `File`-Value
sendThisFile :: File -> Handler TypedContent sendThisFile :: File -> Handler TypedContent
sendThisFile File{..} sendThisFile File{..}

View File

@ -0,0 +1,24 @@
module Handler.Utils.ContentDisposition
( downloadFiles
, setContentDisposition'
) where
import Import
import Utils.Lens
-- | Check whether the user's preference for files is inline-viewing or downloading
downloadFiles :: (MonadHandler m, HandlerSite m ~ UniWorX) => m Bool
downloadFiles = do
mauth <- liftHandlerT maybeAuth
case mauth of
Just (Entity _ User{..}) -> return userDownloadFiles
Nothing -> do
UserDefaultConf{..} <- getsYesod $ view _appUserDefaults
return userDefaultDownloadFiles
setContentDisposition' :: (MonadHandler m, HandlerSite m ~ UniWorX) => Maybe FilePath -> m ()
setContentDisposition' mFileName = do
wantsDownload <- downloadFiles
setContentDisposition (bool ContentInline ContentAttachment wantsDownload) mFileName

71
src/Handler/Utils/Csv.hs Normal file
View File

@ -0,0 +1,71 @@
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Handler.Utils.Csv
( typeCsv, extensionCsv
, decodeCsv
, encodeCsv
, respondCsv, respondCsvDB
, fileSourceCsv
, CsvParseError(..)
, ToNamedRecord(..), FromNamedRecord(..)
, DefaultOrdered(..)
, ToField(..), FromField(..)
) where
import Import
import Data.Csv
import Data.Csv.Conduit
import qualified Data.Conduit.List as C
import qualified Data.Conduit.Combinators as C (sourceLazy)
import qualified Data.Map as Map
deriving instance Typeable CsvParseError
instance Exception CsvParseError
typeCsv :: ContentType
typeCsv = "text/csv"
extensionCsv :: Extension
extensionCsv = fromMaybe "csv" $ listToMaybe [ ext | (ext, mime) <- Map.toList mimeMap, mime == typeCsv ]
decodeCsv :: (MonadThrow m, FromNamedRecord csv) => Conduit ByteString m csv
decodeCsv = transPipe throwExceptT $ fromNamedCsv defaultDecodeOptions
encodeCsv :: ( ToNamedRecord csv
, DefaultOrdered csv
, Monad m
)
=> Conduit csv m ByteString
-- ^ Encode a stream of records
--
-- Currently not streaming
encodeCsv = fmap encodeDefaultOrderedByName (C.foldMap pure) >>= C.sourceLazy
respondCsv :: ( ToNamedRecord csv
, DefaultOrdered csv
)
=> Source (HandlerT site IO) csv
-> HandlerT site IO TypedContent
respondCsv src = respondSource typeCsv $ src .| encodeCsv .| awaitForever sendChunk
respondCsvDB :: ( ToNamedRecord csv
, DefaultOrdered csv
, YesodPersistRunner site
)
=> Source (YesodDB site) csv
-> HandlerT site IO TypedContent
respondCsvDB src = respondSourceDB typeCsv $ src .| encodeCsv .| awaitForever sendChunk
fileSourceCsv :: ( FromNamedRecord csv
, MonadResource m
)
=> FileInfo
-> Source m csv
fileSourceCsv = (.| decodeCsv) . fileSource

View File

@ -6,7 +6,8 @@ module Handler.Utils.Table.Pagination
, FilterColumn(..), IsFilterColumn , FilterColumn(..), IsFilterColumn
, DBRow(..), _dbrOutput, _dbrIndex, _dbrCount , DBRow(..), _dbrOutput, _dbrIndex, _dbrCount
, DBStyle(..), defaultDBSFilterLayout, DBEmptyStyle(..) , DBStyle(..), defaultDBSFilterLayout, DBEmptyStyle(..)
, DBTable(..), IsDBTable(..), DBCell(..) , DBTCsvEncode, DBTCsvDecode
, DBTable(..), noCsvEncode, IsDBTable(..), DBCell(..)
, singletonFilter , singletonFilter
, DBParams(..) , DBParams(..)
, cellAttrs, cellContents , cellAttrs, cellContents
@ -34,6 +35,8 @@ module Handler.Utils.Table.Pagination
import Handler.Utils.Table.Pagination.Types import Handler.Utils.Table.Pagination.Types
import Handler.Utils.Form import Handler.Utils.Form
import Handler.Utils.Csv
import Handler.Utils.ContentDisposition
import Utils import Utils
import Utils.Lens.TH import Utils.Lens.TH
@ -67,7 +70,8 @@ import Text.Hamlet (hamletFile)
import Data.Ratio ((%)) import Data.Ratio ((%))
import Control.Lens import Control.Lens hiding ((<.>))
import Control.Lens.Extras (is)
import Data.List (elemIndex) import Data.List (elemIndex)
@ -89,6 +93,8 @@ import qualified Data.ByteString.Lazy as LBS
import Data.Semigroup as Sem (Semigroup(..)) import Data.Semigroup as Sem (Semigroup(..))
import qualified Data.Conduit.List as C
#if MIN_VERSION_base(4,11,0) #if MIN_VERSION_base(4,11,0)
type Monoid' = Monoid type Monoid' = Monoid
@ -154,12 +160,12 @@ instance IsFilterColumn t (E.SqlExpr (E.Value Bool)) where
filterColumn' fin _ _ = fin filterColumn' fin _ _ = fin
instance IsFilterColumn t cont => IsFilterColumn t (t -> cont) where instance IsFilterColumn t cont => IsFilterColumn t (t -> cont) where
filterColumn' cont is t = filterColumn' (cont t) is t 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 instance {-# OVERLAPPABLE #-} (PathPiece (Element l), IsFilterColumn t cont, MonoPointed l, Monoid l) => IsFilterColumn t (l -> cont) where
filterColumn' cont is = filterColumn' (cont input) is' filterColumn' cont is0 = filterColumn' (cont input) is'
where where
(input, ($ []) -> is') = go (mempty, id) is (input, ($ []) -> is') = go (mempty, id) is0
go acc [] = acc go acc [] = acc
go (acc, is3) (i:is2) go (acc, is3) (i:is2)
| Just i' <- fromPathPiece i = go (acc `mappend` singleton i', is3) is2 | Just i' <- fromPathPiece i = go (acc `mappend` singleton i', is3) is2
@ -263,6 +269,37 @@ piIsUnset PaginationInput{..} = and
, isNothing piPage , isNothing piPage
] ]
data ButtonCsvMode = BtnCsvExport | BtnCsvImport
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic, Typeable)
instance Universe ButtonCsvMode
instance Finite ButtonCsvMode
embedRenderMessage ''UniWorX ''ButtonCsvMode id
nullaryPathPiece ''ButtonCsvMode $ camelToPathPiece' 1
instance Button UniWorX ButtonCsvMode where
btnLabel BtnCsvExport
= [whamlet|
$newline never
#{fontAwesomeIcon "file-csv"}
\ _{BtnCsvExport}
|]
btnLabel BtnCsvImport
= [whamlet|
$newline never
_{BtnCsvImport}
|]
data DBCsvMode = DBCsvNormal
| DBCsvExport
| DBCsvImport
{ _dbCsvFiles :: [FileInfo]
, _dbCsvModifyExisting, _dbCsvAddNew, _dbCsvDeleteMissing :: Bool
}
type DBTableKey k' = (ToJSON k', FromJSON k', Ord k', Binary k') type DBTableKey k' = (ToJSON k', FromJSON k', Ord k', Binary k')
data DBRow r = forall k'. DBTableKey k' => DBRow data DBRow r = forall k'. DBTableKey k' => DBRow
{ dbrKey :: k' { dbrKey :: k'
@ -402,7 +439,10 @@ instance PathPiece x => PathPiece (WithIdent x) where
WithIdent <$> pure ident <*> fromPathPiece rest WithIdent <$> pure ident <*> fromPathPiece rest
data DBTable m x = forall a r r' h i t k k'. type DBTCsvEncode r' csv = DictMaybe (ToNamedRecord csv, DefaultOrdered csv) (Conduit r' (YesodDB UniWorX) csv)
type DBTCsvDecode csv = DictMaybe (FromNamedRecord csv) (Sink csv (YesodDB UniWorX) ())
data DBTable m x = forall a r r' h i t k k' csv.
( ToSortable h, Functor h ( ToSortable h, Functor h
, E.SqlSelect a r, E.SqlIn k k', DBTableKey k' , E.SqlSelect a r, E.SqlIn k k', DBTableKey k'
, PathPiece i, Eq i , PathPiece i, Eq i
@ -410,16 +450,21 @@ data DBTable m x = forall a r r' h i t k k'.
) => DBTable ) => DBTable
{ dbtSQLQuery :: t -> E.SqlQuery a { dbtSQLQuery :: t -> E.SqlQuery a
, dbtRowKey :: t -> k -- ^ required for table forms; always same key for repeated requests. For joins: return unique tuples. , dbtRowKey :: t -> k -- ^ required for table forms; always same key for repeated requests. For joins: return unique tuples.
, dbtProj :: DBRow r -> MaybeT (ReaderT SqlBackend (HandlerT UniWorX IO)) r' , dbtProj :: DBRow r -> MaybeT (YesodDB UniWorX) r'
, dbtColonnade :: Colonnade h r' (DBCell m x) , dbtColonnade :: Colonnade h r' (DBCell m x)
, dbtSorting :: Map SortingKey (SortColumn t) , dbtSorting :: Map SortingKey (SortColumn t)
, dbtFilter :: Map FilterKey (FilterColumn t) , dbtFilter :: Map FilterKey (FilterColumn t)
, dbtFilterUI :: Maybe (Map FilterKey [Text]) -> AForm (ReaderT SqlBackend (HandlerT UniWorX IO)) (Map FilterKey [Text]) , dbtFilterUI :: Maybe (Map FilterKey [Text]) -> AForm (YesodDB UniWorX) (Map FilterKey [Text])
, dbtStyle :: DBStyle , dbtStyle :: DBStyle
, dbtParams :: DBParams m x , dbtParams :: DBParams m x
, dbtCsvEncode :: DBTCsvEncode r' csv
, dbtCsvDecode :: DBTCsvDecode csv
, dbtIdent :: i , dbtIdent :: i
} }
noCsvEncode :: DictMaybe (ToNamedRecord Void, DefaultOrdered Void) (Conduit r' (YesodDB UniWorX) Void)
noCsvEncode = Nothing
class (MonadHandler m, HandlerSite m ~ UniWorX, Monoid' x, Monoid' (DBCell m x), Default (DBParams m x)) => IsDBTable (m :: * -> *) (x :: *) where class (MonadHandler m, HandlerSite m ~ UniWorX, Monoid' x, Monoid' (DBCell m x), Default (DBParams m x)) => IsDBTable (m :: * -> *) (x :: *) where
data DBParams m x :: * data DBParams m x :: *
type DBResult m x :: * type DBResult m x :: *
@ -691,18 +736,68 @@ dbTable PSValidator{..} dbtable@DBTable{ dbtIdent = dbtIdent'@(toPathPiece -> db
mapM_ (addMessageI Warning) errs mapM_ (addMessageI Warning) errs
Just currentRoute <- getCurrentRoute -- `dbTable` should never be called from a 404-handler
getParams <- liftHandlerT $ 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) getParams
substPi = foldr (.) id
[ setParams (wIdent "sorting") . map toPathPiece $ fromMaybe [] piSorting
, foldr (.) id . map (\k -> setParams (wIdent $ toPathPiece 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
((csvExportRes, csvExportWdgt), csvExportEnctype) <- lift . runFormGet . identifyForm FIDDBTableCsvExport . set (mapped . mapped . _1 . mapped) DBCsvExport $ buttonForm' [BtnCsvExport]
((csvImportRes, csvImportWdgt), csvImportEnctype) <- lift . runFormPost . identifyForm FIDDBTableCsvImport . renderAForm FormDBTableCsvImport $ DBCsvImport
<$> areq fileFieldMultiple (fslI MsgCsvFile) Nothing
<*> apopt checkBoxField (fslI MsgCsvModifyExisting) (Just True)
<*> apopt checkBoxField (fslI MsgCsvAddNew) (Just True)
<*> apopt checkBoxField (fslI MsgCsvDeleteMissing) (Just False)
let
csvMode = asum
[ csvExportRes <* guard (is _Just dbtCsvEncode)
, csvImportRes <* guard (is _Just dbtCsvDecode)
, FormSuccess DBCsvNormal
]
csvExportWdgt' = wrapForm csvExportWdgt FormSettings
{ formMethod = GET
, formAction = Just $ tblLink id
, formEncoding = csvExportEnctype
, formAttrs = [("target", "_blank")]
, formSubmit = FormNoSubmit
, formAnchor = Nothing :: Maybe Text
}
csvImportWdgt' = wrapForm' BtnCsvImport csvImportWdgt FormSettings
{ formMethod = POST
, formAction = Just $ tblLink id
, formEncoding = csvImportEnctype
, formAttrs = []
, formSubmit = FormSubmit
, formAnchor = Nothing :: Maybe Text
}
rows' <- E.select . E.from $ \t -> do rows' <- E.select . E.from $ \t -> do
res <- dbtSQLQuery t res <- dbtSQLQuery t
E.orderBy (map (sqlSortDirection t) psSorting') E.orderBy (map (sqlSortDirection t) psSorting')
case previousKeys of case csvMode of
Nothing FormSuccess DBCsvExport -> return ()
| PagesizeLimit l <- psLimit FormSuccess DBCsvImport{} -> return ()
-> do _other -> do
E.limit l case previousKeys of
E.offset (psPage * l) Nothing
Just ps -> E.where_ $ dbtRowKey t `E.sqlIn` ps | PagesizeLimit l <- psLimit
_other -> return () -> do
Map.foldrWithKey (\key args expr -> E.where_ (filterColumn (Map.findWithDefault (error $ "Invalid filter key: " <> show key) key dbtFilter) args t) >> expr) (return ()) psFilter E.limit l
E.offset (psPage * l)
Just ps -> E.where_ $ dbtRowKey t `E.sqlIn` ps
_other -> return ()
Map.foldrWithKey (\key args expr -> E.where_ (filterColumn (Map.findWithDefault (error $ "Invalid filter key: " <> show key) key dbtFilter) args t) >> expr) (return ()) psFilter
return (E.unsafeSqlValue "count(*) OVER ()" :: E.SqlExpr (E.Value Int64), dbtRowKey t, res) return (E.unsafeSqlValue "count(*) OVER ()" :: E.SqlExpr (E.Value Int64), dbtRowKey t, res)
let mapMaybeM f = fmap catMaybes . mapM (\(k, v) -> runMaybeT $ (,) <$> pure k <*> f v) let mapMaybeM f = fmap catMaybes . mapM (\(k, v) -> runMaybeT $ (,) <$> pure k <*> f v)
@ -720,20 +815,17 @@ dbTable PSValidator{..} dbtable@DBTable{ dbtIdent = dbtIdent'@(toPathPiece -> db
(currentKeys, rows) <- fmap unzip . mapMaybeM dbtProj . map (\(dbrIndex, (E.Value dbrCount, dbrKey, dbrOutput)) -> (dbrKey, DBRow{..})) . zip [firstRow..] $ reproduceSorting rows' (currentKeys, rows) <- fmap unzip . mapMaybeM dbtProj . map (\(dbrIndex, (E.Value dbrCount, dbrKey, dbrOutput)) -> (dbrKey, DBRow{..})) . zip [firstRow..] $ reproduceSorting rows'
Just currentRoute <- getCurrentRoute -- `dbTable` should never be called from a 404-handler
getParams <- liftHandlerT $ queryToQueryText . Wai.queryString . reqWaiRequest <$> getRequest formResult csvMode $ \case
let DBCsvExport
tblLink :: (QueryText -> QueryText) -> SomeRoute UniWorX | Just (Dict, dbtCsvEncode') <- dbtCsvEncode
tblLink f = SomeRoute . (currentRoute, ) . over (mapped . _2) (fromMaybe Text.empty) $ (f . substPi . setParam "_hasdata" Nothing) getParams -> do
substPi = foldr (.) id setContentDisposition' . Just $ unpack dbtIdent <.> unpack extensionCsv
[ setParams (wIdent "sorting") . map toPathPiece $ fromMaybe [] piSorting sendResponse <=< liftHandlerT . respondCsvDB $ C.sourceList rows .| dbtCsvEncode'
, foldr (.) id . map (\k -> setParams (wIdent $ toPathPiece k) . fromMaybe [] . join $ traverse (Map.lookup k) piFilter) $ Map.keys dbtFilter DBCsvImport{}
, setParam (wIdent "pagesize") $ fmap toPathPiece piLimit | Just (Dict, _dbtCsvDecode) <- dbtCsvDecode
, setParam (wIdent "page") $ fmap toPathPiece piPage -> error "dbCsvImport"
, setParam (wIdent "pagination") Nothing _other -> return ()
]
tblLink' :: (QueryText -> QueryText) -> Widget
tblLink' = toWidget <=< toTextUrl . tblLink
let let
rowCount rowCount
@ -786,6 +878,9 @@ dbTable PSValidator{..} dbtable@DBTable{ dbtIdent = dbtIdent'@(toPathPiece -> db
, formSubmit = FormAutoSubmit , formSubmit = FormAutoSubmit
, formAnchor = Just $ wIdent "pagesize-form" , formAnchor = Just $ wIdent "pagesize-form"
} }
csvWdgt = $(widgetFile "table/csv-transcode")
uiLayout table = dbsFilterLayout filterWdgt filterEnc (SomeRoute $ rawAction :#: wIdent "table-wrapper") $(widgetFile "table/layout") uiLayout table = dbsFilterLayout filterWdgt filterEnc (SomeRoute $ rawAction :#: wIdent "table-wrapper") $(widgetFile "table/layout")
dbInvalidateResult' = foldr (<=<) return . catMaybes $ dbInvalidateResult' = foldr (<=<) return . catMaybes $

View File

@ -74,6 +74,9 @@ import Network.Mime as Import
import Data.Aeson.TH as Import import Data.Aeson.TH as Import
import Data.Aeson.Types as Import (FromJSON(..), ToJSON(..), FromJSONKey(..), ToJSONKey(..), toJSONKeyText, FromJSONKeyFunction(..), ToJSONKeyFunction(..), Value) import Data.Aeson.Types as Import (FromJSON(..), ToJSON(..), FromJSONKey(..), ToJSONKey(..), toJSONKeyText, FromJSONKeyFunction(..), ToJSONKeyFunction(..), Value)
import Data.Constraint as Import (Dict(..))
import Data.Void as Import (Void)
import Language.Haskell.TH.Instances as Import () import Language.Haskell.TH.Instances as Import ()
import Data.List.NonEmpty.Instances as Import () import Data.List.NonEmpty.Instances as Import ()
import Data.NonNull.Instances as Import () import Data.NonNull.Instances as Import ()

View File

@ -77,6 +77,8 @@ import Data.List.NonEmpty (NonEmpty, nonEmpty)
import Algebra.Lattice (top, bottom, (/\), (\/), BoundedJoinSemiLattice, BoundedMeetSemiLattice) import Algebra.Lattice (top, bottom, (/\), (\/), BoundedJoinSemiLattice, BoundedMeetSemiLattice)
import Data.Constraint (Dict(..))
{-# ANN choice ("HLint: ignore Use asum" :: String) #-} {-# ANN choice ("HLint: ignore Use asum" :: String) #-}
@ -978,3 +980,12 @@ foldJoin = foldr (\/) bottom
foldMeet :: (MonoFoldable mono, BoundedMeetSemiLattice (Element mono)) => mono -> Element mono foldMeet :: (MonoFoldable mono, BoundedMeetSemiLattice (Element mono)) => mono -> Element mono
foldMeet = foldr (/\) top foldMeet = foldr (/\) top
-----------------
-- Constraints --
-----------------
type DictMaybe constr a = Maybe (Dict constr, a)
pattern DictJust :: constr => a -> DictMaybe constr a
pattern DictJust a = Just (Dict, a)

View File

@ -8,6 +8,7 @@ import Yesod.Core.Instances ()
import Settings import Settings
import Utils.Parameters import Utils.Parameters
import Utils.Lens
import Text.Blaze (Markup) import Text.Blaze (Markup)
import qualified Text.Blaze.Internal as Blaze (null) import qualified Text.Blaze.Internal as Blaze (null)
@ -32,8 +33,6 @@ import Control.Monad.Morph (MFunctor(..))
import Data.List ((!!)) import Data.List ((!!))
import Control.Lens
import Web.PathPieces import Web.PathPieces
import Data.UUID import Data.UUID
@ -197,6 +196,8 @@ data FormIdentifier
| FIDDBTableFilter | FIDDBTableFilter
| FIDDBTablePagesize | FIDDBTablePagesize
| FIDDBTable | FIDDBTable
| FIDDBTableCsvExport
| FIDDBTableCsvImport
| FIDDelete | FIDDelete
| FIDCourseRegister | FIDCourseRegister
| FIDuserRights | FIDuserRights
@ -591,6 +592,19 @@ htmlFieldSmall = checkMMap sanitize (pack . renderHtml) textField
sanitize :: Text -> m (Either FormMessage Html) sanitize :: Text -> m (Either FormMessage Html)
sanitize = return . Right . preEscapedText . sanitizeBalance sanitize = return . Right . preEscapedText . sanitizeBalance
fileFieldMultiple :: Monad m => Field m [FileInfo]
fileFieldMultiple = Field
{ fieldParse = \_ files -> return $ case files of
[] -> Right Nothing
fs -> Right $ Just fs
, fieldView = \id' name attrs _ isReq ->
[whamlet|
$newline never
<input type="file" uw-file-input id=#{id'} name=#{name} *{attrs} multiple :isReq:required="required">
|]
, fieldEnctype = Multipart
}
----------- -----------
-- Forms -- -- Forms --
----------- -----------
@ -635,7 +649,7 @@ wrapForm' btn formWidget FormSettings{..} = do
------------------- -------------------
-- | Use this type to pass information to the form template -- | Use this type to pass information to the form template
data FormLayout = FormStandard | FormDBTableFilter | FormDBTablePagesize data FormLayout = FormStandard | FormDBTableFilter | FormDBTablePagesize | FormDBTableCsvImport
renderAForm :: Monad m => FormLayout -> FormRender m a renderAForm :: Monad m => FormLayout -> FormRender m a
renderAForm formLayout aform fragment = do renderAForm formLayout aform fragment = do
@ -932,3 +946,20 @@ apreq f fs mx = formToAForm $ over _2 pure <$> mpreq f fs mx
wpreq :: (RenderMessage site FormMessage, HandlerSite m ~ site, MonadHandler m) wpreq :: (RenderMessage site FormMessage, HandlerSite m ~ site, MonadHandler m)
=> Field m a -> FieldSettings site -> Maybe a -> WForm m (FormResult a) => Field m a -> FieldSettings site -> Maybe a -> WForm m (FormResult a)
wpreq f fs mx = mFormToWForm $ mpreq f fs mx wpreq f fs mx = mFormToWForm $ mpreq f fs mx
mpopt :: (RenderMessage site FormMessage, HandlerSite m ~ site, MonadHandler m)
=> Field m a -> FieldSettings site -> Maybe a -> MForm m (FormResult a, FieldView site)
-- ^ Pseudo optional
--
-- `FieldView` has `fvRequired` set to `False`
-- Otherwise acts exactly like `mreq`.
mpopt f fs mx = set (_2 . _fvRequired) False <$> mreq f fs mx
apopt :: (RenderMessage site FormMessage, HandlerSite m ~ site, MonadHandler m)
=> Field m a -> FieldSettings site -> Maybe a -> AForm m a
apopt f fs mx = formToAForm $ over _2 pure <$> mpopt f fs mx
wpopt :: (RenderMessage site FormMessage, HandlerSite m ~ site, MonadHandler m)
=> Field m a -> FieldSettings site -> Maybe a -> WForm m (FormResult a)
wpopt f fs mx = mFormToWForm $ mpopt f fs mx

View File

@ -143,6 +143,8 @@ makeLenses_ ''ExamGradingRule
makeLenses_ ''UTCTime makeLenses_ ''UTCTime
makeLenses_ ''ExamOccurrence
-- makeClassy_ ''Load -- makeClassy_ ''Load

View File

@ -0,0 +1,7 @@
$newline never
$if is _Just dbtCsvDecode
<div .csv-import>
^{csvImportWdgt'}
$if is _Just dbtCsvEncode
<div .csv-export>
^{csvExportWdgt'}

View File

@ -5,6 +5,7 @@ $else
<div .table-header> <div .table-header>
<div .table__row-count> <div .table__row-count>
_{MsgRowCount rowCount} _{MsgRowCount rowCount}
^{csvWdgt}
^{table} ^{table}