feat(lms): configurable csv settings for lms direct import and export routes

This commit is contained in:
Steffen Jost 2022-09-08 18:11:07 +02:00
parent 243d468c98
commit 6159403b27
12 changed files with 107 additions and 41 deletions

View File

@ -125,6 +125,13 @@ ldap:
ldap-re-test-failover: 60 ldap-re-test-failover: 60
lms:
upload-headedness: "_env:LMSUPLOADHEADEDNESS:true"
upload-delimiter: "_env:LMSUPLOADDELIMITER:,"
download-headedness: "_env:LMSDOWNLOADHEADEDNESS:true"
download-delimiter: "_env:LMSDOWNLOADDELIMITER:,"
download-cr-lf: "_env:LMSDOWNLOADCRLF:true"
avs: avs:
host: "_env:AVSHOST:skytest.fra.fraport.de" host: "_env:AVSHOST:skytest.fra.fraport.de"
port: "_env:AVSPORT:443" port: "_env:AVSPORT:443"

View File

@ -130,7 +130,8 @@ MenuLmsUsers: Export E-Lernen Benutzer
MenuLmsUserlist: Melden E-Lernen Benutzer MenuLmsUserlist: Melden E-Lernen Benutzer
MenuLmsResult: Melden Ergebnisse E-Lernen MenuLmsResult: Melden Ergebnisse E-Lernen
MenuLmsUpload: Hochladen MenuLmsUpload: Hochladen
MenuLmsDirect: Direkter Upload MenuLmsDirectUpload: Direkter Upload
MenuLmsDirectDownload: Direkter Download
MenuLmsFake: Testnutzer generieren MenuLmsFake: Testnutzer generieren
MenuAvs: Schnittstelle AVS MenuAvs: Schnittstelle AVS

View File

@ -131,7 +131,8 @@ MenuLmsUsers: Download E-Learning Users
MenuLmsUserlist: Upload E-Learning Users MenuLmsUserlist: Upload E-Learning Users
MenuLmsResult: Upload E-Learning Results MenuLmsResult: Upload E-Learning Results
MenuLmsUpload: Upload MenuLmsUpload: Upload
MenuLmsDirect: Direct Upload MenuLmsDirectUpload: Direct Upload
MenuLmsDirectDownload: Direct Download
MenuLmsFake: Generate test users MenuLmsFake: Generate test users
MenuAvs: AVS Interface MenuAvs: AVS Interface

View File

