feat(submissions): display authorship statements

This commit is contained in:
Gregor Kleen 2021-08-12 17:38:59 +02:00
parent cbd6d7d2b0
commit 7749238e55
48 changed files with 299 additions and 96 deletions

View File

@ -1732,3 +1732,8 @@ video
.authorship-statement-accept__accept-label .authorship-statement-accept__accept-label
grid-area: label grid-area: label
font-weight: 600 font-weight: 600
.authorship-statement__id
font-size: .5em
font-family: var(--font-monospace)
color: var(--color-fontsec)

View File

@ -210,6 +210,19 @@ SubmissionUserMatriculation: Matrikelnummer
SubmissionUserEmail: E-Mail SubmissionUserEmail: E-Mail
SubmissionUserAuthorshipStatementState: Eigenständigkeitserklärung SubmissionUserAuthorshipStatementState: Eigenständigkeitserklärung
SubmissionAuthorshipStatementStateOkay: In Ordnung SubmissionAuthorshipStatementStateExists: Vorhanden
SubmissionAuthorshipStatementStateOldStatement: Unpassender Wortlaut SubmissionAuthorshipStatementStateOldStatement: Unpassender Wortlaut
SubmissionAuthorshipStatementStateMissing: Fehlt SubmissionAuthorshipStatementStateMissing: Fehlt
SubmissionTitle tid@TermId ssh@SchoolId csh@CourseShorthand shn@SheetName cID@CryptoFileNameSubmission !ident-ok: #{tid}-#{ssh}-#{csh} #{shn}: #{toPathPiece cID}
SubmissionHeadingEdit tid@TermId ssh@SchoolId csh@CourseShorthand shn@SheetName cID@CryptoFileNameSubmission: #{tid}-#{ssh}-#{csh} #{shn}: Abgabe #{toPathPiece cID} editieren
SubmissionHeadingShow tid@TermId ssh@SchoolId csh@CourseShorthand shn@SheetName cID@CryptoFileNameSubmission: #{tid}-#{ssh}-#{csh} #{shn}: Abgabe #{toPathPiece cID}
SubmissionTitleNew tid@TermId ssh@SchoolId csh@CourseShorthand shn@SheetName: #{tid}-#{ssh}-#{csh} #{shn}: Abgabe anlegen
SubmissionHeadingNew tid@TermId ssh@SchoolId csh@CourseShorthand shn@SheetName: #{tid}-#{ssh}-#{csh} #{shn}: Abgabe anlegen
SubmissionAuthorshipStatementsHeading tid@TermId ssh@SchoolId csh@CourseShorthand shn@SheetName cID@CryptoFileNameSubmission: #{tid}-#{ssh}-#{csh} #{shn}: Eigenständigkeitserklärungen #{toPathPiece cID}
SubmissionAuthorshipStatementsTitle tid@TermId ssh@SchoolId csh@CourseShorthand shn@SheetName cID@CryptoFileNameSubmission: #{tid}-#{ssh}-#{csh} #{shn}: Eigenständigkeitserklärungen #{toPathPiece cID}
SubmissionColumnAuthorshipStatementTime: Zeitstempel
SubmissionColumnAuthorshipStatementWording: Wortlaut
SubmissionFilterAuthorshipStatementCurrent: Aktueller Wortlaut

View File

@ -209,6 +209,19 @@ SubmissionUserMatriculation: Matriculation
SubmissionUserEmail: Email SubmissionUserEmail: Email
SubmissionUserAuthorshipStatementState: Statement of Authorship SubmissionUserAuthorshipStatementState: Statement of Authorship
SubmissionAuthorshipStatementStateOkay: Okay SubmissionAuthorshipStatementStateExists: Exists
SubmissionAuthorshipStatementStateOldStatement: Wrong wording SubmissionAuthorshipStatementStateOldStatement: Wrong wording
SubmissionAuthorshipStatementStateMissing: Missing SubmissionAuthorshipStatementStateMissing: Missing
SubmissionTitle tid ssh csh shn cID !ident-ok: #{tid}-#{ssh}-#{csh} #{shn}: #{toPathPiece cID}
SubmissionHeadingEdit tid ssh csh shn cID: #{tid}-#{ssh}-#{csh} #{shn}: Edit Submission #{toPathPiece cID}
SubmissionHeadingShow tid ssh csh shn cID: #{tid}-#{ssh}-#{csh} #{shn}: Submission #{toPathPiece cID}
SubmissionTitleNew tid ssh csh shn: #{tid}-#{ssh}-#{csh} #{shn}: Create Submission
SubmissionHeadingNew tid ssh csh shn: #{tid}-#{ssh}-#{csh} #{shn}: Create Submission
SubmissionAuthorshipStatementsHeading tid ssh csh shn cID: #{tid}-#{ssh}-#{csh} #{shn}: Authorship Statements #{toPathPiece cID}
SubmissionAuthorshipStatementsTitle tid ssh csh shn cID: #{tid}-#{ssh}-#{csh} #{shn}: Authorship Statements #{toPathPiece cID}
SubmissionColumnAuthorshipStatementTime: Timestamp
SubmissionColumnAuthorshipStatementWording: Wording
SubmissionFilterAuthorshipStatementCurrent: Current wording

View File

@ -186,4 +186,5 @@ BreadcrumbCorrectionsGrade: Korrekturen eintragen
BreadcrumbMessageList: Systemnachrichten BreadcrumbMessageList: Systemnachrichten
BreadcrumbGlossary: Begriffsverzeichnis BreadcrumbGlossary: Begriffsverzeichnis
BreadcrumbLogin !ident-ok: Login BreadcrumbLogin !ident-ok: Login
BreadcrumbNews: Aktuell BreadcrumbNews: Aktuell
BreadcrumbSubmissionAuthorshipStatements: Eigenständigkeitserklärungen

View File

@ -187,3 +187,4 @@ BreadcrumbSheetCurrent: Current exercise sheet
BreadcrumbSheetOldUnassigned: Submissions without corrector BreadcrumbSheetOldUnassigned: Submissions without corrector
BreadcrumbLogin: Login BreadcrumbLogin: Login
BreadcrumbNews: News BreadcrumbNews: News
BreadcrumbSubmissionAuthorshipStatements: Statements of Authorship

1
routes
View File

@ -218,6 +218,7 @@
/assign SubAssignR GET POST !lecturerANDtime /assign SubAssignR GET POST !lecturerANDtime
/correction CorrectionR GET POST !corrector !ownerANDreadANDratedANDexam-time /correction CorrectionR GET POST !corrector !ownerANDreadANDratedANDexam-time
/invite SInviteR GET POST !ownerANDtimeANDuser-submissionsANDsubmission-groupANDexam-registeredANDpersonalised-sheet-files /invite SInviteR GET POST !ownerANDtimeANDuser-submissionsANDsubmission-groupANDexam-registeredANDpersonalised-sheet-files
/authorship-statements SubAuthorshipStatementsR GET !corrector
!/#SubmissionFileType SubArchiveR GET !owner !corrector !/#SubmissionFileType SubArchiveR GET !owner !corrector
!/#SubmissionFileType/*FilePath SubDownloadR GET !owner !corrector !/#SubmissionFileType/*FilePath SubDownloadR GET !owner !corrector
/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

View File

@ -204,7 +204,7 @@ campusLogin pool mode = AuthPlugin{..}
searchResults <- findUser conf ldap campusIdent [ldapUserPrincipalName] searchResults <- findUser conf ldap campusIdent [ldapUserPrincipalName]
case searchResults of case searchResults of
[Ldap.SearchEntry (Ldap.Dn userDN) userAttrs] [Ldap.SearchEntry (Ldap.Dn userDN) userAttrs]
| [principalName] <- nub $ fold [ v | (k, v) <- userAttrs, k == ldapUserPrincipalName ] | [principalName] <- nubOrd $ fold [ v | (k, v) <- userAttrs, k == ldapUserPrincipalName ]
, Right credsIdent <- Text.decodeUtf8' principalName , Right credsIdent <- Text.decodeUtf8' principalName
-> handleIf isInvalidCredentials (return . Left) $ do -> handleIf isInvalidCredentials (return . Left) $ do
Ldap.bind ldap (Ldap.Dn credsIdent) . Ldap.Password $ Text.encodeUtf8 campusPassword Ldap.bind ldap (Ldap.Dn credsIdent) . Ldap.Password $ Text.encodeUtf8 campusPassword

View File

@ -14,7 +14,8 @@ import Data.Universe.Helpers (interleave)
import Control.Monad (unless) import Control.Monad (unless)
import Data.List (elemIndex, nub) import Data.List (elemIndex)
import Data.Containers.ListUtils
import Control.Lens hiding (universe) import Control.Lens hiding (universe)
import Data.Generics.Product.Types import Data.Generics.Product.Types
@ -81,7 +82,7 @@ deriveUniverse' interleaveExp universeExp mkCxt tName = do
usesVar ConstructorInfo{..} n usesVar ConstructorInfo{..} n
| n `elem` map getTVBName constructorVars = False | n `elem` map getTVBName constructorVars = False
| otherwise = any (elemOf types n) constructorFields | otherwise = any (elemOf types n) constructorFields
fieldTypes = nub $ concatMap constructorFields datatypeCons fieldTypes = nubOrd $ concatMap constructorFields datatypeCons
iCxt' <- cxt iCxt iCxt' <- cxt iCxt

View File

