Merge branch 'master' into i18n

This commit is contained in:
Gregor Kleen 2019-10-28 12:08:37 +01:00
commit 4ff50b0147
20 changed files with 338 additions and 84 deletions

View File

@ -2,6 +2,51 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
### [7.19.1](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/compare/v7.19.0...v7.19.1) (2019-10-25)
### Bug Fixes
* **datepicker:** workaround for new Date(..) inconsistency ([d24ebf8](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/d24ebf8))
## [7.19.0](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/compare/v7.18.3...v7.19.0) (2019-10-24)
### Bug Fixes
* **datepicker:** handle output format when reformatting ([09622bd](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/09622bd))
### Features
* **csv:** encoding ([81415e1](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/81415e1))
### [7.18.3](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/compare/v7.18.2...v7.18.3) (2019-10-23)
### Bug Fixes
* **submission-form:** fix display of all courseParticipants ([b67819d](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/b67819d))
### [7.18.2](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/compare/v7.18.1...v7.18.2) (2019-10-20)
### [7.18.1](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/compare/v7.18.0...v7.18.1) (2019-10-20)
### Bug Fixes
* **assign-submissions:** avoid division by zero ([640326c](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/640326c))
## [7.18.0](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/compare/v7.17.14...v7.18.0) (2019-10-17) ## [7.18.0](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/compare/v7.17.14...v7.18.0) (2019-10-17)

View File

@ -33,7 +33,7 @@ const FORM_DATE_FORMAT_MOMENT = {
* @param {*} formatOut format string of the desired output date string * @param {*} formatOut format string of the desired output date string
*/ */
function reformatDateString(dateStr, formatIn, formatOut) { function reformatDateString(dateStr, formatIn, formatOut) {
const parsedMomentDate = moment(dateStr, formatIn); const parsedMomentDate = moment(dateStr, [formatIn, formatOut]);
return parsedMomentDate.isValid() ? parsedMomentDate.format(formatOut) : dateStr; return parsedMomentDate.isValid() ? parsedMomentDate.format(formatOut) : dateStr;
} }
@ -123,12 +123,23 @@ export class Datepicker {
throw new Error('Datepicker utility called on unsupported element!'); throw new Error('Datepicker utility called on unsupported element!');
} }
// format any existing dates to fancy display format on pageload // FIXME dirty hack below; fix tail.datetime instead
this.formatElementValue(true);
// initialize tail.datetime (datepicker) instance // get date object from internal format before datetime does nasty things with it
var parsedMomentDate = moment(this._element.value, [ FORM_DATE_FORMAT[this.elementType], FORM_DATE_FORMAT_MOMENT[this.elementType] ], true);
if (parsedMomentDate && parsedMomentDate.isValid()) {
parsedMomentDate = parsedMomentDate.toDate();
} else {
parsedMomentDate = undefined;
}
// initialize tail.datetime (datepicker) instance and let it do weird stuff with the element value
this.datepickerInstance = datetime(this._element, { ...datepickerGlobalConfig, ...datepickerConfig }); this.datepickerInstance = datetime(this._element, { ...datepickerGlobalConfig, ...datepickerConfig });
// reset date to something sane
if (parsedMomentDate)
this.datepickerInstance.selectDate(parsedMomentDate);
// insert the datepicker element (dt) after the form // insert the datepicker element (dt) after the form
this._element.form.parentNode.insertBefore(this.datepickerInstance.dt, this._element.form.nextSibling); this._element.form.parentNode.insertBefore(this.datepickerInstance.dt, this._element.form.nextSibling);

View File

@ -1945,7 +1945,10 @@ AcceptApplicationsSecondaryRandom: Zufällig
AcceptApplicationsSecondaryTime: Nach Zeitpunkt der Bewerbung AcceptApplicationsSecondaryTime: Nach Zeitpunkt der Bewerbung
CsvOptions: CSV-Optionen CsvOptions: CSV-Optionen
CsvOptionsTip: Diese Einstellungen betreffen nur den CSV-Export; beim Import werden die verwendeten Einstellungen automatisch ermittelt. CsvOptionsTip: Diese Einstellungen betreffen nur den CSV-Export; beim Import werden die verwendeten Einstellungen automatisch ermittelt. Als Zeichenkodierung wird beim Import stets Unicode erwartet.
CsvFormatOptions: Dateiformat
CsvTimestamp: Zeitstempel
CsvTimestampTip: Soll an den Namen jeder exportierten CSV-Datei ein Zeitstempel vorne angehängt werden?
CsvPresetRFC: Standard-Konform (RFC 4180) CsvPresetRFC: Standard-Konform (RFC 4180)
CsvPresetExcel: Excel-Kompatibel CsvPresetExcel: Excel-Kompatibel
CsvCustom: Benutzerdefiniert CsvCustom: Benutzerdefiniert
@ -1953,6 +1956,10 @@ CsvDelimiter: Trennzeichen
CsvUseCrLf: Zeilenumbrüche CsvUseCrLf: Zeilenumbrüche
CsvQuoting: Quoting CsvQuoting: Quoting
CsvQuotingTip: Wann sollen Anführungszeichen (") um Felder platziert werden, um Interpretation von im Feld enthaltenen Zeichen als Trennzeichen zu verhindern? CsvQuotingTip: Wann sollen Anführungszeichen (") um Felder platziert werden, um Interpretation von im Feld enthaltenen Zeichen als Trennzeichen zu verhindern?
CsvEncoding: Encoding
CsvEncodingTip: CSV-Dateien können in einer anderen Zeichenkodierung als dem vom System standardmäßig verwendeten UTF-8 exportiert werden. Beachten Sie dass es bei nicht-UTF-8 Zeichenkodierungen wmgl. zu Darstellungsproblemen mit Sonderzeichen kommt.
CsvUTF8: UTF-8 (Unicode)
CsvCP1252: Windows CP-1252 ("ANSI")
CsvDelimiterNull: Null-Byte CsvDelimiterNull: Null-Byte
CsvDelimiterTab: Tabulator CsvDelimiterTab: Tabulator
CsvDelimiterComma: Komma CsvDelimiterComma: Komma

