feat(personalised-sheet-files): introduce routes & work on crypto
This commit is contained in:
parent
5e584048f5
commit
9ee44aa2f1
@ -227,3 +227,6 @@ token-buckets:
|
|||||||
depth: 1572864000 # 1500MiB
|
depth: 1572864000 # 1500MiB
|
||||||
inv-rate: 1.9e-6 # 2MiB/s
|
inv-rate: 1.9e-6 # 2MiB/s
|
||||||
initial-value: 0
|
initial-value: 0
|
||||||
|
|
||||||
|
|
||||||
|
fallback-personalised-sheet-files-keys-expire: 2419200
|
||||||
|
|||||||
@ -1340,6 +1340,8 @@ MenuAllocationPriorities: Zentrale Dringlichkeiten
|
|||||||
MenuAllocationCompute: Platzvergabe berechnen
|
MenuAllocationCompute: Platzvergabe berechnen
|
||||||
MenuAllocationAccept: Platzvergabe akzeptieren
|
MenuAllocationAccept: Platzvergabe akzeptieren
|
||||||
MenuFaq: FAQ
|
MenuFaq: FAQ
|
||||||
|
MenuSheetPersonalisedFiles: Personalisierte Dateien herunterladen
|
||||||
|
MenuCourseSheetPersonalisedFiles: Vorlage für personalisierte Übungsblatt-Dateien herunterladen
|
||||||
|
|
||||||
BreadcrumbSubmissionFile: Datei
|
BreadcrumbSubmissionFile: Datei
|
||||||
BreadcrumbSubmissionUserInvite: Einladung zur Abgabe
|
BreadcrumbSubmissionUserInvite: Einladung zur Abgabe
|
||||||
@ -1411,6 +1413,8 @@ BreadcrumbAllocationCompute: Platzvergabe berechnen
|
|||||||
BreadcrumbAllocationAccept: Platzvergabe akzeptieren
|
BreadcrumbAllocationAccept: Platzvergabe akzeptieren
|
||||||
BreadcrumbMessageHide: Verstecken
|
BreadcrumbMessageHide: Verstecken
|
||||||
BreadcrumbFaq: FAQ
|
BreadcrumbFaq: FAQ
|
||||||
|
BreadcrumbSheetPersonalisedFiles: Personalisierte Dateien herunterladen
|
||||||
|
BreadcrumbCourseSheetPersonalisedFiles: Vorlage für personalisierte Übungsblatt-Dateien herunterladen
|
||||||
|
|
||||||
ExternalExamEdit coursen@CourseName examn@ExamName: Bearbeiten: #{coursen}, #{examn}
|
ExternalExamEdit coursen@CourseName examn@ExamName: Bearbeiten: #{coursen}, #{examn}
|
||||||
ExternalExamGrades coursen@CourseName examn@ExamName: Prüfungsleistungen: #{coursen}, #{examn}
|
ExternalExamGrades coursen@CourseName examn@ExamName: Prüfungsleistungen: #{coursen}, #{examn}
|
||||||
@ -2664,4 +2668,19 @@ SubmissionDoneNever: Nie
|
|||||||
SubmissionDoneByFile: Je nach Bewertungsdatei
|
SubmissionDoneByFile: Je nach Bewertungsdatei
|
||||||
SubmissionDoneAlways: Immer
|
SubmissionDoneAlways: Immer
|
||||||
CorrUploadSubmissionDoneMode: Bewertung abgeschlossen
|
CorrUploadSubmissionDoneMode: Bewertung abgeschlossen
|
||||||
CorrUploadSubmissionDoneModeTip: Sollen hochgeladene Korrekturen als abgeschlossen markiert werden? Bewertungen sind erst für Studierende sichtbar und zählen gegen Examboni, wenn sie abgeschlossen sind.
|
CorrUploadSubmissionDoneModeTip: Sollen hochgeladene Korrekturen als abgeschlossen markiert werden? Bewertungen sind erst für Studierende sichtbar und zählen gegen Examboni, wenn sie abgeschlossen sind.
|
||||||
|
|
||||||
|
SheetPersonalisedFiles: Personalisierte Dateien
|
||||||
|
SheetPersonalisedFilesTip: Sollen zusätzlich zu den oben angegebenen Dateien noch pro Kursteilnehmer personalisierte Dateien hinterlegt werden? Nur die jeweiligen Kursteilnehmer können ihre jeweiligen personalisierten Dateien einsehen.
|
||||||
|
SheetPersonalisedFilesUpload: Personalisierte Dateien
|
||||||
|
SheetPersonalisedFilesUploadTip: Laden Sie das Vorlage-Archiv herunter, sortieren Sie darin die personalisierten Dateien in die jeweiligen Verzeichnise der Kursteilnehmer ein und laden sie das Archiv dann hier wieder hoch.
|
||||||
|
SheetPersonalisedFilesKeepExisting: Bestehende Dateien behalten
|
||||||
|
SheetPersonalisedFilesKeepExistingTip: Sollen die hier neu hochgeladenen personalisierten Dateien zu den bestehenden (sofern vorhanden) hinzugefügt werden? Ansonsten werden die bestehenden Dateien vollständig durch die neu hochgeladenen ersetzt.
|
||||||
|
SheetPersonalisedFilesAllowNonPersonalisedSubmission: Nicht-personalisierte Abgabe erlauben
|
||||||
|
SheetPersonalisedFilesAllowNonPersonalisedSubmissionTip: Sollen auch Kursteilnehmer abgeben dürfen, für die keine personalisierten Dateien hinterlegt wurden?
|
||||||
|
SheetPersonalisedFilesDownloadTemplateHere: Sie können hier ein Vorlage-Archiv für die vom System erwartete Verzeichnisstruktur für personalisierte Übungsblatt-Dateien herunterladen:
|
||||||
|
PersonalisedSheetFilesDownloadAnonymous: Anonymisiert
|
||||||
|
PersonalisedSheetFilesDownloadSurnames: Mit Nachnamen
|
||||||
|
PersonalisedSheetFilesDownloadMatriculations: Mit Matrikelnummern
|
||||||
|
PersonalisedSheetFilesDownloadGroups: Mit festen Abgabegruppen
|
||||||
|
CoursePersonalisedSheetFilesArchiveName tid@TermId ssh@SchoolId csh@CourseShorthand: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-personalisierte_dateien
|
||||||
@ -14,6 +14,7 @@ Sheet -- exercise sheet for a given course
|
|||||||
autoDistribute Bool default=false -- Should correctors be assigned submissions automagically?
|
autoDistribute Bool default=false -- Should correctors be assigned submissions automagically?
|
||||||
anonymousCorrection Bool default=true
|
anonymousCorrection Bool default=true
|
||||||
requireExamRegistration ExamId Maybe -- Students may only submit if they are registered for the given exam
|
requireExamRegistration ExamId Maybe -- Students may only submit if they are registered for the given exam
|
||||||
|
allowNonPersonalisedSubmission Bool default=true
|
||||||
CourseSheet course name
|
CourseSheet course name
|
||||||
deriving Generic
|
deriving Generic
|
||||||
SheetEdit -- who edited when a row in table "Course", kept indefinitely
|
SheetEdit -- who edited when a row in table "Course", kept indefinitely
|
||||||
@ -44,3 +45,18 @@ SheetFile -- a file that is part of an exercise sheet
|
|||||||
content FileContentReference Maybe
|
content FileContentReference Maybe
|
||||||
modified UTCTime
|
modified UTCTime
|
||||||
UniqueSheetFile sheet type title
|
UniqueSheetFile sheet type title
|
||||||
|
PersonalisedSheetFile
|
||||||
|
sheet SheetId
|
||||||
|
user UserId
|
||||||
|
type SheetFileType
|
||||||
|
title FilePath
|
||||||
|
content FileContentReference Maybe
|
||||||
|
modified UTCTime
|
||||||
|
UniquePersonalisedSheetFile sheet user type title
|
||||||
|
|
||||||
|
FallbackPersonalisedSheetFilesKey
|
||||||
|
course CourseId
|
||||||
|
index Word24
|
||||||
|
secret ByteString
|
||||||
|
generated UTCTime
|
||||||
|
UniqueFallbackPersonalisedSheetFilesKey course index
|
||||||
@ -42,6 +42,7 @@ dependencies:
|
|||||||
- cryptonite-conduit
|
- cryptonite-conduit
|
||||||
- saltine
|
- saltine
|
||||||
- base64-bytestring
|
- base64-bytestring
|
||||||
|
- base32
|
||||||
- memory
|
- memory
|
||||||
- http-api-data
|
- http-api-data
|
||||||
- profunctors
|
- profunctors
|
||||||
|
|||||||
2
routes
2
routes
@ -165,6 +165,7 @@
|
|||||||
/iscorrector SIsCorrR GET !corrector -- Route is used to check for corrector access to this sheet
|
/iscorrector SIsCorrR GET !corrector -- Route is used to check for corrector access to this sheet
|
||||||
/pseudonym SPseudonymR GET POST !course-registeredANDcorrector-submissionsANDexam-registered
|
/pseudonym SPseudonymR GET POST !course-registeredANDcorrector-submissionsANDexam-registered
|
||||||
/corrector-invite/ SCorrInviteR GET POST
|
/corrector-invite/ SCorrInviteR GET POST
|
||||||
|
/personalised-files SPersonalFilesR GET
|
||||||
!/#SheetFileType SZipR GET !timeANDcourse-registeredANDexam-registered !timeANDmaterialsANDexam-registered !corrector !timeANDtutor
|
!/#SheetFileType SZipR GET !timeANDcourse-registeredANDexam-registered !timeANDmaterialsANDexam-registered !corrector !timeANDtutor
|
||||||
!/#SheetFileType/*FilePath SFileR GET !timeANDcourse-registeredANDexam-registered !timeANDmaterialsANDexam-registered !corrector !timeANDtutor
|
!/#SheetFileType/*FilePath SFileR GET !timeANDcourse-registeredANDexam-registered !timeANDmaterialsANDexam-registered !corrector !timeANDtutor
|
||||||
/file MaterialListR GET !course-registered !materials !corrector !tutor
|
/file MaterialListR GET !course-registered !materials !corrector !tutor
|
||||||
@ -214,6 +215,7 @@
|
|||||||
/events/#CryptoUUIDCourseEvent CourseEventR:
|
/events/#CryptoUUIDCourseEvent CourseEventR:
|
||||||
/edit CEvEditR GET POST
|
/edit CEvEditR GET POST
|
||||||
/delete CEvDeleteR GET POST
|
/delete CEvDeleteR GET POST
|
||||||
|
/personalised-sheet-files CPersonalFilesR GET
|
||||||
|
|
||||||
|
|
||||||
/subs CorrectionsR GET POST !corrector !lecturer
|
/subs CorrectionsR GET POST !corrector !lecturer
|
||||||
|
|||||||
@ -74,6 +74,8 @@ decCryptoIDs [ ''SubmissionId
|
|||||||
, ''TutorialId
|
, ''TutorialId
|
||||||
]
|
]
|
||||||
|
|
||||||
|
decCryptoIDKeySize
|
||||||
|
|
||||||
-- CryptoIDNamespace (CI FilePath) SubmissionId ~ "Submission"
|
-- CryptoIDNamespace (CI FilePath) SubmissionId ~ "Submission"
|
||||||
instance {-# OVERLAPS #-} PathPiece (E.CryptoID "Submission" (CI FilePath)) where
|
instance {-# OVERLAPS #-} PathPiece (E.CryptoID "Submission" (CI FilePath)) where
|
||||||
fromPathPiece (Text.unpack -> piece) = do
|
fromPathPiece (Text.unpack -> piece) = do
|
||||||
@ -91,3 +93,21 @@ instance {-# OVERLAPS #-} FromJSONKey (E.CryptoID "Submission" (CI FilePath)) wh
|
|||||||
fromJSONKey = FromJSONKeyTextParser $ maybe (fail "Could not parse CryptoFileNameSubmission") return . fromPathPiece
|
fromJSONKey = FromJSONKeyTextParser $ maybe (fail "Could not parse CryptoFileNameSubmission") return . fromPathPiece
|
||||||
instance {-# OVERLAPS #-} ToMarkup (E.CryptoID "Submission" (CI FilePath)) where
|
instance {-# OVERLAPS #-} ToMarkup (E.CryptoID "Submission" (CI FilePath)) where
|
||||||
toMarkup = toMarkup . toPathPiece
|
toMarkup = toMarkup . toPathPiece
|
||||||
|
|
||||||
|
-- CryptoIDNamespace (CI FilePath) UserId ~ "User"
|
||||||
|
instance {-# OVERLAPS #-} PathPiece (E.CryptoID "User" (CI FilePath)) where
|
||||||
|
fromPathPiece (Text.unpack -> piece) = do
|
||||||
|
piece' <- (stripPrefix `on` map CI.mk) "uwb" piece
|
||||||
|
return . CryptoID . CI.mk $ map CI.original piece'
|
||||||
|
toPathPiece = Text.pack . ("uwb" <>) . CI.foldedCase . ciphertext
|
||||||
|
|
||||||
|
instance {-# OVERLAPS #-} ToJSON (E.CryptoID "User" (CI FilePath)) where
|
||||||
|
toJSON = String . toPathPiece
|
||||||
|
instance {-# OVERLAPS #-} ToJSONKey (E.CryptoID "User" (CI FilePath)) where
|
||||||
|
toJSONKey = ToJSONKeyText toPathPiece (text . toPathPiece)
|
||||||
|
instance {-# OVERLAPS #-} FromJSON (E.CryptoID "User" (CI FilePath)) where
|
||||||
|
parseJSON = withText "CryptoFileNameUser" $ maybe (fail "Could not parse CryptoFileNameUser") return . fromPathPiece
|
||||||
|
instance {-# OVERLAPS #-} FromJSONKey (E.CryptoID "User" (CI FilePath)) where
|
||||||
|
fromJSONKey = FromJSONKeyTextParser $ maybe (fail "Could not parse CryptoFileNameUser") return . fromPathPiece
|
||||||
|
instance {-# OVERLAPS #-} ToMarkup (E.CryptoID "User" (CI FilePath)) where
|
||||||
|
toMarkup = toMarkup . toPathPiece
|
||||||
|
|||||||
@ -15,6 +15,9 @@ import qualified Data.Binary as Binary
|
|||||||
|
|
||||||
import Database.Persist.Sql
|
import Database.Persist.Sql
|
||||||
|
|
||||||
|
import qualified Data.CryptoID.ByteString as CryptoID.BS
|
||||||
|
import Crypto.Cipher.Types (cipherKeySize, KeySizeSpecifier(..))
|
||||||
|
|
||||||
|
|
||||||
decCryptoIDs :: [Name] -> DecsQ
|
decCryptoIDs :: [Name] -> DecsQ
|
||||||
decCryptoIDs = fmap concat . mapM decCryptoID
|
decCryptoIDs = fmap concat . mapM decCryptoID
|
||||||
@ -45,3 +48,13 @@ decCryptoIDs = fmap concat . mapM decCryptoID
|
|||||||
where
|
where
|
||||||
ns = (\nb -> fromMaybe nb $ stripSuffix "Id" nb) $ nameBase n
|
ns = (\nb -> fromMaybe nb $ stripSuffix "Id" nb) $ nameBase n
|
||||||
cryptoIDSyn (ct, str) = tySynD (mkName $ "Crypto" ++ str ++ ns) [] $ conT ''CryptoID `appT` return ct `appT` t
|
cryptoIDSyn (ct, str) = tySynD (mkName $ "Crypto" ++ str ++ ns) [] $ conT ''CryptoID `appT` return ct `appT` t
|
||||||
|
|
||||||
|
decCryptoIDKeySize :: DecsQ
|
||||||
|
decCryptoIDKeySize = sequence
|
||||||
|
[ tySynD (mkName "CryptoIDCipherKeySize") [] . litT . numTyLit $ fromIntegral cryptoIDKeySize
|
||||||
|
]
|
||||||
|
where
|
||||||
|
cryptoIDKeySize = case cipherKeySize (error "Cipher inspected during cipherKeySize" :: CryptoID.BS.CryptoCipher) of
|
||||||
|
KeySizeRange mins maxs -> max mins maxs
|
||||||
|
KeySizeEnum ss -> maximumEx ss
|
||||||
|
KeySizeFixed s -> s
|
||||||
|
|||||||
58
src/Data/Word/Word24/Instances.hs
Normal file
58
src/Data/Word/Word24/Instances.hs
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
||||||
|
|
||||||
|
module Data.Word.Word24.Instances
|
||||||
|
(
|
||||||
|
) where
|
||||||
|
|
||||||
|
import ClassyPrelude
|
||||||
|
import Database.Persist
|
||||||
|
import Database.Persist.Sql
|
||||||
|
import System.Random (Random(..))
|
||||||
|
|
||||||
|
import Data.Aeson (FromJSON(..), ToJSON(..))
|
||||||
|
import qualified Data.Aeson.Types as Aeson
|
||||||
|
|
||||||
|
import Data.Word.Word24
|
||||||
|
|
||||||
|
import Control.Lens
|
||||||
|
|
||||||
|
import Control.Monad.Fail
|
||||||
|
|
||||||
|
import qualified Data.Scientific as Scientific
|
||||||
|
|
||||||
|
import Data.Binary
|
||||||
|
import Data.Bits
|
||||||
|
|
||||||
|
|
||||||
|
instance PersistField Word24 where
|
||||||
|
toPersistValue p = toPersistValue (fromIntegral p :: Word32)
|
||||||
|
fromPersistValue v = do
|
||||||
|
w <- fromPersistValue v :: Either Text Word32
|
||||||
|
if
|
||||||
|
| 0 <= w
|
||||||
|
, w <= fromIntegral (maxBound :: Word24)
|
||||||
|
-> return $ fromIntegral w
|
||||||
|
| otherwise
|
||||||
|
-> Left "Word24 out of range"
|
||||||
|
|
||||||
|
instance PersistFieldSql Word24 where
|
||||||
|
sqlType _ = SqlInt32
|
||||||
|
|
||||||
|
instance Random Word24 where
|
||||||
|
randomR (max minBound -> lo, min maxBound -> hi) gen = over _1 (fromIntegral :: Word32 -> Word24) $ randomR (fromIntegral lo, fromIntegral hi) gen
|
||||||
|
random = randomR (minBound, maxBound)
|
||||||
|
|
||||||
|
instance FromJSON Word24 where
|
||||||
|
parseJSON (Aeson.Number n) = case Scientific.toBoundedInteger n of
|
||||||
|
Just n' -> return n'
|
||||||
|
Nothing -> fail "parsing Word24 failed, out of range or not integral"
|
||||||
|
parseJSON _ = fail "parsing Word24 failed, expected Number"
|
||||||
|
|
||||||
|
instance ToJSON Word24 where
|
||||||
|
toJSON = Aeson.Number . fromIntegral
|
||||||
|
|
||||||
|
|
||||||
|
-- | Big Endian
|
||||||
|
instance Binary Word24 where
|
||||||
|
put w = forM_ [2,1..0] $ putWord8 . fromIntegral . shiftR w . (* 8)
|
||||||
|
get = foldlM (\w i -> (.|. w) . flip shiftL (8 * i) . fromIntegral <$> getWord8) 0 [2,1..0]
|
||||||
@ -2548,6 +2548,7 @@ instance YesodBreadcrumbs UniWorX where
|
|||||||
SCorrInviteR -> i18nCrumb MsgBreadcrumbSheetCorrectorInvite . Just $ CSheetR tid ssh csh shn SShowR
|
SCorrInviteR -> i18nCrumb MsgBreadcrumbSheetCorrectorInvite . Just $ CSheetR tid ssh csh shn SShowR
|
||||||
SZipR sft -> i18nCrumb sft . Just $ CSheetR tid ssh csh shn SShowR
|
SZipR sft -> i18nCrumb sft . Just $ CSheetR tid ssh csh shn SShowR
|
||||||
SFileR _ _ -> i18nCrumb MsgBreadcrumbSheetFile . Just $ CSheetR tid ssh csh shn SShowR
|
SFileR _ _ -> i18nCrumb MsgBreadcrumbSheetFile . Just $ CSheetR tid ssh csh shn SShowR
|
||||||
|
SPersonalFilesR -> i18nCrumb MsgBreadcrumbSheetPersonalisedFiles . Just $ CSheetR tid ssh csh shn SShowR
|
||||||
|
|
||||||
breadcrumb (CourseR tid ssh csh MaterialListR) = i18nCrumb MsgMenuMaterialList . Just $ CourseR tid ssh csh CShowR
|
breadcrumb (CourseR tid ssh csh MaterialListR) = i18nCrumb MsgMenuMaterialList . Just $ CourseR tid ssh csh CShowR
|
||||||
breadcrumb (CourseR tid ssh csh MaterialNewR ) = i18nCrumb MsgMenuMaterialNew . Just $ CourseR tid ssh csh MaterialListR
|
breadcrumb (CourseR tid ssh csh MaterialNewR ) = i18nCrumb MsgMenuMaterialNew . Just $ CourseR tid ssh csh MaterialListR
|
||||||
@ -2560,6 +2561,8 @@ instance YesodBreadcrumbs UniWorX where
|
|||||||
MArchiveR -> i18nCrumb MsgBreadcrumbMaterialArchive . Just $ CMaterialR tid ssh csh mnm MShowR
|
MArchiveR -> i18nCrumb MsgBreadcrumbMaterialArchive . Just $ CMaterialR tid ssh csh mnm MShowR
|
||||||
MFileR _ -> i18nCrumb MsgBreadcrumbMaterialFile . Just $ CMaterialR tid ssh csh mnm MShowR
|
MFileR _ -> i18nCrumb MsgBreadcrumbMaterialFile . Just $ CMaterialR tid ssh csh mnm MShowR
|
||||||
|
|
||||||
|
breadcrumb (CourseR tid ssh csh CPersonalFilesR) = i18nCrumb MsgBreadcrumbCourseSheetPersonalisedFiles . Just $ CourseR tid ssh csh CShowR
|
||||||
|
|
||||||
breadcrumb CorrectionsR = i18nCrumb MsgMenuCorrections Nothing
|
breadcrumb CorrectionsR = i18nCrumb MsgMenuCorrections Nothing
|
||||||
breadcrumb CorrectionsUploadR = i18nCrumb MsgMenuCorrectionsUpload $ Just CorrectionsR
|
breadcrumb CorrectionsUploadR = i18nCrumb MsgMenuCorrectionsUpload $ Just CorrectionsR
|
||||||
breadcrumb CorrectionsCreateR = i18nCrumb MsgMenuCorrectionsCreate $ Just CorrectionsR
|
breadcrumb CorrectionsCreateR = i18nCrumb MsgMenuCorrectionsCreate $ Just CorrectionsR
|
||||||
@ -3982,6 +3985,32 @@ pageActions (CSheetR tid ssh csh shn SShowR) = do
|
|||||||
, navSubmissions
|
, navSubmissions
|
||||||
] ++ guardOnM (not showSubmissions) [ NavPageActionPrimary{ navLink, navChildren = [] } | navLink <- subsSecondary ] ++
|
] ++ guardOnM (not showSubmissions) [ NavPageActionPrimary{ navLink, navChildren = [] } | navLink <- subsSecondary ] ++
|
||||||
[ NavPageActionPrimary
|
[ NavPageActionPrimary
|
||||||
|
{ navLink = NavLink
|
||||||
|
{ navLabel = MsgMenuSheetPersonalisedFiles
|
||||||
|
, navRoute = CSheetR tid ssh csh shn SPersonalFilesR
|
||||||
|
, navAccess' =
|
||||||
|
let onlyPersonalised = fmap (maybe False $ not . E.unValue) . E.selectMaybe . E.from $ \(sheet `E.InnerJoin` course) -> do
|
||||||
|
E.on $ sheet E.^. SheetCourse E.==. course E.^. CourseId
|
||||||
|
E.where_$ sheet E.^. SheetName E.==. E.val shn
|
||||||
|
E.&&. course E.^. CourseTerm E.==. E.val tid
|
||||||
|
E.&&. course E.^. CourseSchool E.==. E.val ssh
|
||||||
|
E.&&. course E.^. CourseShorthand E.==. E.val csh
|
||||||
|
return $ sheet E.^. SheetAllowNonPersonalisedSubmission
|
||||||
|
hasPersonalised = E.selectExists . E.from $ \(sheet `E.InnerJoin` course `E.InnerJoin` personalisedSheetFile) -> do
|
||||||
|
E.on $ personalisedSheetFile E.^. PersonalisedSheetFileSheet E.==. sheet E.^. SheetId
|
||||||
|
E.on $ sheet E.^. SheetCourse E.==. course E.^. CourseId
|
||||||
|
E.where_$ sheet E.^. SheetName E.==. E.val shn
|
||||||
|
E.&&. course E.^. CourseTerm E.==. E.val tid
|
||||||
|
E.&&. course E.^. CourseSchool E.==. E.val ssh
|
||||||
|
E.&&. course E.^. CourseShorthand E.==. E.val csh
|
||||||
|
in runDB $ or2M onlyPersonalised hasPersonalised
|
||||||
|
, navType = NavTypeLink { navModal = True }
|
||||||
|
, navQuick' = mempty
|
||||||
|
, navForceActive = False
|
||||||
|
}
|
||||||
|
, navChildren = []
|
||||||
|
}
|
||||||
|
, NavPageActionPrimary
|
||||||
{ navLink = NavLink
|
{ navLink = NavLink
|
||||||
{ navLabel = MsgMenuSheetEdit
|
{ navLabel = MsgMenuSheetEdit
|
||||||
, navRoute = CSheetR tid ssh csh shn SEditR
|
, navRoute = CSheetR tid ssh csh shn SEditR
|
||||||
|
|||||||
@ -19,6 +19,7 @@ import Handler.Course.Application as Handler.Course
|
|||||||
import Handler.ExamOffice.Course as Handler.Course
|
import Handler.ExamOffice.Course as Handler.Course
|
||||||
import Handler.Course.News as Handler.Course
|
import Handler.Course.News as Handler.Course
|
||||||
import Handler.Course.Events as Handler.Course
|
import Handler.Course.Events as Handler.Course
|
||||||
|
import Handler.Sheet.PersonalisedFiles as Handler.Course (getCPersonalFilesR)
|
||||||
|
|
||||||
|
|
||||||
getCHiWisR :: TermId -> SchoolId -> CourseShorthand -> Handler Html
|
getCHiWisR :: TermId -> SchoolId -> CourseShorthand -> Handler Html
|
||||||
|
|||||||
@ -18,6 +18,7 @@ import Handler.Sheet.Current as Handler.Sheet
|
|||||||
import Handler.Sheet.Download as Handler.Sheet
|
import Handler.Sheet.Download as Handler.Sheet
|
||||||
import Handler.Sheet.New as Handler.Sheet
|
import Handler.Sheet.New as Handler.Sheet
|
||||||
import Handler.Sheet.Show as Handler.Sheet
|
import Handler.Sheet.Show as Handler.Sheet
|
||||||
|
import Handler.Sheet.PersonalisedFiles as Handler.Sheet (getSPersonalFilesR)
|
||||||
|
|
||||||
|
|
||||||
getSIsCorrR :: TermId -> SchoolId -> CourseShorthand -> SheetName -> Handler Html
|
getSIsCorrR :: TermId -> SchoolId -> CourseShorthand -> SheetName -> Handler Html
|
||||||
|
|||||||
@ -16,18 +16,20 @@ import qualified Data.Map as Map
|
|||||||
|
|
||||||
import Handler.Sheet.Form
|
import Handler.Sheet.Form
|
||||||
import Handler.Sheet.CorrectorInvite
|
import Handler.Sheet.CorrectorInvite
|
||||||
|
import Handler.Sheet.PersonalisedFiles
|
||||||
|
|
||||||
|
|
||||||
getSEditR, postSEditR :: TermId -> SchoolId -> CourseShorthand -> SheetName -> Handler Html
|
getSEditR, postSEditR :: TermId -> SchoolId -> CourseShorthand -> SheetName -> Handler Html
|
||||||
getSEditR = postSEditR
|
getSEditR = postSEditR
|
||||||
postSEditR tid ssh csh shn = do
|
postSEditR tid ssh csh shn = do
|
||||||
(Entity sid Sheet{..}, sheetFileIds, currentLoads) <- runDB $ do
|
(Entity sid Sheet{..}, sheetFileIds, currentLoads, hasPersonalisedFiles) <- runDB $ do
|
||||||
ent@(Entity sid _) <- fetchSheet tid ssh csh shn
|
ent@(Entity sid _) <- fetchSheet tid ssh csh shn
|
||||||
fti <- getFtIdMap $ entityKey ent
|
fti <- getFtIdMap $ entityKey ent
|
||||||
cLoads <- Map.union
|
cLoads <- Map.union
|
||||||
<$> fmap (foldMap $ \(Entity _ SheetCorrector{..}) -> Map.singleton (Right sheetCorrectorUser) (InvDBDataSheetCorrector sheetCorrectorLoad sheetCorrectorState, InvTokenDataSheetCorrector)) (selectList [ SheetCorrectorSheet ==. sid ] [])
|
<$> fmap (foldMap $ \(Entity _ SheetCorrector{..}) -> Map.singleton (Right sheetCorrectorUser) (InvDBDataSheetCorrector sheetCorrectorLoad sheetCorrectorState, InvTokenDataSheetCorrector)) (selectList [ SheetCorrectorSheet ==. sid ] [])
|
||||||
<*> fmap (fmap (, InvTokenDataSheetCorrector) . Map.mapKeysMonotonic Left) (sourceInvitationsF sid)
|
<*> fmap (fmap (, InvTokenDataSheetCorrector) . Map.mapKeysMonotonic Left) (sourceInvitationsF sid)
|
||||||
return (ent, fti, cLoads)
|
hasPersonalisedFiles <- exists [ PersonalisedSheetFileSheet ==. sid ]
|
||||||
|
return (ent, fti, cLoads, hasPersonalisedFiles)
|
||||||
let template = Just $ SheetForm
|
let template = Just $ SheetForm
|
||||||
{ sfName = sheetName
|
{ sfName = sheetName
|
||||||
, sfDescription = sheetDescription
|
, sfDescription = sheetDescription
|
||||||
@ -48,6 +50,11 @@ postSEditR tid ssh csh shn = do
|
|||||||
, sfAnonymousCorrection = sheetAnonymousCorrection
|
, sfAnonymousCorrection = sheetAnonymousCorrection
|
||||||
, sfCorrectors = currentLoads
|
, sfCorrectors = currentLoads
|
||||||
, sfRequireExamRegistration = sheetRequireExamRegistration
|
, sfRequireExamRegistration = sheetRequireExamRegistration
|
||||||
|
, sfPersonalF = guardOn (hasPersonalisedFiles || not sheetAllowNonPersonalisedSubmission) SheetPersonalisedFilesForm
|
||||||
|
{ spffFilesKeepExisting = hasPersonalisedFiles
|
||||||
|
, spffAllowNonPersonalisedSubmission = sheetAllowNonPersonalisedSubmission
|
||||||
|
, spffFiles = Nothing
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let action = uniqueReplace sid -- More specific error message for edit old sheet could go here by using myReplaceUnique instead
|
let action = uniqueReplace sid -- More specific error message for edit old sheet could go here by using myReplaceUnique instead
|
||||||
@ -79,6 +86,7 @@ handleSheetEdit tid ssh csh msId template dbAction = do
|
|||||||
, sheetAutoDistribute = sfAutoDistribute
|
, sheetAutoDistribute = sfAutoDistribute
|
||||||
, sheetAnonymousCorrection = sfAnonymousCorrection
|
, sheetAnonymousCorrection = sfAnonymousCorrection
|
||||||
, sheetRequireExamRegistration = sfRequireExamRegistration
|
, sheetRequireExamRegistration = sfRequireExamRegistration
|
||||||
|
, sheetAllowNonPersonalisedSubmission = fromMaybe True $ spffAllowNonPersonalisedSubmission <$> sfPersonalF
|
||||||
}
|
}
|
||||||
mbsid <- dbAction newSheet
|
mbsid <- dbAction newSheet
|
||||||
case mbsid of
|
case mbsid of
|
||||||
@ -88,6 +96,9 @@ handleSheetEdit tid ssh csh msId template dbAction = do
|
|||||||
insertSheetFile' sid SheetHint $ fromMaybe (return ()) sfHintF
|
insertSheetFile' sid SheetHint $ fromMaybe (return ()) sfHintF
|
||||||
insertSheetFile' sid SheetSolution $ fromMaybe (return ()) sfSolutionF
|
insertSheetFile' sid SheetSolution $ fromMaybe (return ()) sfSolutionF
|
||||||
insertSheetFile' sid SheetMarking $ fromMaybe (return ()) sfMarkingF
|
insertSheetFile' sid SheetMarking $ fromMaybe (return ()) sfMarkingF
|
||||||
|
runConduit $
|
||||||
|
maybe (return ()) (transPipe liftHandler) (spffFiles =<< sfPersonalF)
|
||||||
|
.| sinkPersonalisedSheetFiles cid (Just sid) (fromMaybe False $ spffFilesKeepExisting <$> sfPersonalF)
|
||||||
insert_ $ SheetEdit aid actTime sid
|
insert_ $ SheetEdit aid actTime sid
|
||||||
addMessageI Success $ MsgSheetEditOk tid ssh csh sfName
|
addMessageI Success $ MsgSheetEditOk tid ssh csh sfName
|
||||||
-- Sanity checks generating warnings only, but not errors!
|
-- Sanity checks generating warnings only, but not errors!
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
module Handler.Sheet.Form
|
module Handler.Sheet.Form
|
||||||
( SheetForm(..), Loads
|
( SheetForm(..), SheetPersonalisedFilesForm(..), Loads
|
||||||
, makeSheetForm
|
, makeSheetForm
|
||||||
, getFtIdMap
|
, getFtIdMap
|
||||||
) where
|
) where
|
||||||
@ -29,6 +29,7 @@ data SheetForm = SheetForm
|
|||||||
, sfDescription :: Maybe Html
|
, sfDescription :: Maybe Html
|
||||||
, sfRequireExamRegistration :: Maybe ExamId
|
, sfRequireExamRegistration :: Maybe ExamId
|
||||||
, sfSheetF, sfHintF, sfSolutionF, sfMarkingF :: Maybe FileUploads
|
, sfSheetF, sfHintF, sfSolutionF, sfMarkingF :: Maybe FileUploads
|
||||||
|
, sfPersonalF :: Maybe SheetPersonalisedFilesForm
|
||||||
, sfVisibleFrom :: Maybe UTCTime
|
, sfVisibleFrom :: Maybe UTCTime
|
||||||
, sfActiveFrom :: Maybe UTCTime
|
, sfActiveFrom :: Maybe UTCTime
|
||||||
, sfActiveTo :: Maybe UTCTime
|
, sfActiveTo :: Maybe UTCTime
|
||||||
@ -44,6 +45,12 @@ data SheetForm = SheetForm
|
|||||||
-- Keine SheetId im Formular!
|
-- Keine SheetId im Formular!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data SheetPersonalisedFilesForm = SheetPersonalisedFilesForm
|
||||||
|
{ spffFiles :: Maybe FileUploads
|
||||||
|
, spffFilesKeepExisting :: Bool
|
||||||
|
, spffAllowNonPersonalisedSubmission :: Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
getFtIdMap :: Key Sheet -> DB (SheetFileType -> Set FileReference)
|
getFtIdMap :: Key Sheet -> DB (SheetFileType -> Set FileReference)
|
||||||
getFtIdMap sId = do
|
getFtIdMap sId = do
|
||||||
@ -59,6 +66,7 @@ makeSheetForm cId msId template = identifyForm FIDsheet . validateForm validateS
|
|||||||
(Just sId) -> liftHandler $ runDB $ getFtIdMap sId
|
(Just sId) -> liftHandler $ runDB $ getFtIdMap sId
|
||||||
MsgRenderer mr <- getMsgRenderer
|
MsgRenderer mr <- getMsgRenderer
|
||||||
ctime <- ceilingQuarterHour <$> liftIO getCurrentTime
|
ctime <- ceilingQuarterHour <$> liftIO getCurrentTime
|
||||||
|
sheetPersonalisedFilesForm <- makeSheetPersonalisedFilesForm $ template >>= sfPersonalF
|
||||||
flip (renderAForm FormStandard) html $ SheetForm
|
flip (renderAForm FormStandard) html $ SheetForm
|
||||||
<$> areq (textField & cfStrip & cfCI) (fslI MsgSheetName) (sfName <$> template)
|
<$> areq (textField & cfStrip & cfCI) (fslI MsgSheetName) (sfName <$> template)
|
||||||
<*> aopt htmlField (fslI MsgSheetDescription) (sfDescription <$> template)
|
<*> aopt htmlField (fslI MsgSheetDescription) (sfDescription <$> template)
|
||||||
@ -69,6 +77,7 @@ makeSheetForm cId msId template = identifyForm FIDsheet . validateForm validateS
|
|||||||
<*> aopt (multiFileField $ oldFileIds SheetSolution) (fslI MsgSheetSolution) (sfSolutionF <$> template)
|
<*> aopt (multiFileField $ oldFileIds SheetSolution) (fslI MsgSheetSolution) (sfSolutionF <$> template)
|
||||||
<*> aopt (multiFileField $ oldFileIds SheetMarking) (fslI MsgSheetMarkingFiles
|
<*> aopt (multiFileField $ oldFileIds SheetMarking) (fslI MsgSheetMarkingFiles
|
||||||
& setTooltip MsgSheetMarkingTip) (sfMarkingF <$> template)
|
& setTooltip MsgSheetMarkingTip) (sfMarkingF <$> template)
|
||||||
|
<*> optionalActionA sheetPersonalisedFilesForm (fslI MsgSheetPersonalisedFiles & setTooltip MsgSheetPersonalisedFilesTip) (is _Just . sfPersonalF <$> template)
|
||||||
<* aformSection MsgSheetFormTimes
|
<* aformSection MsgSheetFormTimes
|
||||||
<*> aopt utcTimeField (fslI MsgSheetVisibleFrom
|
<*> aopt utcTimeField (fslI MsgSheetVisibleFrom
|
||||||
& setTooltip MsgSheetVisibleFromTip)
|
& setTooltip MsgSheetVisibleFromTip)
|
||||||
@ -90,6 +99,25 @@ makeSheetForm cId msId template = identifyForm FIDsheet . validateForm validateS
|
|||||||
<*> apopt checkBoxField (fslI MsgSheetAnonymousCorrection & setTooltip MsgSheetAnonymousCorrectionTip) (sfAnonymousCorrection <$> template)
|
<*> apopt checkBoxField (fslI MsgSheetAnonymousCorrection & setTooltip MsgSheetAnonymousCorrectionTip) (sfAnonymousCorrection <$> template)
|
||||||
<*> correctorForm (fromMaybe mempty $ sfCorrectors <$> template)
|
<*> correctorForm (fromMaybe mempty $ sfCorrectors <$> template)
|
||||||
where
|
where
|
||||||
|
makeSheetPersonalisedFilesForm :: Maybe SheetPersonalisedFilesForm -> MForm Handler (AForm Handler SheetPersonalisedFilesForm)
|
||||||
|
makeSheetPersonalisedFilesForm template' = do
|
||||||
|
templateDownloadMessage <- runMaybeT . hoist (liftHandler . runDB) $ do
|
||||||
|
Sheet{..} <- MaybeT . fmap join $ traverse get msId
|
||||||
|
Course{..} <- MaybeT $ get cId
|
||||||
|
let downloadRoute = CSheetR courseTerm courseSchool courseShorthand sheetName SPersonalFilesR
|
||||||
|
guardM $ hasReadAccessTo downloadRoute
|
||||||
|
messageIconWidget Info IconFileZip
|
||||||
|
[whamlet|
|
||||||
|
$newline never
|
||||||
|
_{MsgSheetPersonalisedFilesDownloadTemplateHere}<br />
|
||||||
|
^{modal (i18n MsgMenuSheetPersonalisedFiles) (Left (SomeRoute downloadRoute))}
|
||||||
|
|]
|
||||||
|
return $ SheetPersonalisedFilesForm
|
||||||
|
<$ maybe (pure ()) aformMessage templateDownloadMessage
|
||||||
|
<*> aopt (zipFileField True Nothing) (fslI MsgSheetPersonalisedFilesUpload & setTooltip MsgSheetPersonalisedFilesUploadTip) Nothing
|
||||||
|
<*> apopt checkBoxField (fslI MsgSheetPersonalisedFilesKeepExisting & setTooltip MsgSheetPersonalisedFilesKeepExistingTip) (fmap spffFilesKeepExisting template' <|> Just True)
|
||||||
|
<*> apopt checkBoxField (fslI MsgSheetPersonalisedFilesAllowNonPersonalisedSubmission & setTooltip MsgSheetPersonalisedFilesAllowNonPersonalisedSubmissionTip) (fmap spffAllowNonPersonalisedSubmission template' <|> Just True)
|
||||||
|
|
||||||
validateSheet :: FormValidator SheetForm Handler ()
|
validateSheet :: FormValidator SheetForm Handler ()
|
||||||
validateSheet = do
|
validateSheet = do
|
||||||
SheetForm{..} <- State.get
|
SheetForm{..} <- State.get
|
||||||
|
|||||||
@ -65,6 +65,7 @@ postSheetNewR tid ssh csh = do
|
|||||||
, sfCorrectors = loads
|
, sfCorrectors = loads
|
||||||
, sfAnonymousCorrection = sheetAnonymousCorrection
|
, sfAnonymousCorrection = sheetAnonymousCorrection
|
||||||
, sfRequireExamRegistration = Nothing
|
, sfRequireExamRegistration = Nothing
|
||||||
|
, sfPersonalF = Nothing
|
||||||
}
|
}
|
||||||
_other -> Nothing
|
_other -> Nothing
|
||||||
let action = -- More specific error message for new sheet could go here, if insertUnique returns Nothing
|
let action = -- More specific error message for new sheet could go here, if insertUnique returns Nothing
|
||||||
|
|||||||
220
src/Handler/Sheet/PersonalisedFiles.hs
Normal file
220
src/Handler/Sheet/PersonalisedFiles.hs
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
{-# OPTIONS_GHC -Wno-error=redundant-constraints -Wno-error=unused-top-binds -Wno-error=deprecations #-}
|
||||||
|
|
||||||
|
module Handler.Sheet.PersonalisedFiles
|
||||||
|
( sinkPersonalisedSheetFiles
|
||||||
|
, getSPersonalFilesR, getCPersonalFilesR
|
||||||
|
, PersonalisedSheetFilesKeyException(..)
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Import
|
||||||
|
|
||||||
|
import Handler.Utils
|
||||||
|
|
||||||
|
import qualified Data.Conduit.Combinators as C
|
||||||
|
|
||||||
|
import qualified Crypto.MAC.KMAC as Crypto
|
||||||
|
import qualified Data.ByteArray as BA
|
||||||
|
import qualified Data.Binary as Binary
|
||||||
|
import Crypto.Hash.Algorithms (SHAKE256)
|
||||||
|
|
||||||
|
import Data.ByteString.Lazy.Base32
|
||||||
|
|
||||||
|
import qualified Data.CaseInsensitive as CI
|
||||||
|
|
||||||
|
import Language.Haskell.TH (nameBase)
|
||||||
|
|
||||||
|
import qualified Data.CryptoID.ByteString as CryptoID
|
||||||
|
import qualified Data.CryptoID.Class.ImplicitNamespace as I
|
||||||
|
|
||||||
|
import qualified Database.Esqueleto as E
|
||||||
|
|
||||||
|
import qualified Data.Map as Map
|
||||||
|
import qualified Data.Set as Set
|
||||||
|
import qualified Data.Text as Text
|
||||||
|
|
||||||
|
import Text.Unidecode (unidecode)
|
||||||
|
import Data.Char (isAlphaNum)
|
||||||
|
|
||||||
|
import GHC.Stack
|
||||||
|
|
||||||
|
|
||||||
|
resolvePersonalisedSheetFiles
|
||||||
|
:: forall a m.
|
||||||
|
( MonadHandler m
|
||||||
|
, HandlerSite m ~ UniWorX
|
||||||
|
)
|
||||||
|
=> Lens' a FilePath
|
||||||
|
-> CourseId
|
||||||
|
-> Maybe SheetId
|
||||||
|
-> ConduitT a (Either a (a, FileReferenceResidual PersonalisedSheetFile)) m ()
|
||||||
|
resolvePersonalisedSheetFiles fpL _cid _mbsid = do
|
||||||
|
C.mapM $ \fRef -> maybeT (return $ Left fRef) . fmap (Right . swap) . flip runStateT fRef . zoom fpL $ do
|
||||||
|
error "not implemented" :: StateT FilePath (MaybeT m) (FileReferenceResidual PersonalisedSheetFile)
|
||||||
|
|
||||||
|
|
||||||
|
sinkPersonalisedSheetFiles :: forall m.
|
||||||
|
( MonadHandler m
|
||||||
|
, HandlerSite m ~ UniWorX
|
||||||
|
)
|
||||||
|
=> CourseId
|
||||||
|
-> Maybe SheetId
|
||||||
|
-> Bool -- ^ Keep existing?
|
||||||
|
-> ConduitT FileReference Void (SqlPersistT m) ()
|
||||||
|
sinkPersonalisedSheetFiles cid mbsid _keep
|
||||||
|
= resolvePersonalisedSheetFiles _fileReferenceTitle cid mbsid
|
||||||
|
.| error "not implemented"
|
||||||
|
|
||||||
|
|
||||||
|
data PersonalisedSheetFilesDownloadAnonymous
|
||||||
|
= PersonalisedSheetFilesDownloadAnonymous
|
||||||
|
| PersonalisedSheetFilesDownloadSurnames
|
||||||
|
| PersonalisedSheetFilesDownloadMatriculations
|
||||||
|
| PersonalisedSheetFilesDownloadGroups
|
||||||
|
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable)
|
||||||
|
deriving anyclass (Universe, Finite)
|
||||||
|
nullaryPathPiece ''PersonalisedSheetFilesDownloadAnonymous $ camelToPathPiece' 4
|
||||||
|
embedRenderMessage ''UniWorX ''PersonalisedSheetFilesDownloadAnonymous id
|
||||||
|
|
||||||
|
sourcePersonalisedSheetFiles :: forall m.
|
||||||
|
( MonadHandler m
|
||||||
|
, HandlerSite m ~ UniWorX
|
||||||
|
, MonadThrow m
|
||||||
|
, MonadRandom m
|
||||||
|
)
|
||||||
|
=> CourseId
|
||||||
|
-> Maybe SheetId
|
||||||
|
-> PersonalisedSheetFilesDownloadAnonymous
|
||||||
|
-> ConduitT () (Either PersonalisedSheetFile File) (SqlPersistT m) ()
|
||||||
|
sourcePersonalisedSheetFiles cId mbsid anonMode = do
|
||||||
|
(mbIdx, cIDKey) <- lift . newPersonalisedFilesKey $ maybe (Left cId) Right mbsid
|
||||||
|
let
|
||||||
|
genSuffixes uid = case anonMode of
|
||||||
|
PersonalisedSheetFilesDownloadGroups -> do
|
||||||
|
subGroups <- E.select . E.from $ \(submissionGroup `E.InnerJoin` submissionGroupUser) -> do
|
||||||
|
E.on $ submissionGroup E.^. SubmissionGroupId E.==. submissionGroupUser E.^. SubmissionGroupUserSubmissionGroup
|
||||||
|
E.where_ $ submissionGroup E.^. SubmissionGroupCourse E.==. E.val cId
|
||||||
|
E.where_ $ submissionGroupUser E.^. SubmissionGroupUserUser E.==. E.val uid
|
||||||
|
return $ submissionGroup E.^. SubmissionGroupName
|
||||||
|
return . nub . sort $ map (filter isAlphaNum . foldMap unidecode . unpack . CI.original . E.unValue) subGroups
|
||||||
|
otherAnon
|
||||||
|
| Just f <- userFeature otherAnon -> do
|
||||||
|
features <- E.select . E.from $ \user -> do
|
||||||
|
E.where_ $ user E.^. UserId E.==. E.val uid
|
||||||
|
return $ f user
|
||||||
|
return . sort $ mapMaybe (fmap (filter isAlphaNum . foldMap unidecode . unpack) . E.unValue) features
|
||||||
|
_other -> return mempty
|
||||||
|
where userFeature PersonalisedSheetFilesDownloadSurnames
|
||||||
|
= Just $ E.just . (E.^. UserSurname)
|
||||||
|
userFeature PersonalisedSheetFilesDownloadMatriculations
|
||||||
|
= Just $ E.castString . (E.^. UserMatrikelnummer)
|
||||||
|
userFeature _
|
||||||
|
= Nothing
|
||||||
|
|
||||||
|
sqlSource = E.selectSource . E.from $ \(courseParticipant `E.LeftOuterJoin` personalisedSheetFile) -> do
|
||||||
|
E.on $ E.just (courseParticipant E.^. CourseParticipantUser) E.==. personalisedSheetFile E.?. PersonalisedSheetFileUser
|
||||||
|
E.&&. E.val mbsid E.==. personalisedSheetFile E.?. PersonalisedSheetFileSheet
|
||||||
|
E.where_ $ courseParticipant E.^. CourseParticipantCourse E.==. E.val cId
|
||||||
|
return (courseParticipant, personalisedSheetFile)
|
||||||
|
|
||||||
|
toRefs = awaitForever $ \(Entity _ CourseParticipant{..}, mbPFile) -> do
|
||||||
|
MsgRenderer mr <- getMsgRenderer
|
||||||
|
suffix <- do
|
||||||
|
sufCache <- uses _sufCache $ Map.lookup courseParticipantUser
|
||||||
|
case sufCache of
|
||||||
|
Just suf -> return suf
|
||||||
|
Nothing -> do
|
||||||
|
suf <- lift . lift $ genSuffixes courseParticipantUser
|
||||||
|
_sufCache %= Map.insert courseParticipantUser suf
|
||||||
|
return suf
|
||||||
|
cID <- either throwM return . (runReaderT ?? cIDKey) $ I.encrypt courseParticipantUser
|
||||||
|
let dirName = unpack . Text.intercalate "_" . map pack $ suffix `snoc` mkPersonalisedFilesDirectory mbIdx cID
|
||||||
|
unlessM (uses _dirCache $ Set.member dirName) $ do
|
||||||
|
yield $ Right File
|
||||||
|
{ fileTitle = dirName
|
||||||
|
, fileContent = Nothing
|
||||||
|
, fileModified = courseParticipantRegistration
|
||||||
|
}
|
||||||
|
forM_ [SheetExercise, SheetHint, SheetSolution] $ \sfType ->
|
||||||
|
yield $ Right File
|
||||||
|
{ fileTitle = dirName </> unpack (mr $ SheetArchiveFileTypeDirectory sfType)
|
||||||
|
, fileContent = Nothing
|
||||||
|
, fileModified = courseParticipantRegistration
|
||||||
|
}
|
||||||
|
-- TODO: meta.yml
|
||||||
|
_dirCache %= Set.insert dirName
|
||||||
|
whenIsJust mbPFile $ \(Entity _ pFile@PersonalisedSheetFile{..}) -> do
|
||||||
|
let dirName' = dirName </> unpack (mr $ SheetArchiveFileTypeDirectory personalisedSheetFileType)
|
||||||
|
yield . Left $ over (_FileReference . _1 . _fileReferenceTitle) (dirName' </>) pFile
|
||||||
|
where
|
||||||
|
_sufCache :: Lens' _ _
|
||||||
|
_sufCache = _1
|
||||||
|
_dirCache :: Lens' _ _
|
||||||
|
_dirCache = _2
|
||||||
|
|
||||||
|
|
||||||
|
sqlSource .| evalStateC (Map.empty, Set.empty) toRefs
|
||||||
|
|
||||||
|
|
||||||
|
data PersonalisedSheetFilesKeyException
|
||||||
|
= PersonalisedSheetFilesKeyCouldNotDecodeRandom
|
||||||
|
| FallbackPersonalisedSheetFilesKeysExhausted
|
||||||
|
| PersonalisedSheetFilesKeyInsufficientContext
|
||||||
|
| PersonalisedSheetFilesKeyNotFound
|
||||||
|
deriving (Eq, Ord, Read, Show, Generic, Typeable)
|
||||||
|
deriving anyclass (Exception)
|
||||||
|
|
||||||
|
newPersonalisedFilesKey :: forall m.
|
||||||
|
( MonadHandler m
|
||||||
|
, HandlerSite m ~ UniWorX
|
||||||
|
, MonadThrow m, MonadRandom m
|
||||||
|
, HasCallStack
|
||||||
|
)
|
||||||
|
=> Either CourseId SheetId -> SqlPersistT m (Maybe Word24, CryptoIDKey)
|
||||||
|
newPersonalisedFilesKey (Right shId) = cryptoIDKey $ \cIDKey -> fmap (Nothing,) $
|
||||||
|
either (const $ throwM PersonalisedSheetFilesKeyCouldNotDecodeRandom) (views _3 return) . Binary.decodeOrFail . fromStrict . BA.convert $
|
||||||
|
Crypto.kmac @(SHAKE256 CryptoIDCipherKeySize) (encodeUtf8 . (pack :: String -> Text) $ nameBase 'newPersonalisedFilesKey) (toStrict $ Binary.encode shId) cIDKey
|
||||||
|
newPersonalisedFilesKey (Left cId) = do
|
||||||
|
now <- liftIO getCurrentTime
|
||||||
|
secret <- CryptoID.genKey
|
||||||
|
let secret' = toStrict $ Binary.encode secret
|
||||||
|
firstN <- getRandom
|
||||||
|
traceM $ "newPersonalisedFilesKey: " <> prettyCallStack callStack
|
||||||
|
|
||||||
|
let loop :: Word24 -> SqlPersistT m (Maybe Word24, CryptoIDKey)
|
||||||
|
loop n = do
|
||||||
|
traceM "insertUnique"
|
||||||
|
didInsert <- is _Just <$> insertUnique (FallbackPersonalisedSheetFilesKey cId n secret' now)
|
||||||
|
if | didInsert
|
||||||
|
-> return (Just n, secret)
|
||||||
|
| (firstN == minBound && n == maxBound)
|
||||||
|
|| n == pred firstN
|
||||||
|
-> throwM FallbackPersonalisedSheetFilesKeysExhausted
|
||||||
|
| n == maxBound
|
||||||
|
-> loop minBound
|
||||||
|
| otherwise
|
||||||
|
-> loop $ succ n
|
||||||
|
in loop firstN
|
||||||
|
|
||||||
|
getPersonalisedFilesKey :: CourseId -> Maybe SheetId -> Maybe Word24 -> DB CryptoIDKey
|
||||||
|
getPersonalisedFilesKey _ Nothing Nothing = throwM PersonalisedSheetFilesKeyInsufficientContext
|
||||||
|
getPersonalisedFilesKey _ (Just shId) Nothing = view _2 <$> newPersonalisedFilesKey (Right shId)
|
||||||
|
getPersonalisedFilesKey cId _ (Just idx) = maybeT (throwM PersonalisedSheetFilesKeyNotFound) $ do
|
||||||
|
Entity _ FallbackPersonalisedSheetFilesKey{..} <- MaybeT . getBy $ UniqueFallbackPersonalisedSheetFilesKey cId idx
|
||||||
|
either (const $ throwM PersonalisedSheetFilesKeyCouldNotDecodeRandom) (views _3 return) . Binary.decodeOrFail . fromStrict $ BA.convert fallbackPersonalisedSheetFilesKeySecret
|
||||||
|
|
||||||
|
mkPersonalisedFilesDirectory :: Maybe Word24 -> CryptoFileNameUser -> FilePath
|
||||||
|
mkPersonalisedFilesDirectory Nothing cID = unpack $ toPathPiece cID
|
||||||
|
mkPersonalisedFilesDirectory (Just idx) cID = unpack $ toPathPiece cID <> "-" <> CI.foldCase (toStrict . encodeBase32Unpadded $ Binary.encode idx)
|
||||||
|
|
||||||
|
resolvePersonalisedFilesDirectory :: FilePath -> [(Maybe Word24, CryptoFileNameUser)]
|
||||||
|
resolvePersonalisedFilesDirectory = error "not implemented"
|
||||||
|
|
||||||
|
|
||||||
|
getSPersonalFilesR :: TermId -> SchoolId -> CourseShorthand -> SheetName -> Handler TypedContent
|
||||||
|
getSPersonalFilesR = error "not implemented"
|
||||||
|
|
||||||
|
getCPersonalFilesR :: TermId -> SchoolId -> CourseShorthand -> Handler TypedContent
|
||||||
|
getCPersonalFilesR tid ssh csh = do
|
||||||
|
cId <- runDB . getKeyBy404 $ TermSchoolCourseShort tid ssh csh
|
||||||
|
archiveName <- fmap (flip addExtension (unpack extensionZip) . unpack) . ap getMessageRender . pure $ MsgCoursePersonalisedSheetFilesArchiveName tid ssh csh
|
||||||
|
serveZipArchive' archiveName $ sourcePersonalisedSheetFiles cId Nothing PersonalisedSheetFilesDownloadAnonymous -- TODO: get Form for anonymisiation
|
||||||
@ -60,7 +60,7 @@ serveSomeFiles archiveName source = serveSomeFiles' archiveName $ source .| C.ma
|
|||||||
|
|
||||||
serveSomeFiles' :: forall file. HasFileReference file => FilePath -> ConduitT () (Either file File) (YesodDB UniWorX) () -> Handler TypedContent
|
serveSomeFiles' :: forall file. HasFileReference file => FilePath -> ConduitT () (Either file File) (YesodDB UniWorX) () -> Handler TypedContent
|
||||||
serveSomeFiles' archiveName source = do
|
serveSomeFiles' archiveName source = do
|
||||||
results <- runDB . runConduit $ source .| peekN 2
|
(source', results) <- runDB $ runPeekN 2 source
|
||||||
|
|
||||||
$logDebugS "serveSomeFiles" . tshow $ length results
|
$logDebugS "serveSomeFiles" . tshow $ length results
|
||||||
|
|
||||||
@ -71,14 +71,17 @@ serveSomeFiles' archiveName source = do
|
|||||||
setContentDisposition' $ Just archiveName
|
setContentDisposition' $ Just archiveName
|
||||||
respondSourceDB typeZip $ do
|
respondSourceDB typeZip $ do
|
||||||
let zipComment = T.encodeUtf8 $ pack archiveName
|
let zipComment = T.encodeUtf8 $ pack archiveName
|
||||||
source .| eitherC sourceFiles' (C.map id) .| produceZip ZipInfo{..} .| Conduit.map toFlushBuilder
|
source' .| eitherC sourceFiles' (C.map id) .| produceZip ZipInfo{..} .| Conduit.map toFlushBuilder
|
||||||
|
|
||||||
-- | Serve any number of files as a zip-archive of files, identified through a given DB query
|
-- | Serve any number of files as a zip-archive of files, identified through a given DB query
|
||||||
--
|
--
|
||||||
-- Like `serveSomeFiles`, but always sends a zip-archive, even if a single file is returned
|
-- Like `serveSomeFiles`, but always sends a zip-archive, even if a single file is returned
|
||||||
serveZipArchive :: forall file. HasFileReference file => FilePath -> ConduitT () file (YesodDB UniWorX) () -> Handler TypedContent
|
serveZipArchive :: forall file. HasFileReference file => FilePath -> ConduitT () file (YesodDB UniWorX) () -> Handler TypedContent
|
||||||
serveZipArchive archiveName source = do
|
serveZipArchive archiveName source = serveZipArchive' archiveName $ source .| C.map Left
|
||||||
results <- runDB . runConduit $ source .| peekN 1
|
|
||||||
|
serveZipArchive' :: forall file. HasFileReference file => FilePath -> ConduitT () (Either file File) (YesodDB UniWorX) () -> Handler TypedContent
|
||||||
|
serveZipArchive' archiveName source = do
|
||||||
|
(source', results) <- runDB $ runPeekN 1 source
|
||||||
|
|
||||||
$logDebugS "serveZipArchive" . tshow $ length results
|
$logDebugS "serveZipArchive" . tshow $ length results
|
||||||
|
|
||||||
@ -88,7 +91,7 @@ serveZipArchive archiveName source = do
|
|||||||
setContentDisposition' $ Just archiveName
|
setContentDisposition' $ Just archiveName
|
||||||
respondSourceDB typeZip $ do
|
respondSourceDB typeZip $ do
|
||||||
let zipComment = T.encodeUtf8 $ pack archiveName
|
let zipComment = T.encodeUtf8 $ pack archiveName
|
||||||
source .| sourceFiles' .| produceZip ZipInfo{..} .| Conduit.map toFlushBuilder
|
source' .| eitherC sourceFiles' (C.map id) .| produceZip ZipInfo{..} .| Conduit.map toFlushBuilder
|
||||||
|
|
||||||
|
|
||||||
-- | Prefix a message with a short course id,
|
-- | Prefix a message with a short course id,
|
||||||
|
|||||||
@ -972,7 +972,7 @@ genericFileField mkOpts = Field{..}
|
|||||||
)
|
)
|
||||||
.| C.map (\(fileReferenceTitle, (fileReferenceContent, fileReferenceModified, _)) -> FileReference{..})
|
.| C.map (\(fileReferenceTitle, (fileReferenceContent, fileReferenceModified, _)) -> FileReference{..})
|
||||||
mapM_ handleFile (bool (take 1) id fieldMultiple files) .| transPipe runDB (handleUpload opts mIdent)
|
mapM_ handleFile (bool (take 1) id fieldMultiple files) .| transPipe runDB (handleUpload opts mIdent)
|
||||||
(unsealConduitT -> fSrc', length -> nFiles) <- liftHandler $ fSrc $$+ peekN 2
|
(fSrc', length -> nFiles) <- liftHandler $ runPeekN 2 fSrc
|
||||||
$logDebugS "genericFileField.fieldParse" $ tshow nFiles
|
$logDebugS "genericFileField.fieldParse" $ tshow nFiles
|
||||||
if
|
if
|
||||||
| nFiles <= 0 -> return Nothing
|
| nFiles <= 0 -> return Nothing
|
||||||
|
|||||||
@ -339,6 +339,10 @@ submissionMultiArchive anonymous (Set.toList -> ids) = do
|
|||||||
E.on $ submissionGroup E.^. SubmissionGroupId E.==. submissionGroupUser E.^. SubmissionGroupUserSubmissionGroup
|
E.on $ submissionGroup E.^. SubmissionGroupId E.==. submissionGroupUser E.^. SubmissionGroupUserSubmissionGroup
|
||||||
E.where_ $ submissionUser E.^. SubmissionUserSubmission E.==. E.val submissionID
|
E.where_ $ submissionUser E.^. SubmissionUserSubmission E.==. E.val submissionID
|
||||||
E.where_ $ E.subSelectForeign submissionUser SubmissionUserSubmission (\submission -> E.subSelectForeign submission SubmissionSheet (E.^. SheetGrouping)) E.==. E.val RegisteredGroups
|
E.where_ $ E.subSelectForeign submissionUser SubmissionUserSubmission (\submission -> E.subSelectForeign submission SubmissionSheet (E.^. SheetGrouping)) E.==. E.val RegisteredGroups
|
||||||
|
E.where_ . E.exists . E.from $ \(submission `E.InnerJoin` sheet) -> do
|
||||||
|
E.on $ submission E.^. SubmissionSheet E.==. sheet E.^. SheetId
|
||||||
|
E.where_ $ submission E.^. SubmissionId E.==. E.val submissionID
|
||||||
|
E.where_ $ sheet E.^. SheetCourse E.==. submissionGroup E.^. SubmissionGroupCourse
|
||||||
return $ submissionGroup E.^. SubmissionGroupName
|
return $ submissionGroup E.^. SubmissionGroupName
|
||||||
let asciiGroups = nub . sort $ map (filter isAlphaNum . foldMap unidecode . unpack . CI.original . E.unValue) groups
|
let asciiGroups = nub . sort $ map (filter isAlphaNum . foldMap unidecode . unpack . CI.original . E.unValue) groups
|
||||||
return . intercalate "_" $ asciiGroups `snoc` fp
|
return . intercalate "_" $ asciiGroups `snoc` fp
|
||||||
|
|||||||
@ -164,6 +164,7 @@ import Network.HTTP.Types.Method.Instances as Import ()
|
|||||||
import Crypto.Random.Instances as Import ()
|
import Crypto.Random.Instances as Import ()
|
||||||
import Network.Minio.Instances as Import ()
|
import Network.Minio.Instances as Import ()
|
||||||
import System.Clock.Instances as Import ()
|
import System.Clock.Instances as Import ()
|
||||||
|
import Data.Word.Word24.Instances as Import ()
|
||||||
|
|
||||||
import Crypto.Hash as Import (Digest, SHA3_256, SHA3_512)
|
import Crypto.Hash as Import (Digest, SHA3_256, SHA3_512)
|
||||||
import Crypto.Random as Import (ChaChaDRG, Seed)
|
import Crypto.Random as Import (ChaChaDRG, Seed)
|
||||||
@ -184,6 +185,8 @@ import Data.Encoding.UTF8 as Import (UTF8(UTF8))
|
|||||||
|
|
||||||
import GHC.TypeLits as Import (KnownSymbol)
|
import GHC.TypeLits as Import (KnownSymbol)
|
||||||
|
|
||||||
|
import Data.Word.Word24 as Import
|
||||||
|
|
||||||
|
|
||||||
import Control.Monad.Trans.RWS (RWST)
|
import Control.Monad.Trans.RWS (RWST)
|
||||||
|
|
||||||
|
|||||||
@ -61,6 +61,7 @@ import Jobs.Handler.SynchroniseLdap
|
|||||||
import Jobs.Handler.PruneInvitations
|
import Jobs.Handler.PruneInvitations
|
||||||
import Jobs.Handler.ChangeUserDisplayEmail
|
import Jobs.Handler.ChangeUserDisplayEmail
|
||||||
import Jobs.Handler.Files
|
import Jobs.Handler.Files
|
||||||
|
import Jobs.Handler.PersonalisedSheetFiles
|
||||||
|
|
||||||
import Jobs.HealthReport
|
import Jobs.HealthReport
|
||||||
|
|
||||||
|
|||||||
@ -78,6 +78,17 @@ determineCrontab = execWriterT $ do
|
|||||||
, cronNotAfter = Right CronNotScheduled
|
, cronNotAfter = Right CronNotScheduled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
oldestFallbackPersonalisedSheetFilesKey <- lift $ preview (_head . _entityVal . _fallbackPersonalisedSheetFilesKeyGenerated) <$> selectList [] [Asc FallbackPersonalisedSheetFilesKeyGenerated, LimitTo 1]
|
||||||
|
whenIsJust oldestFallbackPersonalisedSheetFilesKey $ \oldest -> tell $ HashMap.singleton
|
||||||
|
(JobCtlQueue JobPruneFallbackPersonalisedSheetFilesKeys)
|
||||||
|
Cron
|
||||||
|
{ cronInitial = CronTimestamp . utcToLocalTime $ addUTCTime appFallbackPersonalisedSheetFilesKeysExpire oldest
|
||||||
|
, cronRepeat = CronRepeatOnChange
|
||||||
|
, cronRateLimit = appFallbackPersonalisedSheetFilesKeysExpire / 2
|
||||||
|
, cronNotAfter = Right CronNotScheduled
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
whenIsJust (appInjectFiles <* appUploadCacheConf) $ \iInterval ->
|
whenIsJust (appInjectFiles <* appUploadCacheConf) $ \iInterval ->
|
||||||
tell $ HashMap.singleton
|
tell $ HashMap.singleton
|
||||||
(JobCtlQueue JobInjectFiles)
|
(JobCtlQueue JobInjectFiles)
|
||||||
|
|||||||
15
src/Jobs/Handler/PersonalisedSheetFiles.hs
Normal file
15
src/Jobs/Handler/PersonalisedSheetFiles.hs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
module Jobs.Handler.PersonalisedSheetFiles
|
||||||
|
( dispatchJobPruneFallbackPersonalisedSheetFilesKeys
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Import
|
||||||
|
|
||||||
|
import Database.Persist.Sql (deleteWhereCount)
|
||||||
|
|
||||||
|
|
||||||
|
dispatchJobPruneFallbackPersonalisedSheetFilesKeys :: JobHandler UniWorX
|
||||||
|
dispatchJobPruneFallbackPersonalisedSheetFilesKeys = JobHandlerAtomic . hoist lift $ do
|
||||||
|
now <- liftIO getCurrentTime
|
||||||
|
expires <- getsYesod $ view _appFallbackPersonalisedSheetFilesKeysExpire
|
||||||
|
n <- deleteWhereCount [ FallbackPersonalisedSheetFilesKeyGenerated <. addUTCTime (- expires) now ]
|
||||||
|
$logInfoS "PruneFallbackPersonalisedSheetFilesKeys" [st|Deleted #{n} expired fallback personalised sheet files keys|]
|
||||||
@ -81,6 +81,7 @@ data Job = JobSendNotification { jRecipient :: UserId, jNotification :: Notifica
|
|||||||
| JobPruneSessionFiles
|
| JobPruneSessionFiles
|
||||||
| JobPruneUnreferencedFiles
|
| JobPruneUnreferencedFiles
|
||||||
| JobInjectFiles
|
| JobInjectFiles
|
||||||
|
| JobPruneFallbackPersonalisedSheetFilesKeys
|
||||||
deriving (Eq, Ord, Show, Read, Generic, Typeable)
|
deriving (Eq, Ord, Show, Read, Generic, Typeable)
|
||||||
data Notification = NotificationSubmissionRated { nSubmission :: SubmissionId }
|
data Notification = NotificationSubmissionRated { nSubmission :: SubmissionId }
|
||||||
| NotificationSheetActive { nSheet :: SheetId }
|
| NotificationSheetActive { nSheet :: SheetId }
|
||||||
|
|||||||
44
src/Model.hs
44
src/Model.hs
@ -122,14 +122,14 @@ instance HasFileReference SheetFile where
|
|||||||
}
|
}
|
||||||
, SheetFileResidual
|
, SheetFileResidual
|
||||||
{ sheetFileResidualSheet = sheetFileSheet
|
{ sheetFileResidualSheet = sheetFileSheet
|
||||||
, sheetFileResidualType = sheetFileType
|
, sheetFileResidualType = sheetFileType
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
(\( FileReference{..}
|
(\( FileReference{..}
|
||||||
, SheetFileResidual{..}
|
, SheetFileResidual{..}
|
||||||
) -> SheetFile
|
) -> SheetFile
|
||||||
{ sheetFileSheet = sheetFileResidualSheet
|
{ sheetFileSheet = sheetFileResidualSheet
|
||||||
, sheetFileType = sheetFileResidualType
|
, sheetFileType = sheetFileResidualType
|
||||||
, sheetFileTitle = fileReferenceTitle
|
, sheetFileTitle = fileReferenceTitle
|
||||||
, sheetFileContent = fileReferenceContent
|
, sheetFileContent = fileReferenceContent
|
||||||
@ -137,9 +137,45 @@ instance HasFileReference SheetFile where
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
fileReferenceTitleField = SheetFileTitle
|
fileReferenceTitleField = SheetFileTitle
|
||||||
fileReferenceContentField = SheetFileContent
|
fileReferenceContentField = SheetFileContent
|
||||||
fileReferenceModifiedField = SheetFileModified
|
fileReferenceModifiedField = SheetFileModified
|
||||||
|
|
||||||
|
instance HasFileReference PersonalisedSheetFile where
|
||||||
|
data FileReferenceResidual PersonalisedSheetFile = PersonalisedSheetFileResidual
|
||||||
|
{ personalisedSheetFileResidualSheet :: SheetId
|
||||||
|
, personalisedSheetFileResidualUser :: UserId
|
||||||
|
, personalisedSheetFileResidualType :: SheetFileType
|
||||||
|
}
|
||||||
|
|
||||||
|
_FileReference
|
||||||
|
= iso (\PersonalisedSheetFile{..} -> ( FileReference
|
||||||
|
{ fileReferenceTitle = personalisedSheetFileTitle
|
||||||
|
, fileReferenceContent = personalisedSheetFileContent
|
||||||
|
, fileReferenceModified = personalisedSheetFileModified
|
||||||
|
}
|
||||||
|
, PersonalisedSheetFileResidual
|
||||||
|
{ personalisedSheetFileResidualSheet = personalisedSheetFileSheet
|
||||||
|
, personalisedSheetFileResidualUser = personalisedSheetFileUser
|
||||||
|
, personalisedSheetFileResidualType = personalisedSheetFileType
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
(\( FileReference{..}
|
||||||
|
, PersonalisedSheetFileResidual{..}
|
||||||
|
) -> PersonalisedSheetFile
|
||||||
|
{ personalisedSheetFileSheet = personalisedSheetFileResidualSheet
|
||||||
|
, personalisedSheetFileUser = personalisedSheetFileResidualUser
|
||||||
|
, personalisedSheetFileType = personalisedSheetFileResidualType
|
||||||
|
, personalisedSheetFileTitle = fileReferenceTitle
|
||||||
|
, personalisedSheetFileContent = fileReferenceContent
|
||||||
|
, personalisedSheetFileModified = fileReferenceModified
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
fileReferenceTitleField = PersonalisedSheetFileTitle
|
||||||
|
fileReferenceContentField = PersonalisedSheetFileContent
|
||||||
|
fileReferenceModifiedField = PersonalisedSheetFileModified
|
||||||
|
|
||||||
instance HasFileReference SubmissionFile where
|
instance HasFileReference SubmissionFile where
|
||||||
data FileReferenceResidual SubmissionFile = SubmissionFileResidual
|
data FileReferenceResidual SubmissionFile = SubmissionFileResidual
|
||||||
|
|||||||
@ -15,8 +15,6 @@ import qualified Data.Aeson.Types as Aeson
|
|||||||
|
|
||||||
import Database.Persist.Sql
|
import Database.Persist.Sql
|
||||||
|
|
||||||
import Data.Word.Word24
|
|
||||||
|
|
||||||
import qualified Data.CaseInsensitive as CI
|
import qualified Data.CaseInsensitive as CI
|
||||||
|
|
||||||
import qualified Data.Text as Text
|
import qualified Data.Text as Text
|
||||||
@ -55,26 +53,9 @@ type PseudonymWord = CI Text
|
|||||||
|
|
||||||
newtype Pseudonym = Pseudonym Word24
|
newtype Pseudonym = Pseudonym Word24
|
||||||
deriving (Eq, Ord, Read, Show, Generic, Data)
|
deriving (Eq, Ord, Read, Show, Generic, Data)
|
||||||
deriving newtype (Bounded, Enum, Integral, Num, Real, Ix)
|
deriving newtype ( Bounded, Enum, Integral, Num, Real, Ix
|
||||||
|
, PersistField, PersistFieldSql, Random
|
||||||
|
)
|
||||||
instance PersistField Pseudonym where
|
|
||||||
toPersistValue p = toPersistValue (fromIntegral p :: Word32)
|
|
||||||
fromPersistValue v = do
|
|
||||||
w <- fromPersistValue v :: Either Text Word32
|
|
||||||
if
|
|
||||||
| 0 <= w
|
|
||||||
, w <= fromIntegral (maxBound :: Pseudonym)
|
|
||||||
-> return $ fromIntegral w
|
|
||||||
| otherwise
|
|
||||||
-> Left "Pseudonym out of range"
|
|
||||||
|
|
||||||
instance PersistFieldSql Pseudonym where
|
|
||||||
sqlType _ = SqlInt32
|
|
||||||
|
|
||||||
instance Random Pseudonym where
|
|
||||||
randomR (max minBound -> lo, min maxBound -> hi) gen = over _1 (fromIntegral :: Word32 -> Pseudonym) $ randomR (fromIntegral lo, fromIntegral hi) gen
|
|
||||||
random = randomR (minBound, maxBound)
|
|
||||||
|
|
||||||
instance FromJSON Pseudonym where
|
instance FromJSON Pseudonym where
|
||||||
parseJSON v@(Aeson.Number _) = do
|
parseJSON v@(Aeson.Number _) = do
|
||||||
|
|||||||
@ -177,6 +177,8 @@ data AppSettings = AppSettings
|
|||||||
|
|
||||||
, appPersistentTokenBuckets :: TokenBucketIdent -> TokenBucketConf
|
, appPersistentTokenBuckets :: TokenBucketIdent -> TokenBucketConf
|
||||||
|
|
||||||
|
, appFallbackPersonalisedSheetFilesKeysExpire :: NominalDiffTime
|
||||||
|
|
||||||
, appInitialInstanceID :: Maybe (Either FilePath UUID)
|
, appInitialInstanceID :: Maybe (Either FilePath UUID)
|
||||||
, appRibbon :: Maybe Text
|
, appRibbon :: Maybe Text
|
||||||
} deriving Show
|
} deriving Show
|
||||||
@ -555,6 +557,8 @@ instance FromJSON AppSettings where
|
|||||||
appUploadCacheConf <- assertM (not . null . Minio.connectHost) <$> o .:? "upload-cache"
|
appUploadCacheConf <- assertM (not . null . Minio.connectHost) <$> o .:? "upload-cache"
|
||||||
appUploadCacheBucket <- o .: "upload-cache-bucket"
|
appUploadCacheBucket <- o .: "upload-cache-bucket"
|
||||||
|
|
||||||
|
appFallbackPersonalisedSheetFilesKeysExpire <- o .: "fallback-personalised-sheet-files-keys-expire"
|
||||||
|
|
||||||
return AppSettings{..}
|
return AppSettings{..}
|
||||||
|
|
||||||
makeClassy_ ''AppSettings
|
makeClassy_ ''AppSettings
|
||||||
|
|||||||
@ -855,6 +855,9 @@ takeWhileTime maxT = do
|
|||||||
let tDelta = now `diffUTCTime` sTime
|
let tDelta = now `diffUTCTime` sTime
|
||||||
return $ tDelta < maxT
|
return $ tDelta < maxT
|
||||||
|
|
||||||
|
runPeekN :: forall o m n. (Integral n, Monad m) => n -> ConduitT () o m () -> m (ConduitT () o m (), [o])
|
||||||
|
runPeekN n src = over (mapped . _1) unsealConduitT $ src $$+ peekN n
|
||||||
|
|
||||||
-----------------
|
-----------------
|
||||||
-- Alternative --
|
-- Alternative --
|
||||||
-----------------
|
-----------------
|
||||||
|
|||||||
@ -233,6 +233,8 @@ makeLenses_ ''ExternalExamResult
|
|||||||
makeLenses_ ''Rating
|
makeLenses_ ''Rating
|
||||||
makeLenses_ ''Rating'
|
makeLenses_ ''Rating'
|
||||||
|
|
||||||
|
makeLenses_ ''FallbackPersonalisedSheetFilesKey
|
||||||
|
|
||||||
-- makeClassy_ ''Load
|
-- makeClassy_ ''Load
|
||||||
|
|
||||||
--------------------------
|
--------------------------
|
||||||
|
|||||||
@ -119,6 +119,8 @@ extra-deps:
|
|||||||
- unordered-containers-0.2.11.0
|
- unordered-containers-0.2.11.0
|
||||||
|
|
||||||
- base64-bytestring-1.1.0.0
|
- base64-bytestring-1.1.0.0
|
||||||
|
- base32-0.2.0.0
|
||||||
|
- ghc-byteorder-4.11.0.0.10
|
||||||
|
|
||||||
resolver: lts-15.12
|
resolver: lts-15.12
|
||||||
allow-newer: true
|
allow-newer: true
|
||||||
|
|||||||
@ -346,6 +346,20 @@ packages:
|
|||||||
sha256: 9ade5b5911df97c37b249b84f123297049f19578cae171c647bf47683633427c
|
sha256: 9ade5b5911df97c37b249b84f123297049f19578cae171c647bf47683633427c
|
||||||
original:
|
original:
|
||||||
hackage: base64-bytestring-1.1.0.0
|
hackage: base64-bytestring-1.1.0.0
|
||||||
|
- completed:
|
||||||
|
hackage: base32-0.2.0.0@sha256:459f0ba6412d58adf1d6ab68d5dc68afddc9f65c69ad564c0a9643d5d8a7e96e,2608
|
||||||
|
pantry-tree:
|
||||||
|
size: 1935
|
||||||
|
sha256: 10c0a5a0a1d4c40b41f0190cf80b114fb527caf7458feec819d87ccfe41317cb
|
||||||
|
original:
|
||||||
|
hackage: base32-0.2.0.0
|
||||||
|
- completed:
|
||||||
|
hackage: ghc-byteorder-4.11.0.0.10@sha256:5ee4a907279bfec27b0f9de7b8fba4cecfd34395a0235a7784494de70ad4e98f,1535
|
||||||
|
pantry-tree:
|
||||||
|
size: 169
|
||||||
|
sha256: 54a4636f72c3b9eff7f081714cb1a7b809fc1f3b2e239caaf0d65d79aa9cb56f
|
||||||
|
original:
|
||||||
|
hackage: ghc-byteorder-4.11.0.0.10
|
||||||
snapshots:
|
snapshots:
|
||||||
- completed:
|
- completed:
|
||||||
size: 494635
|
size: 494635
|
||||||
|
|||||||
@ -579,6 +579,7 @@ fillDb = do
|
|||||||
, sheetAutoDistribute = False
|
, sheetAutoDistribute = False
|
||||||
, sheetAnonymousCorrection = True
|
, sheetAnonymousCorrection = True
|
||||||
, sheetRequireExamRegistration = Nothing
|
, sheetRequireExamRegistration = Nothing
|
||||||
|
, sheetAllowNonPersonalisedSubmission = True
|
||||||
}
|
}
|
||||||
insert_ $ SheetEdit gkleen now adhoc
|
insert_ $ SheetEdit gkleen now adhoc
|
||||||
feste <- insert Sheet
|
feste <- insert Sheet
|
||||||
@ -597,6 +598,7 @@ fillDb = do
|
|||||||
, sheetAutoDistribute = False
|
, sheetAutoDistribute = False
|
||||||
, sheetAnonymousCorrection = True
|
, sheetAnonymousCorrection = True
|
||||||
, sheetRequireExamRegistration = Nothing
|
, sheetRequireExamRegistration = Nothing
|
||||||
|
, sheetAllowNonPersonalisedSubmission = True
|
||||||
}
|
}
|
||||||
insert_ $ SheetEdit gkleen now feste
|
insert_ $ SheetEdit gkleen now feste
|
||||||
keine <- insert Sheet
|
keine <- insert Sheet
|
||||||
@ -615,6 +617,7 @@ fillDb = do
|
|||||||
, sheetAutoDistribute = False
|
, sheetAutoDistribute = False
|
||||||
, sheetAnonymousCorrection = True
|
, sheetAnonymousCorrection = True
|
||||||
, sheetRequireExamRegistration = Nothing
|
, sheetRequireExamRegistration = Nothing
|
||||||
|
, sheetAllowNonPersonalisedSubmission = True
|
||||||
}
|
}
|
||||||
insert_ $ SheetEdit gkleen now keine
|
insert_ $ SheetEdit gkleen now keine
|
||||||
void . insertMany $ map (\(u,sf) -> CourseParticipant ffp u now sf Nothing CourseParticipantActive)
|
void . insertMany $ map (\(u,sf) -> CourseParticipant ffp u now sf Nothing CourseParticipantActive)
|
||||||
@ -827,6 +830,7 @@ fillDb = do
|
|||||||
, sheetAutoDistribute = True
|
, sheetAutoDistribute = True
|
||||||
, sheetAnonymousCorrection = True
|
, sheetAnonymousCorrection = True
|
||||||
, sheetRequireExamRegistration = Nothing
|
, sheetRequireExamRegistration = Nothing
|
||||||
|
, sheetAllowNonPersonalisedSubmission = True
|
||||||
}
|
}
|
||||||
void . insert $ SheetEdit jost now shId
|
void . insert $ SheetEdit jost now shId
|
||||||
when (submissionModeCorrector sheetSubmissionMode) $
|
when (submissionModeCorrector sheetSubmissionMode) $
|
||||||
@ -1062,6 +1066,7 @@ fillDb = do
|
|||||||
, sheetAutoDistribute = False
|
, sheetAutoDistribute = False
|
||||||
, sheetAnonymousCorrection = True
|
, sheetAnonymousCorrection = True
|
||||||
, sheetRequireExamRegistration = Nothing
|
, sheetRequireExamRegistration = Nothing
|
||||||
|
, sheetAllowNonPersonalisedSubmission = True
|
||||||
}
|
}
|
||||||
manyUsers' <- shuffleM $ take 1024 manyUsers
|
manyUsers' <- shuffleM $ take 1024 manyUsers
|
||||||
groupSizes <- getRandomRs (1, 3)
|
groupSizes <- getRandomRs (1, 3)
|
||||||
|
|||||||
@ -32,6 +32,10 @@ import Data.Scientific
|
|||||||
import Utils.Lens hiding (elements)
|
import Utils.Lens hiding (elements)
|
||||||
|
|
||||||
import qualified Data.Char as Char
|
import qualified Data.Char as Char
|
||||||
|
import Data.Word.Word24
|
||||||
|
|
||||||
|
import qualified Data.Binary as Binary
|
||||||
|
import qualified Data.ByteString.Lazy as LBS
|
||||||
|
|
||||||
|
|
||||||
instance (Arbitrary a, MonoFoldable a) => Arbitrary (NonNull a) where
|
instance (Arbitrary a, MonoFoldable a) => Arbitrary (NonNull a) where
|
||||||
@ -280,6 +284,9 @@ instance Arbitrary CsvPreset where
|
|||||||
instance Arbitrary Sex where
|
instance Arbitrary Sex where
|
||||||
arbitrary = genericArbitrary
|
arbitrary = genericArbitrary
|
||||||
|
|
||||||
|
instance Arbitrary Word24 where
|
||||||
|
arbitrary = arbitraryBoundedRandom
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
spec :: Spec
|
spec :: Spec
|
||||||
@ -371,6 +378,8 @@ spec = do
|
|||||||
[ eqLaws, ordLaws, showReadLaws, jsonLaws, persistFieldLaws ]
|
[ eqLaws, ordLaws, showReadLaws, jsonLaws, persistFieldLaws ]
|
||||||
lawsCheckHspec (Proxy @CsvPreset)
|
lawsCheckHspec (Proxy @CsvPreset)
|
||||||
[ eqLaws, ordLaws, showReadLaws, boundedEnumLaws, finiteLaws, pathPieceLaws ]
|
[ eqLaws, ordLaws, showReadLaws, boundedEnumLaws, finiteLaws, pathPieceLaws ]
|
||||||
|
lawsCheckHspec (Proxy @Word24)
|
||||||
|
[ persistFieldLaws, jsonLaws, binaryLaws ]
|
||||||
|
|
||||||
describe "TermIdentifier" $ do
|
describe "TermIdentifier" $ do
|
||||||
it "has compatible encoding/decoding to/from Text" . property $
|
it "has compatible encoding/decoding to/from Text" . property $
|
||||||
@ -405,6 +414,23 @@ 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 "Word24" $ do
|
||||||
|
it "encodes to the expected length" . property $
|
||||||
|
\w -> olength (Binary.encode (w :: Word24)) == 3
|
||||||
|
it "encodes some examples correctly" $ do
|
||||||
|
let decode' inp = case Binary.decodeOrFail inp of
|
||||||
|
Right (unc, _, res)
|
||||||
|
| null unc -> Just res
|
||||||
|
_other
|
||||||
|
-> Nothing
|
||||||
|
encEx w str = example $ do
|
||||||
|
Binary.encode (w :: Word24) `shouldBe` LBS.pack str
|
||||||
|
decode' (LBS.pack str) `shouldBe` Just w
|
||||||
|
encEx 1 [0, 0, 1]
|
||||||
|
encEx 256 [0, 1, 0]
|
||||||
|
encEx 65536 [1, 0, 0]
|
||||||
|
encEx 65537 [1, 0, 1]
|
||||||
|
encEx 197121 [3, 2, 1]
|
||||||
|
|
||||||
termExample :: (TermIdentifier, Text) -> Expectation
|
termExample :: (TermIdentifier, Text) -> Expectation
|
||||||
termExample (term, encoded) = example $ do
|
termExample (term, encoded) = example $ do
|
||||||
|
|||||||
@ -65,6 +65,7 @@ instance Arbitrary Sheet where
|
|||||||
<*> arbitrary
|
<*> arbitrary
|
||||||
<*> arbitrary
|
<*> arbitrary
|
||||||
<*> return Nothing
|
<*> return Nothing
|
||||||
|
<*> arbitrary
|
||||||
shrink = genericShrink
|
shrink = genericShrink
|
||||||
|
|
||||||
instance Arbitrary Tutorial where
|
instance Arbitrary Tutorial where
|
||||||
|
|||||||
@ -12,6 +12,11 @@ import Data.Binary.Put
|
|||||||
|
|
||||||
binaryLaws :: forall a. (Arbitrary a, Binary a, Eq a, Show a) => Proxy a -> Laws
|
binaryLaws :: forall a. (Arbitrary a, Binary a, Eq a, Show a) => Proxy a -> Laws
|
||||||
binaryLaws _ = Laws "Binary"
|
binaryLaws _ = Laws "Binary"
|
||||||
[ ("Partial Isomorphism", property $ \(a :: a) -> decode (encode a) == Just a)
|
[ ("Partial Isomorphism", property $ \(a :: a) -> decode' (encode a) === Just a)
|
||||||
, ("Valid list encoding", property $ \(as :: [a]) -> runPut (putList as) == runPut (put as))
|
, ("Valid list encoding", property $ \(as :: [a]) -> runPut (putList as) === runPut (put as))
|
||||||
]
|
]
|
||||||
|
where decode' inp = case decodeOrFail inp of
|
||||||
|
Right (unc, _, res)
|
||||||
|
| null unc -> Just res
|
||||||
|
_other
|
||||||
|
-> Nothing
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user