@ -509,18 +509,18 @@ instance RenderMessage UniWorX RouteWorkflowScope where
mr = renderMessage foundation ls mr = renderMessage foundation ls
unRenderMessage' :: (Eq a, Finite a, RenderMessage master a) => (Text -> Text -> Bool) -> master -> Text -> [a] unRenderMessage' :: (Ord a, Finite a, RenderMessage master a) => (Text -> Text -> Bool) -> master -> Text -> [a]
unRenderMessage' cmp foundation inp = nub $ do unRenderMessage' cmp foundation inp = nubOrd $ do
l <- appLanguages' l <- appLanguages'
x <- universeF x <- universeF
guard $ renderMessage foundation (l : filter (/= l) appLanguages') x `cmp` inp guard $ renderMessage foundation (l : filter (/= l) appLanguages') x `cmp` inp
return x return x
where appLanguages' = toList appLanguages where appLanguages' = toList appLanguages
unRenderMessage :: (Eq a, Finite a, RenderMessage master a) => master -> Text -> [a] unRenderMessage :: (Ord a, Finite a, RenderMessage master a) => master -> Text -> [a]
unRenderMessage = unRenderMessage' (==) unRenderMessage = unRenderMessage' (==)
unRenderMessageLenient :: forall a master. (Eq a, Finite a, RenderMessage master a) => master -> Text -> [a] unRenderMessageLenient :: forall a master. (Ord a, Finite a, RenderMessage master a) => master -> Text -> [a]
unRenderMessageLenient = unRenderMessage' cmp unRenderMessageLenient = unRenderMessage' cmp
where cmp = (==) `on` mk . under packed (filter Char.isAlphaNum . concatMap unidecode) where cmp = (==) `on` mk . under packed (filter Char.isAlphaNum . concatMap unidecode)

View File

@ -325,17 +325,16 @@ breadcrumb (CourseR tid ssh csh (SheetR shn sRoute)) = case sRoute of
SubmissionR cid sRoute' -> case sRoute' of SubmissionR cid sRoute' -> case sRoute' of
SubShowR -> useRunDB $ do SubShowR -> useRunDB $ do
mayList <- hasReadAccessTo $ CSheetR tid ssh csh shn SSubsR mayList <- hasReadAccessTo $ CSheetR tid ssh csh shn SSubsR
if return ( toPathPiece cid
| mayList , Just . CSheetR tid ssh csh shn $ bool SShowR SSubsR mayList
-> i18nCrumb MsgBreadcrumbSubmission . Just $ CSheetR tid ssh csh shn SSubsR )
| otherwise
-> i18nCrumb MsgBreadcrumbSubmission . Just $ CSheetR tid ssh csh shn SShowR
CorrectionR -> i18nCrumb MsgMenuCorrection . Just $ CSubmissionR tid ssh csh shn cid SubShowR CorrectionR -> i18nCrumb MsgMenuCorrection . Just $ CSubmissionR tid ssh csh shn cid SubShowR
SubDelR -> i18nCrumb MsgMenuSubmissionDelete . Just $ CSubmissionR tid ssh csh shn cid SubShowR SubDelR -> i18nCrumb MsgMenuSubmissionDelete . Just $ CSubmissionR tid ssh csh shn cid SubShowR
SubAssignR -> i18nCrumb MsgCorrectorAssignTitle . Just $ CSubmissionR tid ssh csh shn cid SubShowR SubAssignR -> i18nCrumb MsgCorrectorAssignTitle . Just $ CSubmissionR tid ssh csh shn cid SubShowR
SInviteR -> i18nCrumb MsgBreadcrumbSubmissionUserInvite . Just $ CSubmissionR tid ssh csh shn cid SubShowR SInviteR -> i18nCrumb MsgBreadcrumbSubmissionUserInvite . Just $ CSubmissionR tid ssh csh shn cid SubShowR
SubArchiveR sft -> i18nCrumb sft . Just $ CSubmissionR tid ssh csh shn cid SubShowR SubArchiveR sft -> i18nCrumb sft . Just $ CSubmissionR tid ssh csh shn cid SubShowR
SubDownloadR _ _ -> i18nCrumb MsgBreadcrumbSubmissionFile . Just $ CSubmissionR tid ssh csh shn cid SubShowR SubDownloadR _ _ -> i18nCrumb MsgBreadcrumbSubmissionFile . Just $ CSubmissionR tid ssh csh shn cid SubShowR
SubAuthorshipStatementsR -> i18nCrumb MsgBreadcrumbSubmissionAuthorshipStatements . Just $ CSubmissionR tid ssh csh shn cid SubShowR
SArchiveR -> i18nCrumb MsgBreadcrumbSheetArchive . Just $ CSheetR tid ssh csh shn SShowR SArchiveR -> i18nCrumb MsgBreadcrumbSheetArchive . Just $ CSheetR tid ssh csh shn SShowR
SIsCorrR -> i18nCrumb MsgBreadcrumbSheetIsCorrector . Just $ CSheetR tid ssh csh shn SShowR SIsCorrR -> i18nCrumb MsgBreadcrumbSheetIsCorrector . Just $ CSheetR tid ssh csh shn SShowR
SPseudonymR -> i18nCrumb MsgBreadcrumbSheetPseudonym . Just $ CSheetR tid ssh csh shn SShowR SPseudonymR -> i18nCrumb MsgBreadcrumbSheetPseudonym . Just $ CSheetR tid ssh csh shn SShowR

View File

@ -296,7 +296,7 @@ upsertCampusUser upsertMode ldapData = do
Right str <- return $ Text.decodeUtf8' v' Right str <- return $ Text.decodeUtf8' v'
return str return str
termNames = nubBy ((==) `on` CI.mk) $ do termNames = nubOrdOn CI.mk $ do
(k, v) <- ldapData (k, v) <- ldapData
guard $ k == ldapUserFieldName guard $ k == ldapUserFieldName
v' <- v v' <- v
@ -505,7 +505,7 @@ updateUserLanguage (Just lang) = do
muid <- maybeAuthId muid <- maybeAuthId
for_ muid $ \uid -> do for_ muid $ \uid -> do
langs <- languages langs <- languages
update uid [ UserLanguages =. Just (Languages $ lang : nub (filter ((&&) <$> (`elem` appLanguages) <*> (/= lang)) langs)) ] update uid [ UserLanguages =. Just (Languages $ lang : nubOrd (filter ((&&) <$> (`elem` appLanguages) <*> (/= lang)) langs)) ]
setRegisteredCookie CookieLang lang setRegisteredCookie CookieLang lang
return $ Just lang return $ Just lang
updateUserLanguage Nothing = runMaybeT $ do updateUserLanguage Nothing = runMaybeT $ do

View File

@ -109,12 +109,12 @@ allocationAcceptForm aId = runMaybeT $ do
let let
showTerms showTerms
| [_] <- nubOn (view $ _1 . _2 . _entityVal . _courseTerm) allocationCourses' | [_] <- nubOrdOn (view $ _1 . _2 . _entityVal . _courseTerm) allocationCourses'
= False = False
| otherwise | otherwise
= True = True
showSchools showSchools
| [_] <- nubOn (view $ _1 . _2 . _entityVal . _courseSchool) allocationCourses' | [_] <- nubOrdOn (view $ _1 . _2 . _entityVal . _courseSchool) allocationCourses'
= False = False
| otherwise | otherwise
= True = True

View File

@ -118,7 +118,7 @@ postAShowR tid ssh ash = do
wouldNotifyNewCourse <- fmap (maybe False E.unValue . join) . for muid $ E.selectMaybe . pure . allocationNotifyNewCourses (E.val aId) . E.val wouldNotifyNewCourse <- fmap (maybe False E.unValue . join) . for muid $ E.selectMaybe . pure . allocationNotifyNewCourses (E.val aId) . E.val
return (alloc, school, isAnyLecturer, isAdmin, nubOn (view $ resultCourse . _entityKey) courses, registration, wouldNotifyNewCourse) return (alloc, school, isAnyLecturer, isAdmin, nubOrdOn (view $ resultCourse . _entityKey) courses, registration, wouldNotifyNewCourse)
let nextSubstitutesDeadline = minimumOf (folded . resultAllocationCourse . _allocationCourseAcceptSubstitutes . _Just . filtered (>= now)) courses let nextSubstitutesDeadline = minimumOf (folded . resultAllocationCourse . _allocationCourseAcceptSubstitutes . _Just . filtered (>= now)) courses
freeCapacity = fmap getSum . getAp . flip foldMap courses $ \cEntry -> Ap . fmap (Sum . max 0) $ subtract (cEntry ^. resultParticipantCount) <$> preview (resultCourse . _entityVal . _courseCapacity . _Just) cEntry freeCapacity = fmap getSum . getAp . flip foldMap courses $ \cEntry -> Ap . fmap (Sum . max 0) $ subtract (cEntry ^. resultParticipantCount) <$> preview (resultCourse . _entityVal . _courseCapacity . _Just) cEntry

View File

