Merge branch 'master' into i18n
This commit is contained in:
commit
4ff50b0147
45
CHANGELOG.md
45
CHANGELOG.md
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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);
|
||||||
|
|
||||||
|
|||||||
@ -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
2
package-lock.json
generated
@ -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": {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "uni2work",
|
"name": "uni2work",
|
||||||
"version": "7.18.0",
|
"version": "7.19.1",
|
||||||
"description": "",
|
"description": "",
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
33
src/Data/Encoding/Instances.hs
Normal file
33
src/Data/Encoding/Instances.hs
Normal 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
|
||||||
@ -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 ]
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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 ()
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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)
|
||||||
|
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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)
|
||||||
|
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user