@ -2464,21 +2464,21 @@ pageActions (LmsR sid qsh) = return
[ NavPageActionPrimary [ NavPageActionPrimary
{ navLink = defNavLink MsgMenuLmsUsers $ LmsUsersR sid qsh { navLink = defNavLink MsgMenuLmsUsers $ LmsUsersR sid qsh
, navChildren = , navChildren =
[ defNavLink MsgMenuLmsDirect $ LmsUsersDirectR sid qsh [ defNavLink MsgMenuLmsDirectDownload $ LmsUsersDirectR sid qsh
] ]
} }
, NavPageActionPrimary , NavPageActionPrimary
{ navLink = defNavLink MsgMenuLmsUserlist $ LmsUserlistR sid qsh { navLink = defNavLink MsgMenuLmsUserlist $ LmsUserlistR sid qsh
, navChildren = , navChildren =
[ defNavLink MsgMenuLmsUpload $ LmsUserlistUploadR sid qsh [ defNavLink MsgMenuLmsUpload $ LmsUserlistUploadR sid qsh
, defNavLink MsgMenuLmsDirect $ LmsUserlistDirectR sid qsh , defNavLink MsgMenuLmsDirectUpload $ LmsUserlistDirectR sid qsh
] ]
} }
, NavPageActionPrimary , NavPageActionPrimary
{ navLink = defNavLink MsgMenuLmsResult $ LmsResultR sid qsh { navLink = defNavLink MsgMenuLmsResult $ LmsResultR sid qsh
, navChildren = , navChildren =
[ defNavLink MsgMenuLmsUpload $ LmsResultUploadR sid qsh [ defNavLink MsgMenuLmsUpload $ LmsResultUploadR sid qsh
, defNavLink MsgMenuLmsDirect $ LmsResultDirectR sid qsh , defNavLink MsgMenuLmsDirectUpload $ LmsResultDirectR sid qsh
] ]
} }
, NavPageActionSecondary { , NavPageActionSecondary {

View File

@ -29,9 +29,9 @@ data LmsResultTableCsv = LmsResultTableCsv
deriving Generic deriving Generic
makeLenses_ ''LmsResultTableCsv makeLenses_ ''LmsResultTableCsv
-- csv without headers -- TODO not yet supported -- csv without headers
--instance Csv.ToRecord LmsResultTableCsv -- default suffices instance Csv.ToRecord LmsResultTableCsv -- default suffices
--instance Csv.FromRecord LmsResultTableCsv -- default suffices instance Csv.FromRecord LmsResultTableCsv -- default suffices
-- csv with headers -- csv with headers
lmsResultTableCsvHeader :: Csv.Header lmsResultTableCsvHeader :: Csv.Header
@ -262,10 +262,15 @@ postLmsResultDirectR sid qsh = do
(_params, files) <- runRequestBody (_params, files) <- runRequestBody
(status, msg) <- case files of (status, msg) <- case files of
[(fhead,file)] -> do [(fhead,file)] -> do
LmsConf{..} <- getsYesod $ view _appLmsConf
let fmtOpts = def { csvDelimiter = lmsUploadDelimiter
, csvIncludeHeader = lmsUploadHeadedness
}
csvOpts = def { csvFormat = fmtOpts }
runDBJobs $ do runDBJobs $ do
qid <- getKeyBy404 $ SchoolQualificationShort sid qsh qid <- getKeyBy404 $ SchoolQualificationShort sid qsh
enr <- try $ runConduit $ fileSource file enr <- try $ runConduit $ fileSource file
.| decodeCsv .| decodeCsvWith csvOpts
.| foldMC (saveResultCsv qid) 0 .| foldMC (saveResultCsv qid) 0
case enr of case enr of
Left (e :: SomeException) -> do -- catch all to avoid ok220 in case of any error Left (e :: SomeException) -> do -- catch all to avoid ok220 in case of any error

View File

@ -28,9 +28,9 @@ data LmsUserlistTableCsv = LmsUserlistTableCsv
deriving Generic deriving Generic
makeLenses_ ''LmsUserlistTableCsv makeLenses_ ''LmsUserlistTableCsv
-- csv without headers -- TODO not yet supported -- csv without headers
--instance Csv.ToRecord LmsUserlistTableCsv instance Csv.ToRecord LmsUserlistTableCsv
--instance Csv.FromRecord LmsUserlistTableCsv instance Csv.FromRecord LmsUserlistTableCsv
-- csv with headers -- csv with headers
instance DefaultOrdered LmsUserlistTableCsv where instance DefaultOrdered LmsUserlistTableCsv where
@ -258,10 +258,15 @@ postLmsUserlistDirectR sid qsh = do
(_params, files) <- runRequestBody (_params, files) <- runRequestBody
(status, msg) <- case files of (status, msg) <- case files of
[(fhead,file)] -> do [(fhead,file)] -> do
LmsConf{..} <- getsYesod $ view _appLmsConf
let fmtOpts = def { csvDelimiter = lmsUploadDelimiter
, csvIncludeHeader = lmsUploadHeadedness
}
csvOpts = def { csvFormat = fmtOpts }
runDBJobs $ do runDBJobs $ do
qid <- getKeyBy404 $ SchoolQualificationShort sid qsh qid <- getKeyBy404 $ SchoolQualificationShort sid qsh
enr <- try $ runConduit $ fileSource file enr <- try $ runConduit $ fileSource file
.| decodeCsv .| decodeCsvWith csvOpts
.| foldMC (saveUserlistCsv qid) 0 .| foldMC (saveUserlistCsv qid) 0
case enr of case enr of
Left (e :: SomeException) -> do Left (e :: SomeException) -> do

View File

@ -39,9 +39,9 @@ lmsUser2csv lu@LmsUser{..} = LmsUserTableCsv
, csvLUTstaff = False & LmsBool , csvLUTstaff = False & LmsBool
} }
-- csv without headers -- TODO not yet supported -- csv without headers
-- instance Csv.ToRecord LmsUserTableCsv instance Csv.ToRecord LmsUserTableCsv
-- instance Csv.FromRecord LmsUserTableCsv instance Csv.FromRecord LmsUserTableCsv
-- csv with headers -- csv with headers
lmsUserTableCsvHeader :: Csv.Header lmsUserTableCsvHeader :: Csv.Header
@ -165,11 +165,19 @@ getLmsUsersDirectR sid qsh = do
, csvLUTstaff = LmsBool False , csvLUTstaff = LmsBool False
} }
-} -}
let csvRenderedData = toNamedRecord . lmsUser2csv . entityVal <$> lms_users LmsConf{..} <- getsYesod $ view _appLmsConf
csvRenderedHeader = lmsUserTableCsvHeader let --csvRenderedData = toNamedRecord . lmsUser2csv . entityVal <$> lms_users
--csvRenderedHeader = lmsUserTableCsvHeader
--cvsRendered = CsvRendered {..}
csvRendered = toCsvRendered lmsUserTableCsvHeader $ lmsUser2csv . entityVal <$> lms_users
fmtOpts = def { csvDelimiter = lmsDownloadDelimiter
, csvUseCrLf = lmsDownloadCrLf
, csvIncludeHeader = lmsDownloadHeadedness
}
csvOpts = def { csvFormat = fmtOpts }
csvSheetName <- csvFilenameLmsUser qsh csvSheetName <- csvFilenameLmsUser qsh
addHeader "Content-Disposition" $ "attachment; filename=\"" <> csvSheetName <> "\"" addHeader "Content-Disposition" $ "attachment; filename=\"" <> csvSheetName <> "\""
csvRenderedToTypedContent csvSheetName CsvRendered{..} csvRenderedToTypedContentWith csvOpts csvSheetName csvRendered
-- direct Download see: -- direct Download see:
-- https://ersocon.net/blog/2017/2/22/creating-csv-files-in-yesod -- https://ersocon.net/blog/2017/2/22/creating-csv-files-in-yesod