@ -612,7 +612,7 @@ postCApplicationsR tid ssh csh = do
sortedApplications <- unstableSortBy cmp applications sortedApplications <- unstableSortBy cmp applications
let applicants = sortedApplications let applicants = sortedApplications
& nubOn (view $ _1 . _entityKey) & nubOrdOn (view $ _1 . _entityKey)
& maybe id take openCapacity & maybe id take openCapacity
& setOf (case invMode of & setOf (case invMode of
AcceptApplicationsDirect -> folded . _1 . _entityKey . to Right AcceptApplicationsDirect -> folded . _1 . _entityKey . to Right

View File

@ -121,7 +121,7 @@ makeCourseForm miButtonAction template = identifyForm FIDcourse . validateFormDB
adminSchools <- filterM (hasWriteAccessTo . flip SchoolR SchoolEditR) protoAdminSchools adminSchools <- filterM (hasWriteAccessTo . flip SchoolR SchoolEditR) protoAdminSchools
oldSchool <- forM (cfCourseId =<< template) $ fmap courseSchool . getJust oldSchool <- forM (cfCourseId =<< template) $ fmap courseSchool . getJust
return (lecturerSchools, adminSchools, oldSchool) return (lecturerSchools, adminSchools, oldSchool)
let userSchools = nub . maybe id (:) oldSchool $ lecturerSchools ++ adminSchools let userSchools = nubOrd . maybe id (:) oldSchool $ lecturerSchools ++ adminSchools
(termsField, userTerms) <- liftHandler $ case template of (termsField, userTerms) <- liftHandler $ case template of
-- Change of term is only allowed if user may delete the course (i.e. no participants) or admin -- Change of term is only allowed if user may delete the course (i.e. no participants) or admin

View File

@ -623,7 +623,7 @@ postCUsersR tid ssh csh = do
, E.desc $ sheet E.^. SheetActiveFrom , E.desc $ sheet E.^. SheetActiveFrom
] ]
return $ sheet E.^. SheetName return $ sheet E.^. SheetName
let exams = nubOn entityKey $ examOccurrencesPerExam ^.. folded . _1 let exams = nubOrdOn entityKey $ examOccurrencesPerExam ^.. folded . _1
let colChoices = mconcat $ catMaybes let colChoices = mconcat $ catMaybes
[ pure . cap' $ dbSelect (applying _2) id (return . view (hasEntity . _entityKey)) [ pure . cap' $ dbSelect (applying _2) id (return . view (hasEntity . _entityKey))
, pure . cap' $ colUserNameLink (CourseR tid ssh csh . CUserR) , pure . cap' $ colUserNameLink (CourseR tid ssh csh . CUserR)

View File

@ -160,7 +160,7 @@ examForm (Entity _ Course{..}) template csrf = hoist liftHandler $ do
(fslI MsgExamAuthorshipStatementContent & setTooltip MsgExamAuthorshipStatementContentForcedTip) (fslI MsgExamAuthorshipStatementContent & setTooltip MsgExamAuthorshipStatementContentForcedTip)
contentField ttipReq contentField ttipReq
| not schoolSheetExamAuthorshipStatementAllowOther | not schoolSheetExamAuthorshipStatementAllowOther
= traverse forcedContentField $ authorshipStatementDefinitionContent . entityVal <$> mSchoolAuthorshipStatement = fmap (fmap authorshipStatementDefinitionContent) . traverse forcedContentField $ entityVal <$> mSchoolAuthorshipStatement
| otherwise | otherwise
= Just <$> reqContentField ttipReq = Just <$> reqContentField ttipReq
in case schoolSheetExamAuthorshipStatementMode of in case schoolSheetExamAuthorshipStatementMode of

View File

@ -101,7 +101,7 @@ makeExamOfficeUsersForm template = renderWForm FormStandard $ do
| null newUsers | null newUsers
-> pure oldUsers -> pure oldUsers
| otherwise | otherwise
-> pure . nub $ oldUsers ++ Set.toList newUsers -> pure . nubOrd $ oldUsers ++ Set.toList newUsers
return (res', $(widgetFile "widgets/massinput/examOfficeUsers/add")) return (res', $(widgetFile "widgets/massinput/examOfficeUsers/add"))
miCell' :: Either UserEmail UserId -> Widget miCell' :: Either UserEmail UserId -> Widget
miCell' (Left email) = do miCell' (Left email) = do

View File

@ -46,7 +46,7 @@ externalExamForm template = validateForm validateExternalExam $ \html -> do
adminSchools <- filterM (hasWriteAccessTo . flip SchoolR SchoolEditR) protoAdminSchools adminSchools <- filterM (hasWriteAccessTo . flip SchoolR SchoolEditR) protoAdminSchools
let oldSchool = eefSchool <$> template let oldSchool = eefSchool <$> template
return (lecturerSchools, adminSchools, oldSchool) return (lecturerSchools, adminSchools, oldSchool)
let userSchools = nub . maybe id (:) oldSchool $ lecturerSchools ++ adminSchools let userSchools = nubOrd . maybe id (:) oldSchool $ lecturerSchools ++ adminSchools
templateSchool = eefSchool <$> template <|> case userSchools of templateSchool = eefSchool <$> template <|> case userSchools of
[ssh] -> pure ssh [ssh] -> pure ssh
_ -> mzero _ -> mzero

View File

@ -44,7 +44,7 @@ getMetricsR = selectRep $ do
-> suffix -> suffix
| otherwise | otherwise
= sName = sName
getLabels = nub . concatMap (\(Sample _ lPairs _) -> lPairs ^.. folded . _1) getLabels = nubOrd . concatMap (\(Sample _ lPairs _) -> lPairs ^.. folded . _1)
singleSample base [Sample sName lPairs sValue] singleSample base [Sample sName lPairs sValue]
| sName == base = Just (lPairs, sValue) | sName == base = Just (lPairs, sValue)
singleSample _ _ = Nothing singleSample _ _ = Nothing

View File

@ -139,7 +139,7 @@ makeSheetForm cId msId template = identifyForm FIDsheet . validateForm validateS
if | not schoolSheetAuthorshipStatementAllowOther if | not schoolSheetAuthorshipStatementAllowOther
-> (pure SheetAuthorshipStatementModeEnabled, pure sfAuthorshipStatementExam', ) -> (pure SheetAuthorshipStatementModeEnabled, pure sfAuthorshipStatementExam', )
<$> fmap sequenceA (traverse forcedContentField $ authorshipStatementDefinitionContent . entityVal <$> mSchoolAuthorshipStatement) <$> fmap sequenceA (fmap (fmap $ fmap authorshipStatementDefinitionContent) . traverse forcedContentField $ entityVal <$> mSchoolAuthorshipStatement)
| otherwise -> do | otherwise -> do
examOpts <- examOpts <-
let examFieldQuery = E.from $ \exam -> do let examFieldQuery = E.from $ \exam -> do

View File

@ -171,7 +171,7 @@ sinkPersonalisedSheetFiles cid sid keep
Right nSink -> State.modify . Map.insert redResidual $ Map.insert residual nSink sinks Right nSink -> State.modify . Map.insert redResidual $ Map.insert residual nSink sinks
openSinks <- State.get openSinks <- State.get
lift . lift . mapM_ closeResumableSink $ openSinks ^.. folded . folded lift . lift . mapM_ closeResumableSink $ openSinks ^.. folded . folded
let (nub -> sinkSheets, nub -> sinkUsers) = unzip $ Map.keys openSinks let (nubOrd -> sinkSheets, nubOrd -> sinkUsers) = unzip $ Map.keys openSinks
unless keep $ unless keep $
lift . lift $ deleteWhere [ PersonalisedSheetFileSheet <-. sinkSheets lift . lift $ deleteWhere [ PersonalisedSheetFileSheet <-. sinkSheets
, PersonalisedSheetFileUser /<-. sinkUsers , PersonalisedSheetFileUser /<-. sinkUsers
@ -227,7 +227,7 @@ sourcePersonalisedSheetFiles cId mbsid mbuids anonMode restrs = do
E.where_ $ submissionGroup E.^. SubmissionGroupCourse E.==. E.val cId E.where_ $ submissionGroup E.^. SubmissionGroupCourse E.==. E.val cId
E.where_ $ submissionGroupUser E.^. SubmissionGroupUserUser E.==. E.val uid E.where_ $ submissionGroupUser E.^. SubmissionGroupUserUser E.==. E.val uid
return $ submissionGroup E.^. SubmissionGroupName return $ submissionGroup E.^. SubmissionGroupName
return . nub . sort $ map (filter isAlphaNum . foldMap unidecode . unpack . CI.original . E.unValue) subGroups return . Set.toList . Set.fromList $ map (filter isAlphaNum . foldMap unidecode . unpack . CI.original . E.unValue) subGroups
otherAnon otherAnon
| Just f <- userFeature otherAnon -> do | Just f <- userFeature otherAnon -> do
features <- E.select . E.from $ \user -> do features <- E.select . E.from $ \user -> do

View File

@ -11,6 +11,7 @@ module Handler.Submission
, module Handler.Submission.Create , module Handler.Submission.Create
, module Handler.Submission.Grade , module Handler.Submission.Grade
, module Handler.Submission.Upload , module Handler.Submission.Upload
, module Handler.Submission.AuthorshipStatements
) where ) where
import Handler.Submission.New import Handler.Submission.New
@ -24,6 +25,7 @@ import Handler.Submission.Correction
import Handler.Submission.Create import Handler.Submission.Create
import Handler.Submission.Grade import Handler.Submission.Grade
import Handler.Submission.Upload import Handler.Submission.Upload
import Handler.Submission.AuthorshipStatements
import Handler.Utils import Handler.Utils

View File

@ -0,0 +1,142 @@
{-# OPTIONS_GHC -fno-warn-redundant-constraints #-}
module Handler.Submission.AuthorshipStatements
( getSubAuthorshipStatementsR
) where
import Import
import Handler.Utils
import qualified Database.Esqueleto.Legacy as E
import qualified Database.Esqueleto.Utils as E
type AuthorshipStatementsExpr = E.SqlExpr (Entity AuthorshipStatementSubmission)
`E.InnerJoin` E.SqlExpr (Entity User)
`E.InnerJoin` E.SqlExpr (Entity AuthorshipStatementDefinition)
queryAuthorshipStatement :: Getter AuthorshipStatementsExpr (E.SqlExpr (Entity AuthorshipStatementSubmission))
queryAuthorshipStatement = to $(E.sqlIJproj 3 1)
queryUser :: Getter AuthorshipStatementsExpr (E.SqlExpr (Entity User))
queryUser = to $(E.sqlIJproj 3 2)
queryDefinition :: Getter AuthorshipStatementsExpr (E.SqlExpr (Entity AuthorshipStatementDefinition))
queryDefinition = to $(E.sqlIJproj 3 3)
type AuthorshipStatementsData = DBRow ( Entity AuthorshipStatementSubmission
, Entity User
, Entity AuthorshipStatementDefinition
)
resultAuthorshipStatement :: Lens' AuthorshipStatementsData (Entity AuthorshipStatementSubmission)
resultAuthorshipStatement = _dbrOutput . _1
resultUser :: Lens' AuthorshipStatementsData (Entity User)
resultUser = _dbrOutput . _2
resultDefinition :: Lens' AuthorshipStatementsData (Entity AuthorshipStatementDefinition)
resultDefinition = _dbrOutput . _3
getSubAuthorshipStatementsR :: TermId -> SchoolId -> CourseShorthand -> SheetName -> CryptoFileNameSubmission -> Handler Html
getSubAuthorshipStatementsR tid ssh csh shn cID = do
authorshipStatementTable <- runDB $ do
subId <- decrypt cID
Submission{..} <- get404 subId
isLecturer <- (== Authorized) <$> evalAccessDB (CSheetR tid ssh csh shn SSubsR) True
mASDefinition <- getSheetAuthorshipStatement =<< getEntity404 submissionSheet
let dbtIdent :: Text
dbtIdent = "authorship-statements"
dbtSQLQuery = runReaderT $ do
authorshipStatement <- view queryAuthorshipStatement
user <- view queryUser
definition <- view queryDefinition
lift $ do
E.on $ definition E.^. AuthorshipStatementDefinitionId E.==. authorshipStatement E.^. AuthorshipStatementSubmissionStatement
E.on $ user E.^. UserId E.==. authorshipStatement E.^. AuthorshipStatementSubmissionUser
E.where_ $ authorshipStatement E.^. AuthorshipStatementSubmissionSubmission E.==. E.val subId
return (authorshipStatement, user, definition)
dbtRowKey = views queryAuthorshipStatement (E.^. AuthorshipStatementSubmissionId)
dbtProj = dbtProjId
dbtColonnade :: Colonnade Sortable AuthorshipStatementsData (DBCell (HandlerFor UniWorX) ())
dbtColonnade = mconcat $ catMaybes
[ pure . sortable (Just "authorship-statement-time") (i18nCell MsgSubmissionColumnAuthorshipStatementTime) $ views (resultAuthorshipStatement . _entityVal . _authorshipStatementSubmissionTime) dateTimeCell
, pure $ colUserDisplayName (resultUser . _entityVal . $(multifocusG 2) _userDisplayName _userSurname)
, guardOn isLecturer $ colUserMatriculation (resultUser . _entityVal . _userMatrikelnummer)
, pure $ lmap (view $ resultUser . _entityVal) colUserEmail
, pure . sortable Nothing (i18nCell MsgSubmissionColumnAuthorshipStatementWording) $ views resultDefinition definitionCell
]
where
definitionCell (Entity asdId asd)
= withColor . (cellAttrs %~ addAttrsClass "table__td--center") . modalCell $ authorshipStatementWidget asd
where
withColor c
| Just (Entity currASDId _) <- mASDefinition
= c
& cellAttrs %~ addAttrsClass "heated"
& cellAttrs <>~ pure ("style", [st|--hotness: #{tshow (boolHeat (asdId /= currASDId))}|])
| otherwise
= c
dbtStyle = def { dbsFilterLayout = defaultDBSFilterLayout }
dbtSorting = mconcat
[ singletonMap "authorship-statement-time" . SortColumn $ views queryAuthorshipStatement (E.^. AuthorshipStatementSubmissionTime)
, sortUserName' (queryUser . $(multifocusG 2) (to (E.^. UserDisplayName)) (to (E.^. UserSurname)))
, sortUserMatriculation (queryUser . to (E.^. UserMatrikelnummer))
, uncurry singletonMap $ sortUserEmail (view queryUser)
]
dbtFilter = mconcat
[ fltrUserName' (queryUser . to (E.^. UserDisplayName))
, fltrUserMatriculation (queryUser . to (E.^. UserMatrikelnummer))
, uncurry singletonMap $ fltrUserEmail (view queryUser)
, singletonMap "authorship-statement-current" . FilterColumn $ \(view queryAuthorshipStatement -> subStmt) (Last isCurrent)
-> let isCurrent'
| Just (Entity asdId _) <- mASDefinition
= subStmt E.^. AuthorshipStatementSubmissionStatement E.==. E.val asdId
| otherwise
= E.false
in maybe E.true ((E.==. isCurrent') . E.val) isCurrent
]
dbtFilterUI = mconcat $ catMaybes
[ pure fltrUserNameUI'
, guardOn isLecturer fltrUserMatriculationUI
, pure fltrUserEmailUI
, pure . flip (prismAForm $ singletonFilter "authorship-statement-current" . maybePrism _PathPiece) $ aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgSubmissionFilterAuthorshipStatementCurrent)
]
dbtParams = def
dbtCsvEncode = noCsvEncode
dbtCsvDecode = Nothing
dbtExtraReps = []
asPSValidator = def
& defaultSorting [SortDescBy "authorship-statement-time"]
& restrictFilter lecturerFilter & restrictSorting lecturerSorting
where
lecturerFilter fk _
| isLecturer = True
| otherwise = fk /= "user-matriculation"
lecturerSorting sk _
| isLecturer = True
| otherwise = sk /= "user-matriculation"
in dbTableWidget' asPSValidator DBTable{..}
let (heading, title) = ( MsgSubmissionAuthorshipStatementsHeading tid ssh csh shn cID
, MsgSubmissionAuthorshipStatementsTitle tid ssh csh shn cID
)
siteLayoutMsg heading $ do
setTitleI title
authorshipStatementTable

View File

@ -49,13 +49,13 @@ postCorrectionsGradeR = do
] ]
courseOptions = runDB $ do courseOptions = runDB $ do
courses <- selectList [] [Asc CourseShorthand] >>= filterM (\(Entity _ Course{..}) -> (== Authorized) <$> evalAccessCorrector courseTerm courseSchool courseShorthand) courses <- selectList [] [Asc CourseShorthand] >>= filterM (\(Entity _ Course{..}) -> (== Authorized) <$> evalAccessCorrector courseTerm courseSchool courseShorthand)
optionsPairs $ map (id &&& id) $ nub $ map (CI.original . courseShorthand . entityVal) courses optionsPairs . map (id &&& id) . nubOrd $ map (CI.original . courseShorthand . entityVal) courses
termOptions = runDB $ do termOptions = runDB $ do
courses <- selectList [] [Asc CourseTerm] >>= filterM (\(Entity _ Course{..}) -> (== Authorized) <$> evalAccessCorrector courseTerm courseSchool courseShorthand) courses <- selectList [] [Asc CourseTerm] >>= filterM (\(Entity _ Course{..}) -> (== Authorized) <$> evalAccessCorrector courseTerm courseSchool courseShorthand)
optionsPairs $ map (id &&& id) $ nub $ map (termToText . unTermKey . courseTerm . entityVal) courses optionsPairs . map (id &&& id) . nubOrd $ map (termToText . unTermKey . courseTerm . entityVal) courses
schoolOptions = runDB $ do schoolOptions = runDB $ do
courses <- selectList [] [Asc CourseSchool] >>= filterM (\(Entity _ Course{..}) -> (== Authorized) <$> evalAccessCorrector courseTerm courseSchool courseShorthand) courses <- selectList [] [Asc CourseSchool] >>= filterM (\(Entity _ Course{..}) -> (== Authorized) <$> evalAccessCorrector courseTerm courseSchool courseShorthand)
optionsPairs $ map (id &&& id) $ nub $ map (CI.original . unSchoolKey . courseSchool . entityVal) courses optionsPairs . map (id &&& id) . nubOrd $ map (CI.original . unSchoolKey . courseSchool . entityVal) courses
psValidator = def psValidator = def
& restrictAnonymous & restrictAnonymous
& restrictCorrector & restrictCorrector

View File

@ -32,7 +32,7 @@ import qualified Data.Conduit.Combinators as C
data AuthorshipStatementSubmissionState data AuthorshipStatementSubmissionState
= ASOkay = ASExists
| ASOldStatement | ASOldStatement
| ASMissing | ASMissing
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable) deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable)
@ -570,7 +570,7 @@ submissionHelper tid ssh csh shn mcid = do
, formEncoding = formEnctype , formEncoding = formEnctype
} }
((Entity _ Sheet{..}, _, lastEdits, maySubmit, _, _, msubmission, corrector, _), (showCorrection, correctionInvisible), mFileTable, filesCorrected, sheetTypeDesc, multipleSubmissionWarnWidget, subUsers, isLecturer, doAuthorshipStatements) <- runDB $ do ((Entity _ Sheet{..}, _, lastEdits, maySubmit, _, _, msubmission, corrector, _), (showCorrection, correctionInvisible), mFileTable, filesCorrected, sheetTypeDesc, multipleSubmissionWarnWidget, subUsers, isLecturer, isOwner, doAuthorshipStatements) <- runDB $ do
sheetInfo@(Entity shid Sheet{..}, buddies, _, _, isLecturer, isOwner, msubmission, _, mASDefinition) <- getSheetInfo sheetInfo@(Entity shid Sheet{..}, buddies, _, _, isLecturer, isOwner, msubmission, _, mASDefinition) <- getSheetInfo
(showCorrection, correctionInvisible) <- fmap (fromMaybe (False, Nothing)) . for ((,) <$> mcid <*> (Entity <$> msmid <*> msubmission)) $ \(cid, subEnt) -> do (showCorrection, correctionInvisible) <- fmap (fromMaybe (False, Nothing)) . for ((,) <$> mcid <*> (Entity <$> msmid <*> msubmission)) $ \(cid, subEnt) -> do
@ -621,7 +621,7 @@ submissionHelper tid ssh csh shn mcid = do
toPoint (Entity _ AuthorshipStatementSubmission{..}) = Just . Any $ fmap entityKey mASDefinition == Just authorshipStatementSubmissionStatement toPoint (Entity _ AuthorshipStatementSubmission{..}) = Just . Any $ fmap entityKey mASDefinition == Just authorshipStatementSubmissionStatement
toRes :: Maybe Any -> AuthorshipStatementSubmissionState toRes :: Maybe Any -> AuthorshipStatementSubmissionState
toRes = \case toRes = \case
Just (Any True) -> ASOkay Just (Any True) -> ASExists
Just (Any False) -> ASOldStatement Just (Any False) -> ASOldStatement
Nothing -> ASMissing Nothing -> ASMissing
lift $ buddies lift $ buddies
@ -630,12 +630,17 @@ submissionHelper tid ssh csh shn mcid = do
& mapMOf (traverse . _Right) (\uid -> (,,) <$> (encrypt uid :: DB CryptoUUIDUser) <*> getJust uid <*> getUserAuthorshipStatement uid) & mapMOf (traverse . _Right) (\uid -> (,,) <$> (encrypt uid :: DB CryptoUUIDUser) <*> getJust uid <*> getUserAuthorshipStatement uid)
& fmap (sortOn . over _Right $ (,,,) <$> views _2 userSurname <*> views _2 userDisplayName <*> views _2 userEmail <*> view _1) & fmap (sortOn . over _Right $ (,,,) <$> views _2 userSurname <*> views _2 userDisplayName <*> views _2 userEmail <*> view _1)
return (sheetInfo, (showCorrection, correctionInvisible), mFileTable, filesCorrected, sheetTypeDesc, multipleSubmissionWarnWidget, subUsers, isLecturer, is _Just mASDefinition) return (sheetInfo, (showCorrection, correctionInvisible), mFileTable, filesCorrected, sheetTypeDesc, multipleSubmissionWarnWidget, subUsers, isLecturer, isOwner, is _Just mASDefinition)
-- TODO(AuthorshipStatements): discuss whether to display prompt for user to update their authorship statement, if lecturer changed it -- TODO(AuthorshipStatements): discuss whether to display prompt for user to update their authorship statement, if lecturer changed it
let (title, heading)
| Just cID <- mcid, maySubmit, not isLecturer || isOwner = (MsgSubmissionTitle tid ssh csh shn cID, MsgSubmissionHeadingEdit tid ssh csh shn cID)
| Just cID <- mcid = (MsgSubmissionTitle tid ssh csh shn cID, MsgSubmissionHeadingShow tid ssh csh shn cID)
| otherwise = (MsgSubmissionTitleNew tid ssh csh shn, MsgSubmissionHeadingNew tid ssh csh shn)
defaultLayout $ do siteLayoutMsg heading $ do
setTitleI $ MsgHeadingSubmissionEditHead tid ssh csh shn setTitleI title
(urlArchive, urlOriginal) <- fmap ((,) <$> preview (_Just . _1) <*> preview (_Just . _2)) . for mcid $ \cID (urlArchive, urlOriginal) <- fmap ((,) <$> preview (_Just . _1) <*> preview (_Just . _2)) . for mcid $ \cID
-> let mkUrl sft = toTextUrl . CSubmissionR tid ssh csh shn cID $ SubArchiveR sft -> let mkUrl sft = toTextUrl . CSubmissionR tid ssh csh shn cID $ SubArchiveR sft
in liftHandler . runDB $ (,) <$> mkUrl SubmissionCorrected <*> mkUrl SubmissionOriginal in liftHandler . runDB $ (,) <$> mkUrl SubmissionCorrected <*> mkUrl SubmissionOriginal
@ -650,7 +655,8 @@ submissionHelper tid ssh csh shn mcid = do
correctionVisibleWarnWidget = guardOnM (is _Just msubmission && is _Just mcid && showCorrection) correctionInvisible correctionVisibleWarnWidget = guardOnM (is _Just msubmission && is _Just mcid && showCorrection) correctionInvisible
asStatusExplain = $(i18nWidgetFiles "authorship-statement-submission-explanation") asStatusExplain = $(i18nWidgetFiles "authorship-statement-submission-explanation")
asStatuses = setOf (folded . _Right . _3) subUsers asStatuses = setOf (folded . _Right . _3) subUsers
& Set.union (Set.fromList [ASOkay, ASMissing]) & Set.union (Set.fromList [ASExists, ASMissing])
& Set.toList & Set.toList
& mapMaybe (\stmt -> (stmt, ) <$> asStatusExplain Map.!? toPathPiece stmt) & mapMaybe (\stmt -> (stmt, ) <$> asStatusExplain Map.!? toPathPiece stmt)
asStatusExplainWdgt = $(widgetFile "widgets/authorship-statement-submission-explanation")
$(widgetFile "submission") $(widgetFile "submission")

