feat(csv): allow customisation of csv-export-options

This commit is contained in:
Gregor Kleen 2019-09-30 15:53:29 +02:00
parent 64f771518e
commit 95ceeddc83
22 changed files with 367 additions and 52 deletions

View File

@ -1499,8 +1499,8 @@ Proportion c@Text of@Text prop@Rational: #{c}/#{of} (#{rationalToFixed2 (100 * p
ExamUserCsvName tid@TermId ssh@SchoolId csh@CourseShorthand examn@ExamName: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-#{foldedCase examn}-teilnehmer
CourseApplicationsTableCsvName tid@TermId ssh@SchoolId csh@CourseShorthand: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-bewerbungen
CsvColumnsExplanationsLabel: Spalten
CsvColumnsExplanationsTip: Bedeutung der in der CSV-Datei enthaltenen Spalten
CsvColumnsExplanationsLabel: Spalten- & Zellenformat
CsvColumnsExplanationsTip: Bedeutung und Format der in der CSV-Datei enthaltenen Spalten
CsvColumnExamUserSurname: Nachname(n) des Teilnehmers
CsvColumnExamUserFirstName: Vorname(n) des Teilnehmers
CsvColumnExamUserName: Voller Name des Teilnehmers (gewöhnlicherweise inkl. Vor- und Nachname(n))
@ -1797,4 +1797,28 @@ AcceptApplicationsInvite: Einladungen verschicken
AcceptApplicationsSecondary: Gleichstände auflösen
AcceptApplicationsSecondaryTip: Wenn es im Laufe des Verfahrens mehrere Bewerber mit der selben Bewertung für den selben Platz gibt, wie soll der Gleichstand aufgelöst werden?
AcceptApplicationsSecondaryRandom: Zufällig
AcceptApplicationsSecondaryTime: Nach Zeitpunkt der Bewerbung
AcceptApplicationsSecondaryTime: Nach Zeitpunkt der Bewerbung
CsvOptions: CSV-Optionen
CsvOptionsTip: Diese Einstellungen betreffen nur den CSV-Export; beim Import werden die verwendeten Einstellungen automatisch ermittelt.
CsvPresetRFC: Standard-Konform (RFC 4180)
CsvPresetExcel: Excel-Kompatibel
CsvCustom: Benutzerdefiniert
CsvDelimiter: Trennzeichen
CsvUseCrLf: Zeilenumbrüche
CsvQuoting: Quoting
CsvQuotingTip: Wann sollen Anführungszeichen (") um Felder platziert werden, um Interpretation von im Feld enthaltenen Zeichen als Trennzeichen zu verhindern?
CsvDelimiterNull: Null-Byte
CsvDelimiterTab: Tabulator
CsvDelimiterComma: Komma
CsvDelimiterColon: Doppelpunkt
CsvDelimiterBar: Senkrechter Strich
CsvDelimiterSpace: Leerzeichen
CsvDelimiterUnitSep: Teilgruppentrennzeichen
CsvCrLf: DOS (CRLF)
CsvLf: Unix (LF)
CsvQuoteNone: Nie
CsvQuoteMinimal: Nur wenn nötig
CsvQuoteAll: Immer
CsvOptionsUpdated: CSV-Optionen erfolgreich angepasst
CsvChangeOptionsLabel: Export-Optionen

View File

@ -30,6 +30,7 @@ User json -- Each Uni2work user has a corresponding row in this table; create
mailLanguages MailLanguages "default='[]'::jsonb" -- Preferred language for eMail; i18n not yet implemented; user-defined
notificationSettings NotificationSettings -- Bit-array for which events email notifications are requested by user; user-defined
warningDays NominalDiffTime default=1209600 -- timedistance to pending deadlines for homepage infos
csvOptions CsvOptions "default='{}'::jsonb"
UniqueAuthentication ident -- Column 'ident' can be used as a row-key in this table
UniqueEmail email -- Column 'email' can be used as a row-key in this table
deriving Show Eq Ord Generic -- Haskell-specific settings for runtime-value representing a row in memory

1
routes
View File

@ -72,6 +72,7 @@
/user/profile ProfileDataR GET !free
/user/authpreds AuthPredsR GET POST !free
/user/set-display-email SetDisplayEmailR GET POST !free
/user/csv-options CsvOptionsR GET POST !free
/exam-office ExamOfficeR !exam-office:
/ EOExamsR GET

View File

@ -315,6 +315,8 @@ embedRenderMessage ''UniWorX ''UploadModeDescr id
embedRenderMessage ''UniWorX ''SecretJSONFieldException id
embedRenderMessage ''UniWorX ''AFormMessage $ concat . drop 2 . splitCamel
embedRenderMessage ''UniWorX ''SchoolFunction id
embedRenderMessage ''UniWorX ''CsvPreset id
embedRenderMessage ''UniWorX ''Quoting ("Csv" <>)
embedRenderMessage ''UniWorX ''AuthenticationMode id
@ -3287,6 +3289,7 @@ upsertCampusUser ldapData Creds{..} = do
, userWarningDays = userDefaultWarningDays
, userNotificationSettings = def
, userMailLanguages = def
, userCsvOptions = def
, userTokensIssuedAfter = Nothing
, userCreated = now
, userLastLdapSynchronisation = Just now

View File

@ -1,4 +1,11 @@
module Handler.Profile where
module Handler.Profile
( getProfileR, postProfileR
, getProfileDataR, makeProfileData
, getAuthPredsR, postAuthPredsR
, getUserNotificationR, postUserNotificationR
, getSetDisplayEmailR, postSetDisplayEmailR
, getCsvOptionsR, postCsvOptionsR
) where
import Import
@ -796,3 +803,25 @@ postSetDisplayEmailR = do
siteLayoutMsg MsgTitleChangeUserDisplayEmail $ do
setTitleI MsgTitleChangeUserDisplayEmail
$(i18nWidgetFile "set-display-email")
getCsvOptionsR, postCsvOptionsR :: Handler Html
getCsvOptionsR = postCsvOptionsR
postCsvOptionsR = do
Entity uid User{userCsvOptions} <- requireAuth
((optionsRes, optionsWgt'), optionsEnctype) <- runFormPost . renderAForm FormStandard $
csvOptionsForm (fslI MsgCsvOptions & setTooltip MsgCsvOptionsTip) (Just userCsvOptions)
formResultModal optionsRes CsvOptionsR $ \opts -> do
lift . runDB $ update uid [ UserCsvOptions =. opts ]
tell . pure =<< messageI Success MsgCsvOptionsUpdated
siteLayoutMsg MsgCsvOptions $ do
setTitleI MsgCsvOptions
isModal <- hasCustomHeader HeaderIsModal
wrapForm optionsWgt' def
{ formAction = Just $ SomeRoute CsvOptionsR
, formEncoding = optionsEnctype
, formAttrs = [ asyncSubmitAttr | isModal ]
}

View File

@ -73,6 +73,7 @@ postAdminUserAddR = do
, userWarningDays = userDefaultWarningDays
, userNotificationSettings = def
, userMailLanguages = def
, userCsvOptions = def
, userTokensIssuedAfter = Nothing
, userCreated = now
, userLastLdapSynchronisation = Nothing

View File

@ -98,19 +98,23 @@ decodeCsv = transPipe throwExceptT $ do
encodeCsv :: ( ToNamedRecord csv
, Monad m
, MonadHandler m
, HandlerSite m ~ UniWorX
)
=> Header
-> ConduitT csv ByteString m ()
-- ^ Encode a stream of records
--
-- Currently not streaming
encodeCsv hdr = fmap (encodeByName hdr) (C.foldMap pure) >>= C.sourceLazy
encodeCsv hdr = do
csvOpts <- fmap (maybe def (userCsvOptions . entityVal)) . lift $ liftHandler maybeAuth
fmap (encodeByNameWith (csvOpts ^. _CsvEncodeOptions) hdr) (C.foldMap pure) >>= C.sourceLazy
encodeDefaultOrderedCsv :: forall csv m.
( ToNamedRecord csv
, DefaultOrdered csv
, Monad m
, MonadHandler m
, HandlerSite m ~ UniWorX
)
=> ConduitT csv ByteString m ()
encodeDefaultOrderedCsv = encodeCsv $ headerOrder (error "headerOrder" :: csv)
@ -118,33 +122,32 @@ encodeDefaultOrderedCsv = encodeCsv $ headerOrder (error "headerOrder" :: csv)
respondCsv :: ToNamedRecord csv
=> Header
-> ConduitT () csv (HandlerFor site) ()
-> HandlerFor site TypedContent
-> ConduitT () csv Handler ()
-> Handler TypedContent
respondCsv hdr src = respondSource typeCsv' $ src .| encodeCsv hdr .| awaitForever sendChunk
respondDefaultOrderedCsv :: forall csv site.
respondDefaultOrderedCsv :: forall csv.
( ToNamedRecord csv
, DefaultOrdered csv
)
=> ConduitT () csv (HandlerFor site) ()
-> HandlerFor site TypedContent
=> ConduitT () csv Handler ()
-> Handler TypedContent
respondDefaultOrderedCsv = respondCsv $ headerOrder (error "headerOrder" :: csv)
respondCsvDB :: ( ToNamedRecord csv
, YesodPersistRunner site
, YesodPersistRunner UniWorX
)
=> Header
-> ConduitT () csv (YesodDB site) ()
-> HandlerFor site TypedContent
-> ConduitT () csv DB ()
-> Handler TypedContent
respondCsvDB hdr src = respondSourceDB typeCsv' $ src .| encodeCsv hdr .| awaitForever sendChunk
respondDefaultOrderedCsvDB :: forall csv site.
respondDefaultOrderedCsvDB :: forall csv.
( ToNamedRecord csv
, DefaultOrdered csv
, YesodPersistRunner site
)
=> ConduitT () csv (YesodDB site) ()
-> HandlerFor site TypedContent
=> ConduitT () csv DB ()
-> Handler TypedContent
respondDefaultOrderedCsvDB = respondCsvDB $ headerOrder (error "headerOrder" :: csv)
fileSourceCsv :: ( FromNamedRecord csv

View File

@ -12,6 +12,7 @@ import Handler.Utils.Form.Types
import Handler.Utils.DateTime
import Import
import Data.Char (chr, ord)
import qualified Data.Char as Char
import qualified Data.Text as Text
import qualified Data.CaseInsensitive as CI
@ -220,11 +221,7 @@ multiAction :: forall action a.
-> Maybe action
-> (Html -> MForm Handler (FormResult a, [FieldView UniWorX]))
multiAction acts fs@FieldSettings{..} defAction csrf = do
mr <- getMessageRender
let
options = OptionList [ Option (mr a) a (toPathPiece a) | a <- Map.keys acts ] fromPathPiece
(actionRes, actionView) <- mreq (selectField $ return options) fs defAction
(actionRes, actionView) <- mreq (selectField . optionsF $ Map.keysSet acts) fs defAction
results <- mapM (fmap (over _2 ($ [])) . aFormToForm) acts
let actionResults = view _1 <$> results
@ -1199,3 +1196,84 @@ examPassedField :: forall m.
)
=> Field m ExamPassed
examPassedField = hoistField liftHandler $ selectField optionsFinite
data CsvOptions' = CsvOptionsPreset' CsvPreset
| CsvOptionsCustom'
deriving (Eq, Ord, Read, Show, Generic, Typeable)
deriveFinite ''CsvOptions'
instance PathPiece CsvOptions' where
toPathPiece = \case
CsvOptionsPreset' p -> toPathPiece p
CsvOptionsCustom' -> "custom"
fromPathPiece t = fromPathPiece t
<|> guardOn (t == "custom") CsvOptionsCustom'
instance RenderMessage UniWorX CsvOptions' where
renderMessage m ls = \case
CsvOptionsPreset' p -> mr p
CsvOptionsCustom' -> mr MsgCsvCustom
where
mr :: forall msg. RenderMessage UniWorX msg => msg -> Text
mr = renderMessage m ls
csvOptionsForm :: forall m.
( MonadHandler m
, HandlerSite m ~ UniWorX
)
=> FieldSettings UniWorX
-> Maybe CsvOptions
-> AForm m CsvOptions
csvOptionsForm fs mPrev = hoistAForm liftHandler . multiActionA csvActs fs $ classifyCsvOptions <$> mPrev
where
csvActs :: Map CsvOptions' (AForm Handler CsvOptions)
csvActs = mapF $ \case
CsvOptionsPreset' preset
-> pure $ csvPreset # preset
CsvOptionsCustom'
-> CsvOptions
<$> areq (selectField delimiterOpts) (fslI MsgCsvDelimiter) (csvDelimiter <$> mPrev)
<*> areq (selectField lineEndOpts) (fslI MsgCsvUseCrLf) (csvUseCrLf <$> mPrev)
<*> areq (selectField quoteOpts) (fslI MsgCsvQuoting & setTooltip MsgCsvQuotingTip) (csvQuoting <$> mPrev)
delimiterOpts :: Handler (OptionList Char)
delimiterOpts = do
MsgRenderer mr <- getMsgRenderer
let
opts =
[ (MsgCsvDelimiterNull, '\0')
, (MsgCsvDelimiterTab, '\t')
, (MsgCsvDelimiterComma, ',')
, (MsgCsvDelimiterColon, chr 58)
, (MsgCsvDelimiterBar, '|')
, (MsgCsvDelimiterSpace, ' ')
, (MsgCsvDelimiterUnitSep, chr 31)
]
olReadExternal t = do
i <- readMay t
guard $ i >= 0 && i <= 255
let c = chr i
guard $ any ((== c) . view _2) opts
return c
olOptions = [ Option (mr msg) c (tshow $ ord c)
| (msg, c) <- opts
]
return OptionList{..}
lineEndOpts :: Handler (OptionList Bool)
lineEndOpts = optionsPathPiece
[ (MsgCsvCrLf, True )
, (MsgCsvLf, False)
]
quoteOpts :: Handler (OptionList Quoting)
quoteOpts = optionsF
[ QuoteMinimal
, QuoteAll
]
classifyCsvOptions :: CsvOptions -> CsvOptions'
classifyCsvOptions opts
| Just preset <- opts ^? csvPreset
= CsvOptionsPreset' preset
| otherwise
= CsvOptionsCustom'

View File

@ -49,6 +49,7 @@ import Handler.Utils.Form
import Handler.Utils.Csv
import Handler.Utils.ContentDisposition
import Handler.Utils.I18n
import Handler.Utils.Widgets
import Utils
import Utils.Lens

View File

@ -96,3 +96,9 @@ editedByW fmt tm usr = do
heat :: Integral a => a -> a -> Double
heat (toInteger -> full) (toInteger -> achieved)
= roundToDigits 3 $ cutOffPercent 0.3 (fromIntegral full^2) (fromIntegral achieved^2)
i18n :: forall m msg.
( MonadWidget m
, RenderMessage (HandlerSite m) msg
) => msg -> m ()
i18n = toWidget . (SomeMessage :: msg -> SomeMessage (HandlerSite m))

View File

@ -1,3 +1,5 @@
{-# OPTIONS_GHC -fno-warn-orphans #-}
{-|
Module: Model.Types.Misc
Description: Additional uncategorized types
@ -5,6 +7,7 @@ Description: Additional uncategorized types
module Model.Types.Misc
( module Model.Types.Misc
, Quoting(..)
) where
import Import.NoModel
@ -14,6 +17,11 @@ import Data.Maybe (fromJust)
import qualified Data.Text as Text
import qualified Data.Text.Lens as Text
import Data.Csv (Quoting(..))
import qualified Data.Csv as Csv
import qualified Data.Aeson as JSON
data StudyFieldType = FieldPrimary | FieldSecondary
deriving (Eq, Ord, Enum, Show, Read, Bounded, Generic)
@ -43,3 +51,75 @@ nullaryPathPiece ''Theme $ camelToPathPiece' 1
$(deriveSimpleWith ''ToMessage 'toMessage (over Text.packed $ Text.intercalate " " . unsafeTail . splitCamel) ''Theme) -- describe theme to user
derivePersistField "Theme"
deriving instance Generic Quoting
deriving instance Ord Quoting
deriving instance Read Quoting
deriveJSON defaultOptions
{ constructorTagModifier = camelToPathPiece' 1
} ''Quoting
deriveFinite ''Quoting
nullaryPathPiece ''Quoting $ \q -> if
| q == "QuoteNone" -> "never"
| otherwise -> camelToPathPiece' 1 q
data CsvOptions
= CsvOptions
{ csvDelimiter :: Char
, csvUseCrLf :: Bool
, csvQuoting :: Csv.Quoting
}
deriving (Eq, Ord, Read, Show, Generic, Typeable)
instance Default CsvOptions where
def = csvPreset # CsvPresetRFC
data CsvPreset = CsvPresetRFC
| CsvPresetExcel
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable)
instance Universe CsvPreset
instance Finite CsvPreset
csvPreset :: Prism' CsvOptions CsvPreset
csvPreset = prism' fromPreset toPreset
where
fromPreset :: CsvPreset -> CsvOptions
fromPreset CsvPresetRFC = CsvOptions { csvDelimiter = ',', csvUseCrLf = True, csvQuoting = QuoteMinimal }
fromPreset CsvPresetExcel = CsvOptions { csvDelimiter = ';', csvUseCrLf = True, csvQuoting = QuoteAll }
toPreset :: CsvOptions -> Maybe CsvPreset
toPreset opts = case filter (\p -> fromPreset p == opts) universeF of
[p] -> Just p
_other -> Nothing
_CsvEncodeOptions :: Iso' CsvOptions Csv.EncodeOptions
_CsvEncodeOptions = iso toEncode fromEncode
where
toEncode CsvOptions{..} = Csv.defaultEncodeOptions
{ Csv.encDelimiter = fromIntegral $ fromEnum csvDelimiter
, Csv.encUseCrLf = csvUseCrLf
, Csv.encQuoting = csvQuoting
, Csv.encIncludeHeader = True
}
fromEncode encOpts = CsvOptions
{ csvDelimiter = toEnum . fromIntegral $ Csv.encDelimiter encOpts
, csvUseCrLf = Csv.encUseCrLf encOpts
, csvQuoting = Csv.encQuoting encOpts
}
instance ToJSON CsvOptions where
toJSON CsvOptions{..} = JSON.object
[ "delimiter" JSON..= fromEnum csvDelimiter
, "use-cr-lf" JSON..= csvUseCrLf
, "quoting" JSON..= csvQuoting
]
instance FromJSON CsvOptions where
parseJSON = JSON.withObject "CsvOptions" $ \o -> do
csvDelimiter <- fmap (fmap toEnum) (o JSON..:? "delimiter") JSON..!= csvDelimiter def
csvUseCrLf <- o JSON..:? "use-cr-lf" JSON..!= csvUseCrLf def
csvQuoting <- o JSON..:? "quoting" JSON..!= csvQuoting def
return CsvOptions{..}
derivePersistFieldJSON ''CsvOptions
nullaryPathPiece ''CsvPreset $ camelToPathPiece' 2

View File

@ -412,7 +412,8 @@ mapSymmDiff a b = Map.fromListWith Set.union . map (over _2 Set.singleton) . Set
assocsSet :: Ord (k, v) => Map k v -> Set (k, v)
assocsSet = setOf folded . imap (,)
mapF :: (Ord k, Finite k) => (k -> v) -> Map k v
mapF = flip Map.fromSet $ Set.fromList universeF
---------------
-- Functions --

View File

@ -491,6 +491,24 @@ reorderField optList = Field{..}
withNum t n = tshow n <> "." <> t
$(widgetFile "widgets/permutation/permutation")
optionsPathPiece :: ( MonadHandler m
, HandlerSite m ~ site
, MonoFoldable mono
, Element mono ~ (msg, val)
, RenderMessage site msg
, PathPiece val
)
=> mono -> m (OptionList val)
optionsPathPiece (otoList -> opts) = do
mr <- getMessageRender
let
mkOption (m, a) = Option
{ optionDisplay = mr m
, optionInternalValue = a
, optionExternalValue = toPathPiece a
}
return . mkOptionList $ mkOption <$> opts
optionsF :: ( MonadHandler m
, RenderMessage site (Element mono)
, HandlerSite m ~ site
@ -498,16 +516,7 @@ optionsF :: ( MonadHandler m
, MonoFoldable mono
)
=> mono -> m (OptionList (Element mono))
optionsF (otoList -> opts) = do
mr <- getMessageRender
let
mkOption a = Option
{ optionDisplay = mr a
, optionInternalValue = a
, optionExternalValue = toPathPiece a
}
return . mkOptionList $ mkOption <$> opts
optionsF = optionsPathPiece . map (id &&& id) . otoList
optionsFinite :: ( MonadHandler m
, Finite a

View File

@ -27,6 +27,7 @@ import Control.Monad.Base (MonadBase)
import Control.Monad.Trans.Control (MonadBaseControl)
import Control.Monad.Catch (MonadMask, MonadCatch)
import Control.Monad.Random.Class (MonadRandom)
import Control.Monad.Morph (MFunctor, MMonad)
deriving via (ReaderT (HandlerData site site) IO) instance MonadFix (HandlerFor site)
@ -52,6 +53,7 @@ newtype CachedMemoT k v m a = CachedMemoT { runCachedMemoT' :: ReaderT Loc m a }
, MonadThrow, MonadCatch, MonadMask, MonadLogger, MonadLoggerIO
, MonadResource, MonadHandler, MonadWidget
)
deriving newtype ( MFunctor, MMonad, MonadTrans )
deriving newtype instance MonadBase b m => MonadBase b (CachedMemoT k v m)
deriving newtype instance MonadBaseControl b m => MonadBaseControl b (CachedMemoT k v m)
@ -60,7 +62,8 @@ instance MonadReader r m => MonadReader r (CachedMemoT k v m) where
reader = CachedMemoT . lift . reader
local f (CachedMemoT act) = CachedMemoT $ mapReaderT (local f) act
deriving via (ReaderT Loc) instance MonadTrans (CachedMemoT k v)
instance MonadUnliftIO m => MonadUnliftIO (CachedMemoT k v m) where
askUnliftIO = (\UnliftIO{..} -> UnliftIO $ \(CachedMemoT f) -> unliftIO f) <$> CachedMemoT askUnliftIO
-- | Uses `cachedBy` with a `Binary`-encoded @k@
@ -73,3 +76,7 @@ runCachedMemoT :: Q Exp
runCachedMemoT = do
loc <- location
[e| flip runReaderT loc . runCachedMemoT' |]
instance site ~ site' => ToWidget site (SomeMessage site') where
toWidget msg = toWidget =<< (getMessageRender <*> pure msg)

View File

@ -1,12 +1,23 @@
$newline never
<h3>Hinweise zum Import von CSV-Dateien
<dl .deflist>
<dt .deflist__dt>Datenformat
<dd .deflist__dd>
Beim Import wird, pro Spalte, das selbe Datenformat erwartet, wie es beim #
Export produziert wird (siehe <i>Spalten- & Zellenformat</i> #
unter <i>CSV-Export</i>).<br />
Spalten können beliebig permutiert werden und dürfen auch fehlen (in #
diesem Fall wird die fehlende Spalte so behandelt als enthielte sie in #
jeder Zeile eine leere Zelle).<br />
Spalten werden an ihrer Überschrift identifiziert. #
Die Überschrift darf daher nicht verändert oder entfernt werden.
<dt .deflist__dt>Änderungen
<dd .deflist__dd>
Einige Zellen können durch den Import verändert werden.
Einige Zellen können durch den Import verändert werden.<br />
Nicht-änderbare Zellen werden ignoriert, falls diese verändert wurden.
<dt .deflist__dt>Vorschau
<dd .deflist__dd>
Es wird eine Vorschau angezeigt, bevor irgendetwas tatsächlich geändert wird.
Es wird eine Vorschau angezeigt, bevor irgendetwas tatsächlich geändert wird.<br />
In der Vorschau können dann auch nur teilweise Änderungen ausgewählt werden.
<dt .deflist__dt>Leere Zellen
<dd .deflist__dd>
@ -16,22 +27,22 @@
<p>
Es werden nur konsistente Änderungen akzeptiert!
<p>
Daraus folgt, dass es sinnvoll sein kann, gewisse Zellen frei zu lassen;
z.B. ändert man ein Studienfachzuordnung eines Teilnehmers ab,
dann müsste man auch Abschluss und Semesterzahl passend ändern.
Daraus folgt, dass es sinnvoll sein kann, gewisse Zellen frei zu lassen; #
ändert man z.B. die Studienfachzuordnung eines Teilnehmers ab, #
so müsste man auch Abschluss und Fachsemester passend ändern.<br />
Da diese jedoch eindeutig sind, kann man diese Zellen einfach frei lassen.
<dt .deflist__dt>Zeilen Identifikation
<dd .deflist__dd>
Mehrere Spalten werden zur Identifikation der Zeile verwendet.
Es muss nicht in jeder Spalte der Zeile ein Wert vorhanden sein,
so lange die Identifikation noch eindeutig ist.
Mehrere Spalten werden zur Identifikation der Zeile verwendet.<br />
Es muss nicht in jeder Spalte der Zeile ein Wert vorhanden sein, #
so lange die Identifikation noch eindeutig ist.<br />
Sind mehrere Werte vorhanden, so müssen diese natürlich zueinander passen.
<dt .deflist__dt>Zeilen hinzufügen
<dd .deflist__dd>
Es können auch neue Zeilen hinzugefügt werden, so fern ausreichend
eindeutige Informationen vorhanden sind;
Es können auch neue Zeilen hinzugefügt werden, sofern ausreichend #
eindeutige Informationen vorhanden sind; #
z.B. können so Prüfungsteilnehmer nachgemeldet werden.
<dt .deflist__dt>Zeilen löschen
<dd .deflist__dd>
Fehlende Zeilen werden in der Vorschau zur Löschung angeboten
Fehlende Zeilen werden in der Vorschau zur Löschung angeboten #
und dann ggf. gelöscht.

View File

@ -14,5 +14,7 @@ $if is _Just dbtCsvEncode
<div .csv-export__content>
<p>
^{csvColExplanations'}
<p>
^{modal (i18n MsgCsvChangeOptionsLabel) (Left (SomeRoute CsvOptionsR))}
^{csvExportWdgt'}

View File

@ -111,6 +111,7 @@ fillDb = do
, userNotificationSettings = def
, userCreated = now
, userLastLdapSynchronisation = Nothing
, userCsvOptions = csvPreset # CsvPresetRFC
}
fhamann <- insert User
{ userIdent = "felix.hamann@campus.lmu.de"
@ -135,6 +136,7 @@ fillDb = do
, userNotificationSettings = def
, userCreated = now
, userLastLdapSynchronisation = Nothing
, userCsvOptions = csvPreset # CsvPresetExcel
}
jost <- insert User
{ userIdent = "jost@tcs.ifi.lmu.de"
@ -159,6 +161,7 @@ fillDb = do
, userNotificationSettings = def
, userCreated = now
, userLastLdapSynchronisation = Nothing
, userCsvOptions = def
}
maxMuster <- insert User
{ userIdent = "max@campus.lmu.de"
@ -183,6 +186,7 @@ fillDb = do
, userNotificationSettings = def
, userCreated = now
, userLastLdapSynchronisation = Nothing
, userCsvOptions = def
}
tinaTester <- insert $ User
{ userIdent = "tester@campus.lmu.de"
@ -207,6 +211,7 @@ fillDb = do
, userNotificationSettings = def
, userCreated = now
, userLastLdapSynchronisation = Nothing
, userCsvOptions = def
}
svaupel <- insert User
{ userIdent = "vaupel.sarah@campus.lmu.de"
@ -231,6 +236,7 @@ fillDb = do
, userNotificationSettings = def
, userCreated = now
, userLastLdapSynchronisation = Nothing
, userCsvOptions = def
}
void . repsert (TermKey summer2017) $ Term
{ termName = summer2017

View File

@ -18,10 +18,11 @@ instance Arbitrary (Route Auth) where
instance Arbitrary (Route EmbeddedStatic) where
arbitrary = do
let printableText = pack . filter (/= '/') . getPrintableString <$> arbitrary
printableText' = printableText `suchThat` (not . null)
pathLength <- getPositive <$> arbitrary
path <- replicateM pathLength printableText
path <- replicateM pathLength printableText'
paramNum <- getNonNegative <$> arbitrary
params <- replicateM paramNum $ (,) <$> printableText <*> printableText
params <- replicateM paramNum $ (,) <$> printableText' <*> printableText
return $ embeddedResourceR path params
instance Arbitrary SchoolR where

View File

@ -0,0 +1,12 @@
module Model.MigrationSpec where
import TestImport
import Model.Migration
spec :: Spec
spec = withApp $ -- `withApp` does migration, if needed
describe "Migration" $
it "is idempotent" $
(`shouldBe` False) <$> runDB requiresMigration -- Migration shouldn't be needed after `withApp` above

View File

@ -8,6 +8,7 @@ import Settings
import Control.Lens (review, preview)
import Data.Aeson (Value)
import qualified Data.Aeson as Aeson
import qualified Data.Aeson.Types as Aeson
import MailSpec ()
@ -32,6 +33,8 @@ import Data.Scientific
import Utils.Lens
import qualified Data.Char as Char
instance (Arbitrary a, MonoFoldable a) => Arbitrary (NonNull a) where
arbitrary = arbitrary `suchThatMap` fromNullable
@ -250,6 +253,28 @@ instance Arbitrary ExamPassed where
arbitrary = genericArbitrary
shrink = genericShrink
instance Arbitrary Quoting where
arbitrary = genericArbitrary
shrink = genericShrink
instance Arbitrary CsvOptions where
arbitrary = CsvOptions
<$> suchThat arbitrary validDelimiter
<*> arbitrary
<*> arbitrary
where
validDelimiter c = and
[ Char.isLatin1 c
, c /= '"'
, c /= '\r'
, c /= '\n'
]
shrink = genericShrink
instance Arbitrary CsvPreset where
arbitrary = genericArbitrary
shrink = genericShrink
spec :: Spec
spec = do
@ -334,6 +359,12 @@ spec = do
[ eqLaws, ordLaws, showReadLaws, jsonLaws, persistFieldLaws ]
lawsCheckHspec (Proxy @ExamPassed)
[ eqLaws, ordLaws, showReadLaws, finiteLaws, jsonLaws, pathPieceLaws, persistFieldLaws, csvFieldLaws ]
lawsCheckHspec (Proxy @Quoting)
[ eqLaws, ordLaws, jsonLaws, showReadLaws, finiteLaws, pathPieceLaws ]
lawsCheckHspec (Proxy @CsvOptions)
[ eqLaws, ordLaws, showReadLaws, jsonLaws, persistFieldLaws ]
lawsCheckHspec (Proxy @CsvPreset)
[ eqLaws, ordLaws, showReadLaws, boundedEnumLaws, finiteLaws, pathPieceLaws ]
describe "TermIdentifier" $ do
it "has compatible encoding/decoding to/from Text" . property $
@ -365,6 +396,9 @@ spec = do
parse "1.8" `shouldSatisfy` is _Left
parse "voided" `shouldBe` Right ExamVoided
parse "no-show" `shouldBe` Right ExamNoShow
describe "CsvOptions" $
it "json-decodes from empty object" . example $
Aeson.parseMaybe Aeson.parseJSON (Aeson.object []) `shouldBe` Just (def :: CsvOptions)
termExample :: (TermIdentifier, Text) -> Expectation
termExample (term, encoded) = example $ do

View File

@ -22,10 +22,13 @@ import Utils
import System.FilePath
import Data.Time
import Mail (MailLanguages(..))
instance Arbitrary EmailAddress where
arbitrary = do
local <- suchThat arbitrary (\l -> isEmail l (CBS.pack "example.com"))
domain <- suchThat arbitrary (\d -> isEmail (CBS.pack "example") d)
local <- suchThat (CBS.pack . getPrintableString <$> arbitrary) (\l -> isEmail l (CBS.pack "example.com"))
domain <- suchThat (CBS.pack . getPrintableString <$> arbitrary) (\d -> isEmail (CBS.pack "example") d)
let (Just result) = emailAddress (makeEmailLike local domain)
pure result
@ -100,8 +103,9 @@ instance Arbitrary User where
userDownloadFiles <- arbitrary
userWarningDays <- arbitrary
userMailLanguages <- arbitrary
userMailLanguages <- fmap MailLanguages $ sublistOf =<< shuffle (toList appLanguages)
userNotificationSettings <- arbitrary
userCsvOptions <- arbitrary
userCreated <- arbitrary
userLastLdapSynchronisation <- arbitrary

View File

@ -140,6 +140,7 @@ createUser adjUser = do
userNotificationSettings = def
userCreated = now
userLastLdapSynchronisation = Nothing
userCsvOptions = def
runDB . insertEntity $ adjUser User{..}
lawsCheckHspec :: Typeable a => Proxy a -> [Proxy a -> Laws] -> Spec