View File

@ -1,7 +1,7 @@
{-# OPTIONS_GHC -fno-warn-orphans #-} {-# OPTIONS_GHC -fno-warn-orphans #-}
module Handler.Utils.Csv module Handler.Utils.Csv
( decodeCsv, decodeCsvPositional ( decodeCsv, decodeCsvPositional, decodeCsvWith
, encodeCsv, encodeCsvWith, encodeCsvRendered, encodeCsvRenderedWith , encodeCsv, encodeCsvWith, encodeCsvRendered, encodeCsvRenderedWith
, csvRenderedToTypedContent, csvRenderedToTypedContentWith , csvRenderedToTypedContent, csvRenderedToTypedContentWith
, expectedCsvFormat, expectedCsvContentType , expectedCsvFormat, expectedCsvContentType
@ -87,6 +87,15 @@ decodeCsv = decodeCsv' $ \opts -> fromNamedCsvStreamError opts (review _haltingC
decodeCsvPositional :: (MonadHandler m, HandlerSite m ~ UniWorX, MonadThrow m, FromRecord csv) => HasHeader -> ConduitT ByteString csv m () decodeCsvPositional :: (MonadHandler m, HandlerSite m ~ UniWorX, MonadThrow m, FromRecord csv) => HasHeader -> ConduitT ByteString csv m ()
decodeCsvPositional hdr = decodeCsv' $ \opts -> fromCsvStreamError opts hdr (review _haltingCsvParseError) .| throwIncrementalErrors decodeCsvPositional hdr = decodeCsv' $ \opts -> fromCsvStreamError opts hdr (review _haltingCsvParseError) .| throwIncrementalErrors
decodeCsvWith :: (MonadHandler m, HandlerSite m ~ UniWorX, MonadThrow m, FromNamedRecord csv, FromRecord csv) => CsvOptions -> ConduitT ByteString csv m ()
decodeCsvWith opts
| csvIncludeHeader fmtOpts
= decodeCsv' $ \_ -> fromNamedCsvStreamError decOpts (review _haltingCsvParseError) .| throwIncrementalErrors
| otherwise
= decodeCsv' $ \_ -> fromCsvStreamError decOpts NoHeader (review _haltingCsvParseError) .| throwIncrementalErrors
where
fmtOpts = csvFormat opts
decOpts = DecodeOptions { decDelimiter = fromIntegral $ Char.ord $ csvDelimiter fmtOpts }
decodeCsv' :: forall csv m. decodeCsv' :: forall csv m.
( MonadHandler m, HandlerSite m ~ UniWorX ( MonadHandler m, HandlerSite m ~ UniWorX

View File

@ -2067,6 +2067,7 @@ csvFormatOptionsForm fs mPrev = hoistAForm liftHandler . multiActionA csvActs fs
<*> apreq (selectField lineEndOpts) (fslI MsgCsvUseCrLf) (preview _csvUseCrLf =<< mPrev) <*> apreq (selectField lineEndOpts) (fslI MsgCsvUseCrLf) (preview _csvUseCrLf =<< mPrev)
<*> apreq (selectField quoteOpts) (fslI MsgCsvQuoting & setTooltip MsgCsvQuotingTip) (preview _csvQuoting =<< mPrev) <*> apreq (selectField quoteOpts) (fslI MsgCsvQuoting & setTooltip MsgCsvQuotingTip) (preview _csvQuoting =<< mPrev)
<*> apreq (selectField encodingOpts) (fslI MsgCsvEncoding & setTooltip MsgCsvEncodingTip) (preview _csvEncoding =<< mPrev) <*> apreq (selectField encodingOpts) (fslI MsgCsvEncoding & setTooltip MsgCsvEncodingTip) (preview _csvEncoding =<< mPrev)
<*> pure True
FormatXlsx -> pure CsvXlsxFormatOptions FormatXlsx -> pure CsvXlsxFormatOptions
delimiterOpts :: Handler (OptionList Char) delimiterOpts :: Handler (OptionList Char)

View File

@ -61,6 +61,7 @@ data AvsLicence = AvsNoLicence | AvsLicenceVorfeld | AvsLicenceRollfeld
deriving (Bounded, Enum, Eq, Ord, Read, Show, Generic, Typeable) deriving (Bounded, Enum, Eq, Ord, Read, Show, Generic, Typeable)
instance ToJSON AvsLicence where instance ToJSON AvsLicence where
-- toJSON al = Number $ fromEnum AvsLicence -- would do, but...
toJSON AvsNoLicence = Number 0 toJSON AvsNoLicence = Number 0
toJSON AvsLicenceVorfeld = Number 1 toJSON AvsLicenceVorfeld = Number 1
toJSON AvsLicenceRollfeld = Number 2 toJSON AvsLicenceRollfeld = Number 2
@ -229,6 +230,8 @@ deriveJSON defaultOptions
, rejectUnknownFields = False , rejectUnknownFields = False
} ''AvsResponsePerson } ''AvsResponsePerson
------------- -------------
-- Queries -- -- Queries --
------------- -------------

View File

@ -60,10 +60,11 @@ data CsvOptions
data CsvFormatOptions data CsvFormatOptions
= CsvFormatOptions = CsvFormatOptions
{ csvDelimiter :: Char { csvDelimiter :: Char
, csvUseCrLf :: Bool , csvUseCrLf :: Bool
, csvQuoting :: Csv.Quoting , csvQuoting :: Csv.Quoting
, csvEncoding :: DynEncoding , csvEncoding :: DynEncoding
, csvIncludeHeader :: Bool
} }
| CsvXlsxFormatOptions | CsvXlsxFormatOptions
deriving (Eq, Ord, Read, Show, Generic, Typeable) deriving (Eq, Ord, Read, Show, Generic, Typeable)
@ -94,16 +95,18 @@ csvPreset = prism' fromPreset toPreset
where where
fromPreset :: CsvPreset -> CsvFormatOptions fromPreset :: CsvPreset -> CsvFormatOptions
fromPreset CsvPresetRFC = CsvFormatOptions fromPreset CsvPresetRFC = CsvFormatOptions
{ csvDelimiter = ',' { csvDelimiter = ','
, csvUseCrLf = True , csvUseCrLf = True
, csvQuoting = QuoteMinimal , csvIncludeHeader = True
, csvEncoding = "UTF8" , csvQuoting = QuoteMinimal
, csvEncoding = "UTF8"
} }
fromPreset CsvPresetExcel = CsvFormatOptions fromPreset CsvPresetExcel = CsvFormatOptions
{ csvDelimiter = ';' { csvDelimiter = ';'
, csvUseCrLf = True , csvUseCrLf = True
, csvQuoting = QuoteAll , csvIncludeHeader = True
, csvEncoding = "CP1252" , csvQuoting = QuoteAll
, csvEncoding = "CP1252"
} }
fromPreset CsvPresetXlsx = CsvXlsxFormatOptions fromPreset CsvPresetXlsx = CsvXlsxFormatOptions
@ -119,7 +122,7 @@ _CsvEncodeOptions = prism' fromEncode toEncode
{ Csv.encDelimiter = fromIntegral $ fromEnum csvDelimiter { Csv.encDelimiter = fromIntegral $ fromEnum csvDelimiter
, Csv.encUseCrLf = csvUseCrLf , Csv.encUseCrLf = csvUseCrLf
, Csv.encQuoting = csvQuoting , Csv.encQuoting = csvQuoting
, Csv.encIncludeHeader = True , Csv.encIncludeHeader = csvIncludeHeader
} }
toEncode CsvXlsxFormatOptions{} = Nothing toEncode CsvXlsxFormatOptions{} = Nothing
fromEncode encOpts = def fromEncode encOpts = def
@ -180,13 +183,14 @@ instance ToJSON CsvFormatOptions where
instance FromJSON CsvFormatOptions where instance FromJSON CsvFormatOptions where
parseJSON = JSON.withObject "CsvFormatOptions" $ \o -> do parseJSON = JSON.withObject "CsvFormatOptions" $ \o -> do
formatTag <- o JSON..:? "format" JSON..!= FormatCsv formatTag <- o JSON..:? "format" JSON..!= FormatCsv
case formatTag of case formatTag of
FormatCsv -> do FormatCsv -> do
csvDelimiter <- fmap (fmap toEnum) (o JSON..:? "delimiter") JSON..!= csvDelimiter def csvDelimiter <- fmap (fmap toEnum) (o JSON..:? "delimiter") JSON..!= csvDelimiter def
csvUseCrLf <- o JSON..:? "use-cr-lf" JSON..!= csvUseCrLf def csvUseCrLf <- o JSON..:? "use-cr-lf" JSON..!= csvUseCrLf def
csvQuoting <- o JSON..:? "quoting" JSON..!= csvQuoting def csvQuoting <- o JSON..:? "quoting" JSON..!= csvQuoting def
csvEncoding <- o JSON..:? "encoding" JSON..!= csvEncoding def csvEncoding <- o JSON..:? "encoding" JSON..!= csvEncoding def
csvIncludeHeader <- o JSON..:? "include-header" JSON..!= csvIncludeHeader def
return CsvFormatOptions{..} return CsvFormatOptions{..}
FormatXlsx -> return CsvXlsxFormatOptions FormatXlsx -> return CsvXlsxFormatOptions

View File

@ -93,6 +93,8 @@ data AppSettings = AppSettings
-- ^ Configuration settings for accessing the database. -- ^ Configuration settings for accessing the database.
, appAutoDbMigrate :: Bool , appAutoDbMigrate :: Bool
, appLdapConf :: Maybe (PointedList LdapConf) , appLdapConf :: Maybe (PointedList LdapConf)
-- ^ Configuration settings for CSV export/import to LMS (= Learn Management System)
, appLmsConf :: LmsConf
-- ^ Configuration settings for accessing the LDAP-directory -- ^ Configuration settings for accessing the LDAP-directory
, appAvsConf :: Maybe AvsConf , appAvsConf :: Maybe AvsConf
-- ^ Configuration settings for accessing AVS Server (= Ausweis Verwaltungs System) -- ^ Configuration settings for accessing AVS Server (= Ausweis Verwaltungs System)
@ -301,6 +303,14 @@ data LdapConf = LdapConf
, ldapPool :: ResourcePoolConf , ldapPool :: ResourcePoolConf
} deriving (Show) } deriving (Show)
data LmsConf = LmsConf
{ lmsUploadDelimiter :: Char
, lmsUploadHeadedness :: Bool
, lmsDownloadDelimiter :: Char
, lmsDownloadHeadedness :: Bool
, lmsDownloadCrLf :: Bool
} deriving (Show)
data AvsConf = AvsConf data AvsConf = AvsConf
{ avsHost :: String { avsHost :: String
, avsPort :: Int , avsPort :: Int
@ -311,7 +321,7 @@ data AvsConf = AvsConf
data LprConf = LprConf data LprConf = LprConf
{ lprHost :: String { lprHost :: String
, lprPort :: Int , lprPort :: Int
, lprQueue:: String , lprQueue:: String
} deriving (Show) } deriving (Show)
data SmtpConf = SmtpConf data SmtpConf = SmtpConf
@ -480,6 +490,17 @@ deriveFromJSON
} }
''HaskellNet.AuthType ''HaskellNet.AuthType
instance FromJSON LmsConf where
parseJSON = withObject "LmsConf" $ \o -> do
lmsUploadDelimiter <- o .: "upload-delimiter"
lmsUploadHeadedness <- o .: "upload-headedness"
lmsDownloadDelimiter <- o .: "download-delimiter"
lmsDownloadHeadedness <- o .: "download-headedness"
lmsDownloadCrLf <- o .: "download-cr-lf"
return LmsConf{..}
makeLenses_ ''LmsConf
instance FromJSON AvsConf where instance FromJSON AvsConf where
parseJSON = withObject "AvsConf" $ \o -> do parseJSON = withObject "AvsConf" $ \o -> do
avsHost <- o .: "host" avsHost <- o .: "host"
@ -492,7 +513,7 @@ instance FromJSON LprConf where
parseJSON = withObject "LprConf" $ \o -> do parseJSON = withObject "LprConf" $ \o -> do
lprHost <- o .: "host" lprHost <- o .: "host"
lprPort <- o .: "port" lprPort <- o .: "port"
lprQueue <- o .: "queue" lprQueue <- o .: "queue"
return LprConf{..} return LprConf{..}
instance FromJSON SmtpConf where instance FromJSON SmtpConf where
@ -576,6 +597,7 @@ instance FromJSON AppSettings where
Ldap.Tls host _ -> not $ null host Ldap.Tls host _ -> not $ null host
Ldap.Plain host -> not $ null host Ldap.Plain host -> not $ null host
appLdapConf <- P.fromList . mapMaybe (assertM nonEmptyHost) <$> o .:? "ldap" .!= [] appLdapConf <- P.fromList . mapMaybe (assertM nonEmptyHost) <$> o .:? "ldap" .!= []
appLmsConf <- o .: "lms"
appAvsConf <- assertM (not . null . avsPass) <$> o .:? "avs" appAvsConf <- assertM (not . null . avsPass) <$> o .:? "avs"
appLprConf <- o .: "lpr" appLprConf <- o .: "lpr"
appSmtpConf <- assertM (not . null . smtpHost) <$> o .:? "smtp" appSmtpConf <- assertM (not . null . smtpHost) <$> o .:? "smtp"