View File

@ -154,7 +154,7 @@ colSMatrikel = sortable (Just "submittors-matriculation") (i18nCell MsgTableMatr
colSGroups :: IsDBTable m a => Colonnade Sortable CorrectionTableData (DBCell m a) colSGroups :: IsDBTable m a => Colonnade Sortable CorrectionTableData (DBCell m a)
colSGroups = sortable (Just "submittors-group") (i18nCell MsgTableSubmissionGroup) $ \DBRow{ dbrOutput=(_, Entity _ Sheet{..}, _, _, _, users, _, hasAccess) } -> colSGroups = sortable (Just "submittors-group") (i18nCell MsgTableSubmissionGroup) $ \DBRow{ dbrOutput=(_, Entity _ Sheet{..}, _, _, _, users, _, hasAccess) } ->
let protoCell = listCell (nubOn (view _2) . Map.toList $ Map.mapMaybe (view _3) users) $ \(_, sGroup) -> cell $ toWidget sGroup let protoCell = listCell (nubOrdOn (view _2) . Map.toList $ Map.mapMaybe (view _3) users) $ \(_, sGroup) -> cell $ toWidget sGroup
in if | hasAccess in if | hasAccess
, is _RegisteredGroups sheetGrouping , is _RegisteredGroups sheetGrouping
-> protoCell & cellAttrs <>~ [("class", "list--inline list--comma-separated")] -> protoCell & cellAttrs <>~ [("class", "list--inline list--comma-separated")]
@ -680,13 +680,13 @@ postCorrectionsR = do
] ]
courseOptions = runDB $ do courseOptions = runDB $ do
courses <- selectList [] [Asc CourseShorthand] >>= filterM (\(Entity _ Course{..}) -> (== Authorized) <$> evalAccessCorrector courseTerm courseSchool courseShorthand) courses <- selectList [] [Asc CourseShorthand] >>= filterM (\(Entity _ Course{..}) -> (== Authorized) <$> evalAccessCorrector courseTerm courseSchool courseShorthand)
optionsPairs $ map (id &&& id) $ nub $ map (CI.original . courseShorthand . entityVal) courses optionsPairs . map (id &&& id) . nubOrd $ map (CI.original . courseShorthand . entityVal) courses
termOptions = runDB $ do termOptions = runDB $ do
courses <- selectList [] [Desc CourseTerm] >>= filterM (\(Entity _ Course{..}) -> (== Authorized) <$> evalAccessCorrector courseTerm courseSchool courseShorthand) courses <- selectList [] [Desc CourseTerm] >>= filterM (\(Entity _ Course{..}) -> (== Authorized) <$> evalAccessCorrector courseTerm courseSchool courseShorthand)
optionsPairs $ map (id &&& id) $ nub $ map (termToText . unTermKey . courseTerm . entityVal) courses optionsPairs . map (id &&& id) . nubOrd $ map (termToText . unTermKey . courseTerm . entityVal) courses
schoolOptions = runDB $ do schoolOptions = runDB $ do
courses <- selectList [] [Asc CourseSchool] >>= filterM (\(Entity _ Course{..}) -> (== Authorized) <$> evalAccessCorrector courseTerm courseSchool courseShorthand) courses <- selectList [] [Asc CourseSchool] >>= filterM (\(Entity _ Course{..}) -> (== Authorized) <$> evalAccessCorrector courseTerm courseSchool courseShorthand)
optionsPairs $ map (id &&& id) $ nub $ map (CI.original . unSchoolKey . courseSchool . entityVal) courses optionsPairs . map (id &&& id) . nubOrd $ map (CI.original . unSchoolKey . courseSchool . entityVal) courses
psValidator = def psValidator = def
& restrictCorrector & restrictCorrector