2
package-lock.json generated
View File

@ -1,6 +1,6 @@
{ {
"name": "uni2work", "name": "uni2work",
"version": "7.18.0", "version": "7.19.1",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {

View File

@ -1,6 +1,6 @@
{ {
"name": "uni2work", "name": "uni2work",
"version": "7.18.0", "version": "7.19.1",
"description": "", "description": "",
"keywords": [], "keywords": [],
"author": "", "author": "",

View File

@ -1,5 +1,5 @@
name: uniworx name: uniworx
version: 7.18.0 version: 7.19.1
dependencies: dependencies:
- base >=4.9.1.0 && <5 - base >=4.9.1.0 && <5

View File

@ -0,0 +1,33 @@
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Data.Encoding.Instances
(
) where
import ClassyPrelude
import Utils.PathPiece
import Data.String (IsString(..))
import Text.Read
import Web.PathPieces
import Data.Encoding
instance PathPiece DynEncoding where
toPathPiece = showToPathPiece
fromPathPiece = encodingFromStringExplicit . unpack
pathPieceJSON ''DynEncoding
instance IsString DynEncoding where
fromString = encodingFromString
instance Read DynEncoding where
readPrec = parens $ lexP >>= \case
Ident str -> maybe (fail "Could not parse encoding") return $ encodingFromStringExplicit str
_ -> fail "Ident lexeme expected"
instance Ord DynEncoding where
compare = comparing show

View File

@ -827,7 +827,7 @@ postCsvOptionsR = do
Entity uid User{userCsvOptions} <- requireAuth Entity uid User{userCsvOptions} <- requireAuth
((optionsRes, optionsWgt'), optionsEnctype) <- runFormPost . renderAForm FormStandard $ ((optionsRes, optionsWgt'), optionsEnctype) <- runFormPost . renderAForm FormStandard $
csvOptionsForm (fslI MsgCsvOptions & setTooltip MsgCsvOptionsTip) (Just userCsvOptions) csvOptionsForm (Just userCsvOptions)
formResultModal optionsRes CsvOptionsR $ \opts -> do formResultModal optionsRes CsvOptionsR $ \opts -> do
lift . runDB $ update uid [ UserCsvOptions =. opts ] lift . runDB $ update uid [ UserCsvOptions =. opts ]

View File

@ -155,9 +155,24 @@ makeSubmissionForm cid msmid uploadMode grouping isLecturer prefillUsers = ident
E.where_ $ participant E.^. CourseParticipantCourse E.==. E.val cid E.where_ $ participant E.^. CourseParticipantCourse E.==. E.val cid
E.orderBy [E.asc $ user E.^. UserEmail] E.orderBy [E.asc $ user E.^. UserEmail]
return user return user
previousCoSubmittors :: UserId -> E.SqlQuery (E.SqlExpr (Entity User))
previousCoSubmittors uid = E.from $ \(user `E.InnerJoin` submissionUser `E.InnerJoin` submission `E.InnerJoin` sheet) -> do
E.on $ sheet E.^. SheetId E.==. submission E.^. SubmissionSheet
E.&&. sheet E.^. SheetCourse E.==. E.val cid
E.on $ submission E.^. SubmissionId E.==. submissionUser E.^. SubmissionUserSubmission
E.on $ user E.^. UserId E.==. submissionUser E.^. SubmissionUserUser
E.where_ . E.exists . E.from $ \submissionUser' ->
E.where_ $ submissionUser' E.^. SubmissionUserUser E.==. E.val uid
E.&&. submissionUser' E.^. SubmissionUserSubmission E.==. submission E.^. SubmissionId
E.orderBy [E.asc $ user E.^. UserEmail]
return user
addField :: (MonadHandler m, HandlerSite m ~ UniWorX) => Bool -> Field m (Set (Either UserEmail UserId)) addField, addFieldLecturer :: (MonadHandler m, HandlerSite m ~ UniWorX) => UserId -> Field m (Set (Either UserEmail UserId))
addField isAdmin = multiUserField True $ courseUsers <$ guard isAdmin addField = addField' False
addFieldLecturer = addField' True
addField' isAdmin uid = multiUserField True . Just $ if
| isAdmin -> courseUsers
| otherwise -> previousCoSubmittors uid
addFieldSettings, submittorSettings, singleSubSettings :: FieldSettings UniWorX addFieldSettings, submittorSettings, singleSubSettings :: FieldSettings UniWorX
addFieldSettings = fslI MsgSubmissionMembers addFieldSettings = fslI MsgSubmissionMembers
@ -176,12 +191,13 @@ makeSubmissionForm cid msmid uploadMode grouping isLecturer prefillUsers = ident
miButtonAction' mCurrent frag = mCurrent <&> \current -> SomeRoute (current :#: frag) miButtonAction' mCurrent frag = mCurrent <&> \current -> SomeRoute (current :#: frag)
submittorsForm submittorsForm
| isLecturer = do-- Form is being used by lecturer; allow Everything™ | isLecturer = do -- Form is being used by lecturer; allow Everything™
uid <- liftHandler requireAuthId
let let
miAdd :: (Text -> Text) -> FieldView UniWorX -> Form ([Either UserEmail UserId] -> FormResult [Either UserEmail UserId]) miAdd :: (Text -> Text) -> FieldView UniWorX -> Form ([Either UserEmail UserId] -> FormResult [Either UserEmail UserId])
miAdd nudge btn csrf = do miAdd nudge btn csrf = do
MsgRenderer mr <- getMsgRenderer MsgRenderer mr <- getMsgRenderer
(addRes, addView) <- mpreq (addField True) (addFieldSettings & addName (nudge "emails")) Nothing (addRes, addView) <- mpreq (addFieldLecturer uid) (addFieldSettings & addName (nudge "emails")) Nothing
let addRes' = addRes <&> \newData oldData -> if let addRes' = addRes <&> \newData oldData -> if
| existing <- newData `Set.intersection` Set.fromList oldData | existing <- newData `Set.intersection` Set.fromList oldData
, not $ Set.null existing , not $ Set.null existing
@ -208,7 +224,7 @@ makeSubmissionForm cid msmid uploadMode grouping isLecturer prefillUsers = ident
-> Maybe (Form (Map ListPosition (Either UserEmail UserId) -> FormResult (Map ListPosition (Either UserEmail UserId)))) -> Maybe (Form (Map ListPosition (Either UserEmail UserId) -> FormResult (Map ListPosition (Either UserEmail UserId))))
miAdd _ _ nudge btn = Just $ \csrf -> do miAdd _ _ nudge btn = Just $ \csrf -> do
MsgRenderer mr <- getMsgRenderer MsgRenderer mr <- getMsgRenderer
(addRes, addView) <- mpreq (addField True) (addFieldSettings & addName (nudge "emails")) Nothing (addRes, addView) <- mpreq (addField uid) (addFieldSettings & addName (nudge "emails")) Nothing
let addRes' = addRes <&> \newData oldData -> if let addRes' = addRes <&> \newData oldData -> if
| existing <- newData `Set.intersection` setOf folded oldData | existing <- newData `Set.intersection` setOf folded oldData
, not $ Set.null existing , not $ Set.null existing

View File

@ -2,11 +2,13 @@
module Handler.Utils.Csv module Handler.Utils.Csv
( decodeCsv, decodeCsvPositional ( decodeCsv, decodeCsvPositional
, timestampCsv
, encodeCsv , encodeCsv
, encodeDefaultOrderedCsv , encodeDefaultOrderedCsv
, respondCsv, respondCsvDB , respondCsv, respondCsvDB
, respondDefaultOrderedCsv, respondDefaultOrderedCsvDB , respondDefaultOrderedCsv, respondDefaultOrderedCsvDB
, fileSourceCsv , fileSourceCsv
, partIsAttachmentCsv
, CsvParseError(..) , CsvParseError(..)
, ToNamedRecord(..), FromNamedRecord(..) , ToNamedRecord(..), FromNamedRecord(..)
, DefaultOrdered(..) , DefaultOrdered(..)
@ -37,24 +39,42 @@ import qualified Data.Attoparsec.ByteString.Lazy as A
import Control.Monad.Except (ExceptT) import Control.Monad.Except (ExceptT)
import Handler.Utils.DateTime
import Data.Time.Format (iso8601DateFormat)
decodeCsv :: (MonadThrow m, FromNamedRecord csv, MonadLogger m) => ConduitT ByteString csv m ()
decodeCsv :: (MonadHandler m, HandlerSite m ~ UniWorX, MonadThrow m, FromNamedRecord csv) => ConduitT ByteString csv m ()
decodeCsv = decodeCsv' fromNamedCsv decodeCsv = decodeCsv' fromNamedCsv
decodeCsvPositional :: (MonadThrow m, FromRecord csv, MonadLogger m) => HasHeader -> ConduitT ByteString csv m () decodeCsvPositional :: (MonadHandler m, HandlerSite m ~ UniWorX, MonadThrow m, FromRecord csv) => HasHeader -> ConduitT ByteString csv m ()
decodeCsvPositional hdr = decodeCsv' (`fromCsv` hdr) decodeCsvPositional hdr = decodeCsv' (`fromCsv` hdr)
decodeCsv' :: (MonadThrow m, MonadLogger m) => (forall m'. Monad m' => DecodeOptions -> ConduitT ByteString csv (ExceptT CsvParseError m') ()) -> ConduitT ByteString csv m () decodeCsv' :: (MonadHandler m, HandlerSite m ~ UniWorX, MonadThrow m) => (forall m'. Monad m' => DecodeOptions -> ConduitT ByteString csv (ExceptT CsvParseError m') ()) -> ConduitT ByteString csv m ()
decodeCsv' fromCsv' = transPipe throwExceptT $ do decodeCsv' fromCsv' = do
testBuffer <- accumTestBuffer LBS.empty encOpts <- maybe def (userCsvOptions . entityVal) <$> lift maybeAuth
mapM_ leftover $ LBS.toChunks testBuffer
let decodeOptions = defaultDecodeOptions let
& guessDelimiter testBuffer recode'
$logInfoS "decodeCsv" [st|Guessed Csv.DecodeOptions from buffer of size #{tshow (LBS.length testBuffer)}/#{tshow testBufferSize}: #{tshow decodeOptions}|] | enc == "UTF8"
= id
fromCsv' decodeOptions | otherwise
= \act -> do
inp <- sinkLazy
let inp' = encodeLazyByteString UTF8 $ decodeLazyByteString enc inp
sourceLazy inp' .| act
where enc = encOpts ^. _csvFormat . _csvEncoding
recode' decodeCsv''
where where
decodeCsv'' = transPipe throwExceptT $ do
testBuffer <- accumTestBuffer LBS.empty
mapM_ leftover $ LBS.toChunks testBuffer
let decodeOptions = defaultDecodeOptions
& guessDelimiter testBuffer
$logInfoS "decodeCsv" [st|Guessed Csv.DecodeOptions from buffer of size #{tshow (LBS.length testBuffer)}/#{tshow testBufferSize}: #{tshow decodeOptions}|]
fromCsv' decodeOptions
testBufferSize = 4096 testBufferSize = 4096
accumTestBuffer acc accumTestBuffer acc
| LBS.length acc >= testBufferSize = return acc | LBS.length acc >= testBufferSize = return acc
@ -115,8 +135,33 @@ encodeCsv :: ( ToNamedRecord csv
-- --
-- Currently not streaming -- Currently not streaming
encodeCsv hdr = do encodeCsv hdr = do
csvOpts <- fmap (maybe def (userCsvOptions . entityVal)) . lift $ liftHandler maybeAuth csvOpts <- maybe def (userCsvOptions . entityVal) <$> lift maybeAuth
fmap (encodeByNameWith (csvOpts ^. _CsvEncodeOptions) hdr) (C.foldMap pure) >>= C.sourceLazy let recode'
| enc == "UTF8"
= id
| otherwise
= encodeLazyByteString enc . decodeLazyByteString UTF8
where enc = csvOpts ^. _csvFormat . _csvEncoding
fmap (encodeByNameWith (csvOpts ^. _csvFormat . _CsvEncodeOptions) hdr) (C.foldMap pure) >>= C.sourceLazy . recode'
timestampCsv :: ( MonadHandler m
, HandlerSite m ~ UniWorX
)
=> m (FilePath -> FilePath)
timestampCsv = do
csvOpts <- maybe def (userCsvOptions . entityVal) <$> maybeAuth
if
| csvOpts ^. _csvTimestamp -> do
ts <- formatTime' (iso8601DateFormat $ Just "%H%M") =<< liftIO getCurrentTime
return $ (<>) (unpack ts <> "-")
| otherwise -> return id
partIsAttachmentCsv :: (Textual t, MonadMail m, HandlerSite m ~ UniWorX)
=> t
-> StateT Part m ()
partIsAttachmentCsv (repack -> fName) = do
ts <- timestampCsv
partIsAttachment . ts $ fName `addExtension` unpack extensionCsv
encodeDefaultOrderedCsv :: forall csv m. encodeDefaultOrderedCsv :: forall csv m.
( ToNamedRecord csv ( ToNamedRecord csv
@ -157,9 +202,9 @@ respondDefaultOrderedCsvDB :: forall csv.
respondDefaultOrderedCsvDB = respondCsvDB $ headerOrder (error "headerOrder" :: csv) respondDefaultOrderedCsvDB = respondCsvDB $ headerOrder (error "headerOrder" :: csv)
fileSourceCsv :: ( FromNamedRecord csv fileSourceCsv :: ( FromNamedRecord csv
, MonadResource m
, MonadLogger m
, MonadThrow m , MonadThrow m
, MonadHandler m
, HandlerSite m ~ UniWorX
) )
=> FileInfo => FileInfo
-> ConduitT () csv m () -> ConduitT () csv m ()

View File

@ -1117,14 +1117,15 @@ multiUserField onlySuggested suggestions = Field{..}
|] |]
whenIsJust suggestions $ \suggestions' -> do whenIsJust suggestions $ \suggestions' -> do
suggestedEmails <- fmap (Set.fromList . map E.unValue) . liftHandler . runDB . E.select $ do suggestedEmails <- fmap (Set.fromList . map (over _2 E.unValue . over _1 E.unValue)) . liftHandler . runDB . E.select $ do
user <- suggestions' user <- suggestions'
return $ user E.^. UserEmail return $ (user E.^. UserEmail, user E.^. UserDisplayName)
[whamlet| [whamlet|
$newline never $newline never
<datalist id=#{datalistId}> <datalist id=#{datalistId}>
$forall email <- suggestedEmails $forall (email, dName) <- suggestedEmails
<option value=#{email}> <option value=#{email}>
#{email} (#{dName})
|] |]
fieldParse (all Text.null -> True) _ = return $ Right Nothing fieldParse (all Text.null -> True) _ = return $ Right Nothing
fieldParse ts _ = runExceptT . fmap Just $ do fieldParse ts _ = runExceptT . fmap Just $ do
@ -1191,42 +1192,43 @@ examPassedField :: forall m.
examPassedField = hoistField liftHandler $ selectField optionsFinite examPassedField = hoistField liftHandler $ selectField optionsFinite
data CsvOptions' = CsvOptionsPreset' CsvPreset data CsvFormatOptions' = CsvFormatOptionsPreset' CsvPreset
| CsvOptionsCustom' | CsvFormatOptionsCustom'
deriving (Eq, Ord, Read, Show, Generic, Typeable) deriving (Eq, Ord, Read, Show, Generic, Typeable)
deriveFinite ''CsvOptions' deriveFinite ''CsvFormatOptions'
instance PathPiece CsvOptions' where instance PathPiece CsvFormatOptions' where
toPathPiece = \case toPathPiece = \case
CsvOptionsPreset' p -> toPathPiece p CsvFormatOptionsPreset' p -> toPathPiece p
CsvOptionsCustom' -> "custom" CsvFormatOptionsCustom' -> "custom"
fromPathPiece t = fromPathPiece t fromPathPiece t = fromPathPiece t
<|> guardOn (t == "custom") CsvOptionsCustom' <|> guardOn (t == "custom") CsvFormatOptionsCustom'
instance RenderMessage UniWorX CsvOptions' where instance RenderMessage UniWorX CsvFormatOptions' where
renderMessage m ls = \case renderMessage m ls = \case
CsvOptionsPreset' p -> mr p CsvFormatOptionsPreset' p -> mr p
CsvOptionsCustom' -> mr MsgCsvCustom CsvFormatOptionsCustom' -> mr MsgCsvCustom
where where
mr :: forall msg. RenderMessage UniWorX msg => msg -> Text mr :: forall msg. RenderMessage UniWorX msg => msg -> Text
mr = renderMessage m ls mr = renderMessage m ls
csvOptionsForm :: forall m. csvFormatOptionsForm :: forall m.
( MonadHandler m ( MonadHandler m
, HandlerSite m ~ UniWorX , HandlerSite m ~ UniWorX
) )
=> FieldSettings UniWorX => FieldSettings UniWorX
-> Maybe CsvOptions -> Maybe CsvFormatOptions
-> AForm m CsvOptions -> AForm m CsvFormatOptions
csvOptionsForm fs mPrev = hoistAForm liftHandler . multiActionA csvActs fs $ classifyCsvOptions <$> mPrev csvFormatOptionsForm fs mPrev = hoistAForm liftHandler . multiActionA csvActs fs $ classifyCsvFormatOptions <$> mPrev
where where
csvActs :: Map CsvOptions' (AForm Handler CsvOptions) csvActs :: Map CsvFormatOptions' (AForm Handler CsvFormatOptions)
csvActs = mapF $ \case csvActs = mapF $ \case
CsvOptionsPreset' preset CsvFormatOptionsPreset' preset
-> pure $ csvPreset # preset -> pure $ csvPreset # preset
CsvOptionsCustom' CsvFormatOptionsCustom'
-> CsvOptions -> CsvFormatOptions
<$> areq (selectField delimiterOpts) (fslI MsgCsvDelimiter) (csvDelimiter <$> mPrev) <$> areq (selectField delimiterOpts) (fslI MsgCsvDelimiter) (csvDelimiter <$> mPrev)
<*> areq (selectField lineEndOpts) (fslI MsgCsvUseCrLf) (csvUseCrLf <$> mPrev) <*> areq (selectField lineEndOpts) (fslI MsgCsvUseCrLf) (csvUseCrLf <$> mPrev)
<*> areq (selectField quoteOpts) (fslI MsgCsvQuoting & setTooltip MsgCsvQuotingTip) (csvQuoting <$> mPrev) <*> areq (selectField quoteOpts) (fslI MsgCsvQuoting & setTooltip MsgCsvQuotingTip) (csvQuoting <$> mPrev)
<*> areq (selectField encodingOpts) (fslI MsgCsvEncoding & setTooltip MsgCsvEncodingTip) (csvEncoding <$> mPrev)
delimiterOpts :: Handler (OptionList Char) delimiterOpts :: Handler (OptionList Char)
delimiterOpts = do delimiterOpts = do
@ -1264,9 +1266,25 @@ csvOptionsForm fs mPrev = hoistAForm liftHandler . multiActionA csvActs fs $ cla
, QuoteAll , QuoteAll
] ]
classifyCsvOptions :: CsvOptions -> CsvOptions' encodingOpts :: Handler (OptionList DynEncoding)
classifyCsvOptions opts encodingOpts = optionsPathPiece
[ (MsgCsvUTF8, "UTF8")
, (MsgCsvCP1252, "CP1252")
]
classifyCsvFormatOptions :: CsvFormatOptions -> CsvFormatOptions'
classifyCsvFormatOptions opts
| Just preset <- opts ^? csvPreset | Just preset <- opts ^? csvPreset
= CsvOptionsPreset' preset = CsvFormatOptionsPreset' preset
| otherwise | otherwise
= CsvOptionsCustom' = CsvFormatOptionsCustom'
csvOptionsForm :: forall m.
( MonadHandler m
, HandlerSite m ~ UniWorX
)
=> Maybe CsvOptions
-> AForm m CsvOptions
csvOptionsForm mPrev = hoistAForm liftHandler $ CsvOptions
<$> csvFormatOptionsForm (fslI MsgCsvFormatOptions & setTooltip MsgCsvOptionsTip) (csvFormat <$> mPrev)
<*> apopt checkBoxField (fslI MsgCsvTimestamp & setTooltip MsgCsvTimestampTip) (csvTimestamp <$> mPrev)

View File

@ -197,6 +197,10 @@ planSubmissions sid restriction = do
proportionSum = getSum . foldMap corrProportion . fromMaybe Map.empty $ correctors !? sheetId proportionSum = getSum . foldMap corrProportion . fromMaybe Map.empty $ correctors !? sheetId
where corrProportion (_, CorrectorExcused) = mempty where corrProportion (_, CorrectorExcused) = mempty
corrProportion (Load{..}, _) = Sum byProportion corrProportion (Load{..}, _) = Sum byProportion
relativeProportion :: Rational -> Rational
relativeProportion prop
| proportionSum == 0 = 0
| otherwise = prop / proportionSum
extra extra
| Just (Load{..}, corrState) <- correctors !? sheetId >>= Map.lookup corrector | Just (Load{..}, corrState) <- correctors !? sheetId >>= Map.lookup corrector
= sum = sum
@ -208,7 +212,7 @@ planSubmissions sid restriction = do
return . negate . fromIntegral . Map.size $ Map.filter (\(mCorr, tutors, sheetId') -> mCorr == Just corrector && sheetId == sheetId' && Map.member corrector tutors) submissionState return . negate . fromIntegral . Map.size $ Map.filter (\(mCorr, tutors, sheetId') -> mCorr == Just corrector && sheetId == sheetId' && Map.member corrector tutors) submissionState
, fromMaybe 0 $ do , fromMaybe 0 $ do
guard $ corrState /= CorrectorExcused guard $ corrState /= CorrectorExcused
return . negate $ (byProportion / proportionSum) * fromIntegral sheetSize return . negate $ relativeProportion byProportion * fromIntegral sheetSize
] ]
| otherwise | otherwise
= assigned = assigned

View File

@ -985,7 +985,8 @@ dbTable PSValidator{..} dbtable@DBTable{ dbtIdent = dbtIdent'@(toPathPiece -> db
, Just exportData <- fromDynamic dbCsvExportData -> do , Just exportData <- fromDynamic dbCsvExportData -> do
hdr <- dbtCsvHeader $ Just exportData hdr <- dbtCsvHeader $ Just exportData
let ensureExtension ext fName = bool (`addExtension` ext) id (ext `isExtensionOf` fName) fName let ensureExtension ext fName = bool (`addExtension` ext) id (ext `isExtensionOf` fName) fName
setContentDisposition' . Just $ ensureExtension (unpack extensionCsv) dbtCsvName dbtCsvName' <- timestampCsv <*> pure dbtCsvName
setContentDisposition' . Just $ ensureExtension (unpack extensionCsv) dbtCsvName'
sendResponse <=< liftHandler . respondCsvDB hdr $ C.sourceList (zip currentKeys rows) .| dbtCsvDoEncode exportData >> lift E.transactionSave sendResponse <=< liftHandler . respondCsvDB hdr $ C.sourceList (zip currentKeys rows) .| dbtCsvDoEncode exportData >> lift E.transactionSave
DBCsvImport{..} DBCsvImport{..}
| Just DBTCsvEncode{..} <- dbtCsvEncode | Just DBTCsvEncode{..} <- dbtCsvEncode

View File

@ -151,6 +151,7 @@ import Data.Void.Instances as Import ()
import Crypto.Hash.Instances as Import () import Crypto.Hash.Instances as Import ()
import Colonnade.Instances as Import () import Colonnade.Instances as Import ()
import Data.Bool.Instances as Import () import Data.Bool.Instances as Import ()
import Data.Encoding.Instances as Import ()
import Control.Lens as Import import Control.Lens as Import
hiding ( (<.>) hiding ( (<.>)
@ -163,6 +164,9 @@ import Data.Set.Lens as Import
import Control.Arrow as Import (Kleisli(..)) import Control.Arrow as Import (Kleisli(..))
import Data.Encoding as Import (DynEncoding, decodeLazyByteString, encodeLazyByteString)
import Data.Encoding.UTF8 as Import (UTF8(UTF8))
import Control.Monad.Trans.RWS (RWST) import Control.Monad.Trans.RWS (RWST)

View File

@ -7,6 +7,7 @@ import Import
import Handler.Utils import Handler.Utils
import qualified Data.CaseInsensitive as CI import qualified Data.CaseInsensitive as CI
import Handler.Utils.Csv (partIsAttachmentCsv)
dispatchJobSendCourseCommunication :: Either UserEmail UserId dispatchJobSendCourseCommunication :: Either UserEmail UserId
@ -32,5 +33,5 @@ dispatchJobSendCourseCommunication jRecipientEmail jAllRecipientAddresses jCours
void $ addPart jMailContent void $ addPart jMailContent
when (jRecipientEmail == Right jSender) $ when (jRecipientEmail == Right jSender) $
addPart' $ do addPart' $ do
partIsAttachment $ unpack (mr MsgCommAllRecipients) `addExtension` unpack extensionCsv partIsAttachmentCsv $ mr MsgCommAllRecipients
toMailPart (toDefaultOrderedCsvRendered jAllRecipientAddresses, userCsvOptions sender) toMailPart (toDefaultOrderedCsvRendered jAllRecipientAddresses, userCsvOptions sender)

View File

@ -17,6 +17,8 @@ import Data.Maybe (fromJust)
import qualified Data.Text as Text import qualified Data.Text as Text
import qualified Data.Text.Lens as Text import qualified Data.Text.Lens as Text
import qualified Data.ByteString.Lazy as LBS
import Data.Csv (Quoting(..)) import Data.Csv (Quoting(..))
import qualified Data.Csv as Csv import qualified Data.Csv as Csv
@ -24,6 +26,8 @@ import qualified Data.Aeson as JSON
import Database.Persist.Sql (PersistFieldSql(..)) import Database.Persist.Sql (PersistFieldSql(..))
import Utils.Lens.TH
data StudyFieldType = FieldPrimary | FieldSecondary data StudyFieldType = FieldPrimary | FieldSecondary
deriving (Eq, Ord, Enum, Show, Read, Bounded, Generic) deriving (Eq, Ord, Enum, Show, Read, Bounded, Generic)
@ -68,13 +72,30 @@ nullaryPathPiece ''Quoting $ \q -> if
data CsvOptions data CsvOptions
= CsvOptions = CsvOptions
{ csvDelimiter :: Char { csvFormat :: CsvFormatOptions
, csvUseCrLf :: Bool , csvTimestamp :: Bool
, csvQuoting :: Csv.Quoting
} }
deriving (Eq, Ord, Read, Show, Generic, Typeable) deriving (Eq, Ord, Read, Show, Generic, Typeable)
data CsvFormatOptions
= CsvFormatOptions
{ csvDelimiter :: Char
, csvUseCrLf :: Bool
, csvQuoting :: Csv.Quoting
, csvEncoding :: DynEncoding
}
deriving (Eq, Ord, Read, Show, Generic, Typeable)
makeLenses_ ''CsvOptions
makeLenses_ ''CsvFormatOptions
instance Default CsvOptions where instance Default CsvOptions where
def = CsvOptions
{ csvFormat = def
, csvTimestamp = False
}
instance Default CsvFormatOptions where
def = csvPreset # CsvPresetRFC def = csvPreset # CsvPresetRFC
data CsvPreset = CsvPresetRFC data CsvPreset = CsvPresetRFC
@ -83,28 +104,38 @@ data CsvPreset = CsvPresetRFC
instance Universe CsvPreset instance Universe CsvPreset
instance Finite CsvPreset instance Finite CsvPreset
csvPreset :: Prism' CsvOptions CsvPreset csvPreset :: Prism' CsvFormatOptions CsvPreset
csvPreset = prism' fromPreset toPreset csvPreset = prism' fromPreset toPreset
where where
fromPreset :: CsvPreset -> CsvOptions fromPreset :: CsvPreset -> CsvFormatOptions
fromPreset CsvPresetRFC = CsvOptions { csvDelimiter = ',', csvUseCrLf = True, csvQuoting = QuoteMinimal } fromPreset CsvPresetRFC = CsvFormatOptions
fromPreset CsvPresetExcel = CsvOptions { csvDelimiter = ';', csvUseCrLf = True, csvQuoting = QuoteAll } { csvDelimiter = ','
, csvUseCrLf = True
, csvQuoting = QuoteMinimal
, csvEncoding = "UTF8"
}
fromPreset CsvPresetExcel = CsvFormatOptions
{ csvDelimiter = ';'
, csvUseCrLf = True
, csvQuoting = QuoteAll
, csvEncoding = "CP1252"
}
toPreset :: CsvOptions -> Maybe CsvPreset toPreset :: CsvFormatOptions -> Maybe CsvPreset
toPreset opts = case filter (\p -> fromPreset p == opts) universeF of toPreset opts = case filter (\p -> fromPreset p == opts) universeF of
[p] -> Just p [p] -> Just p
_other -> Nothing _other -> Nothing
_CsvEncodeOptions :: Iso' CsvOptions Csv.EncodeOptions _CsvEncodeOptions :: Iso' CsvFormatOptions Csv.EncodeOptions
_CsvEncodeOptions = iso toEncode fromEncode _CsvEncodeOptions = iso toEncode fromEncode
where where
toEncode CsvOptions{..} = Csv.defaultEncodeOptions toEncode CsvFormatOptions{..} = Csv.defaultEncodeOptions
{ 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 = True
} }
fromEncode encOpts = CsvOptions fromEncode encOpts = def
{ csvDelimiter = toEnum . fromIntegral $ Csv.encDelimiter encOpts { csvDelimiter = toEnum . fromIntegral $ Csv.encDelimiter encOpts
, csvUseCrLf = Csv.encUseCrLf encOpts , csvUseCrLf = Csv.encUseCrLf encOpts
, csvQuoting = Csv.encQuoting encOpts , csvQuoting = Csv.encQuoting encOpts
@ -112,16 +143,31 @@ _CsvEncodeOptions = iso toEncode fromEncode
instance ToJSON CsvOptions where instance ToJSON CsvOptions where
toJSON CsvOptions{..} = JSON.object toJSON CsvOptions{..} = JSON.object
[ "format" JSON..= csvFormat
, "timestamp" JSON..= csvTimestamp
]
instance FromJSON CsvOptions where
parseJSON = JSON.withObject "CsvOptions" $ \o -> do
csvFormat <- o JSON..:? "format" JSON..!= csvFormat def
csvTimestamp <- o JSON..:? "timestamp" JSON..!= csvTimestamp def
return CsvOptions{..}
instance ToJSON CsvFormatOptions where
toJSON CsvFormatOptions{..} = JSON.object
[ "delimiter" JSON..= fromEnum csvDelimiter [ "delimiter" JSON..= fromEnum csvDelimiter
, "use-cr-lf" JSON..= csvUseCrLf , "use-cr-lf" JSON..= csvUseCrLf
, "quoting" JSON..= csvQuoting , "quoting" JSON..= csvQuoting
, "encoding" JSON..= csvEncoding
] ]
instance FromJSON CsvOptions where instance FromJSON CsvFormatOptions where
parseJSON = JSON.withObject "CsvOptions" $ \o -> do parseJSON = JSON.withObject "CsvFormatOptions" $ \o -> 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
return CsvOptions{..} csvEncoding <- o JSON..:? "encoding" JSON..!= csvEncoding def
return CsvFormatOptions{..}
derivePersistFieldJSON ''CsvOptions derivePersistFieldJSON ''CsvOptions
nullaryPathPiece ''CsvPreset $ camelToPathPiece' 2 nullaryPathPiece ''CsvPreset $ camelToPathPiece' 2
@ -130,7 +176,16 @@ instance YesodMail site => ToMailPart site (CsvRendered, CsvOptions) where
toMailPart (CsvRendered{..}, encOpts) = do toMailPart (CsvRendered{..}, encOpts) = do
_partType .= decodeUtf8 typeCsv' _partType .= decodeUtf8 typeCsv'
_partEncoding .= QuotedPrintableText _partEncoding .= QuotedPrintableText
_partContent .= Csv.encodeByNameWith (encOpts ^. _CsvEncodeOptions) csvRenderedHeader csvRenderedData _partContent .= recode' (Csv.encodeByNameWith (encOpts ^. _csvFormat . _CsvEncodeOptions) csvRenderedHeader csvRenderedData)
where
recode' :: LBS.ByteString -> LBS.ByteString
recode'
| enc == "UTF8"
= id
| otherwise
= encodeLazyByteString enc . decodeLazyByteString UTF8
where enc = encOpts ^. _csvFormat . _csvEncoding
instance YesodMail site => ToMailPart site CsvRendered where instance YesodMail site => ToMailPart site CsvRendered where
toMailPart = toMailPart . (, def :: CsvOptions) toMailPart = toMailPart . (, def :: CsvOptions)

View File

@ -316,11 +316,12 @@ derivePersistField "CorrectorState"
showCompactCorrectorLoad :: Load -> CorrectorState -> Text showCompactCorrectorLoad :: Load -> CorrectorState -> Text
showCompactCorrectorLoad load CorrectorMissing = "[" <> showCompactCorrectorLoad load CorrectorNormal <> "]" showCompactCorrectorLoad load CorrectorMissing = "[" <> showCompactCorrectorLoad load CorrectorNormal <> "]"
showCompactCorrectorLoad load CorrectorExcused = "{" <> showCompactCorrectorLoad load CorrectorNormal <> "}" showCompactCorrectorLoad load CorrectorExcused = "{" <> showCompactCorrectorLoad load CorrectorNormal <> "}"
showCompactCorrectorLoad Load{..} CorrectorNormal = proportionText <> tutorialText showCompactCorrectorLoad Load{..} CorrectorNormal | byProportion == 0 = tutorialText
| otherwise = proportionText <> " + " <> tutorialText
where where
proportionText = let propDbl :: Double proportionText = let propDbl :: Double
propDbl = fromRational byProportion propDbl = fromRational byProportion
in tshow $ roundToDigits 2 propDbl in tshow $ roundToDigits 2 propDbl
tutorialText = case byTutorial of Nothing -> mempty tutorialText = case byTutorial of Nothing -> mempty
Just True -> " (T)" Just True -> "(T)"
Just False -> " +T " Just False -> "T"

View File

@ -1,5 +1,12 @@
$newline never $newline never
<dl .deflist> <dl .deflist>
<dt .deflist__dt>
^{formatGregorianW 2019 10 23}
<dd .deflist__dd>
<ul>
<li>Option um an die Namen aller heruntergeladenen CSV-Dateien einen Zeitstempel vorne anzuhängen
<li>CSV-Export-Option um den beim import und export verwendeten Zeichensatz einzustellen
<dt .deflist__dt> <dt .deflist__dt>
^{formatGregorianW 2019 10 14} ^{formatGregorianW 2019 10 14}
<dd .deflist__dd> <dd .deflist__dd>

View File

@ -112,7 +112,7 @@ fillDb = do
, userNotificationSettings = def , userNotificationSettings = def
, userCreated = now , userCreated = now
, userLastLdapSynchronisation = Nothing , userLastLdapSynchronisation = Nothing
, userCsvOptions = csvPreset # CsvPresetRFC , userCsvOptions = def { csvFormat = csvPreset # CsvPresetRFC }
, userSex = Just SexMale , userSex = Just SexMale
, userShowSex = userDefaultShowSex , userShowSex = userDefaultShowSex
} }
@ -140,7 +140,7 @@ fillDb = do
, userNotificationSettings = def , userNotificationSettings = def
, userCreated = now , userCreated = now
, userLastLdapSynchronisation = Nothing , userLastLdapSynchronisation = Nothing
, userCsvOptions = csvPreset # CsvPresetExcel , userCsvOptions = def { csvFormat = csvPreset # CsvPresetExcel }
, userSex = Just SexMale , userSex = Just SexMale
, userShowSex = userDefaultShowSex , userShowSex = userDefaultShowSex
} }

View File

@ -32,7 +32,7 @@ import Web.PathPieces
import qualified Data.Csv as Csv import qualified Data.Csv as Csv
import Data.Scientific import Data.Scientific
import Utils.Lens import Utils.Lens hiding (elements)
import qualified Data.Char as Char import qualified Data.Char as Char
@ -258,11 +258,12 @@ instance Arbitrary Quoting where
arbitrary = genericArbitrary arbitrary = genericArbitrary
shrink = genericShrink shrink = genericShrink
instance Arbitrary CsvOptions where instance Arbitrary CsvFormatOptions where
arbitrary = CsvOptions arbitrary = CsvFormatOptions
<$> suchThat arbitrary validDelimiter <$> suchThat arbitrary validDelimiter
<*> arbitrary <*> arbitrary
<*> arbitrary <*> arbitrary
<*> elements ["UTF8", "CP1252"]
where where
validDelimiter c = and validDelimiter c = and
[ Char.isLatin1 c [ Char.isLatin1 c
@ -270,6 +271,11 @@ instance Arbitrary CsvOptions where
, c /= '\r' , c /= '\r'
, c /= '\n' , c /= '\n'
] ]
instance Arbitrary CsvOptions where
arbitrary = CsvOptions
<$> arbitrary
<*> arbitrary
shrink = genericShrink shrink = genericShrink
instance Arbitrary CsvPreset where instance Arbitrary CsvPreset where