feat(csv-export): .xlsx
This commit is contained in:
parent
78c54959b6
commit
5c513946c1
@ -1629,6 +1629,7 @@ CommSuccess n@Int: Nachricht wurde an #{n} Empfänger versandt
|
|||||||
CommTestSuccess: Nachricht wurde zu Testzwecken nur an Sie selbst versandt
|
CommTestSuccess: Nachricht wurde zu Testzwecken nur an Sie selbst versandt
|
||||||
CommUndisclosedRecipients: Verborgene Empfänger
|
CommUndisclosedRecipients: Verborgene Empfänger
|
||||||
CommAllRecipients: alle-empfaenger
|
CommAllRecipients: alle-empfaenger
|
||||||
|
CommAllRecipientsSheet: Empfänger
|
||||||
|
|
||||||
CommCourseHeading: Kursmitteilung
|
CommCourseHeading: Kursmitteilung
|
||||||
CommTutorialHeading: Tutorium-Mitteilung
|
CommTutorialHeading: Tutorium-Mitteilung
|
||||||
@ -2148,10 +2149,15 @@ Proportion c@Text of'@Text prop@Rational: #{c}/#{of'} (#{rationalToFixed2 (100 *
|
|||||||
ProportionNoRatio c@Text of'@Text: #{c}/#{of'}
|
ProportionNoRatio c@Text of'@Text: #{c}/#{of'}
|
||||||
|
|
||||||
CourseUserCsvName tid@TermId ssh@SchoolId csh@CourseShorthand: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-teilnehmer
|
CourseUserCsvName tid@TermId ssh@SchoolId csh@CourseShorthand: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-teilnehmer
|
||||||
|
CourseUserCsvSheetName tid@TermId ssh@SchoolId csh@CourseShorthand: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh} Teilnehmer
|
||||||
ExamUserCsvName tid@TermId ssh@SchoolId csh@CourseShorthand examn@ExamName: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-#{foldedCase examn}-teilnehmer
|
ExamUserCsvName tid@TermId ssh@SchoolId csh@CourseShorthand examn@ExamName: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-#{foldedCase examn}-teilnehmer
|
||||||
|
ExamUserCsvSheetName tid@TermId ssh@SchoolId csh@CourseShorthand examn@ExamName: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-#{foldedCase examn} Teilnehmer
|
||||||
ExternalExamUserCsvName tid@TermId ssh@SchoolId coursen@CourseName examn@ExamName: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase coursen}-#{foldedCase examn}-teilnehmer
|
ExternalExamUserCsvName tid@TermId ssh@SchoolId coursen@CourseName examn@ExamName: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase coursen}-#{foldedCase examn}-teilnehmer
|
||||||
|
ExternalExamUserCsvSheetName tid@TermId ssh@SchoolId coursen@CourseName examn@ExamName: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase coursen}-#{foldedCase examn} Teilnehmer
|
||||||
CourseApplicationsTableCsvName tid@TermId ssh@SchoolId csh@CourseShorthand: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-bewerbungen
|
CourseApplicationsTableCsvName tid@TermId ssh@SchoolId csh@CourseShorthand: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-bewerbungen
|
||||||
|
CourseApplicationsTableCsvSheetName tid@TermId ssh@SchoolId csh@CourseShorthand: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh} Bewerbungen
|
||||||
ParticipantsCsvName tid@TermId ssh@SchoolId: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-kursteilnehmer
|
ParticipantsCsvName tid@TermId ssh@SchoolId: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-kursteilnehmer
|
||||||
|
ParticipantsCsvSheetName tid@TermId ssh@SchoolId: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)} Kursteilnehmer
|
||||||
|
|
||||||
CourseUserCsvIncludeSheets: Übungsblätter
|
CourseUserCsvIncludeSheets: Übungsblätter
|
||||||
CourseUserCsvIncludeSheetsTip: Soll die exportierte CSV-Datei zusätzlich eine Spalte pro Übungsblatt enthalten?
|
CourseUserCsvIncludeSheetsTip: Soll die exportierte CSV-Datei zusätzlich eine Spalte pro Übungsblatt enthalten?
|
||||||
@ -2566,8 +2572,9 @@ CsvOptionsTip: Diese Einstellungen betreffen primär den CSV-Export; beim Import
|
|||||||
CsvFormatOptions: Dateiformat
|
CsvFormatOptions: Dateiformat
|
||||||
CsvTimestamp: Zeitstempel
|
CsvTimestamp: Zeitstempel
|
||||||
CsvTimestampTip: Soll an den Namen jeder exportierten CSV-Datei ein Zeitstempel vorne angehängt werden?
|
CsvTimestampTip: Soll an den Namen jeder exportierten CSV-Datei ein Zeitstempel vorne angehängt werden?
|
||||||
CsvPresetRFC: Standard-Konform (RFC 4180)
|
CsvPresetRFC: Standard-Konforme .csv Dateien (RFC 4180)
|
||||||
CsvPresetExcel: Excel-Kompatibel
|
CsvPresetExcel: Excel-Kompatible .csv Dateien (Excel <2010)
|
||||||
|
CsvPresetXlsx: .xlsx Dateien (ECMA-376; Excel ≥2010)
|
||||||
CsvCustom: Benutzerdefiniert
|
CsvCustom: Benutzerdefiniert
|
||||||
CsvDelimiter: Trennzeichen
|
CsvDelimiter: Trennzeichen
|
||||||
CsvUseCrLf: Zeilenumbrüche
|
CsvUseCrLf: Zeilenumbrüche
|
||||||
@ -2592,6 +2599,9 @@ CsvQuoteMinimal: Nur wenn nötig
|
|||||||
CsvQuoteAll: Immer
|
CsvQuoteAll: Immer
|
||||||
CsvOptionsUpdated: CSV-Optionen erfolgreich angepasst
|
CsvOptionsUpdated: CSV-Optionen erfolgreich angepasst
|
||||||
CsvChangeOptionsLabel: Export-Optionen
|
CsvChangeOptionsLabel: Export-Optionen
|
||||||
|
CsvFormatField: Dateiformat
|
||||||
|
CsvFormatCsv: .csv (Comma-Separated Values)
|
||||||
|
CsvFormatXlsx: .xlsx (Office Open XML)
|
||||||
|
|
||||||
CourseNews: Aktuelles
|
CourseNews: Aktuelles
|
||||||
CourseNewsArchiveName tid@TermId ssh@SchoolId csh@CourseShorthand newsTitle@Text: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-#{foldCase newsTitle}
|
CourseNewsArchiveName tid@TermId ssh@SchoolId csh@CourseShorthand newsTitle@Text: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-#{foldCase newsTitle}
|
||||||
@ -2844,6 +2854,7 @@ CsvColumnAllocationUserAssigned: Anzahl von Plätzen, die der Bewerber durch die
|
|||||||
CsvColumnAllocationUserNewAssigned: Anzahl von Plätzen, die der Bewerber, nach Akzeptieren der berechneten Verteilung, zusätzlich erhalten würde
|
CsvColumnAllocationUserNewAssigned: Anzahl von Plätzen, die der Bewerber, nach Akzeptieren der berechneten Verteilung, zusätzlich erhalten würde
|
||||||
CsvColumnAllocationUserPriority: Zentrale Dringlichkeit des Bewerbers; entweder einzelne Zahl für Sortierungsbasierte Dringlichkeiten (höhere Dringlichkeit entspricht größerer Zahl) oder Komma-separierte Liste von numerischen Dringlichkeiten in eckigen Klammern (z.B. [1, 2, 3])
|
CsvColumnAllocationUserPriority: Zentrale Dringlichkeit des Bewerbers; entweder einzelne Zahl für Sortierungsbasierte Dringlichkeiten (höhere Dringlichkeit entspricht größerer Zahl) oder Komma-separierte Liste von numerischen Dringlichkeiten in eckigen Klammern (z.B. [1, 2, 3])
|
||||||
AllocationUsersCsvName tid@TermId ssh@SchoolId ash@AllocationShorthand: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase ash}-bewerber
|
AllocationUsersCsvName tid@TermId ssh@SchoolId ash@AllocationShorthand: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase ash}-bewerber
|
||||||
|
AllocationUsersCsvSheetName tid@TermId ssh@SchoolId ash@AllocationShorthand: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase ash} Bewerber
|
||||||
|
|
||||||
AllocationPrioritiesMode: Modus
|
AllocationPrioritiesMode: Modus
|
||||||
AllocationPrioritiesNumeric: Numerische Dringlichkeiten
|
AllocationPrioritiesNumeric: Numerische Dringlichkeiten
|
||||||
|
|||||||
@ -1629,6 +1629,7 @@ CommSuccess n: Message was sent to #{n} #{pluralEN n "recipient" "recipients"}
|
|||||||
CommTestSuccess: Message was sent only to yourself for testing purposes
|
CommTestSuccess: Message was sent only to yourself for testing purposes
|
||||||
CommUndisclosedRecipients: Undisclosed recipients
|
CommUndisclosedRecipients: Undisclosed recipients
|
||||||
CommAllRecipients: all-recipients
|
CommAllRecipients: all-recipients
|
||||||
|
CommAllRecipientsSheet: Recipients
|
||||||
|
|
||||||
CommCourseHeading: Course message
|
CommCourseHeading: Course message
|
||||||
CommTutorialHeading: Tutorial message
|
CommTutorialHeading: Tutorial message
|
||||||
@ -2147,10 +2148,15 @@ Proportion c of' prop: #{c}/#{of'} (#{rationalToFixed2 (100 * prop)}%)
|
|||||||
ProportionNoRatio c of': #{c}/#{of'}
|
ProportionNoRatio c of': #{c}/#{of'}
|
||||||
|
|
||||||
CourseUserCsvName tid ssh csh: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-participants
|
CourseUserCsvName tid ssh csh: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-participants
|
||||||
|
CourseUserCsvSheetName tid ssh csh: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh} Participants
|
||||||
ExamUserCsvName tid ssh csh examn: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-#{foldedCase examn}-participants
|
ExamUserCsvName tid ssh csh examn: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-#{foldedCase examn}-participants
|
||||||
|
ExamUserCsvSheetName tid ssh csh examn: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-#{foldedCase examn} Participants
|
||||||
ExternalExamUserCsvName tid@TermId ssh@SchoolId coursen@CourseName examn@ExamName: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase coursen}-#{foldedCase examn}-participants
|
ExternalExamUserCsvName tid@TermId ssh@SchoolId coursen@CourseName examn@ExamName: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase coursen}-#{foldedCase examn}-participants
|
||||||
|
ExternalExamUserCsvSheetName tid@TermId ssh@SchoolId coursen@CourseName examn@ExamName: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase coursen}-#{foldedCase examn} Participants
|
||||||
CourseApplicationsTableCsvName tid ssh csh: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-applications
|
CourseApplicationsTableCsvName tid ssh csh: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-applications
|
||||||
|
CourseApplicationsTableCsvSheetName tid ssh csh: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh} Applications
|
||||||
ParticipantsCsvName tid ssh: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-participants
|
ParticipantsCsvName tid ssh: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-participants
|
||||||
|
ParticipantsCsvSheetName tid ssh: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)} Participants
|
||||||
|
|
||||||
CourseUserCsvIncludeSheets: Exercise sheets
|
CourseUserCsvIncludeSheets: Exercise sheets
|
||||||
CourseUserCsvIncludeSheetsTip: Should the exportet CSV-file additionally contain one column per exercise sheet?
|
CourseUserCsvIncludeSheetsTip: Should the exportet CSV-file additionally contain one column per exercise sheet?
|
||||||
@ -2566,8 +2572,9 @@ CsvOptionsTip: These settings primarily affect CSV export. During import most se
|
|||||||
CsvFormatOptions: File format
|
CsvFormatOptions: File format
|
||||||
CsvTimestamp: Timestamp
|
CsvTimestamp: Timestamp
|
||||||
CsvTimestampTip: Should the name of every exported csv file contain a timestamp?
|
CsvTimestampTip: Should the name of every exported csv file contain a timestamp?
|
||||||
CsvPresetRFC: Standards-compliant (RFC 4180)
|
CsvPresetRFC: Standards-compliant .csv files (RFC 4180)
|
||||||
CsvPresetExcel: Excel compatible
|
CsvPresetExcel: Excel compatible .csv files (Excel <2010)
|
||||||
|
CsvPresetXlsx: .xlsx files (ECMA-376; Excel ≥2010)
|
||||||
CsvCustom: User defined
|
CsvCustom: User defined
|
||||||
CsvDelimiter: Separator character
|
CsvDelimiter: Separator character
|
||||||
CsvUseCrLf: Linebreaks
|
CsvUseCrLf: Linebreaks
|
||||||
@ -2592,6 +2599,9 @@ CsvQuoteMinimal: Only when necessary
|
|||||||
CsvQuoteAll: Always
|
CsvQuoteAll: Always
|
||||||
CsvOptionsUpdated: Successfully changed CSV options
|
CsvOptionsUpdated: Successfully changed CSV options
|
||||||
CsvChangeOptionsLabel: Export options
|
CsvChangeOptionsLabel: Export options
|
||||||
|
CsvFormatField: File format
|
||||||
|
CsvFormatCsv: .csv (comma-separated values)
|
||||||
|
CsvFormatXlsx: .xlsx (Office Open XML)
|
||||||
|
|
||||||
CourseNews: News
|
CourseNews: News
|
||||||
CourseNewsArchiveName tid ssh csh newsTitle: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-#{foldCase newsTitle}
|
CourseNewsArchiveName tid ssh csh newsTitle: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-#{foldCase newsTitle}
|
||||||
@ -2844,6 +2854,7 @@ CsvColumnAllocationUserAssigned: Number of assignments the applicant has already
|
|||||||
CsvColumnAllocationUserNewAssigned: Number of assignments the applicant would receive, if the calculated matching is accepted
|
CsvColumnAllocationUserNewAssigned: Number of assignments the applicant would receive, if the calculated matching is accepted
|
||||||
CsvColumnAllocationUserPriority: Central priority of this applicant; either a number based on the applicants position in the list sorted by priority (higher numbers mean a higher priority) or a comma-separated list of numerical priorities in square brackets (e.g. [1, 2, 3])
|
CsvColumnAllocationUserPriority: Central priority of this applicant; either a number based on the applicants position in the list sorted by priority (higher numbers mean a higher priority) or a comma-separated list of numerical priorities in square brackets (e.g. [1, 2, 3])
|
||||||
AllocationUsersCsvName tid ssh ash: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase ash}-applicants
|
AllocationUsersCsvName tid ssh ash: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase ash}-applicants
|
||||||
|
AllocationUsersCsvSheetName tid ssh ash: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase ash} Applicants
|
||||||
|
|
||||||
AllocationPrioritiesMode: Mode
|
AllocationPrioritiesMode: Mode
|
||||||
AllocationPrioritiesNumeric: Numeric priorities
|
AllocationPrioritiesNumeric: Numeric priorities
|
||||||
|
|||||||
@ -163,6 +163,7 @@ dependencies:
|
|||||||
- IntervalMap
|
- IntervalMap
|
||||||
- haskell-src-meta
|
- haskell-src-meta
|
||||||
- either
|
- either
|
||||||
|
- xlsx
|
||||||
other-extensions:
|
other-extensions:
|
||||||
- GeneralizedNewtypeDeriving
|
- GeneralizedNewtypeDeriving
|
||||||
- IncoherentInstances
|
- IncoherentInstances
|
||||||
|
|||||||
@ -307,7 +307,7 @@ makeFoundation appSettings''@AppSettings{..} = do
|
|||||||
conn <- Minio.connect minioConf
|
conn <- Minio.connect minioConf
|
||||||
let isBucketExists Minio.BucketAlreadyOwnedByYou = True
|
let isBucketExists Minio.BucketAlreadyOwnedByYou = True
|
||||||
isBucketExists _ = False
|
isBucketExists _ = False
|
||||||
either throwM return <=< Minio.runMinioWith conn $ do
|
throwLeft <=< Minio.runMinioWith conn $ do
|
||||||
handleIf isBucketExists (const $ return ()) $ Minio.makeBucket appUploadCacheBucket Nothing
|
handleIf isBucketExists (const $ return ()) $ Minio.makeBucket appUploadCacheBucket Nothing
|
||||||
handleIf isBucketExists (const $ return ()) $ Minio.makeBucket appUploadTmpBucket Nothing
|
handleIf isBucketExists (const $ return ()) $ Minio.makeBucket appUploadTmpBucket Nothing
|
||||||
return conn
|
return conn
|
||||||
|
|||||||
@ -129,14 +129,14 @@ campusUserWith withLdap' pool mode Creds{..} = either (throwM . CampusUserLdapEr
|
|||||||
_otherwise -> throwE CampusUserAmbiguous
|
_otherwise -> throwE CampusUserAmbiguous
|
||||||
|
|
||||||
campusUserReTest :: (MonadUnliftIO m, MonadMask m, MonadLogger m) => Failover (LdapConf, LdapPool) -> (Nano -> Bool) -> FailoverMode -> Creds site -> m (Ldap.AttrList [])
|
campusUserReTest :: (MonadUnliftIO m, MonadMask m, MonadLogger m) => Failover (LdapConf, LdapPool) -> (Nano -> Bool) -> FailoverMode -> Creds site -> m (Ldap.AttrList [])
|
||||||
campusUserReTest pool doTest mode creds = either throwM return =<< campusUserWith (\l -> flip (withLdapFailoverReTest l) doTest) pool mode creds
|
campusUserReTest pool doTest mode creds = throwLeft =<< campusUserWith (\l -> flip (withLdapFailoverReTest l) doTest) pool mode creds
|
||||||
|
|
||||||
campusUserReTest' :: (MonadMask m, MonadLogger m, MonadUnliftIO m) => Failover (LdapConf, LdapPool) -> (Nano -> Bool) -> FailoverMode -> User -> m (Maybe (Ldap.AttrList []))
|
campusUserReTest' :: (MonadMask m, MonadLogger m, MonadUnliftIO m) => Failover (LdapConf, LdapPool) -> (Nano -> Bool) -> FailoverMode -> User -> m (Maybe (Ldap.AttrList []))
|
||||||
campusUserReTest' pool doTest mode User{userIdent}
|
campusUserReTest' pool doTest mode User{userIdent}
|
||||||
= runMaybeT . catchIfMaybeT (is _CampusUserNoResult) $ campusUserReTest pool doTest mode (Creds apLdap (CI.original userIdent) [])
|
= runMaybeT . catchIfMaybeT (is _CampusUserNoResult) $ campusUserReTest pool doTest mode (Creds apLdap (CI.original userIdent) [])
|
||||||
|
|
||||||
campusUser :: (MonadUnliftIO m, MonadMask m, MonadLogger m) => Failover (LdapConf, LdapPool) -> FailoverMode -> Creds site -> m (Ldap.AttrList [])
|
campusUser :: (MonadUnliftIO m, MonadMask m, MonadLogger m) => Failover (LdapConf, LdapPool) -> FailoverMode -> Creds site -> m (Ldap.AttrList [])
|
||||||
campusUser pool mode creds = either throwM return =<< campusUserWith withLdapFailover pool mode creds
|
campusUser pool mode creds = throwLeft =<< campusUserWith withLdapFailover pool mode creds
|
||||||
|
|
||||||
campusUser' :: (MonadMask m, MonadUnliftIO m, MonadLogger m) => Failover (LdapConf, LdapPool) -> FailoverMode -> User -> m (Maybe (Ldap.AttrList []))
|
campusUser' :: (MonadMask m, MonadUnliftIO m, MonadLogger m) => Failover (LdapConf, LdapPool) -> FailoverMode -> User -> m (Maybe (Ldap.AttrList []))
|
||||||
campusUser' pool mode User{userIdent}
|
campusUser' pool mode User{userIdent}
|
||||||
|
|||||||
@ -32,3 +32,10 @@ instance Read DynEncoding where
|
|||||||
|
|
||||||
instance Ord DynEncoding where
|
instance Ord DynEncoding where
|
||||||
compare = comparing show
|
compare = comparing show
|
||||||
|
|
||||||
|
instance Hashable DynEncoding where
|
||||||
|
hashWithSalt s = hashWithSalt s . show
|
||||||
|
|
||||||
|
|
||||||
|
instance NFData DynEncoding where
|
||||||
|
rnf enc = rnf $ show enc
|
||||||
|
|||||||
@ -259,7 +259,7 @@ isDryRun = $cachedHere . liftHandler $ orM
|
|||||||
let noTokenAuth :: AuthDNF -> AuthDNF
|
let noTokenAuth :: AuthDNF -> AuthDNF
|
||||||
noTokenAuth = over _dnfTerms . Set.filter . noneOf (re _nullable . folded) $ (== AuthToken) . plVar
|
noTokenAuth = over _dnfTerms . Set.filter . noneOf (re _nullable . folded) $ (== AuthToken) . plVar
|
||||||
|
|
||||||
dnf <- either throwM return $ routeAuthTags currentRoute
|
dnf <- throwLeft $ routeAuthTags currentRoute
|
||||||
let eval :: forall m'. MonadAP m' => AuthTagsEval m'
|
let eval :: forall m'. MonadAP m' => AuthTagsEval m'
|
||||||
eval dnf' mAuthId' route' isWrite' = evalAuthTags 'isDryRun (AuthTagActive $ const True) eval (noTokenAuth dnf') mAuthId' route' isWrite'
|
eval dnf' mAuthId' route' isWrite' = evalAuthTags 'isDryRun (AuthTagActive $ const True) eval (noTokenAuth dnf') mAuthId' route' isWrite'
|
||||||
in guardAuthResult <=< evalWriterT $ eval dnf mAuthId currentRoute isWrite
|
in guardAuthResult <=< evalWriterT $ eval dnf mAuthId currentRoute isWrite
|
||||||
@ -340,7 +340,7 @@ validateBearer mAuthId' route' isWrite' token' = $runCachedMemoT $ for4 memo val
|
|||||||
guardMExceptT (Just bearerIssuedAt >= userTokensIssuedAfter) (unauthorizedI MsgUnauthorizedTokenExpired)
|
guardMExceptT (Just bearerIssuedAt >= userTokensIssuedAfter) (unauthorizedI MsgUnauthorizedTokenExpired)
|
||||||
|
|
||||||
authorityVal <- do
|
authorityVal <- do
|
||||||
dnf <- either throwM return $ routeAuthTags route
|
dnf <- throwLeft $ routeAuthTags route
|
||||||
lift . evalWriterT $ eval (noTokenAuth dnf) (Just uid) route isWrite
|
lift . evalWriterT $ eval (noTokenAuth dnf) (Just uid) route isWrite
|
||||||
guardExceptT (is _Authorized authorityVal) authorityVal
|
guardExceptT (is _Authorized authorityVal) authorityVal
|
||||||
|
|
||||||
@ -1807,7 +1807,7 @@ evalAccessWithFor assumptions mAuthId route isWrite = do
|
|||||||
tagActive <- if
|
tagActive <- if
|
||||||
| isSelf -> fromMaybe def <$> lookupSessionJson SessionActiveAuthTags
|
| isSelf -> fromMaybe def <$> lookupSessionJson SessionActiveAuthTags
|
||||||
| otherwise -> return . AuthTagActive $ const True
|
| otherwise -> return . AuthTagActive $ const True
|
||||||
dnf <- either throwM return $ routeAuthTags route
|
dnf <- throwLeft $ routeAuthTags route
|
||||||
let adjDNF = ala Endo foldMap (map ((=<<) . uncurry dnfAssumeValue) assumptions) . Just
|
let adjDNF = ala Endo foldMap (map ((=<<) . uncurry dnfAssumeValue) assumptions) . Just
|
||||||
evalAdj :: forall m'. MonadAP m' => AuthTagsEval m'
|
evalAdj :: forall m'. MonadAP m' => AuthTagsEval m'
|
||||||
evalAdj (adjDNF -> dnf') mAuthId' route' isWrite' = case dnf' of
|
evalAdj (adjDNF -> dnf') mAuthId' route' isWrite' = case dnf' of
|
||||||
|
|||||||
@ -226,6 +226,7 @@ embedRenderMessage ''UniWorX ''SchoolFunction id
|
|||||||
embedRenderMessage ''UniWorX ''SystemFunction id
|
embedRenderMessage ''UniWorX ''SystemFunction id
|
||||||
embedRenderMessage ''UniWorX ''CsvPreset id
|
embedRenderMessage ''UniWorX ''CsvPreset id
|
||||||
embedRenderMessage ''UniWorX ''Quoting ("Csv" <>)
|
embedRenderMessage ''UniWorX ''Quoting ("Csv" <>)
|
||||||
|
embedRenderMessage ''UniWorX ''CsvFormat ("Csv" <>)
|
||||||
embedRenderMessage ''UniWorX ''FavouriteReason id
|
embedRenderMessage ''UniWorX ''FavouriteReason id
|
||||||
embedRenderMessage ''UniWorX ''Sex id
|
embedRenderMessage ''UniWorX ''Sex id
|
||||||
embedRenderMessage ''UniWorX ''ExamGradingMode id
|
embedRenderMessage ''UniWorX ''ExamGradingMode id
|
||||||
|
|||||||
@ -42,6 +42,7 @@ emailTestForm = (,)
|
|||||||
<*> areq (selectField $ dateTimeFormatOptions SelFormatDate) (fslI MsgDateFormat) Nothing
|
<*> areq (selectField $ dateTimeFormatOptions SelFormatDate) (fslI MsgDateFormat) Nothing
|
||||||
<*> areq (selectField $ dateTimeFormatOptions SelFormatTime) (fslI MsgTimeFormat) Nothing
|
<*> areq (selectField $ dateTimeFormatOptions SelFormatTime) (fslI MsgTimeFormat) Nothing
|
||||||
)
|
)
|
||||||
|
<*> pure def
|
||||||
)
|
)
|
||||||
where
|
where
|
||||||
toMailDateTimeFormat dt d t = \case
|
toMailDateTimeFormat dt d t = \case
|
||||||
|
|||||||
@ -165,8 +165,6 @@ postAUsersR tid ssh ash = do
|
|||||||
allocMatching <- fmap (view _4) . hoistMaybe $ allocMap !? (tid, ssh, ash)
|
allocMatching <- fmap (view _4) . hoistMaybe $ allocMap !? (tid, ssh, ash)
|
||||||
return $ Map.fromListWith (<>) [ (uid, opoint cid) | (uid, cid) <- Set.toList allocMatching ] :: _ (Map UserId (NonNull (Set CourseId)))
|
return $ Map.fromListWith (<>) [ (uid, opoint cid) | (uid, cid) <- Set.toList allocMatching ] :: _ (Map UserId (NonNull (Set CourseId)))
|
||||||
|
|
||||||
csvName <- getMessageRender <*> pure (MsgAllocationUsersCsvName tid ssh ash)
|
|
||||||
|
|
||||||
let
|
let
|
||||||
allocationUsersDBTable = DBTable{..}
|
allocationUsersDBTable = DBTable{..}
|
||||||
where
|
where
|
||||||
@ -296,6 +294,8 @@ postAUsersR tid ssh ash = do
|
|||||||
dbtParams = def
|
dbtParams = def
|
||||||
dbtIdent :: Text
|
dbtIdent :: Text
|
||||||
dbtIdent = "allocation-users"
|
dbtIdent = "allocation-users"
|
||||||
|
dbtCsvName = MsgAllocationUsersCsvName tid ssh ash
|
||||||
|
dbtCsvSheetName = MsgAllocationUsersCsvSheetName tid ssh ash
|
||||||
dbtCsvEncode = return DBTCsvEncode
|
dbtCsvEncode = return DBTCsvEncode
|
||||||
{ dbtCsvExportForm = pure ()
|
{ dbtCsvExportForm = pure ()
|
||||||
, dbtCsvDoEncode = \() -> C.mapM $ \(_, row) -> flip runReaderT row $
|
, dbtCsvDoEncode = \() -> C.mapM $ \(_, row) -> flip runReaderT row $
|
||||||
@ -311,7 +311,7 @@ postAUsersR tid ssh ash = do
|
|||||||
<*> view (resultAssignedCourses . _Integral)
|
<*> view (resultAssignedCourses . _Integral)
|
||||||
<*> views (resultUser . _entityKey) (\uid -> maybe 0 (fromIntegral . olength) . Map.lookup uid <$> allocMatching)
|
<*> views (resultUser . _entityKey) (\uid -> maybe 0 (fromIntegral . olength) . Map.lookup uid <$> allocMatching)
|
||||||
<*> view (resultAllocationUser . _entityVal . _allocationUserPriority)
|
<*> view (resultAllocationUser . _entityVal . _allocationUserPriority)
|
||||||
, dbtCsvName = unpack csvName
|
, dbtCsvName, dbtCsvSheetName
|
||||||
, dbtCsvNoExportData = Just id
|
, dbtCsvNoExportData = Just id
|
||||||
, dbtCsvHeader = \_ -> return . userTableCsvHeader $ is _Just allocMatching
|
, dbtCsvHeader = \_ -> return . userTableCsvHeader $ is _Just allocMatching
|
||||||
, dbtCsvExampleData = Nothing
|
, dbtCsvExampleData = Nothing
|
||||||
|
|||||||
@ -232,7 +232,6 @@ postCApplicationsR tid ssh csh = do
|
|||||||
now <- liftIO getCurrentTime
|
now <- liftIO getCurrentTime
|
||||||
Entity cid Course{..} <- getBy404 $ TermSchoolCourseShort tid ssh csh
|
Entity cid Course{..} <- getBy404 $ TermSchoolCourseShort tid ssh csh
|
||||||
|
|
||||||
csvName <- getMessageRender <*> pure (MsgCourseApplicationsTableCsvName tid ssh csh)
|
|
||||||
let
|
let
|
||||||
allocationLink :: Allocation -> SomeRoute UniWorX
|
allocationLink :: Allocation -> SomeRoute UniWorX
|
||||||
allocationLink Allocation{..} = SomeRoute $ AllocationR allocationTerm allocationSchool allocationShorthand AShowR
|
allocationLink Allocation{..} = SomeRoute $ AllocationR allocationTerm allocationSchool allocationShorthand AShowR
|
||||||
@ -358,7 +357,9 @@ postCApplicationsR tid ssh csh = do
|
|||||||
}
|
}
|
||||||
dbtParams = def
|
dbtParams = def
|
||||||
|
|
||||||
dbtCsvEncode = simpleCsvEncodeM csvName $ CourseApplicationsTableCsv
|
dbtCsvName = MsgCourseApplicationsTableCsvName tid ssh csh
|
||||||
|
dbtCsvSheetName = MsgCourseApplicationsTableCsvSheetName tid ssh csh
|
||||||
|
dbtCsvEncode = simpleCsvEncodeM dbtCsvName dbtCsvSheetName $ CourseApplicationsTableCsv
|
||||||
<$> preview (resultAllocation . _entityVal . _allocationShorthand)
|
<$> preview (resultAllocation . _entityVal . _allocationShorthand)
|
||||||
<*> (preview (resultCourseApplication . _entityKey) >>= traverse encrypt)
|
<*> (preview (resultCourseApplication . _entityKey) >>= traverse encrypt)
|
||||||
<*> preview (resultUser . _entityVal . _userDisplayName)
|
<*> preview (resultUser . _entityVal . _userDisplayName)
|
||||||
|
|||||||
@ -294,7 +294,6 @@ makeCourseUserTable :: forall h p cols act act'.
|
|||||||
makeCourseUserTable cid acts restrict colChoices psValidator csvColumns = do
|
makeCourseUserTable cid acts restrict colChoices psValidator csvColumns = do
|
||||||
currentRoute <- fromMaybe (error "makeCourseUserTable called from 404-handler") <$> liftHandler getCurrentRoute
|
currentRoute <- fromMaybe (error "makeCourseUserTable called from 404-handler") <$> liftHandler getCurrentRoute
|
||||||
Course{..} <- getJust cid
|
Course{..} <- getJust cid
|
||||||
csvName <- getMessageRender <*> pure (MsgCourseUserCsvName courseTerm courseSchool courseShorthand)
|
|
||||||
tutorials <- selectList [ TutorialCourse ==. cid ] []
|
tutorials <- selectList [ TutorialCourse ==. cid ] []
|
||||||
exams <- selectList [ ExamCourse ==. cid ] []
|
exams <- selectList [ ExamCourse ==. cid ] []
|
||||||
sheets <- selectList [SheetCourse ==. cid] [Desc SheetActiveTo, Desc SheetActiveFrom]
|
sheets <- selectList [SheetCourse ==. cid] [Desc SheetActiveTo, Desc SheetActiveFrom]
|
||||||
@ -452,6 +451,8 @@ makeCourseUserTable cid acts restrict colChoices psValidator csvColumns = do
|
|||||||
, dbParamsFormResult = id
|
, dbParamsFormResult = id
|
||||||
, dbParamsFormIdent = def
|
, dbParamsFormIdent = def
|
||||||
}
|
}
|
||||||
|
dbtCsvName = MsgCourseUserCsvName courseTerm courseSchool courseShorthand
|
||||||
|
dbtCsvSheetName = MsgCourseUserCsvSheetName courseTerm courseSchool courseShorthand
|
||||||
dbtCsvEncode = do
|
dbtCsvEncode = do
|
||||||
csvColumns' <- csvColumns
|
csvColumns' <- csvColumns
|
||||||
return $ DBTCsvEncode
|
return $ DBTCsvEncode
|
||||||
@ -471,7 +472,7 @@ makeCourseUserTable cid acts restrict colChoices psValidator csvColumns = do
|
|||||||
-- <*> (over (_2.traverse._Just) (examName . entityVal) . over (_1.traverse) (examName . entityVal) <$> view _userExams)
|
-- <*> (over (_2.traverse._Just) (examName . entityVal) . over (_1.traverse) (examName . entityVal) <$> view _userExams)
|
||||||
<*> (over traverse (examName . entityVal) <$> view _userExams)
|
<*> (over traverse (examName . entityVal) <$> view _userExams)
|
||||||
<*> views _userSheets (set (mapped . _1 . mapped) ())
|
<*> views _userSheets (set (mapped . _1 . mapped) ())
|
||||||
, dbtCsvName = unpack csvName
|
, dbtCsvName, dbtCsvSheetName
|
||||||
, dbtCsvNoExportData = Nothing
|
, dbtCsvNoExportData = Nothing
|
||||||
, dbtCsvHeader = return . Vector.filter csvColumns' . userTableCsvHeader showSex tutorials sheets . fromMaybe def
|
, dbtCsvHeader = return . Vector.filter csvColumns' . userTableCsvHeader showSex tutorials sheets . fromMaybe def
|
||||||
, dbtCsvExampleData = Nothing
|
, dbtCsvExampleData = Nothing
|
||||||
@ -482,7 +483,7 @@ makeCourseUserTable cid acts restrict colChoices psValidator csvColumns = do
|
|||||||
CourseUserNote{..} <- lift . lift $ getJust noteId
|
CourseUserNote{..} <- lift . lift $ getJust noteId
|
||||||
return courseUserNoteNote
|
return courseUserNoteNote
|
||||||
dbtCsvDecode = Nothing
|
dbtCsvDecode = Nothing
|
||||||
dbtExtraReps = withCsvExtraRep (UserCsvExportData True) dbtCsvEncode []
|
dbtExtraReps = withCsvExtraRep dbtCsvSheetName (UserCsvExportData True) dbtCsvEncode []
|
||||||
over _1 postprocess <$> dbTable psValidator DBTable{..}
|
over _1 postprocess <$> dbTable psValidator DBTable{..}
|
||||||
where
|
where
|
||||||
postprocess :: FormResult (First act', DBFormResult UserId Bool UserTableData) -> FormResult (act', Set UserId)
|
postprocess :: FormResult (First act', DBFormResult UserId Bool UserTableData) -> FormResult (act', Set UserId)
|
||||||
|
|||||||
@ -420,8 +420,6 @@ postEUsersR tid ssh csh examn = do
|
|||||||
| otherwise
|
| otherwise
|
||||||
-> i18nCell man & cellAttrs <>~ [("class", "table__td--overriden")] & tellCell (Any True, mempty)
|
-> i18nCell man & cellAttrs <>~ [("class", "table__td--overriden")] & tellCell (Any True, mempty)
|
||||||
|
|
||||||
csvName <- getMessageRender <*> pure (MsgExamUserCsvName tid ssh csh examn)
|
|
||||||
|
|
||||||
let
|
let
|
||||||
examUsersDBTable = DBTable{..}
|
examUsersDBTable = DBTable{..}
|
||||||
where
|
where
|
||||||
@ -590,10 +588,12 @@ postEUsersR tid ssh csh examn = do
|
|||||||
}
|
}
|
||||||
dbtIdent :: Text
|
dbtIdent :: Text
|
||||||
dbtIdent = "exam-users"
|
dbtIdent = "exam-users"
|
||||||
|
dbtCsvName = MsgExamUserCsvName tid ssh csh examn
|
||||||
|
dbtCsvSheetName = MsgExamUserCsvSheetName tid ssh csh examn
|
||||||
dbtCsvEncode = Just DBTCsvEncode
|
dbtCsvEncode = Just DBTCsvEncode
|
||||||
{ dbtCsvExportForm = pure ()
|
{ dbtCsvExportForm = pure ()
|
||||||
, dbtCsvDoEncode = \() -> C.map (doEncode' . view _2)
|
, dbtCsvDoEncode = \() -> C.map (doEncode' . view _2)
|
||||||
, dbtCsvName = unpack csvName
|
, dbtCsvName, dbtCsvSheetName
|
||||||
, dbtCsvNoExportData = Just id
|
, dbtCsvNoExportData = Just id
|
||||||
, dbtCsvHeader = const . return . examUserTableCsvHeader allBoni doBonus $ examParts ^.. folded . _entityVal . _examPartNumber
|
, dbtCsvHeader = const . return . examUserTableCsvHeader allBoni doBonus $ examParts ^.. folded . _entityVal . _examPartNumber
|
||||||
, dbtCsvExampleData = Nothing
|
, dbtCsvExampleData = Nothing
|
||||||
|
|||||||
@ -190,7 +190,6 @@ postEGradesR tid ssh csh examn = do
|
|||||||
exam@(Entity eid Exam{..}) <- fetchExam tid ssh csh examn
|
exam@(Entity eid Exam{..}) <- fetchExam tid ssh csh examn
|
||||||
Course{..} <- getJust examCourse
|
Course{..} <- getJust examCourse
|
||||||
|
|
||||||
csvName <- getMessageRender <*> pure (MsgExamUserCsvName tid ssh csh examn)
|
|
||||||
isLecturer <- hasReadAccessTo $ CExamR tid ssh csh examn EUsersR
|
isLecturer <- hasReadAccessTo $ CExamR tid ssh csh examn EUsersR
|
||||||
userFunctions <- selectList [ UserFunctionUser ==. uid, UserFunctionFunction ==. SchoolExamOffice ] []
|
userFunctions <- selectList [ UserFunctionUser ==. uid, UserFunctionFunction ==. SchoolExamOffice ] []
|
||||||
|
|
||||||
@ -386,6 +385,8 @@ postEGradesR tid ssh csh examn = do
|
|||||||
}
|
}
|
||||||
dbtIdent :: Text
|
dbtIdent :: Text
|
||||||
dbtIdent = "exam-results"
|
dbtIdent = "exam-results"
|
||||||
|
dbtCsvName = MsgExamUserCsvName tid ssh csh examn
|
||||||
|
dbtCsvSheetName = MsgExamUserCsvSheetName tid ssh csh examn
|
||||||
dbtCsvEncode = Just DBTCsvEncode
|
dbtCsvEncode = Just DBTCsvEncode
|
||||||
{ dbtCsvExportForm = ExamUserCsvExportData
|
{ dbtCsvExportForm = ExamUserCsvExportData
|
||||||
<$> apopt checkBoxField (fslI MsgExamUserMarkSynchronisedCsv & setTooltip MsgExamUserMarkSynchronisedCsvTip) (Just False)
|
<$> apopt checkBoxField (fslI MsgExamUserMarkSynchronisedCsv & setTooltip MsgExamUserMarkSynchronisedCsvTip) (Just False)
|
||||||
@ -399,7 +400,7 @@ postEGradesR tid ssh csh examn = do
|
|||||||
(row ^. resultStudyFeatures)
|
(row ^. resultStudyFeatures)
|
||||||
(row ^? (resultExamOccurrence . _entityVal . _examOccurrenceStart <> like examStart . _Just) . to utcToZonedTime)
|
(row ^? (resultExamOccurrence . _entityVal . _examOccurrenceStart <> like examStart . _Just) . to utcToZonedTime)
|
||||||
(row ^. resultExamResult . _entityVal . _examResultResult)
|
(row ^. resultExamResult . _entityVal . _examResultResult)
|
||||||
, dbtCsvName = unpack csvName
|
, dbtCsvName, dbtCsvSheetName
|
||||||
, dbtCsvNoExportData = Nothing
|
, dbtCsvNoExportData = Nothing
|
||||||
, dbtCsvHeader = const . return $ Csv.headerOrder (error "headerOrder" :: ExamUserTableCsv)
|
, dbtCsvHeader = const . return $ Csv.headerOrder (error "headerOrder" :: ExamUserTableCsv)
|
||||||
, dbtCsvExampleData = Nothing
|
, dbtCsvExampleData = Nothing
|
||||||
|
|||||||
@ -67,9 +67,8 @@ getParticipantsListR = do
|
|||||||
|
|
||||||
getParticipantsR :: TermId -> SchoolId -> Handler TypedContent
|
getParticipantsR :: TermId -> SchoolId -> Handler TypedContent
|
||||||
getParticipantsR tid ssh = do
|
getParticipantsR tid ssh = do
|
||||||
csvName <- timestampCsv <*> fmap ((flip addExtension `on` unpack) extensionCsv) (getMessageRender <*> pure (MsgParticipantsCsvName tid ssh))
|
setContentDispositionCsv $ MsgParticipantsCsvName tid ssh
|
||||||
setContentDisposition' $ Just csvName
|
respondDefaultOrderedCsvDB (MsgParticipantsCsvSheetName tid ssh) $ E.selectSource partQuery .| C.map toParticipantEntry
|
||||||
respondDefaultOrderedCsvDB $ E.selectSource partQuery .| C.map toParticipantEntry
|
|
||||||
where
|
where
|
||||||
partQuery = E.from $ \(course `E.InnerJoin` participant `E.InnerJoin` user) -> do
|
partQuery = E.from $ \(course `E.InnerJoin` participant `E.InnerJoin` user) -> do
|
||||||
E.on $ user E.^. UserId E.==. participant E.^. CourseParticipantUser
|
E.on $ user E.^. UserId E.==. participant E.^. CourseParticipantUser
|
||||||
|
|||||||
@ -260,7 +260,7 @@ sourcePersonalisedSheetFiles cId mbsid mbuids anonMode restrs = do
|
|||||||
suf <- lift . lift $ genSuffixes courseParticipantUser
|
suf <- lift . lift $ genSuffixes courseParticipantUser
|
||||||
_sufCache %= Map.insert courseParticipantUser suf
|
_sufCache %= Map.insert courseParticipantUser suf
|
||||||
return suf
|
return suf
|
||||||
cID <- either throwM return . (runReaderT ?? cIDKey) $ I.encrypt courseParticipantUser
|
cID <- throwLeft . (runReaderT ?? cIDKey) $ I.encrypt courseParticipantUser
|
||||||
let dirName = unpack . Text.intercalate "_" . map pack $ suffix `snoc` mkPersonalisedFilesDirectory mbIdx cID
|
let dirName = unpack . Text.intercalate "_" . map pack $ suffix `snoc` mkPersonalisedFilesDirectory mbIdx cID
|
||||||
unlessM (uses _dirCache $ Set.member dirName) $ do
|
unlessM (uses _dirCache $ Set.member dirName) $ do
|
||||||
yield $ Right File
|
yield $ Right File
|
||||||
|
|||||||
@ -2,17 +2,20 @@
|
|||||||
|
|
||||||
module Handler.Utils.Csv
|
module Handler.Utils.Csv
|
||||||
( decodeCsv, decodeCsvPositional
|
( decodeCsv, decodeCsvPositional
|
||||||
, timestampCsv
|
, encodeCsv, encodeCsvWith, encodeCsvRendered, encodeCsvRenderedWith
|
||||||
, encodeCsv
|
, csvRenderedToTypedContent, csvRenderedToTypedContentWith
|
||||||
|
, expectedCsvFormat, expectedCsvContentType
|
||||||
, encodeDefaultOrderedCsv
|
, encodeDefaultOrderedCsv
|
||||||
, respondCsv, respondCsvDB
|
, respondCsv, respondCsvDB
|
||||||
, respondDefaultOrderedCsv, respondDefaultOrderedCsvDB
|
, respondDefaultOrderedCsv, respondDefaultOrderedCsvDB
|
||||||
, fileSourceCsv, fileSourceCsvPositional
|
, fileSourceCsv, fileSourceCsvPositional
|
||||||
, partIsAttachmentCsv
|
, partIsAttachmentCsv, setContentDispositionCsv
|
||||||
|
, csvOptionsForFormat
|
||||||
, CsvParseError(..)
|
, CsvParseError(..)
|
||||||
, ToNamedRecord(..), FromNamedRecord(..)
|
, ToNamedRecord(..), FromNamedRecord(..)
|
||||||
, DefaultOrdered(..)
|
, DefaultOrdered(..)
|
||||||
, ToField(..), FromField(..)
|
, ToField(..), FromField(..)
|
||||||
|
, recodeCsv
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import Import hiding (Header, mapM_)
|
import Import hiding (Header, mapM_)
|
||||||
@ -21,14 +24,15 @@ import Data.Csv
|
|||||||
import Data.Csv.Conduit
|
import Data.Csv.Conduit
|
||||||
|
|
||||||
import Handler.Utils.Form (uploadContents)
|
import Handler.Utils.Form (uploadContents)
|
||||||
|
import Handler.Utils.ContentDisposition (setContentDisposition')
|
||||||
|
|
||||||
import Control.Monad (mapM_)
|
import Control.Monad (mapM_)
|
||||||
|
|
||||||
-- import qualified Data.Csv.Util as Csv
|
-- import qualified Data.Csv.Util as Csv
|
||||||
import qualified Data.Csv.Parser as Csv
|
import qualified Data.Csv.Parser as Csv
|
||||||
|
|
||||||
import qualified Data.Conduit.List as C
|
import qualified Data.Conduit.List as C (mapMaybe)
|
||||||
import qualified Data.Conduit.Combinators as C (sourceLazy)
|
import qualified Data.Conduit.Combinators as C
|
||||||
|
|
||||||
import qualified Data.Map as Map
|
import qualified Data.Map as Map
|
||||||
import qualified Data.Vector as Vector
|
import qualified Data.Vector as Vector
|
||||||
@ -38,13 +42,18 @@ import qualified Data.ByteString.Lazy as LBS
|
|||||||
|
|
||||||
import qualified Data.Attoparsec.ByteString.Lazy as A
|
import qualified Data.Attoparsec.ByteString.Lazy as A
|
||||||
|
|
||||||
import Handler.Utils.DateTime
|
|
||||||
import Data.Time.Format (iso8601DateFormat)
|
import Data.Time.Format (iso8601DateFormat)
|
||||||
|
|
||||||
import qualified Data.Char as Char
|
import qualified Data.Char as Char
|
||||||
|
|
||||||
import Control.Monad.Error.Class (MonadError(..))
|
import Control.Monad.Error.Class (MonadError(..))
|
||||||
|
|
||||||
|
import Data.Time.Clock.POSIX (getPOSIXTime)
|
||||||
|
|
||||||
|
import qualified Data.Time.Format as Time
|
||||||
|
|
||||||
|
-- import qualified Codec.Archive.Zip as Zip
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
_haltingCsvParseError :: Prism' CsvParseError CsvStreamHaltParseError
|
_haltingCsvParseError :: Prism' CsvParseError CsvStreamHaltParseError
|
||||||
@ -82,19 +91,7 @@ decodeCsvPositional hdr = decodeCsv' $ \opts -> fromCsvStreamError opts hdr (rev
|
|||||||
decodeCsv' :: (MonadHandler m, HandlerSite m ~ UniWorX, MonadThrow m) => (forall m'. Monad m' => DecodeOptions -> ConduitT ByteString (MaybeEmptyRecord csv) (ExceptT CsvParseError m') ()) -> ConduitT ByteString csv m ()
|
decodeCsv' :: (MonadHandler m, HandlerSite m ~ UniWorX, MonadThrow m) => (forall m'. Monad m' => DecodeOptions -> ConduitT ByteString (MaybeEmptyRecord csv) (ExceptT CsvParseError m') ()) -> ConduitT ByteString csv m ()
|
||||||
decodeCsv' fromCsv' = do
|
decodeCsv' fromCsv' = do
|
||||||
encOpts <- maybe def (userCsvOptions . entityVal) <$> lift maybeAuth
|
encOpts <- maybe def (userCsvOptions . entityVal) <$> lift maybeAuth
|
||||||
|
recodeCsv encOpts False decodeCsv'' .| C.mapMaybe unMaybeEmptyRecord
|
||||||
let
|
|
||||||
recode'
|
|
||||||
| enc == "UTF8"
|
|
||||||
= id
|
|
||||||
| otherwise
|
|
||||||
= \act -> do
|
|
||||||
inp <- sinkLazy
|
|
||||||
let inp' = encodeLazyByteString UTF8 $ decodeLazyByteString enc inp
|
|
||||||
sourceLazy inp' .| act
|
|
||||||
where enc = encOpts ^. _csvFormat . _csvEncoding
|
|
||||||
|
|
||||||
recode' decodeCsv'' .| C.mapMaybe unMaybeEmptyRecord
|
|
||||||
where
|
where
|
||||||
decodeCsv'' = transPipe throwExceptT $ do
|
decodeCsv'' = transPipe throwExceptT $ do
|
||||||
testBuffer <- accumTestBuffer LBS.empty
|
testBuffer <- accumTestBuffer LBS.empty
|
||||||
@ -160,78 +157,197 @@ decodeCsv' fromCsv' = do
|
|||||||
encodeCsv :: ( ToNamedRecord csv
|
encodeCsv :: ( ToNamedRecord csv
|
||||||
, MonadHandler m
|
, MonadHandler m
|
||||||
, HandlerSite m ~ UniWorX
|
, HandlerSite m ~ UniWorX
|
||||||
|
, RenderMessage UniWorX msg
|
||||||
)
|
)
|
||||||
=> Header
|
=> msg -- ^ Sheet name for .xlsx
|
||||||
-> ConduitT csv ByteString m ()
|
-> Header
|
||||||
|
-> ConduitT csv ByteString m CsvFormat
|
||||||
-- ^ Encode a stream of records
|
-- ^ Encode a stream of records
|
||||||
--
|
--
|
||||||
-- Currently not streaming
|
-- Currently not streaming
|
||||||
encodeCsv hdr = do
|
encodeCsv sheetName hdr = do
|
||||||
csvOpts <- maybe def (userCsvOptions . entityVal) <$> lift maybeAuth
|
encOpts <- maybe def (userCsvOptions . entityVal) <$> lift maybeAuth
|
||||||
let recode'
|
encodeCsvWith encOpts sheetName hdr
|
||||||
| enc == "UTF8"
|
|
||||||
= id
|
encodeCsvWith :: ( ToNamedRecord csv
|
||||||
| otherwise
|
, MonadHandler m
|
||||||
= encodeLazyByteString enc . decodeLazyByteString UTF8
|
, HandlerSite m ~ UniWorX
|
||||||
where enc = csvOpts ^. _csvFormat . _csvEncoding
|
, RenderMessage UniWorX msg
|
||||||
C.foldMap pure >>= (C.sourceLazy . recode') . encodeByNameWith (csvOpts ^. _csvFormat . _CsvEncodeOptions) hdr
|
)
|
||||||
|
=> CsvOptions
|
||||||
|
-> msg -- ^ Sheet name for .xlsx
|
||||||
|
-> Header
|
||||||
|
-> ConduitT csv ByteString m CsvFormat
|
||||||
|
-- ^ Encode a stream of records
|
||||||
|
--
|
||||||
|
-- Currently not streaming
|
||||||
|
encodeCsvWith encOpts sheetName hdr = transPipe liftHandler $ case encOpts ^. _csvFormat of
|
||||||
|
CsvFormatOptions{}
|
||||||
|
| Just csvOpts <- encOpts ^? _csvFormat . _CsvEncodeOptions, has (_csvFormat . _CsvFormat . _FormatCsv) encOpts -> do
|
||||||
|
(C.sourceLazy . encodeByNameWith csvOpts hdr =<< C.foldMap pure) .| recode'
|
||||||
|
return FormatCsv
|
||||||
|
| otherwise -> error "encOpts is CsvFormatOptions but cannot be converted via _CsvEncodeOptions or hasn't _FormatCsv"
|
||||||
|
CsvXlsxFormatOptions{}
|
||||||
|
| has (_csvFormat . _CsvFormat . _FormatXlsx) encOpts -> do
|
||||||
|
rendered <- toCsvRendered hdr <$> C.foldMap (pure @Seq)
|
||||||
|
sheetName' <- ($ sheetName) <$> getMessageRender
|
||||||
|
pNow <- liftIO getPOSIXTime
|
||||||
|
C.sourceLazy (fromXlsx pNow $ csvRenderedToXlsx sheetName' rendered) .| recode'
|
||||||
|
return FormatXlsx
|
||||||
|
| otherwise -> error "encOpts hasn't _FormatXlsx"
|
||||||
|
where recode' = recodeCsv encOpts True $ C.map id
|
||||||
|
|
||||||
|
encodeCsvRendered :: ( MonadHandler m
|
||||||
|
, HandlerSite m ~ UniWorX
|
||||||
|
, RenderMessage UniWorX msg
|
||||||
|
)
|
||||||
|
=> msg -- ^ Sheet name for .xlsx
|
||||||
|
-> CsvRendered
|
||||||
|
-> m (CsvFormat, LBS.ByteString)
|
||||||
|
encodeCsvRendered sheetName CsvRendered{..} = runConduit $ yieldMany csvRenderedData .| (encodeCsv sheetName csvRenderedHeader `fuseBoth` C.sinkLazy)
|
||||||
|
|
||||||
|
encodeCsvRenderedWith :: ( MonadHandler m
|
||||||
|
, HandlerSite m ~ UniWorX
|
||||||
|
, RenderMessage UniWorX msg
|
||||||
|
)
|
||||||
|
=> CsvOptions
|
||||||
|
-> msg -- ^ Sheet name for .xlsx
|
||||||
|
-> CsvRendered
|
||||||
|
-> m (CsvFormat, LBS.ByteString)
|
||||||
|
encodeCsvRenderedWith encOpts sheetName CsvRendered{..} = runConduit $ yieldMany csvRenderedData .| (encodeCsvWith encOpts sheetName csvRenderedHeader `fuseBoth` C.sinkLazy)
|
||||||
|
|
||||||
|
csvRenderedToTypedContent :: ( MonadHandler m
|
||||||
|
, HandlerSite m ~ UniWorX
|
||||||
|
, RenderMessage UniWorX msg
|
||||||
|
)
|
||||||
|
=> msg -- ^ Sheet name for .xlsx
|
||||||
|
-> CsvRendered
|
||||||
|
-> m TypedContent
|
||||||
|
csvRenderedToTypedContent sheetName csvRendered = do
|
||||||
|
encOpts <- maybe def (userCsvOptions . entityVal) <$> maybeAuth
|
||||||
|
csvRenderedToTypedContentWith encOpts sheetName csvRendered
|
||||||
|
|
||||||
|
csvRenderedToTypedContentWith :: ( MonadHandler m
|
||||||
|
, HandlerSite m ~ UniWorX
|
||||||
|
, RenderMessage UniWorX msg
|
||||||
|
)
|
||||||
|
=> CsvOptions
|
||||||
|
-> msg -- ^ Sheet name for .xlsx
|
||||||
|
-> CsvRendered
|
||||||
|
-> m TypedContent
|
||||||
|
csvRenderedToTypedContentWith encOpts sheetName csvRendered = do
|
||||||
|
(csvFormat, resp) <- encodeCsvRenderedWith encOpts sheetName csvRendered
|
||||||
|
let cType = case csvFormat of
|
||||||
|
FormatCsv -> typeCsv'
|
||||||
|
FormatXlsx -> typeXlsx
|
||||||
|
return . TypedContent cType $ toContent resp
|
||||||
|
|
||||||
|
|
||||||
timestampCsv :: ( MonadHandler m
|
timestampCsv :: ( MonadHandler m
|
||||||
, HandlerSite m ~ UniWorX
|
, HandlerSite m ~ UniWorX
|
||||||
)
|
)
|
||||||
=> m (FilePath -> FilePath)
|
=> m (FilePath -> FilePath)
|
||||||
timestampCsv = do
|
timestampCsv = do
|
||||||
csvOpts <- maybe def (userCsvOptions . entityVal) <$> maybeAuth
|
csvOpts <- fmap (maybe def $ userCsvOptions . entityVal) maybeAuth
|
||||||
if
|
timestampCsv' csvOpts . review _Wrapped =<< languages
|
||||||
| 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
|
timestampCsv' :: MonadIO m
|
||||||
|
=> CsvOptions -> Languages -> m (FilePath -> FilePath)
|
||||||
|
timestampCsv' csvOpts (Languages langs) = liftIO $ if
|
||||||
|
| csvOpts ^. _csvTimestamp -> do
|
||||||
|
ts <- getCurrentTime <&> Time.formatTime (getTimeLocale' langs) (iso8601DateFormat $ Just "%H%M")
|
||||||
|
return $ (<>) (ts <> "-")
|
||||||
|
| otherwise -> return id
|
||||||
|
|
||||||
|
expectedCsvFormat :: ( MonadHandler m
|
||||||
|
, HandlerSite m ~ UniWorX
|
||||||
|
)
|
||||||
|
=> m CsvFormat
|
||||||
|
expectedCsvFormat = view (_csvFormat . _CsvFormat) . maybe def (userCsvOptions . entityVal) <$> maybeAuth
|
||||||
|
|
||||||
|
expectedCsvContentType :: ( MonadHandler m
|
||||||
|
, HandlerSite m ~ UniWorX
|
||||||
|
)
|
||||||
|
=> m ContentType
|
||||||
|
expectedCsvContentType = expectedCsvFormat <&> \case
|
||||||
|
FormatCsv -> typeCsv'
|
||||||
|
FormatXlsx -> typeXlsx
|
||||||
|
|
||||||
|
partIsAttachmentCsv :: (RenderMessage UniWorX msg, MonadMail m, HandlerSite m ~ UniWorX)
|
||||||
|
=> msg
|
||||||
-> StateT Part m ()
|
-> StateT Part m ()
|
||||||
partIsAttachmentCsv (repack -> fName) = do
|
partIsAttachmentCsv fName' = do
|
||||||
ts <- timestampCsv
|
csvOpts <- lift askMailCsvOptions
|
||||||
partIsAttachment . ts $ fName `addExtension` unpack extensionCsv
|
langs <- lift askMailLanguages
|
||||||
|
fName <- ($ fName') <$> lift getMailMessageRender
|
||||||
|
ts <- timestampCsv' csvOpts langs
|
||||||
|
let ext = case csvOpts ^. _csvFormat . _CsvFormat of
|
||||||
|
FormatCsv -> extensionCsv
|
||||||
|
FormatXlsx -> extensionXlsx
|
||||||
|
partIsAttachment . ts $ unpack fName `addExtension` unpack ext
|
||||||
|
|
||||||
encodeDefaultOrderedCsv :: forall csv m.
|
setContentDispositionCsv :: (RenderMessage UniWorX msg, MonadHandler m, HandlerSite m ~ UniWorX)
|
||||||
|
=> msg
|
||||||
|
-> m ()
|
||||||
|
setContentDispositionCsv fName' = do
|
||||||
|
fName <- unpack . ($ fName') <$> getMessageRender
|
||||||
|
ts <- timestampCsv
|
||||||
|
fmt <- expectedCsvFormat
|
||||||
|
let ext = case fmt of
|
||||||
|
FormatCsv -> extensionCsv
|
||||||
|
FormatXlsx -> extensionXlsx
|
||||||
|
setContentDisposition' . Just $ ensureExtension (unpack ext) (ts fName)
|
||||||
|
|
||||||
|
encodeDefaultOrderedCsv :: forall csv m msg.
|
||||||
( ToNamedRecord csv
|
( ToNamedRecord csv
|
||||||
, DefaultOrdered csv
|
, DefaultOrdered csv
|
||||||
, MonadHandler m
|
, MonadHandler m
|
||||||
, HandlerSite m ~ UniWorX
|
, HandlerSite m ~ UniWorX
|
||||||
|
, RenderMessage UniWorX msg
|
||||||
)
|
)
|
||||||
=> ConduitT csv ByteString m ()
|
=> msg -- ^ Sheet name for .xlsx
|
||||||
encodeDefaultOrderedCsv = encodeCsv $ headerOrder (error "headerOrder" :: csv)
|
-> ConduitT csv ByteString m CsvFormat
|
||||||
|
encodeDefaultOrderedCsv sheetName = encodeCsv sheetName $ headerOrder (error "headerOrder" :: csv)
|
||||||
|
|
||||||
|
|
||||||
respondCsv :: ToNamedRecord csv
|
respondCsv :: ( ToNamedRecord csv
|
||||||
=> Header
|
, RenderMessage UniWorX msg
|
||||||
|
)
|
||||||
|
=> msg -- ^ Sheet name for .xlsx
|
||||||
|
-> Header
|
||||||
-> ConduitT () csv Handler ()
|
-> ConduitT () csv Handler ()
|
||||||
-> Handler TypedContent
|
-> Handler TypedContent
|
||||||
respondCsv hdr src = respondSource typeCsv' $ src .| encodeCsv hdr .| awaitForever sendChunk
|
respondCsv sheetName hdr src = respondSource typeCsv' $ src .| void (encodeCsv sheetName hdr) .| awaitForever sendChunk
|
||||||
|
|
||||||
respondDefaultOrderedCsv :: forall csv.
|
respondDefaultOrderedCsv :: forall csv msg.
|
||||||
( ToNamedRecord csv
|
( ToNamedRecord csv
|
||||||
, DefaultOrdered csv
|
, DefaultOrdered csv
|
||||||
|
, RenderMessage UniWorX msg
|
||||||
)
|
)
|
||||||
=> ConduitT () csv Handler ()
|
=> msg -- ^ Sheet name for .xlsx
|
||||||
|
-> ConduitT () csv Handler ()
|
||||||
-> Handler TypedContent
|
-> Handler TypedContent
|
||||||
respondDefaultOrderedCsv = respondCsv $ headerOrder (error "headerOrder" :: csv)
|
respondDefaultOrderedCsv sheetName = respondCsv sheetName $ headerOrder (error "headerOrder" :: csv)
|
||||||
|
|
||||||
respondCsvDB :: ToNamedRecord csv
|
respondCsvDB :: ( ToNamedRecord csv
|
||||||
=> Header
|
, RenderMessage UniWorX msg
|
||||||
|
)
|
||||||
|
=> msg -- ^ Sheet name for .xlsx
|
||||||
|
-> Header
|
||||||
-> ConduitT () csv DB ()
|
-> ConduitT () csv DB ()
|
||||||
-> Handler TypedContent
|
-> Handler TypedContent
|
||||||
respondCsvDB hdr src = respondSourceDB typeCsv' $ src .| encodeCsv hdr .| awaitForever sendChunk
|
respondCsvDB sheetName hdr src = respondSourceDB typeCsv' $ src .| void (encodeCsv sheetName hdr) .| awaitForever sendChunk
|
||||||
|
|
||||||
respondDefaultOrderedCsvDB :: forall csv.
|
respondDefaultOrderedCsvDB :: forall csv msg.
|
||||||
( ToNamedRecord csv
|
( ToNamedRecord csv
|
||||||
, DefaultOrdered csv
|
, DefaultOrdered csv
|
||||||
|
, RenderMessage UniWorX msg
|
||||||
)
|
)
|
||||||
=> ConduitT () csv DB ()
|
=> msg -- ^ Sheet name for .xlsx
|
||||||
|
-> ConduitT () csv DB ()
|
||||||
-> Handler TypedContent
|
-> Handler TypedContent
|
||||||
respondDefaultOrderedCsvDB = respondCsvDB $ headerOrder (error "headerOrder" :: csv)
|
respondDefaultOrderedCsvDB sheetName = respondCsvDB sheetName $ headerOrder (error "headerOrder" :: csv)
|
||||||
|
|
||||||
fileSourceCsv :: ( FromNamedRecord csv
|
fileSourceCsv :: ( FromNamedRecord csv
|
||||||
, MonadThrow m
|
, MonadThrow m
|
||||||
@ -261,3 +377,15 @@ instance ToWidget UniWorX CsvRendered where
|
|||||||
]
|
]
|
||||||
|
|
||||||
headers = decodeUtf8 <$> Vector.toList csvRenderedHeader
|
headers = decodeUtf8 <$> Vector.toList csvRenderedHeader
|
||||||
|
|
||||||
|
|
||||||
|
csvOptionsForFormat :: ( MonadHandler m, HandlerSite m ~ UniWorX )
|
||||||
|
=> CsvFormat
|
||||||
|
-> m CsvOptions
|
||||||
|
csvOptionsForFormat fmt = do
|
||||||
|
csvOpts <- fmap (maybe def $ userCsvOptions . entityVal) maybeAuth
|
||||||
|
return $ if
|
||||||
|
| fmt == csvOpts ^. _csvFormat . _CsvFormat
|
||||||
|
-> csvOpts
|
||||||
|
| otherwise
|
||||||
|
-> csvOpts & _csvFormat .~ (csvPreset . _CsvFormatPreset # fmt)
|
||||||
|
|||||||
@ -193,7 +193,6 @@ makeExternalExamUsersTable mode (Entity eeId ExternalExam{..}) = do
|
|||||||
examn = externalExamExamName
|
examn = externalExamExamName
|
||||||
|
|
||||||
uid <- requireAuthId
|
uid <- requireAuthId
|
||||||
csvName <- getMessageRender <*> pure (MsgExternalExamUserCsvName tid ssh coursen examn)
|
|
||||||
isLecturer <- hasReadAccessTo $ EExamR tid ssh coursen examn EEUsersR
|
isLecturer <- hasReadAccessTo $ EExamR tid ssh coursen examn EEUsersR
|
||||||
currentRoute <- fromMaybe (error "makeExternalExamUsersTable called from 404-handler") <$> getCurrentRoute
|
currentRoute <- fromMaybe (error "makeExternalExamUsersTable called from 404-handler") <$> getCurrentRoute
|
||||||
MsgRenderer mr <- getMsgRenderer
|
MsgRenderer mr <- getMsgRenderer
|
||||||
@ -358,6 +357,8 @@ makeExternalExamUsersTable mode (Entity eeId ExternalExam{..}) = do
|
|||||||
, dbParamsFormIdent = def
|
, dbParamsFormIdent = def
|
||||||
}
|
}
|
||||||
dbtIdent = mode
|
dbtIdent = mode
|
||||||
|
dbtCsvName = MsgExternalExamUserCsvName tid ssh coursen examn
|
||||||
|
dbtCsvSheetName = MsgExternalExamUserCsvSheetName tid ssh coursen examn
|
||||||
dbtCsvEncode = case mode of
|
dbtCsvEncode = case mode of
|
||||||
EEUMGrades -> Just DBTCsvEncode
|
EEUMGrades -> Just DBTCsvEncode
|
||||||
{ dbtCsvExportForm = ExternalExamUserCsvExportDataGrades
|
{ dbtCsvExportForm = ExternalExamUserCsvExportDataGrades
|
||||||
@ -365,13 +366,13 @@ makeExternalExamUsersTable mode (Entity eeId ExternalExam{..}) = do
|
|||||||
, dbtCsvDoEncode = \ExternalExamUserCsvExportDataGrades{..} -> C.mapM $ \(E.Value k, row) -> do
|
, dbtCsvDoEncode = \ExternalExamUserCsvExportDataGrades{..} -> C.mapM $ \(E.Value k, row) -> do
|
||||||
when csvEEUserMarkSynchronised $ externalExamResultMarkSynchronised k
|
when csvEEUserMarkSynchronised $ externalExamResultMarkSynchronised k
|
||||||
return $ encodeCsv' row
|
return $ encodeCsv' row
|
||||||
, dbtCsvName = unpack csvName
|
, dbtCsvName, dbtCsvSheetName
|
||||||
, dbtCsvNoExportData = Nothing
|
, dbtCsvNoExportData = Nothing
|
||||||
, dbtCsvHeader = const . return $ headerOrder (error "headerOrder" :: ExternalExamUserTableCsv)
|
, dbtCsvHeader = const . return $ headerOrder (error "headerOrder" :: ExternalExamUserTableCsv)
|
||||||
, dbtCsvExampleData = Nothing
|
, dbtCsvExampleData = Nothing
|
||||||
}
|
}
|
||||||
EEUMUsers ->
|
EEUMUsers ->
|
||||||
let baseEncode = simpleCsvEncode csvName encodeCsv'
|
let baseEncode = simpleCsvEncode dbtCsvName dbtCsvSheetName encodeCsv'
|
||||||
csvEUserStudyFeatures = mempty
|
csvEUserStudyFeatures = mempty
|
||||||
in baseEncode <&> \enc -> enc
|
in baseEncode <&> \enc -> enc
|
||||||
{ dbtCsvExampleData = Just
|
{ dbtCsvExampleData = Just
|
||||||
|
|||||||
@ -1964,11 +1964,16 @@ csvFormatOptionsForm fs mPrev = hoistAForm liftHandler . multiActionA csvActs fs
|
|||||||
CsvFormatOptionsPreset' preset
|
CsvFormatOptionsPreset' preset
|
||||||
-> pure $ csvPreset # preset
|
-> pure $ csvPreset # preset
|
||||||
CsvFormatOptionsCustom'
|
CsvFormatOptionsCustom'
|
||||||
|
-> multiActionA csvFormatActs (fslI MsgCsvFormatField) $ view _CsvFormat <$> mPrev
|
||||||
|
csvFormatActs :: Map CsvFormat (AForm Handler CsvFormatOptions)
|
||||||
|
csvFormatActs = mapF $ \case
|
||||||
|
FormatCsv
|
||||||
-> CsvFormatOptions
|
-> CsvFormatOptions
|
||||||
<$> areq (selectField delimiterOpts) (fslI MsgCsvDelimiter) (csvDelimiter <$> mPrev)
|
<$> apreq (selectField delimiterOpts) (fslI MsgCsvDelimiter) (preview _csvDelimiter =<< mPrev)
|
||||||
<*> areq (selectField lineEndOpts) (fslI MsgCsvUseCrLf) (csvUseCrLf <$> mPrev)
|
<*> apreq (selectField lineEndOpts) (fslI MsgCsvUseCrLf) (preview _csvUseCrLf =<< mPrev)
|
||||||
<*> areq (selectField quoteOpts) (fslI MsgCsvQuoting & setTooltip MsgCsvQuotingTip) (csvQuoting <$> mPrev)
|
<*> apreq (selectField quoteOpts) (fslI MsgCsvQuoting & setTooltip MsgCsvQuotingTip) (preview _csvQuoting =<< mPrev)
|
||||||
<*> areq (selectField encodingOpts) (fslI MsgCsvEncoding & setTooltip MsgCsvEncodingTip) (csvEncoding <$> mPrev)
|
<*> apreq (selectField encodingOpts) (fslI MsgCsvEncoding & setTooltip MsgCsvEncodingTip) (preview _csvEncoding =<< mPrev)
|
||||||
|
FormatXlsx -> pure CsvXlsxFormatOptions
|
||||||
|
|
||||||
delimiterOpts :: Handler (OptionList Char)
|
delimiterOpts :: Handler (OptionList Char)
|
||||||
delimiterOpts = do
|
delimiterOpts = do
|
||||||
|
|||||||
@ -53,6 +53,7 @@ userMailT uid mAct = do
|
|||||||
, userDateTimeFormat
|
, userDateTimeFormat
|
||||||
, userDateFormat
|
, userDateFormat
|
||||||
, userTimeFormat
|
, userTimeFormat
|
||||||
|
, userCsvOptions
|
||||||
} <- liftHandler . runDB $ getJust uid
|
} <- liftHandler . runDB $ getJust uid
|
||||||
let
|
let
|
||||||
ctx = MailContext
|
ctx = MailContext
|
||||||
@ -61,6 +62,7 @@ userMailT uid mAct = do
|
|||||||
SelFormatDateTime -> userDateTimeFormat
|
SelFormatDateTime -> userDateTimeFormat
|
||||||
SelFormatDate -> userDateFormat
|
SelFormatDate -> userDateFormat
|
||||||
SelFormatTime -> userTimeFormat
|
SelFormatTime -> userTimeFormat
|
||||||
|
, mcCsvOptions = userCsvOptions
|
||||||
}
|
}
|
||||||
mailT ctx $ do
|
mailT ctx $ do
|
||||||
_mailTo .= pure (userAddress user)
|
_mailTo .= pure (userAddress user)
|
||||||
|
|||||||
@ -17,7 +17,7 @@ runAppMinio :: ( MonadHandler m, HandlerSite m ~ UniWorX
|
|||||||
=> Minio a -> m a
|
=> Minio a -> m a
|
||||||
runAppMinio act = do
|
runAppMinio act = do
|
||||||
conn <- hoistMaybe =<< getsYesod appUploadCache
|
conn <- hoistMaybe =<< getsYesod appUploadCache
|
||||||
either throwM return <=< liftIO $ Minio.runMinioWith conn act
|
throwLeft <=< liftIO $ Minio.runMinioWith conn act
|
||||||
|
|
||||||
minioIsDoesNotExist :: HttpException -> Bool
|
minioIsDoesNotExist :: HttpException -> Bool
|
||||||
minioIsDoesNotExist (HttpExceptionRequest _ (StatusCodeException resp _))
|
minioIsDoesNotExist (HttpExceptionRequest _ (StatusCodeException resp _))
|
||||||
|
|||||||
@ -53,7 +53,6 @@ import Handler.Utils.Table.Pagination.Types
|
|||||||
import Handler.Utils.Table.Pagination.CsvColumnExplanations
|
import Handler.Utils.Table.Pagination.CsvColumnExplanations
|
||||||
import Handler.Utils.Form
|
import Handler.Utils.Form
|
||||||
import Handler.Utils.Csv
|
import Handler.Utils.Csv
|
||||||
import Handler.Utils.ContentDisposition
|
|
||||||
import Handler.Utils.I18n
|
import Handler.Utils.I18n
|
||||||
import Utils
|
import Utils
|
||||||
import Utils.Lens
|
import Utils.Lens
|
||||||
@ -581,24 +580,34 @@ singletonFilter key = prism' fromInner (fmap Just . fromOuter)
|
|||||||
fromOuter = Map.lookup key >=> listToMaybe
|
fromOuter = Map.lookup key >=> listToMaybe
|
||||||
|
|
||||||
|
|
||||||
data DBTCsvEncode r' k' csv = forall exportData.
|
data DBTCsvEncode r' k' csv = forall exportData filename sheetName.
|
||||||
( ToNamedRecord csv, CsvColumnsExplained csv
|
( ToNamedRecord csv, CsvColumnsExplained csv
|
||||||
, DBTableKey k'
|
, DBTableKey k'
|
||||||
, Typeable exportData
|
, Typeable exportData
|
||||||
|
, RenderMessage UniWorX filename, RenderMessage UniWorX sheetName
|
||||||
) => DBTCsvEncode
|
) => DBTCsvEncode
|
||||||
{ dbtCsvExportForm :: AForm DB exportData
|
{ dbtCsvExportForm :: AForm DB exportData
|
||||||
, dbtCsvHeader :: Maybe exportData -> DB Csv.Header -- ^ @exportData@ is @Nothing@, if we're reporting an error or exporting example data
|
, dbtCsvHeader :: Maybe exportData -> DB Csv.Header -- ^ @exportData@ is @Nothing@, if we're reporting an error or exporting example data
|
||||||
, dbtCsvExampleData :: Maybe [csv]
|
, dbtCsvExampleData :: Maybe [csv]
|
||||||
, dbtCsvDoEncode :: exportData -> ConduitT (k', r') csv DB ()
|
, dbtCsvDoEncode :: exportData -> ConduitT (k', r') csv DB ()
|
||||||
, dbtCsvName :: FilePath
|
, dbtCsvName :: filename
|
||||||
|
, dbtCsvSheetName :: sheetName
|
||||||
, dbtCsvNoExportData :: Maybe (AnIso' exportData ())
|
, dbtCsvNoExportData :: Maybe (AnIso' exportData ())
|
||||||
}
|
}
|
||||||
data DBTExtraRep r' k' = forall rep.
|
data DBTExtraRep r' k'
|
||||||
( HasContentType rep
|
= forall rep.
|
||||||
, DBTableKey k'
|
( HasContentType rep
|
||||||
) => DBTExtraRep
|
, DBTableKey k'
|
||||||
{ dbtERepDoEncode :: ConduitT (k', r') Void DB rep
|
) => DBTExtraRep
|
||||||
}
|
{ dbtERepDoEncode :: ConduitT (k', r') Void DB rep
|
||||||
|
}
|
||||||
|
| forall rep.
|
||||||
|
( ToContent rep
|
||||||
|
, DBTableKey k'
|
||||||
|
) => DBTExtraRepFor
|
||||||
|
{ dbtERepContentType :: ContentType
|
||||||
|
, dbtERepDoEncode :: ConduitT (k', r') Void DB rep
|
||||||
|
}
|
||||||
data DBTCsvDecode r' k' csv = forall route csvAction csvActionClass csvException.
|
data DBTCsvDecode r' k' csv = forall route csvAction csvActionClass csvException.
|
||||||
( FromNamedRecord csv, ToNamedRecord csv
|
( FromNamedRecord csv, ToNamedRecord csv
|
||||||
, DBTableKey k'
|
, DBTableKey k'
|
||||||
@ -646,48 +655,58 @@ type DBFilterUI = Maybe (Map FilterKey [Text]) -> AForm DB (Map FilterKey [Text]
|
|||||||
noCsvEncode :: Maybe (DBTCsvEncode r' k' Void)
|
noCsvEncode :: Maybe (DBTCsvEncode r' k' Void)
|
||||||
noCsvEncode = Nothing
|
noCsvEncode = Nothing
|
||||||
|
|
||||||
simpleCsvEncode :: forall fp r' k' csv.
|
simpleCsvEncode :: forall filename sheetName r' k' csv.
|
||||||
( ToNamedRecord csv, DefaultOrdered csv, CsvColumnsExplained csv
|
( ToNamedRecord csv, DefaultOrdered csv, CsvColumnsExplained csv
|
||||||
, DBTableKey k'
|
, DBTableKey k'
|
||||||
, Textual fp
|
, RenderMessage UniWorX filename, RenderMessage UniWorX sheetName
|
||||||
)
|
)
|
||||||
=> fp -> (r' -> csv) -> Maybe (DBTCsvEncode r' k' csv)
|
=> filename -> sheetName -> (r' -> csv) -> Maybe (DBTCsvEncode r' k' csv)
|
||||||
simpleCsvEncode fName f = Just DBTCsvEncode
|
simpleCsvEncode fName sName f = Just DBTCsvEncode
|
||||||
{ dbtCsvExportForm = pure ()
|
{ dbtCsvExportForm = pure ()
|
||||||
, dbtCsvDoEncode = \() -> C.map (f . view _2)
|
, dbtCsvDoEncode = \() -> C.map (f . view _2)
|
||||||
, dbtCsvName = unpack fName
|
, dbtCsvName = fName
|
||||||
|
, dbtCsvSheetName = sName
|
||||||
, dbtCsvNoExportData = Just id
|
, dbtCsvNoExportData = Just id
|
||||||
, dbtCsvHeader = const . return $ headerOrder (error "headerOrder" :: csv)
|
, dbtCsvHeader = const . return $ headerOrder (error "headerOrder" :: csv)
|
||||||
, dbtCsvExampleData = Nothing
|
, dbtCsvExampleData = Nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
simpleCsvEncodeM :: forall fp r' k' csv.
|
simpleCsvEncodeM :: forall filename sheetName r' k' csv.
|
||||||
( ToNamedRecord csv, DefaultOrdered csv, CsvColumnsExplained csv
|
( ToNamedRecord csv, DefaultOrdered csv, CsvColumnsExplained csv
|
||||||
, DBTableKey k'
|
, DBTableKey k'
|
||||||
, Textual fp
|
, RenderMessage UniWorX filename, RenderMessage UniWorX sheetName
|
||||||
)
|
)
|
||||||
=> fp -> ReaderT r' DB csv -> Maybe (DBTCsvEncode r' k' csv)
|
=> filename -> sheetName -> ReaderT r' DB csv -> Maybe (DBTCsvEncode r' k' csv)
|
||||||
simpleCsvEncodeM fName f = Just DBTCsvEncode
|
simpleCsvEncodeM fName sName f = Just DBTCsvEncode
|
||||||
{ dbtCsvExportForm = pure ()
|
{ dbtCsvExportForm = pure ()
|
||||||
, dbtCsvDoEncode = \() -> C.mapM (runReaderT f . view _2)
|
, dbtCsvDoEncode = \() -> C.mapM (runReaderT f . view _2)
|
||||||
, dbtCsvName = unpack fName
|
, dbtCsvName = fName
|
||||||
|
, dbtCsvSheetName = sName
|
||||||
, dbtCsvNoExportData = Just id
|
, dbtCsvNoExportData = Just id
|
||||||
, dbtCsvHeader = const . return $ headerOrder (error "headerOrder" :: csv)
|
, dbtCsvHeader = const . return $ headerOrder (error "headerOrder" :: csv)
|
||||||
, dbtCsvExampleData = Nothing
|
, dbtCsvExampleData = Nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
withCsvExtraRep :: forall exportData csv r' k'.
|
withCsvExtraRep :: forall exportData csv sheetName r' k'.
|
||||||
Typeable exportData
|
( Typeable exportData
|
||||||
=> exportData
|
, RenderMessage UniWorX sheetName
|
||||||
|
)
|
||||||
|
=> sheetName
|
||||||
|
-> exportData
|
||||||
-> Maybe (DBTCsvEncode r' k' csv)
|
-> Maybe (DBTCsvEncode r' k' csv)
|
||||||
-> [DBTExtraRep r' k'] -> [DBTExtraRep r' k']
|
-> [DBTExtraRep r' k'] -> [DBTExtraRep r' k']
|
||||||
withCsvExtraRep exportData mEncode = maybe id (flip snoc) csvExtraRep
|
withCsvExtraRep sheetName exportData mEncode = maybe id (flip snoc) (csvExtraRep FormatCsv) <> maybe id (flip snoc) (csvExtraRep FormatXlsx)
|
||||||
where csvExtraRep = do
|
where
|
||||||
DBTCsvEncode{ dbtCsvNoExportData = (_ :: Maybe (AnIso' exportData' ())), .. } <- mEncode
|
csvExtraRep fmt = do
|
||||||
Refl <- eqT @exportData @exportData'
|
DBTCsvEncode{ dbtCsvNoExportData = (_ :: Maybe (AnIso' exportData' ())), .. } <- mEncode
|
||||||
return DBTExtraRep
|
Refl <- eqT @exportData @exportData'
|
||||||
{ dbtERepDoEncode = toCsvRendered <$> lift (dbtCsvHeader $ Just exportData) <*> (dbtCsvDoEncode exportData .| C.foldMap (pure @[]))
|
return DBTExtraRepFor
|
||||||
}
|
{ dbtERepContentType = typeCsv'
|
||||||
|
, dbtERepDoEncode = do
|
||||||
|
csvRendered <- toCsvRendered <$> lift (dbtCsvHeader $ Just exportData) <*> (dbtCsvDoEncode exportData .| C.foldMap (pure @[]))
|
||||||
|
encOpts <- csvOptionsForFormat fmt
|
||||||
|
csvRenderedToTypedContentWith encOpts sheetName csvRendered
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class (MonadHandler m, HandlerSite m ~ UniWorX, Monoid' x, Monoid' (DBCell m x), Default (DBParams m x)) => IsDBTable (m :: Type -> Type) (x :: Type) where
|
class (MonadHandler m, HandlerSite m ~ UniWorX, Monoid' x, Monoid' (DBCell m x), Default (DBParams m x)) => IsDBTable (m :: Type -> Type) (x :: Type) where
|
||||||
@ -1125,14 +1144,14 @@ dbTable PSValidator{..} dbtable@DBTable{ dbtIdent = dbtIdent'@(toPathPiece -> db
|
|||||||
| Just DBTCsvEncode{..} <- dbtCsvEncode
|
| Just DBTCsvEncode{..} <- dbtCsvEncode
|
||||||
, Just exData <- dbtCsvExampleData -> do
|
, Just exData <- dbtCsvExampleData -> do
|
||||||
hdr <- dbtCsvHeader Nothing
|
hdr <- dbtCsvHeader Nothing
|
||||||
sendResponse <=< liftHandler . respondCsv hdr $ C.sourceList exData
|
setContentDispositionCsv dbtCsvName
|
||||||
|
sendResponse <=< liftHandler . respondCsv dbtCsvSheetName hdr $ C.sourceList exData
|
||||||
DBCsvExport{..}
|
DBCsvExport{..}
|
||||||
| Just DBTCsvEncode{..} <- dbtCsvEncode
|
| Just DBTCsvEncode{..} <- dbtCsvEncode
|
||||||
, Just exportData <- fromDynamic dbCsvExportData -> do
|
, Just exportData <- fromDynamic dbCsvExportData -> do
|
||||||
hdr <- dbtCsvHeader $ Just exportData
|
hdr <- dbtCsvHeader $ Just exportData
|
||||||
dbtCsvName' <- timestampCsv <*> pure dbtCsvName
|
setContentDispositionCsv dbtCsvName
|
||||||
setContentDisposition' . Just $ ensureExtension (unpack extensionCsv) dbtCsvName'
|
sendResponse <=< liftHandler . respondCsvDB dbtCsvSheetName 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
|
||||||
, Just (DBTCsvDecode{ dbtCsvClassifyAction = dbtCsvClassifyAction :: csvAction -> csvActionClass
|
, Just (DBTCsvDecode{ dbtCsvClassifyAction = dbtCsvClassifyAction :: csvAction -> csvActionClass
|
||||||
@ -1290,15 +1309,16 @@ dbTable PSValidator{..} dbtable@DBTable{ dbtIdent = dbtIdent'@(toPathPiece -> db
|
|||||||
]
|
]
|
||||||
_other -> return ()
|
_other -> return ()
|
||||||
|
|
||||||
let extraReps = maybe id (flip snoc) csvRep dbtExtraReps
|
let extraReps = maybe id ($) addCSVReps dbtExtraReps
|
||||||
where csvRep = do
|
where addCSVReps = do
|
||||||
DBTCsvEncode{..} <- dbtCsvEncode
|
DBTCsvEncode{..} <- dbtCsvEncode
|
||||||
noExportData' <- cloneIso <$> dbtCsvNoExportData
|
noExportData' <- cloneIso <$> dbtCsvNoExportData
|
||||||
let exportData = noExportData' # ()
|
let exportData = noExportData' # ()
|
||||||
return DBTExtraRep
|
return $ withCsvExtraRep dbtCsvSheetName exportData dbtCsvEncode
|
||||||
{ dbtERepDoEncode = toCsvRendered <$> lift (dbtCsvHeader $ Just exportData) <*> (dbtCsvDoEncode exportData .| C.foldMap (pure @[]))
|
extraRepContentType = \case
|
||||||
}
|
DBTExtraRep{..} -> getContentType dbtERepDoEncode
|
||||||
extraReps' = (typeHtml, Nothing) : map ((,) <$> (\DBTExtraRep{..} -> getContentType dbtERepDoEncode) <*> Just) extraReps
|
DBTExtraRepFor{..} -> dbtERepContentType
|
||||||
|
extraReps' = (typeHtml, Nothing) : map ((,) <$> extraRepContentType <*> Just) extraReps
|
||||||
doAltRep = maybe True (== dbtIdent) <$> lookupGlobalGetParam GetSelectTable
|
doAltRep = maybe True (== dbtIdent) <$> lookupGlobalGetParam GetSelectTable
|
||||||
|
|
||||||
maybeT (return ()) $ do
|
maybeT (return ()) $ do
|
||||||
@ -1308,7 +1328,9 @@ dbTable PSValidator{..} dbtable@DBTable{ dbtIdent = dbtIdent'@(toPathPiece -> db
|
|||||||
|
|
||||||
altRep <- hoistMaybe <=< asum $ do
|
altRep <- hoistMaybe <=< asum $ do
|
||||||
mRep <- hoistMaybe . selectRep' extraReps' =<< cts
|
mRep <- hoistMaybe . selectRep' extraReps' =<< cts
|
||||||
return . return $ mRep <&> \DBTExtraRep{..} -> fmap toTypedContent . runConduit $ C.sourceList (zip currentKeys rows) .| dbtERepDoEncode
|
return . return $ mRep <&> \case
|
||||||
|
DBTExtraRep{..} -> fmap toTypedContent . runConduit $ C.sourceList (zip currentKeys rows) .| dbtERepDoEncode
|
||||||
|
DBTExtraRepFor{..} -> fmap (TypedContent dbtERepContentType . toContent) . runConduit $ C.sourceList (zip currentKeys rows) .| dbtERepDoEncode
|
||||||
|
|
||||||
lift $ sendResponse =<< altRep
|
lift $ sendResponse =<< altRep
|
||||||
|
|
||||||
|
|||||||
@ -212,7 +212,7 @@ decodeZipEntryName :: MonadThrow m => Either Text ByteString -> m FilePath
|
|||||||
-- Throws 'Data.Encoding.Exception.DecodingException's.
|
-- Throws 'Data.Encoding.Exception.DecodingException's.
|
||||||
decodeZipEntryName = \case
|
decodeZipEntryName = \case
|
||||||
Left t -> return $ unpack t
|
Left t -> return $ unpack t
|
||||||
Right cp437 -> either throwM return $ decodeStrictByteStringExplicit CP437 cp437
|
Right cp437 -> throwLeft $ decodeStrictByteStringExplicit CP437 cp437
|
||||||
|
|
||||||
encodeZipEntryName :: FilePath -> Either Text ByteString
|
encodeZipEntryName :: FilePath -> Either Text ByteString
|
||||||
-- ^ Encode a filename for use in a 'ZipEntry', encodes as
|
-- ^ Encode a filename for use in a 'ZipEntry', encodes as
|
||||||
|
|||||||
@ -11,5 +11,6 @@ import Utils.SystemMessage as Import
|
|||||||
import Utils.Metrics as Import
|
import Utils.Metrics as Import
|
||||||
import Utils.Files as Import
|
import Utils.Files as Import
|
||||||
import Utils.PersistentTokenBucket as Import
|
import Utils.PersistentTokenBucket as Import
|
||||||
|
import Utils.Csv.Mail as Import
|
||||||
|
|
||||||
import Jobs.Types as Import (JobHandler(..))
|
import Jobs.Types as Import (JobHandler(..))
|
||||||
|
|||||||
@ -38,5 +38,5 @@ dispatchJobSendCourseCommunication jRecipientEmail jAllRecipientAddresses jCours
|
|||||||
addHtmlMarkdownAlternatives ($(ihamletFile "templates/mail/courseCommunication.hamlet") :: HtmlUrlI18n UniWorXMessage (Route UniWorX))
|
addHtmlMarkdownAlternatives ($(ihamletFile "templates/mail/courseCommunication.hamlet") :: HtmlUrlI18n UniWorXMessage (Route UniWorX))
|
||||||
when (jRecipientEmail == Right jSender) $
|
when (jRecipientEmail == Right jSender) $
|
||||||
addPart' $ do
|
addPart' $ do
|
||||||
partIsAttachmentCsv $ mr MsgCommAllRecipients
|
partIsAttachmentCsv MsgCommAllRecipients
|
||||||
toMailPart (toDefaultOrderedCsvRendered jAllRecipientAddresses, userCsvOptions sender)
|
toMailPart (MsgCommAllRecipientsSheet, toDefaultOrderedCsvRendered jAllRecipientAddresses)
|
||||||
|
|||||||
@ -170,6 +170,7 @@ type family ChildrenJobChildren a where
|
|||||||
ChildrenJobChildren (Key a) = '[]
|
ChildrenJobChildren (Key a) = '[]
|
||||||
ChildrenJobChildren (CI a) = '[]
|
ChildrenJobChildren (CI a) = '[]
|
||||||
ChildrenJobChildren (Set a) = '[]
|
ChildrenJobChildren (Set a) = '[]
|
||||||
|
ChildrenJobChildren MailContext = '[]
|
||||||
|
|
||||||
ChildrenJobChildren a = Children ChGeneric a
|
ChildrenJobChildren a = Children ChGeneric a
|
||||||
|
|
||||||
|
|||||||
@ -41,6 +41,7 @@ import ClassyPrelude.Yesod hiding (snoc, (.=), getMessageRender, derivePersistFi
|
|||||||
import Data.Kind (Type)
|
import Data.Kind (Type)
|
||||||
|
|
||||||
import Model.Types.Languages
|
import Model.Types.Languages
|
||||||
|
import Model.Types.Csv
|
||||||
|
|
||||||
import Network.Mail.Mime hiding (addPart, addAttachment)
|
import Network.Mail.Mime hiding (addPart, addAttachment)
|
||||||
import qualified Network.Mail.Mime as Mime (addPart)
|
import qualified Network.Mail.Mime as Mime (addPart)
|
||||||
@ -171,6 +172,7 @@ _MailSmtpDataSet = to $ \MailSmtpData{..} -> none id
|
|||||||
data MailContext = MailContext
|
data MailContext = MailContext
|
||||||
{ mcLanguages :: Languages
|
{ mcLanguages :: Languages
|
||||||
, mcDateTimeFormat :: SelDateTimeFormat -> DateTimeFormat
|
, mcDateTimeFormat :: SelDateTimeFormat -> DateTimeFormat
|
||||||
|
, mcCsvOptions :: CsvOptions
|
||||||
} deriving (Eq, Ord, Read, Show, Generic, Typeable)
|
} deriving (Eq, Ord, Read, Show, Generic, Typeable)
|
||||||
|
|
||||||
deriveJSON defaultOptions
|
deriveJSON defaultOptions
|
||||||
@ -183,6 +185,7 @@ instance Default MailContext where
|
|||||||
def = MailContext
|
def = MailContext
|
||||||
{ mcLanguages = def
|
{ mcLanguages = def
|
||||||
, mcDateTimeFormat = def
|
, mcDateTimeFormat = def
|
||||||
|
, mcCsvOptions = def
|
||||||
}
|
}
|
||||||
|
|
||||||
makeLenses_ ''MailContext
|
makeLenses_ ''MailContext
|
||||||
@ -192,11 +195,13 @@ makeLenses_ ''MailSmtpData
|
|||||||
class (MonadHandler m, MonadState Mail m) => MonadMail m where
|
class (MonadHandler m, MonadState Mail m) => MonadMail m where
|
||||||
askMailLanguages :: m Languages
|
askMailLanguages :: m Languages
|
||||||
askMailDateTimeFormat :: SelDateTimeFormat -> m DateTimeFormat
|
askMailDateTimeFormat :: SelDateTimeFormat -> m DateTimeFormat
|
||||||
|
askMailCsvOptions :: m CsvOptions
|
||||||
tellMailSmtpData :: MailSmtpData -> m ()
|
tellMailSmtpData :: MailSmtpData -> m ()
|
||||||
|
|
||||||
instance MonadHandler m => MonadMail (MailT m) where
|
instance MonadHandler m => MonadMail (MailT m) where
|
||||||
askMailLanguages = view _mcLanguages
|
askMailLanguages = view _mcLanguages
|
||||||
askMailDateTimeFormat = (view _mcDateTimeFormat ??)
|
askMailDateTimeFormat = (view _mcDateTimeFormat ??)
|
||||||
|
askMailCsvOptions = view _mcCsvOptions
|
||||||
tellMailSmtpData = tell
|
tellMailSmtpData = tell
|
||||||
|
|
||||||
getMailMessageRender :: ( MonadMail m
|
getMailMessageRender :: ( MonadMail m
|
||||||
|
|||||||
@ -21,3 +21,4 @@ import Model.Types.Workflow as Types
|
|||||||
import Model.Types.Changelog as Types
|
import Model.Types.Changelog as Types
|
||||||
import Model.Types.Markup as Types
|
import Model.Types.Markup as Types
|
||||||
import Model.Types.Room as Types
|
import Model.Types.Room as Types
|
||||||
|
import Model.Types.Csv as Types
|
||||||
|
|||||||
191
src/Model/Types/Csv.hs
Normal file
191
src/Model/Types/Csv.hs
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
||||||
|
|
||||||
|
module Model.Types.Csv
|
||||||
|
( Quoting(..)
|
||||||
|
, CsvOptions(..), _csvFormat, _csvTimestamp
|
||||||
|
, CsvFormatOptions(..), _csvDelimiter, _csvUseCrLf, _csvQuoting, _csvEncoding
|
||||||
|
, CsvPreset(..)
|
||||||
|
, csvPreset
|
||||||
|
, _CsvEncodeOptions
|
||||||
|
, CsvFormat(..), _FormatCsv, _FormatXlsx
|
||||||
|
, _CsvFormat, _CsvFormatPreset
|
||||||
|
) where
|
||||||
|
|
||||||
|
import ClassyPrelude
|
||||||
|
|
||||||
|
import Data.Csv (Quoting(..))
|
||||||
|
import qualified Data.Csv as Csv
|
||||||
|
|
||||||
|
import Model.Types.TH.JSON
|
||||||
|
import Utils.PathPiece
|
||||||
|
import Data.Universe.TH
|
||||||
|
import Data.Aeson.TH
|
||||||
|
|
||||||
|
import Data.Aeson (FromJSON, ToJSON)
|
||||||
|
import qualified Data.Aeson as JSON
|
||||||
|
|
||||||
|
import Data.Encoding (DynEncoding)
|
||||||
|
|
||||||
|
import Data.Encoding.Instances ()
|
||||||
|
|
||||||
|
import Control.Lens
|
||||||
|
|
||||||
|
import Utils.Lens.TH
|
||||||
|
|
||||||
|
import Data.Default
|
||||||
|
import Data.Universe
|
||||||
|
|
||||||
|
|
||||||
|
deriving stock instance Generic Quoting
|
||||||
|
deriving stock instance Ord Quoting
|
||||||
|
deriving stock instance Read Quoting
|
||||||
|
deriving anyclass instance Hashable Quoting
|
||||||
|
deriving anyclass instance NFData Quoting
|
||||||
|
deriveJSON defaultOptions
|
||||||
|
{ constructorTagModifier = camelToPathPiece' 1
|
||||||
|
} ''Quoting
|
||||||
|
deriveFinite ''Quoting
|
||||||
|
nullaryPathPiece ''Quoting $ \q -> if
|
||||||
|
| q == "QuoteNone" -> "never"
|
||||||
|
| otherwise -> camelToPathPiece' 1 q
|
||||||
|
|
||||||
|
data CsvOptions
|
||||||
|
= CsvOptions
|
||||||
|
{ csvFormat :: CsvFormatOptions
|
||||||
|
, csvTimestamp :: Bool
|
||||||
|
}
|
||||||
|
deriving (Eq, Ord, Read, Show, Generic, Typeable)
|
||||||
|
deriving anyclass (Hashable, NFData)
|
||||||
|
|
||||||
|
data CsvFormatOptions
|
||||||
|
= CsvFormatOptions
|
||||||
|
{ csvDelimiter :: Char
|
||||||
|
, csvUseCrLf :: Bool
|
||||||
|
, csvQuoting :: Csv.Quoting
|
||||||
|
, csvEncoding :: DynEncoding
|
||||||
|
}
|
||||||
|
| CsvXlsxFormatOptions
|
||||||
|
deriving (Eq, Ord, Read, Show, Generic, Typeable)
|
||||||
|
deriving anyclass (Hashable, NFData)
|
||||||
|
|
||||||
|
makeLenses_ ''CsvOptions
|
||||||
|
makeLenses_ ''CsvFormatOptions
|
||||||
|
|
||||||
|
instance Default CsvOptions where
|
||||||
|
def = CsvOptions
|
||||||
|
{ csvFormat = def
|
||||||
|
, csvTimestamp = False
|
||||||
|
}
|
||||||
|
|
||||||
|
instance Default CsvFormatOptions where
|
||||||
|
def = csvPreset # CsvPresetRFC
|
||||||
|
|
||||||
|
data CsvPreset = CsvPresetRFC
|
||||||
|
| CsvPresetXlsx
|
||||||
|
| CsvPresetExcel
|
||||||
|
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable)
|
||||||
|
instance Universe CsvPreset
|
||||||
|
instance Finite CsvPreset
|
||||||
|
|
||||||
|
csvPreset :: Prism' CsvFormatOptions CsvPreset
|
||||||
|
csvPreset = prism' fromPreset toPreset
|
||||||
|
where
|
||||||
|
fromPreset :: CsvPreset -> CsvFormatOptions
|
||||||
|
fromPreset CsvPresetRFC = CsvFormatOptions
|
||||||
|
{ csvDelimiter = ','
|
||||||
|
, csvUseCrLf = True
|
||||||
|
, csvQuoting = QuoteMinimal
|
||||||
|
, csvEncoding = "UTF8"
|
||||||
|
}
|
||||||
|
fromPreset CsvPresetExcel = CsvFormatOptions
|
||||||
|
{ csvDelimiter = ';'
|
||||||
|
, csvUseCrLf = True
|
||||||
|
, csvQuoting = QuoteAll
|
||||||
|
, csvEncoding = "CP1252"
|
||||||
|
}
|
||||||
|
fromPreset CsvPresetXlsx = CsvXlsxFormatOptions
|
||||||
|
|
||||||
|
toPreset :: CsvFormatOptions -> Maybe CsvPreset
|
||||||
|
toPreset opts = case filter (\p -> fromPreset p == opts) universeF of
|
||||||
|
[p] -> Just p
|
||||||
|
_other -> Nothing
|
||||||
|
|
||||||
|
_CsvEncodeOptions :: Prism' CsvFormatOptions Csv.EncodeOptions
|
||||||
|
_CsvEncodeOptions = prism' fromEncode toEncode
|
||||||
|
where
|
||||||
|
toEncode CsvFormatOptions{..} = Just $ Csv.defaultEncodeOptions
|
||||||
|
{ Csv.encDelimiter = fromIntegral $ fromEnum csvDelimiter
|
||||||
|
, Csv.encUseCrLf = csvUseCrLf
|
||||||
|
, Csv.encQuoting = csvQuoting
|
||||||
|
, Csv.encIncludeHeader = True
|
||||||
|
}
|
||||||
|
toEncode CsvXlsxFormatOptions{} = Nothing
|
||||||
|
fromEncode encOpts = def
|
||||||
|
{ csvDelimiter = toEnum . fromIntegral $ Csv.encDelimiter encOpts
|
||||||
|
, csvUseCrLf = Csv.encUseCrLf encOpts
|
||||||
|
, csvQuoting = Csv.encQuoting encOpts
|
||||||
|
}
|
||||||
|
|
||||||
|
instance ToJSON CsvOptions where
|
||||||
|
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{..}
|
||||||
|
|
||||||
|
data CsvFormat = FormatCsv | FormatXlsx
|
||||||
|
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable)
|
||||||
|
deriving anyclass (Universe, Finite)
|
||||||
|
|
||||||
|
nullaryPathPiece ''CsvFormat $ camelToPathPiece' 1
|
||||||
|
pathPieceJSON ''CsvFormat
|
||||||
|
makePrisms ''CsvFormat
|
||||||
|
|
||||||
|
_CsvFormat :: forall r. Getting r CsvFormatOptions CsvFormat
|
||||||
|
_CsvFormat = to $ \case
|
||||||
|
CsvFormatOptions{} -> FormatCsv
|
||||||
|
CsvXlsxFormatOptions{} -> FormatXlsx
|
||||||
|
|
||||||
|
_CsvFormatPreset :: Prism' CsvPreset CsvFormat
|
||||||
|
_CsvFormatPreset = prism' toPreset fromPreset
|
||||||
|
where
|
||||||
|
toPreset = \case
|
||||||
|
FormatCsv -> CsvPresetRFC
|
||||||
|
FormatXlsx -> CsvPresetXlsx
|
||||||
|
fromPreset = \case
|
||||||
|
CsvPresetRFC -> Just FormatCsv
|
||||||
|
CsvPresetXlsx -> Just FormatXlsx
|
||||||
|
_other -> Nothing
|
||||||
|
|
||||||
|
instance ToJSON CsvFormatOptions where
|
||||||
|
toJSON CsvFormatOptions{..} = JSON.object
|
||||||
|
[ "format" JSON..= FormatCsv
|
||||||
|
, "delimiter" JSON..= fromEnum csvDelimiter
|
||||||
|
, "use-cr-lf" JSON..= csvUseCrLf
|
||||||
|
, "quoting" JSON..= csvQuoting
|
||||||
|
, "encoding" JSON..= csvEncoding
|
||||||
|
]
|
||||||
|
toJSON CsvXlsxFormatOptions = JSON.object
|
||||||
|
[ "format" JSON..= FormatXlsx
|
||||||
|
]
|
||||||
|
instance FromJSON CsvFormatOptions where
|
||||||
|
parseJSON = JSON.withObject "CsvFormatOptions" $ \o -> do
|
||||||
|
formatTag <- o JSON..:? "format" JSON..!= FormatCsv
|
||||||
|
|
||||||
|
case formatTag of
|
||||||
|
FormatCsv -> 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
|
||||||
|
csvEncoding <- o JSON..:? "encoding" JSON..!= csvEncoding def
|
||||||
|
return CsvFormatOptions{..}
|
||||||
|
FormatXlsx -> return CsvXlsxFormatOptions
|
||||||
|
|
||||||
|
derivePersistFieldJSON ''CsvOptions
|
||||||
|
|
||||||
|
nullaryPathPiece ''CsvPreset $ camelToPathPiece' 2
|
||||||
@ -1,5 +1,3 @@
|
|||||||
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
|
||||||
|
|
||||||
{-|
|
{-|
|
||||||
Module: Model.Types.Misc
|
Module: Model.Types.Misc
|
||||||
Description: Additional uncategorized types
|
Description: Additional uncategorized types
|
||||||
@ -7,7 +5,6 @@ Description: Additional uncategorized types
|
|||||||
|
|
||||||
module Model.Types.Misc
|
module Model.Types.Misc
|
||||||
( module Model.Types.Misc
|
( module Model.Types.Misc
|
||||||
, Quoting(..)
|
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import Import.NoModel
|
import Import.NoModel
|
||||||
@ -18,17 +15,10 @@ 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 qualified Data.Csv as Csv
|
import qualified Data.Csv as Csv
|
||||||
|
|
||||||
import qualified Data.Aeson as JSON
|
|
||||||
|
|
||||||
import Database.Persist.Sql (PersistFieldSql(..))
|
import Database.Persist.Sql (PersistFieldSql(..))
|
||||||
|
|
||||||
import Utils.Lens.TH
|
|
||||||
|
|
||||||
import Web.HttpApiData
|
import Web.HttpApiData
|
||||||
|
|
||||||
|
|
||||||
@ -66,135 +56,6 @@ $(deriveSimpleWith ''ToMessage 'toMessage (over Text.packed $ Text.intercalate "
|
|||||||
derivePersistField "Theme"
|
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
|
|
||||||
{ csvFormat :: CsvFormatOptions
|
|
||||||
, csvTimestamp :: Bool
|
|
||||||
}
|
|
||||||
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
|
|
||||||
def = CsvOptions
|
|
||||||
{ csvFormat = def
|
|
||||||
, csvTimestamp = False
|
|
||||||
}
|
|
||||||
|
|
||||||
instance Default CsvFormatOptions 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' CsvFormatOptions CsvPreset
|
|
||||||
csvPreset = prism' fromPreset toPreset
|
|
||||||
where
|
|
||||||
fromPreset :: CsvPreset -> CsvFormatOptions
|
|
||||||
fromPreset CsvPresetRFC = CsvFormatOptions
|
|
||||||
{ csvDelimiter = ','
|
|
||||||
, csvUseCrLf = True
|
|
||||||
, csvQuoting = QuoteMinimal
|
|
||||||
, csvEncoding = "UTF8"
|
|
||||||
}
|
|
||||||
fromPreset CsvPresetExcel = CsvFormatOptions
|
|
||||||
{ csvDelimiter = ';'
|
|
||||||
, csvUseCrLf = True
|
|
||||||
, csvQuoting = QuoteAll
|
|
||||||
, csvEncoding = "CP1252"
|
|
||||||
}
|
|
||||||
|
|
||||||
toPreset :: CsvFormatOptions -> Maybe CsvPreset
|
|
||||||
toPreset opts = case filter (\p -> fromPreset p == opts) universeF of
|
|
||||||
[p] -> Just p
|
|
||||||
_other -> Nothing
|
|
||||||
|
|
||||||
_CsvEncodeOptions :: Iso' CsvFormatOptions Csv.EncodeOptions
|
|
||||||
_CsvEncodeOptions = iso toEncode fromEncode
|
|
||||||
where
|
|
||||||
toEncode CsvFormatOptions{..} = Csv.defaultEncodeOptions
|
|
||||||
{ Csv.encDelimiter = fromIntegral $ fromEnum csvDelimiter
|
|
||||||
, Csv.encUseCrLf = csvUseCrLf
|
|
||||||
, Csv.encQuoting = csvQuoting
|
|
||||||
, Csv.encIncludeHeader = True
|
|
||||||
}
|
|
||||||
fromEncode encOpts = def
|
|
||||||
{ csvDelimiter = toEnum . fromIntegral $ Csv.encDelimiter encOpts
|
|
||||||
, csvUseCrLf = Csv.encUseCrLf encOpts
|
|
||||||
, csvQuoting = Csv.encQuoting encOpts
|
|
||||||
}
|
|
||||||
|
|
||||||
instance ToJSON CsvOptions where
|
|
||||||
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
|
|
||||||
, "use-cr-lf" JSON..= csvUseCrLf
|
|
||||||
, "quoting" JSON..= csvQuoting
|
|
||||||
, "encoding" JSON..= csvEncoding
|
|
||||||
]
|
|
||||||
instance FromJSON CsvFormatOptions where
|
|
||||||
parseJSON = JSON.withObject "CsvFormatOptions" $ \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
|
|
||||||
csvEncoding <- o JSON..:? "encoding" JSON..!= csvEncoding def
|
|
||||||
return CsvFormatOptions{..}
|
|
||||||
|
|
||||||
derivePersistFieldJSON ''CsvOptions
|
|
||||||
|
|
||||||
nullaryPathPiece ''CsvPreset $ camelToPathPiece' 2
|
|
||||||
|
|
||||||
instance YesodMail site => ToMailPart site (CsvRendered, CsvOptions) where
|
|
||||||
toMailPart (CsvRendered{..}, encOpts) = do
|
|
||||||
_partType .= decodeUtf8 typeCsv'
|
|
||||||
_partEncoding .= QuotedPrintableText
|
|
||||||
_partContent .= 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
|
|
||||||
toMailPart = toMailPart . (, def :: CsvOptions)
|
|
||||||
|
|
||||||
|
|
||||||
data FavouriteReason
|
data FavouriteReason
|
||||||
@ -210,7 +71,6 @@ deriveJSON defaultOptions
|
|||||||
} ''FavouriteReason
|
} ''FavouriteReason
|
||||||
derivePersistFieldJSON ''FavouriteReason
|
derivePersistFieldJSON ''FavouriteReason
|
||||||
|
|
||||||
|
|
||||||
data Sex
|
data Sex
|
||||||
= SexNotKnown
|
= SexNotKnown
|
||||||
| SexMale
|
| SexMale
|
||||||
|
|||||||
@ -774,6 +774,8 @@ whenIsRight :: Monad m => Either a b -> (b -> m ()) -> m ()
|
|||||||
whenIsRight (Right x) f = f x
|
whenIsRight (Right x) f = f x
|
||||||
whenIsRight (Left _) _ = return ()
|
whenIsRight (Left _) _ = return ()
|
||||||
|
|
||||||
|
throwLeft :: (MonadThrow m, Exception exc) => Either exc a -> m a
|
||||||
|
throwLeft = either throwM return
|
||||||
|
|
||||||
---------------
|
---------------
|
||||||
-- Exception --
|
-- Exception --
|
||||||
|
|||||||
@ -2,11 +2,13 @@
|
|||||||
|
|
||||||
module Utils.Csv
|
module Utils.Csv
|
||||||
( typeCsv, typeCsv', extensionCsv
|
( typeCsv, typeCsv', extensionCsv
|
||||||
|
, typeXlsx, extensionXlsx
|
||||||
, pathPieceCsv
|
, pathPieceCsv
|
||||||
, (.:??)
|
, (.:??)
|
||||||
, CsvRendered(..)
|
, CsvRendered(..)
|
||||||
, toCsvRendered
|
, toCsvRendered
|
||||||
, toDefaultOrderedCsvRendered
|
, toDefaultOrderedCsvRendered
|
||||||
|
, csvRenderedToXlsx, Xlsx, Xlsx.fromXlsx
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import ClassyPrelude hiding (lookup)
|
import ClassyPrelude hiding (lookup)
|
||||||
@ -14,7 +16,6 @@ import Settings.Mime
|
|||||||
|
|
||||||
import Data.Csv hiding (Name)
|
import Data.Csv hiding (Name)
|
||||||
import Data.Csv.Conduit (CsvParseError)
|
import Data.Csv.Conduit (CsvParseError)
|
||||||
import qualified Data.Csv.Incremental as Incremental
|
|
||||||
|
|
||||||
import Language.Haskell.TH (Name)
|
import Language.Haskell.TH (Name)
|
||||||
import Language.Haskell.TH.Lib
|
import Language.Haskell.TH.Lib
|
||||||
@ -22,6 +23,16 @@ import Language.Haskell.TH.Lib
|
|||||||
import Yesod.Core.Content
|
import Yesod.Core.Content
|
||||||
|
|
||||||
import qualified Data.Map as Map
|
import qualified Data.Map as Map
|
||||||
|
import qualified Data.HashMap.Strict as HashMap
|
||||||
|
|
||||||
|
import Codec.Xlsx (Xlsx)
|
||||||
|
import qualified Codec.Xlsx as Xlsx
|
||||||
|
|
||||||
|
import Data.Monoid (Endo(..))
|
||||||
|
|
||||||
|
import Control.Lens
|
||||||
|
|
||||||
|
import Data.Default
|
||||||
|
|
||||||
|
|
||||||
deriving instance Typeable CsvParseError
|
deriving instance Typeable CsvParseError
|
||||||
@ -30,10 +41,14 @@ instance Exception CsvParseError
|
|||||||
|
|
||||||
typeCsv, typeCsv' :: ContentType
|
typeCsv, typeCsv' :: ContentType
|
||||||
typeCsv = simpleContentType typeCsv'
|
typeCsv = simpleContentType typeCsv'
|
||||||
typeCsv' = "text/csv; charset=UTF-8; header=present"
|
typeCsv' = "text/csv; header=present"
|
||||||
|
|
||||||
extensionCsv :: Extension
|
typeXlsx :: ContentType
|
||||||
|
typeXlsx = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||||
|
|
||||||
|
extensionCsv, extensionXlsx :: Extension
|
||||||
extensionCsv = fromMaybe "csv" $ listToMaybe [ ext | (ext, mime) <- Map.toList mimeMap, mime == typeCsv ]
|
extensionCsv = fromMaybe "csv" $ listToMaybe [ ext | (ext, mime) <- Map.toList mimeMap, mime == typeCsv ]
|
||||||
|
extensionXlsx = fromMaybe "xlsx" $ listToMaybe [ ext | (ext, mime) <- Map.toList mimeMap, mime == typeXlsx ]
|
||||||
|
|
||||||
|
|
||||||
pathPieceCsv :: Name -> DecsQ
|
pathPieceCsv :: Name -> DecsQ
|
||||||
@ -55,17 +70,6 @@ data CsvRendered = CsvRendered
|
|||||||
, csvRenderedData :: [NamedRecord]
|
, csvRenderedData :: [NamedRecord]
|
||||||
} deriving (Eq, Read, Show, Generic, Typeable)
|
} deriving (Eq, Read, Show, Generic, Typeable)
|
||||||
|
|
||||||
instance ToContent CsvRendered where
|
|
||||||
toContent CsvRendered{..} = toContent . Incremental.encodeByName csvRenderedHeader $ foldr ((<>) . Incremental.encodeNamedRecord) mempty csvRenderedData
|
|
||||||
|
|
||||||
instance ToTypedContent CsvRendered where
|
|
||||||
toTypedContent = TypedContent
|
|
||||||
<$> getContentType . Identity
|
|
||||||
<*> toContent
|
|
||||||
|
|
||||||
instance HasContentType CsvRendered where
|
|
||||||
getContentType _ = typeCsv'
|
|
||||||
|
|
||||||
toCsvRendered :: forall mono.
|
toCsvRendered :: forall mono.
|
||||||
( ToNamedRecord (Element mono)
|
( ToNamedRecord (Element mono)
|
||||||
, MonoFoldable mono
|
, MonoFoldable mono
|
||||||
@ -83,3 +87,13 @@ toDefaultOrderedCsvRendered :: forall mono.
|
|||||||
)
|
)
|
||||||
=> mono -> CsvRendered
|
=> mono -> CsvRendered
|
||||||
toDefaultOrderedCsvRendered = toCsvRendered $ headerOrder (error "headerOrder" :: Element mono)
|
toDefaultOrderedCsvRendered = toCsvRendered $ headerOrder (error "headerOrder" :: Element mono)
|
||||||
|
|
||||||
|
|
||||||
|
csvRenderedToXlsx :: Text -- ^ Name of worksheet
|
||||||
|
-> CsvRendered -> Xlsx
|
||||||
|
csvRenderedToXlsx sheetName CsvRendered{..} = def & Xlsx.atSheet sheetName ?~ (def & appEndo (addHeader <> addValues))
|
||||||
|
where
|
||||||
|
addHeader = flip foldMap (zip [1..] $ toList csvRenderedHeader) $ \(c, bs) -> Endo $ Xlsx.cellValueAtRC (1, c) ?~ Xlsx.CellText (decodeUtf8 bs)
|
||||||
|
addValues = flip foldMap (zip [2..] csvRenderedData) $ \(r, nr) -> flip foldMap (zip [1..] $ toList csvRenderedHeader) $ \(c, hBS) -> case HashMap.lookup hBS nr of
|
||||||
|
Nothing -> mempty
|
||||||
|
Just vBS -> Endo $ Xlsx.cellValueAtRC (r, c) ?~ Xlsx.CellText (decodeUtf8 vBS)
|
||||||
|
|||||||
69
src/Utils/Csv/Mail.hs
Normal file
69
src/Utils/Csv/Mail.hs
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
||||||
|
|
||||||
|
module Utils.Csv.Mail
|
||||||
|
( recodeCsv
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Import.NoModel
|
||||||
|
import Model.Types.Csv
|
||||||
|
|
||||||
|
import qualified Data.Csv as Csv
|
||||||
|
|
||||||
|
import Data.Time.Clock.POSIX (getPOSIXTime)
|
||||||
|
|
||||||
|
import qualified Data.Conduit.Combinators as C
|
||||||
|
|
||||||
|
import Data.Encoding (encodeLazyByteStringExplicit, decodeLazyByteStringExplicit)
|
||||||
|
|
||||||
|
|
||||||
|
instance (RenderMessage site msg, YesodMail site) => ToMailPart site (msg, CsvRendered) where
|
||||||
|
toMailPart (sheetName, csvRendered@CsvRendered{..}) = do
|
||||||
|
encOpts <- lift askMailCsvOptions
|
||||||
|
|
||||||
|
case encOpts ^. _csvFormat of
|
||||||
|
CsvFormatOptions{}
|
||||||
|
| Just csvOpts <- encOpts ^? _csvFormat . _CsvEncodeOptions -> do
|
||||||
|
_partType .= decodeUtf8 typeCsv'
|
||||||
|
_partEncoding .= QuotedPrintableText
|
||||||
|
_partContent <~ fmap PartContent (liftHandler . runConduit $ C.sourceLazy (Csv.encodeByNameWith csvOpts csvRenderedHeader csvRenderedData) .| recodeCsv encOpts True C.sinkLazy)
|
||||||
|
| otherwise -> error "encOpts is CsvFormatOptions but cannot be converted via _CsvEncodeOptions"
|
||||||
|
CsvXlsxFormatOptions{} -> do
|
||||||
|
pNow <- liftIO getPOSIXTime
|
||||||
|
sheetName' <- lift $ ($ sheetName) <$> getMailMessageRender
|
||||||
|
_partType .= decodeUtf8 typeXlsx
|
||||||
|
_partEncoding .= Base64
|
||||||
|
_partContent .= PartContent (fromXlsx pNow $ csvRenderedToXlsx sheetName' csvRendered)
|
||||||
|
|
||||||
|
recodeCsv :: MonadThrow m
|
||||||
|
=> CsvOptions
|
||||||
|
-> Bool -- ^ recode from (internal) utf8 to user chosen coding?
|
||||||
|
-> ConduitT ByteString o m a -> ConduitT ByteString o m a
|
||||||
|
recodeCsv encOpts toUser act = fromMaybe act $ do
|
||||||
|
enc <- encOpts ^? _csvFormat . _csvEncoding
|
||||||
|
|
||||||
|
let
|
||||||
|
recode
|
||||||
|
| toUser = either throwM return . encodeLazyByteStringExplicit enc <=< either throwM return . decodeLazyByteStringExplicit UTF8
|
||||||
|
| otherwise = either throwM return . encodeLazyByteStringExplicit UTF8 <=< either throwM return . decodeLazyByteStringExplicit enc
|
||||||
|
|
||||||
|
return $ if
|
||||||
|
| enc == "UTF8" -> act
|
||||||
|
| FormatCsv <- fmt -> do
|
||||||
|
inp <- C.sinkLazy
|
||||||
|
inp' <- recode inp
|
||||||
|
sourceLazy inp' .| act
|
||||||
|
-- | FormatXlsx <- fmt -> do
|
||||||
|
-- inp <- C.sinkLazy
|
||||||
|
-- archive <- throwLeft $ Zip.toArchiveOrFail inp
|
||||||
|
-- archive' <- traverseOf (_zEntries . traverse . _Entrty . _3) recode archive
|
||||||
|
-- sourceLazy (Zip.fromArchive inp') .| act
|
||||||
|
| otherwise -> act
|
||||||
|
where
|
||||||
|
|
||||||
|
fmt = encOpts ^. _csvFormat . _CsvFormat
|
||||||
|
|
||||||
|
-- _zEntries :: Lens' Zip.Archive [Zip.Entry]
|
||||||
|
-- _zEntries = lens (\Zip.Archive{..} -> zEntries) (\archive entries -> archive { zEntries = entries })
|
||||||
|
|
||||||
|
-- _Entry :: Lens' Zip.Entry (FilePath, Integer, Lazy.ByteString)
|
||||||
|
-- _Entry = lens (\entry@Zip.Entry{..} -> (eRelativePath, eLastModified, Zip.fromEntry entry)) (uncurry3 Zip.toEntry)
|
||||||
@ -507,7 +507,7 @@ withJobWorkerStateLbls newLbls act = do
|
|||||||
|
|
||||||
liftIO . withLabel jobWorkerStateDuration newLbls . flip observe . realToFrac $ end - start
|
liftIO . withLabel jobWorkerStateDuration newLbls . flip observe . realToFrac $ end - start
|
||||||
|
|
||||||
either throwM return res
|
throwLeft res
|
||||||
|
|
||||||
observeYesodCacheSize :: MonadHandler m => m ()
|
observeYesodCacheSize :: MonadHandler m => m ()
|
||||||
observeYesodCacheSize = do
|
observeYesodCacheSize = do
|
||||||
@ -525,7 +525,7 @@ observeFavouritesQuickActionsDuration act = do
|
|||||||
|
|
||||||
liftIO . observe favouritesQuickActionsDuration . realToFrac $ end - start
|
liftIO . observe favouritesQuickActionsDuration . realToFrac $ end - start
|
||||||
|
|
||||||
either throwM return res
|
throwLeft res
|
||||||
|
|
||||||
data LoginOutcome
|
data LoginOutcome
|
||||||
= LoginSuccessful
|
= LoginSuccessful
|
||||||
|
|||||||
@ -94,7 +94,7 @@ encodeBearer token = do
|
|||||||
payload <- Jose.Claims . toStrict . JSON.encode <$> bearerToJSON token
|
payload <- Jose.Claims . toStrict . JSON.encode <$> bearerToJSON token
|
||||||
JwkSet jwks <- getsYesod $ view jsonWebKeySet
|
JwkSet jwks <- getsYesod $ view jsonWebKeySet
|
||||||
jwtEncoding <- getsYesod $ view _appBearerEncoding
|
jwtEncoding <- getsYesod $ view _appBearerEncoding
|
||||||
either throwM return =<< liftIO (Jose.encode jwks jwtEncoding payload)
|
throwLeft =<< liftIO (Jose.encode jwks jwtEncoding payload)
|
||||||
|
|
||||||
|
|
||||||
data BearerTokenException
|
data BearerTokenException
|
||||||
|
|||||||
@ -160,7 +160,7 @@ encodeSession :: MonadIO m
|
|||||||
-> SessionToken sess
|
-> SessionToken sess
|
||||||
-> m Jwt
|
-> m Jwt
|
||||||
encodeSession ServerSessionJwtConfig{..} token = liftIO $
|
encodeSession ServerSessionJwtConfig{..} token = liftIO $
|
||||||
either throwM return =<< Jose.encode (Jose.keys sJwtJwkSet) sJwtEncoding payload
|
throwLeft =<< Jose.encode (Jose.keys sJwtJwkSet) sJwtEncoding payload
|
||||||
where payload = Jose.Claims . toStrict $ JSON.encode token
|
where payload = Jose.Claims . toStrict $ JSON.encode token
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
2
templates/i18n/changelog/xlsx.de-de-formal.hamlet
Normal file
2
templates/i18n/changelog/xlsx.de-de-formal.hamlet
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
$newline never
|
||||||
|
Tabellen können nun auch als .xlsx exportiert werden
|
||||||
2
templates/i18n/changelog/xlsx.en-eu.hamlet
Normal file
2
templates/i18n/changelog/xlsx.en-eu.hamlet
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
$newline never
|
||||||
|
Tables can now also be exported as .xlsx
|
||||||
@ -2,7 +2,7 @@ module MailSpec where
|
|||||||
|
|
||||||
import TestImport
|
import TestImport
|
||||||
import Utils.DateTimeSpec ()
|
import Utils.DateTimeSpec ()
|
||||||
import Model.Types.LanguagesSpec ()
|
import Model.TypesSpec ()
|
||||||
|
|
||||||
import Mail
|
import Mail
|
||||||
|
|
||||||
|
|||||||
@ -11,7 +11,6 @@ import Data.Aeson (Value)
|
|||||||
import qualified Data.Aeson as Aeson
|
import qualified Data.Aeson as Aeson
|
||||||
import qualified Data.Aeson.Types as Aeson
|
import qualified Data.Aeson.Types as Aeson
|
||||||
|
|
||||||
import MailSpec ()
|
|
||||||
import Model.Types.LanguagesSpec ()
|
import Model.Types.LanguagesSpec ()
|
||||||
|
|
||||||
import System.IO.Unsafe
|
import System.IO.Unsafe
|
||||||
@ -278,11 +277,14 @@ instance Arbitrary Quoting where
|
|||||||
shrink = genericShrink
|
shrink = genericShrink
|
||||||
|
|
||||||
instance Arbitrary CsvFormatOptions where
|
instance Arbitrary CsvFormatOptions where
|
||||||
arbitrary = CsvFormatOptions
|
arbitrary = oneof
|
||||||
<$> suchThat arbitrary validDelimiter
|
[ CsvFormatOptions
|
||||||
<*> arbitrary
|
<$> suchThat arbitrary validDelimiter
|
||||||
<*> arbitrary
|
<*> arbitrary
|
||||||
<*> elements ["UTF8", "CP1252"]
|
<*> arbitrary
|
||||||
|
<*> elements ["UTF8", "CP1252"]
|
||||||
|
, pure CsvXlsxFormatOptions
|
||||||
|
]
|
||||||
where
|
where
|
||||||
validDelimiter c = and
|
validDelimiter c = and
|
||||||
[ Char.isLatin1 c
|
[ Char.isLatin1 c
|
||||||
@ -300,6 +302,13 @@ instance Arbitrary CsvOptions where
|
|||||||
instance Arbitrary CsvPreset where
|
instance Arbitrary CsvPreset where
|
||||||
arbitrary = genericArbitrary
|
arbitrary = genericArbitrary
|
||||||
shrink = genericShrink
|
shrink = genericShrink
|
||||||
|
instance CoArbitrary CsvPreset
|
||||||
|
instance Function CsvPreset
|
||||||
|
|
||||||
|
instance Arbitrary CsvFormat where
|
||||||
|
arbitrary = genericArbitrary
|
||||||
|
instance CoArbitrary CsvFormat
|
||||||
|
instance Function CsvFormat
|
||||||
|
|
||||||
instance Arbitrary Sex where
|
instance Arbitrary Sex where
|
||||||
arbitrary = genericArbitrary
|
arbitrary = genericArbitrary
|
||||||
@ -415,6 +424,8 @@ spec = do
|
|||||||
[ eqLaws, ordLaws, jsonLaws, showReadLaws, finiteLaws, pathPieceLaws ]
|
[ eqLaws, ordLaws, jsonLaws, showReadLaws, finiteLaws, pathPieceLaws ]
|
||||||
lawsCheckHspec (Proxy @CsvOptions)
|
lawsCheckHspec (Proxy @CsvOptions)
|
||||||
[ eqLaws, ordLaws, showReadLaws, jsonLaws, persistFieldLaws ]
|
[ eqLaws, ordLaws, showReadLaws, jsonLaws, persistFieldLaws ]
|
||||||
|
lawsCheckHspec (Proxy @CsvFormatOptions)
|
||||||
|
[ eqLaws, ordLaws, showReadLaws, jsonLaws ]
|
||||||
lawsCheckHspec (Proxy @CsvPreset)
|
lawsCheckHspec (Proxy @CsvPreset)
|
||||||
[ eqLaws, ordLaws, showReadLaws, boundedEnumLaws, finiteLaws, pathPieceLaws ]
|
[ eqLaws, ordLaws, showReadLaws, boundedEnumLaws, finiteLaws, pathPieceLaws ]
|
||||||
lawsCheckHspec (Proxy @Word24)
|
lawsCheckHspec (Proxy @Word24)
|
||||||
@ -465,6 +476,10 @@ spec = do
|
|||||||
describe "CsvOptions" $
|
describe "CsvOptions" $
|
||||||
it "json-decodes from empty object" . example $
|
it "json-decodes from empty object" . example $
|
||||||
Aeson.parseMaybe Aeson.parseJSON (Aeson.object []) `shouldBe` Just (def :: CsvOptions)
|
Aeson.parseMaybe Aeson.parseJSON (Aeson.object []) `shouldBe` Just (def :: CsvOptions)
|
||||||
|
describe "csvPreset" $
|
||||||
|
it "is a prism" . property $ isPrism csvPreset
|
||||||
|
describe "_CsvFormatPreset" $
|
||||||
|
it "is a prism" . property $ isPrism _CsvFormatPreset
|
||||||
describe "Word24" $ do
|
describe "Word24" $ do
|
||||||
it "encodes to the expected length" . property $
|
it "encodes to the expected length" . property $
|
||||||
\w -> olength (Binary.encode (w :: Word24)) == 3
|
\w -> olength (Binary.encode (w :: Word24)) == 3
|
||||||
|
|||||||
Reference in New Issue
Block a user