View File

@ -16,6 +16,9 @@ import Handler.Utils.Form (i18nLangMap, I18nLang(..))
import qualified Database.Esqueleto.Legacy as E import qualified Database.Esqueleto.Legacy as E
import qualified Database.Esqueleto.Utils as E import qualified Database.Esqueleto.Utils as E
import qualified Data.ByteString.Base64.URL as Base64
import qualified Data.ByteArray as BA
insertAuthorshipStatement :: MonadIO m insertAuthorshipStatement :: MonadIO m
=> I18nStoredMarkup -> SqlWriteT m AuthorshipStatementDefinitionId => I18nStoredMarkup -> SqlWriteT m AuthorshipStatementDefinitionId
@ -26,7 +29,7 @@ insertAuthorshipStatement authorshipStatementDefinitionContent = withCompatibleB
return $ AuthorshipStatementDefinitionKey authorshipStatementDefinitionHash return $ AuthorshipStatementDefinitionKey authorshipStatementDefinitionHash
forcedAuthorshipStatementField :: (MonadHandler handler, HandlerSite handler ~ UniWorX) forcedAuthorshipStatementField :: (MonadHandler handler, HandlerSite handler ~ UniWorX)
=> Field handler I18nStoredMarkup => Field handler AuthorshipStatementDefinition
forcedAuthorshipStatementField = Field{..} forcedAuthorshipStatementField = Field{..}
where where
fieldParse _ _ = pure . Left $ SomeMessage ("Result of forcedAuthorshipStatementField inspected" :: Text) fieldParse _ _ = pure . Left $ SomeMessage ("Result of forcedAuthorshipStatementField inspected" :: Text)
@ -38,23 +41,27 @@ forcedAuthorshipStatementField = Field{..}
^{maybe mempty authorshipStatementWidget mVal} ^{maybe mempty authorshipStatementWidget mVal}
|] |]
authorshipStatementWidget :: I18nStoredMarkup -> Widget authorshipStatementWidget :: AuthorshipStatementDefinition -> Widget
authorshipStatementWidget stmt authorshipStatementWidget AuthorshipStatementDefinition{..}
= [whamlet| = [whamlet|
$newline never $newline never
<dl .authorship-statement> <dl .authorship-statement>
$forall (I18nLang l, t) <- Map.toList (review i18nLangMap stmt) $forall (I18nLang l, t) <- Map.toList (review i18nLangMap authorshipStatementDefinitionContent)
<dt> <dt>
_{MsgLanguageEndonym l} _{MsgLanguageEndonym l}
<dd> <dd>
#{markupOutput t} #{markupOutput t}
<p .authorship-statement__id>
#{hashText}
|] |]
where hashText = decodeUtf8 . Base64.encodeUnpadded $ BA.convert authorshipStatementDefinitionHash
acceptAuthorshipStatementField :: forall m. acceptAuthorshipStatementField :: forall m.
(MonadHandler m, HandlerSite m ~ UniWorX) (MonadHandler m, HandlerSite m ~ UniWorX)
=> Entity AuthorshipStatementDefinition => Entity AuthorshipStatementDefinition
-> Field m AuthorshipStatementDefinitionId -> Field m AuthorshipStatementDefinitionId
acceptAuthorshipStatementField (Entity asdId AuthorshipStatementDefinition{..}) acceptAuthorshipStatementField (Entity asdId asd)
= checkBoxField = checkBoxField
& _fieldView %~ adjFieldView & _fieldView %~ adjFieldView
& checkMap (bool (Left MsgAuthorshipStatementStatementIsRequired) (Right asdId)) (== asdId) & checkMap (bool (Left MsgAuthorshipStatementStatementIsRequired) (Right asdId)) (== asdId)

View File

@ -30,7 +30,7 @@ i18nFile includeFile basename = do
-- Construct list of available translations (@de@, @en@, ...) at compile time -- Construct list of available translations (@de@, @en@, ...) at compile time
let i18nDirectory = "templates" </> "i18n" </> basename let i18nDirectory = "templates" </> "i18n" </> basename
availableFiles <- qRunIO $ listDirectory i18nDirectory availableFiles <- qRunIO $ listDirectory i18nDirectory
let availableTranslations = sortWith (NTop . flip List.elemIndex (NonEmpty.toList appLanguages)) . List.nub $ pack . takeBaseName <$> availableFiles let availableTranslations = sortWith (NTop . flip List.elemIndex (NonEmpty.toList appLanguages)) . nubOrd $ pack . takeBaseName <$> availableFiles
availableTranslations' <- maybe (fail $ "" <> i18nDirectory <> " is empty") return $ NonEmpty.nonEmpty availableTranslations availableTranslations' <- maybe (fail $ "" <> i18nDirectory <> " is empty") return $ NonEmpty.nonEmpty availableTranslations
-- Dispatch to correct language (depending on user settings via `selectLanguage`) at run time -- Dispatch to correct language (depending on user settings via `selectLanguage`) at run time

View File

@ -323,7 +323,7 @@ deleteInvitationsF :: forall junction m mono backend.
-> ReaderT backend m () -> ReaderT backend m ()
-- | Non-conduit version of `deleteInvitations` -- | Non-conduit version of `deleteInvitations`
deleteInvitationsF invitationFor (otoList -> emailList) deleteInvitationsF invitationFor (otoList -> emailList)
= deleteWhere [InvitationEmail <-. nub emailList, InvitationFor ==. invRef @junction invitationFor] = deleteWhere [InvitationEmail <-. nubOrd emailList, InvitationFor ==. invRef @junction invitationFor]
deleteInvitation :: forall junction m backend. deleteInvitation :: forall junction m backend.
( IsInvitableJunction junction ( IsInvitableJunction junction

View File

@ -362,7 +362,7 @@ submissionMultiArchive anonymous sft (Set.toList -> ids) = do
E.where_ $ submission E.^. SubmissionId E.==. E.val submissionID E.where_ $ submission E.^. SubmissionId E.==. E.val submissionID
E.where_ $ sheet E.^. SheetCourse E.==. submissionGroup E.^. SubmissionGroupCourse 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 = Set.toList . Set.fromList $ map (filter isAlphaNum . foldMap unidecode . unpack . CI.original . E.unValue) groups
return . intercalate "_" $ asciiGroups `snoc` fp return . intercalate "_" $ asciiGroups `snoc` fp
| Just feature <- userFeature anonymous | Just feature <- userFeature anonymous
= do = do

View File

@ -18,7 +18,6 @@ import Import
-- import Control.Monad.Trans.Writer (mapWriterT) -- import Control.Monad.Trans.Writer (mapWriterT)
-- import Database.Persist.Sql (fromSqlKey) -- import Database.Persist.Sql (fromSqlKey)
import qualified Data.Set as Set import qualified Data.Set as Set
import qualified Data.List as List
import qualified Data.Map as Map import qualified Data.Map as Map
@ -112,7 +111,7 @@ removeAmbiguousNames = do
) )
E.having $ E.countRows E.!=. E.val (1 :: Int64) E.having $ E.countRows E.!=. E.val (1 :: Int64)
return $ candidate E.^. StudyTermNameCandidateIncidence return $ candidate E.^. StudyTermNameCandidateIncidence
let ambiSet = E.unValue <$> List.nub ambiList let ambiSet = E.unValue <$> nubOrd ambiList
-- Most SQL dialects won't allow deletion and queries on the same table at once, hence we delete in two steps. -- Most SQL dialects won't allow deletion and queries on the same table at once, hence we delete in two steps.
deleteWhere [StudyTermNameCandidateIncidence <-. ambiSet] deleteWhere [StudyTermNameCandidateIncidence <-. ambiSet]
return ambiSet return ambiSet

View File

@ -104,6 +104,10 @@ editedByW fmt tm usr = do
ft <- handlerToWidget $ formatTime fmt tm ft <- handlerToWidget $ formatTime fmt tm
[whamlet|_{MsgUtilEditedBy usr ft}|] [whamlet|_{MsgUtilEditedBy usr ft}|]
boolHeat :: Bool -- ^ @isHot@
-> Milli
boolHeat = bool 0 1
heat :: ( Real a, Real b ) heat :: ( Real a, Real b )
=> a -> b -> Milli => a -> b -> Milli
-- ^ Distinguishes @full@, zero is mapped to 1, @full@ is mapped to 0 -- ^ Distinguishes @full@, zero is mapped to 1, @full@ is mapped to 0

View File

@ -70,7 +70,7 @@ followAutomaticEdges WorkflowGraph{..} = go []
| otherwise = throwM . WorkflowAutomaticEdgeAmbiguity $ Set.fromList automaticEdgeOptions | otherwise = throwM . WorkflowAutomaticEdgeAmbiguity $ Set.fromList automaticEdgeOptions
where where
cState = wpTo $ last history cState = wpTo $ last history
automaticEdgeOptions = nub $ do automaticEdgeOptions = nubOrd $ do
(nodeLbl, WGN{..}) <- Map.toList wgNodes (nodeLbl, WGN{..}) <- Map.toList wgNodes
(edgeLbl, WorkflowGraphEdgeAutomatic{..}) <- Map.toList wgnEdges (edgeLbl, WorkflowGraphEdgeAutomatic{..}) <- Map.toList wgnEdges
guard $ wgeSource == cState guard $ wgeSource == cState

View File

@ -116,7 +116,7 @@ determineNotificationCandidates = awaitForever $ \notif -> do
E.where_ $ admin E.^. UserFunctionSchool `E.in_` E.valList (Set.toList newAdminSchools) E.where_ $ admin E.^. UserFunctionSchool `E.in_` E.valList (Set.toList newAdminSchools)
E.&&. admin E.^. UserFunctionFunction E.==. E.val SchoolAdmin E.&&. admin E.^. UserFunctionFunction E.==. E.val SchoolAdmin
return user return user
withNotif . yieldMany . nub $ affectedUser <> affectedAdmins withNotif . yieldMany . nubOrd $ affectedUser <> affectedAdmins
NotificationUserSystemFunctionsUpdate{..} NotificationUserSystemFunctionsUpdate{..}
-> withNotif $ selectSource [UserId ==. nUser] [] -> withNotif $ selectSource [UserId ==. nUser] []
NotificationUserAuthModeUpdate{..} NotificationUserAuthModeUpdate{..}

View File

@ -56,7 +56,7 @@ dispatchNotificationAllocationRegister (otoList -> nAllocations) jRecipient = us
dispatchNotificationAllocationAllocation :: Set AllocationId -> UserId -> Handler () dispatchNotificationAllocationAllocation :: Set AllocationId -> UserId -> Handler ()
dispatchNotificationAllocationAllocation (otoList -> nAllocations) jRecipient = do dispatchNotificationAllocationAllocation (otoList -> nAllocations) jRecipient = do
courses <- fmap (nubOn $ views _2 entityKey) . runDB . E.select . E.from $ \(course `E.InnerJoin` allocationCourse `E.InnerJoin` allocation `E.InnerJoin` lecturer) -> do courses <- fmap (nubOrdOn $ views _2 entityKey) . runDB . E.select . E.from $ \(course `E.InnerJoin` allocationCourse `E.InnerJoin` allocation `E.InnerJoin` lecturer) -> do
E.on $ lecturer E.^. LecturerCourse E.==. course E.^. CourseId E.on $ lecturer E.^. LecturerCourse E.==. course E.^. CourseId
E.on $ allocation E.^. AllocationId E.==. allocationCourse E.^. AllocationCourseAllocation E.on $ allocation E.^. AllocationId E.==. allocationCourse E.^. AllocationCourseAllocation
E.on $ allocationCourse E.^. AllocationCourseCourse E.==. course E.^. CourseId E.on $ allocationCourse E.^. AllocationCourseCourse E.==. course E.^. CourseId
@ -73,7 +73,7 @@ dispatchNotificationAllocationAllocation (otoList -> nAllocations) jRecipient =
return (allocation, course) return (allocation, course)
let allocations = nubOn entityKey $ courses ^.. folded . _1 let allocations = nubOrdOn entityKey $ courses ^.. folded . _1
unless (null courses) . userMailT jRecipient $ do unless (null courses) . userMailT jRecipient $ do
now <- liftIO getCurrentTime now <- liftIO getCurrentTime
@ -95,7 +95,7 @@ dispatchNotificationAllocationAllocation (otoList -> nAllocations) jRecipient =
dispatchNotificationAllocationUnratedApplications :: Set AllocationId -> UserId -> Handler () dispatchNotificationAllocationUnratedApplications :: Set AllocationId -> UserId -> Handler ()
dispatchNotificationAllocationUnratedApplications (otoList -> nAllocations) jRecipient = do dispatchNotificationAllocationUnratedApplications (otoList -> nAllocations) jRecipient = do
courses <- fmap (nubOn (views _2 entityKey) . over (traverse . _3) (fromIntegral . E.unValue)) . runDB . E.select . E.from $ \(course `E.InnerJoin` allocationCourse `E.InnerJoin` allocation `E.InnerJoin` lecturer) -> do courses <- fmap (nubOrdOn (views _2 entityKey) . over (traverse . _3) (fromIntegral . E.unValue)) . runDB . E.select . E.from $ \(course `E.InnerJoin` allocationCourse `E.InnerJoin` allocation `E.InnerJoin` lecturer) -> do
E.on $ lecturer E.^. LecturerCourse E.==. course E.^. CourseId E.on $ lecturer E.^. LecturerCourse E.==. course E.^. CourseId
E.on $ allocation E.^. AllocationId E.==. allocationCourse E.^. AllocationCourseAllocation E.on $ allocation E.^. AllocationId E.==. allocationCourse E.^. AllocationCourseAllocation
E.on $ allocationCourse E.^. AllocationCourseCourse E.==. course E.^. CourseId E.on $ allocationCourse E.^. AllocationCourseCourse E.==. course E.^. CourseId
@ -121,7 +121,7 @@ dispatchNotificationAllocationUnratedApplications (otoList -> nAllocations) jRec
return (allocation, course, unratedAppCount) return (allocation, course, unratedAppCount)
let allocations = nubOn entityKey $ courses ^.. folded . _1 let allocations = nubOrdOn entityKey $ courses ^.. folded . _1
unless (null courses) . userMailT jRecipient $ do unless (null courses) . userMailT jRecipient $ do
replaceMailHeader "Auto-Submitted" $ Just "auto-generated" replaceMailHeader "Auto-Submitted" $ Just "auto-generated"

View File

@ -23,12 +23,13 @@ import Control.Monad.Writer.Class (MonadWriter(..))
import Control.Monad.Trans.Writer.Lazy (execWriterT) import Control.Monad.Trans.Writer.Lazy (execWriterT)
import Control.Monad.Catch (MonadThrow(..)) import Control.Monad.Catch (MonadThrow(..))
import Utils (nubOn)
import System.FilePath (makeRelative) import System.FilePath (makeRelative)
import Text.Shakespeare.Text (st) import Text.Shakespeare.Text (st)
import Utils ()
import Data.Containers.ListUtils
mkWebpackEntrypoints :: FilePath -- ^ Path to YAML-manifest mkWebpackEntrypoints :: FilePath -- ^ Path to YAML-manifest
-> [FilePath -> Generator] -> [FilePath -> Generator]
@ -62,7 +63,7 @@ mkWebpackEntrypoints manifest mkGen stDir = do
sequence sequence
[ sigD entryName [t|[(Route EmbeddedStatic, MimeType)]|] [ sigD entryName [t|[(Route EmbeddedStatic, MimeType)]|]
, funD entryName , funD entryName
[ clause [] (normalB . listE . map (\(n, mime) -> tupE [varE n, TH.lift mime]) $ nubOn fst entries) [] [ clause [] (normalB . listE . map (\(n, mime) -> tupE [varE n, TH.lift mime]) $ nubOrdOn fst entries) []
] ]
, sigD widgetName [t|forall m. (MonadLogger m, MonadWidget m) => (Route EmbeddedStatic -> Route (HandlerSite m)) -> m ()|] , sigD widgetName [t|forall m. (MonadLogger m, MonadWidget m) => (Route EmbeddedStatic -> Route (HandlerSite m)) -> m ()|]
, funD widgetName , funD widgetName

View File

@ -1,6 +1,6 @@
module Utils module Utils
( module Utils ( module Utils
, List.nub, List.nubBy , module Data.Containers.ListUtils
) where ) where
import ClassyPrelude.Yesod hiding (foldlM, Proxy, handle, catch, bracket) import ClassyPrelude.Yesod hiding (foldlM, Proxy, handle, catch, bracket)
@ -50,7 +50,6 @@ import Data.Text (dropWhileEnd, takeWhileEnd, justifyRight)
import qualified Data.Set as Set import qualified Data.Set as Set
import qualified Data.Map as Map import qualified Data.Map as Map
import qualified Data.List as List
import qualified Data.HashMap.Strict as HashMap import qualified Data.HashMap.Strict as HashMap
import qualified Data.Vector as V import qualified Data.Vector as V
@ -149,6 +148,8 @@ import Data.Ratio ((%))
import Data.UUID (UUID) import Data.UUID (UUID)
import qualified Data.UUID as UUID import qualified Data.UUID as UUID
import Data.Containers.ListUtils
{-# ANN module ("HLint: ignore Use asum" :: String) #-} {-# ANN module ("HLint: ignore Use asum" :: String) #-}
@ -252,6 +253,12 @@ selectRep' cMap' needle = asum
(needleMain, needleSub) = contentTypeTypes needle (needleMain, needleSub) = contentTypeTypes needle
noSpaces = CBS.filter (/= ' ') noSpaces = CBS.filter (/= ' ')
addAttrsClass :: Text -> [(Text, Text)] -> [(Text, Text)]
addAttrsClass cl attrs = ("class", cl') : noClAttrs
where
(clAttrs, noClAttrs) = partition (views _1 $ (== "class") . CI.mk) attrs
cl' = Text.intercalate " " . nubOrd . filter (not . null) $ cl : (views _2 (Text.splitOn " ") =<< clAttrs)
--------------------- ---------------------
-- Text and String -- -- Text and String --
@ -538,9 +545,6 @@ partitionWith f (x:xs) = case f x of
nonEmpty' :: Alternative f => [a] -> f (NonEmpty a) nonEmpty' :: Alternative f => [a] -> f (NonEmpty a)
nonEmpty' = maybe empty pure . nonEmpty nonEmpty' = maybe empty pure . nonEmpty
nubOn :: Eq b => (a -> b) -> [a] -> [a]
nubOn = List.nubBy . ((==) `on`)
dropWhileM :: (IsSequence seq, Monad m) => (Element seq -> m Bool) -> seq -> m seq dropWhileM :: (IsSequence seq, Monad m) => (Element seq -> m Bool) -> seq -> m seq
dropWhileM p xs' dropWhileM p xs'
| Just (x, xs) <- uncons xs' | Just (x, xs) <- uncons xs'

View File

@ -23,6 +23,8 @@ import Control.Monad.Reader.Class (local)
import qualified Data.HashMap.Strict as HashMap import qualified Data.HashMap.Strict as HashMap
import Data.Containers.ListUtils
selectLanguage :: MonadHandler m selectLanguage :: MonadHandler m
=> NonEmpty Lang -- ^ Available translations, first is default => NonEmpty Lang -- ^ Available translations, first is default
@ -39,7 +41,7 @@ selectLanguages (defL :| _) [] = defL :| []
selectLanguages avL (l:ls) selectLanguages avL (l:ls)
| not $ null l | not $ null l
, Just lParts <- nonEmpty $ matchesFor l , Just lParts <- nonEmpty $ matchesFor l
, found <- List.nub , found <- nubOrd
[ l'' | lParts' <- NonEmpty.toList lParts [ l'' | lParts' <- NonEmpty.toList lParts
, l' <- NonEmpty.toList avL , l' <- NonEmpty.toList avL
, l'' <- matchesFor l' , l'' <- matchesFor l'
@ -69,7 +71,7 @@ lowPrioRequestedLangs = maybe [] (mapMaybe (either (const Nothing) Just . Text.d
languagesMiddleware :: forall site a. NonEmpty Lang -> HandlerFor site a -> HandlerFor site a languagesMiddleware :: forall site a. NonEmpty Lang -> HandlerFor site a -> HandlerFor site a
languagesMiddleware avL act = do languagesMiddleware avL act = do
pLangs <- fmap List.nub $ (<>) <$> highPrioRequestedLangs <*> lowPrioRequestedLangs pLangs <- fmap nubOrd $ (<>) <$> highPrioRequestedLangs <*> lowPrioRequestedLangs
let langs = toList $ selectLanguages avL pLangs let langs = toList $ selectLanguages avL pLangs
setLangs hData = hData{ handlerRequest = (handlerRequest hData){ reqLangs = langs } } setLangs hData = hData{ handlerRequest = (handlerRequest hData){ reqLangs = langs } }
local setLangs $ ($logDebugS "languages" . tshow . (pLangs,langs,) =<< languages) *> act local setLangs $ ($logDebugS "languages" . tshow . (pLangs,langs,) =<< languages) *> act

View File

@ -281,6 +281,9 @@ makeLenses_ ''RoomReference
makePrisms ''SchoolAuthorshipStatementMode makePrisms ''SchoolAuthorshipStatementMode
makePrisms ''SheetAuthorshipStatementMode makePrisms ''SheetAuthorshipStatementMode
makeLenses_ ''AuthorshipStatementSubmission
makeLenses_ ''AuthorshipStatementDefinition
-------------------------- --------------------------
-- Fields for `UniWorX` -- -- Fields for `UniWorX` --
-------------------------- --------------------------

View File

@ -29,7 +29,7 @@ import qualified Data.HashMap.Strict as HashMap
import Numeric.Natural import Numeric.Natural
import Data.List (nub, foldl) import Data.List (foldl)
import Data.Aeson.Types import Data.Aeson.Types
import qualified Data.Aeson.Types as Aeson import qualified Data.Aeson.Types as Aeson
@ -47,6 +47,8 @@ import Web.HttpApiData
import Data.ByteString.Lazy.Base32 import Data.ByteString.Lazy.Base32
import qualified Data.CaseInsensitive as CI import qualified Data.CaseInsensitive as CI
import Data.Containers.ListUtils
mkFiniteFromPathPiece :: Name -> Q ([Dec], Exp) mkFiniteFromPathPiece :: Name -> Q ([Dec], Exp)
mkFiniteFromPathPiece finiteType = do mkFiniteFromPathPiece finiteType = do
@ -178,7 +180,7 @@ derivePathPiece adt mangle joinPP = do
usesVar ConstructorInfo{..} n usesVar ConstructorInfo{..} n
| n `elem` map tvarName constructorVars = False | n `elem` map tvarName constructorVars = False
| otherwise = any (elemOf types n) constructorFields | otherwise = any (elemOf types n) constructorFields
fieldTypes = nub $ concatMap constructorFields datatypeCons fieldTypes = nubOrd $ concatMap constructorFields datatypeCons
tvarName (PlainTV n) = n tvarName (PlainTV n) = n
tvarName (KindedTV n _) = n tvarName (KindedTV n _) = n
sequence . (finDecs ++ ) . pure $ sequence . (finDecs ++ ) . pure $

View File

@ -1,5 +1,5 @@
$newline never $newline never
$if is _Just mcid $maybe subCId <- mcid
$maybe wdgt <- correctionWdgt $maybe wdgt <- correctionWdgt
<section> <section>
<h2>_{MsgTableRating} <h2>_{MsgTableRating}
@ -30,10 +30,11 @@ $if is _Just mcid
<th .table__th> <th .table__th>
<div .table__td-content> <div .table__td-content>
_{MsgSubmissionUserEmail} _{MsgSubmissionUserEmail}
$if isLecturer && doAuthorshipStatements $if doAuthorshipStatements
<th .table__th> <th .table__th>
<div .table__td-content> <div .table__td-content>
_{MsgSubmissionUserAuthorshipStatementState} ^{simpleLinkI MsgSubmissionUserAuthorshipStatementState (CSubmissionR tid ssh csh shn subCId SubAuthorshipStatementsR)}
^{iconTooltip asStatusExplainWdgt Nothing True}
<tbody> <tbody>
$forall subUser <- subUsers $forall subUser <- subUsers
$case subUser $case subUser
@ -63,24 +64,10 @@ $if is _Just mcid
<a href="mailto:#{userEmail}"> <a href="mailto:#{userEmail}">
#{userEmail} #{userEmail}
$# TODO(AuthorshipStatements): show authorship statements to submittors? $# TODO(AuthorshipStatements): show authorship statements to submittors?
$if isLecturer && doAuthorshipStatements $if doAuthorshipStatements
<td .table__td> <td .table__td .heated style="--hotness: #{boolHeat (stmt /= ASExists)}">
<div .table__td-content> <div .table__td-content>
_{stmt} _{stmt}
$if isLecturer && doAuthorshipStatements
<tfoot>
<tr .table__row .table__row--foot .no-stripe .no-hover>
<td>
<td>
<td>
<td .table__td>
<div .table__td-content .explanation>
<dl .deflist>
$forall (stmt, explanation) <- asStatuses
<dt .deflist__dt>
_{stmt}
<dd .deflist__dd>
^{explanation}
<section> <section>
$case sheetSubmissionMode $case sheetSubmissionMode
@ -121,5 +108,5 @@ $if is _Just mcid
<section> <section>
<h2>_{MsgSubmissionReplace} <h2>_{MsgSubmissionReplace}
^{formWidget} ^{formWidget}
$else $nothing
^{formWidget} ^{formWidget}

View File

@ -1,7 +1,7 @@
$newline never $newline never
<div *{attrs} .authorship-statement-accept__container> <div *{attrs} .authorship-statement-accept__container>
<div .authorship-statement-accept__statement> <div .authorship-statement-accept__statement>
^{authorshipStatementWidget authorshipStatementDefinitionContent} ^{authorshipStatementWidget asd}
<label for=#{checkboxId} .authorship-statement-accept__accept> <label for=#{checkboxId} .authorship-statement-accept__accept>
<div .authorship-statement-accept__accept-checkbox> <div .authorship-statement-accept__accept-checkbox>

View File

@ -0,0 +1,8 @@
$newline never
<div .explanation>
<dl .deflist>
$forall (stmt, explanation) <- asStatuses
<dt .deflist__dt>
_{stmt}
<dd .deflist__dd>
^{explanation}

View File

@ -1320,7 +1320,7 @@ fillDb = do
bigAllocShorthands <- bigAllocShorthands <-
let go xs = let (csh, xs') = List.splitAt 3 xs let go xs = let (csh, xs') = List.splitAt 3 xs
in pack csh : go xs' in pack csh : go xs'
in take 40 . nub . go <$> getRandomRs ('A', 'Z') in take 40 . nubOrd . go <$> getRandomRs ('A', 'Z')
bigAllocCourses <- forM (zip [1..] bigAllocShorthands) $ \(n :: Natural, csh) -> do bigAllocCourses <- forM (zip [1..] bigAllocShorthands) $ \(n :: Natural, csh) -> do
cap <- getRandomR (10,50) cap <- getRandomR (10,50)

View File

@ -58,6 +58,8 @@ import Data.Ord (Down(..))
import Control.Monad.Catch import Control.Monad.Catch
import Data.Containers.ListUtils
data Translate data Translate
= TranslateMsgs = TranslateMsgs
@ -131,7 +133,7 @@ main = main' =<< run
main' m@TranslateMsgs{..} main' m@TranslateMsgs{..}
| null msgRequiredLangs = main' m{ msgRequiredLangs = ["de-de-formal", "en-eu"] } | null msgRequiredLangs = main' m{ msgRequiredLangs = ["de-de-formal", "en-eu"] }
| otherwise = do | otherwise = do
msgFiles' <- nub . (msgFiles ++) <$> getMissingFiles m msgFiles' <- nubOrd . (msgFiles ++) <$> getMissingFiles m
let let
filePairs :: [(FilePath, FilePath)] filePairs :: [(FilePath, FilePath)]