feat: external exam csv import & ldap lookup during csv import

This commit is contained in:
Gregor Kleen 2020-01-13 16:12:24 +01:00
parent 2b153c1863
commit 1d14b6a69c
13 changed files with 705 additions and 108 deletions

View File

@ -717,6 +717,7 @@ LoginNecessary: Bitte melden Sie sich dazu vorher an!
InternalLdapError: Interner Fehler beim Campus-Login InternalLdapError: Interner Fehler beim Campus-Login
CampusUserInvalidIdent: Konnte anhand des Campus-Logins keine eindeutige Identifikation
CampusUserInvalidEmail: Konnte anhand des Campus-Logins keine EMail-Addresse ermitteln CampusUserInvalidEmail: Konnte anhand des Campus-Logins keine EMail-Addresse ermitteln
CampusUserInvalidDisplayName: Konnte anhand des Campus-Logins keinen vollen Namen ermitteln CampusUserInvalidDisplayName: Konnte anhand des Campus-Logins keinen vollen Namen ermitteln
CampusUserInvalidGivenName: Konnte anhand des Campus-Logins keinen Vornamen ermitteln CampusUserInvalidGivenName: Konnte anhand des Campus-Logins keinen Vornamen ermitteln
@ -1261,6 +1262,8 @@ BreadcrumbExternalExamGrades: Prüfungsleistungen
BreadcrumbExternalExamStaffInvite: Einladung zum Prüfer BreadcrumbExternalExamStaffInvite: Einladung zum Prüfer
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}
ExternalExamUsers coursen@CourseName examn@ExamName: Teilnehmer: #{coursen}, #{examn}
TitleMetrics: Metriken TitleMetrics: Metriken
@ -1674,10 +1677,16 @@ ExamUserSyncTime: Zeitpunkt
ExamUserSyncSchools: Institute ExamUserSyncSchools: Institute
ExamUserSyncLastChange: Zuletzt geändert ExamUserSyncLastChange: Zuletzt geändert
ExamUserMarkSynchronised: Prüfungsleistung als synchronisiert markieren ExamUserMarkSynchronised: Prüfungsleistung als synchronisiert markieren
ExternalExamUserMarkSynchronised: Prüfungsleistung als synchronisiert markieren
ExternalExamUserMarkSynchronisedTip: Sollen beim CSV-Export automatisch alle heruntergeladenen Prüfungsleistungen als synchronisiert markiert werden? Diese Markierung dient als Hinweis an andere Prüfungsbeauftragte und die Kursverwalter, dass die Leistung an der korrekten Stelle vermerkt wurde und keiner weiteren Handlung bedarf.
ExamUserMarkSynchronisedCsv: Prüfungsleistungen beim Export als synchronisiert markieren ExamUserMarkSynchronisedCsv: Prüfungsleistungen beim Export als synchronisiert markieren
ExamUserMarkSynchronisedCsvTip: Sollen beim CSV-Export automatisch alle heruntergeladenen Prüfungsleistungen als synchronisiert markiert werden? Diese Markierung dient als Hinweis an andere Prüfungsbeauftragte und die Kursverwalter, dass die Leistung an der korrekten Stelle vermerkt wurde und keiner weiteren Handlung bedarf.
ExamUserMarkedSynchronised n@Int: #{n} #{pluralDE n "Prüfungsleistung" "Prüfungsleistungen"} als synchronisiert markiert ExamUserMarkedSynchronised n@Int: #{n} #{pluralDE n "Prüfungsleistung" "Prüfungsleistungen"} als synchronisiert markiert
ExternalExamUserMarkSynchronisedCsv: Prüfungsleistungen beim Export als synchronisiert markieren
ExternalExamUserMarkedSynchronised n@Int: #{n} #{pluralDE n "Prüfungsleistung" "Prüfungsleistungen"} als synchronisiert markiert
ExamOfficeExamUsersHeading: Prüfungsleistungen ExamOfficeExamUsersHeading: Prüfungsleistungen
ActionsHead: Aktionen ActionsHead: Aktionen
@ -1780,6 +1789,11 @@ ExamUserCsvExceptionNoMatchingUser: Kursteilnehmer konnte nicht eindeutig identi
ExamUserCsvExceptionNoMatchingStudyFeatures: Das angegebene Studienfach konnte keinem Studienfach des Kursteilnehmers zugeordnet werden ExamUserCsvExceptionNoMatchingStudyFeatures: Das angegebene Studienfach konnte keinem Studienfach des Kursteilnehmers zugeordnet werden
ExamUserCsvExceptionNoMatchingOccurrence: Raum/Termin konnte nicht eindeutig identifiziert werden ExamUserCsvExceptionNoMatchingOccurrence: Raum/Termin konnte nicht eindeutig identifiziert werden
ExternalExamUserCsvRegister: Prüfungsleistung hinterlegen
ExternalExamUserCsvSetTime: Zeitpunkt anpassen
ExternalExamUserCsvSetResult: Ergebnis anpassen
ExternalExamUserCsvDeregister: Hinterlegte Prüfungsleistung löschen
CourseApplicationsTableCsvSetField: Bewerbungs-assoziiertes Studienfach ändern CourseApplicationsTableCsvSetField: Bewerbungs-assoziiertes Studienfach ändern
CourseApplicationsTableCsvSetVeto: Veto setzen/entfernen CourseApplicationsTableCsvSetVeto: Veto setzen/entfernen
CourseApplicationsTableCsvSetRating: Bewertung eintragen CourseApplicationsTableCsvSetRating: Bewertung eintragen

View File

@ -169,6 +169,15 @@ data Transaction
, transactionEmail :: UserEmail , transactionEmail :: UserEmail
} }
| TransactionExternalExamResultEdit
{ transactionExternalExam :: ExternalExamId
, transactionUser :: UserId
}
| TransactionExternalExamResultDelete
{ transactionExternalExam :: ExternalExamId
, transactionUser :: UserId
}
deriving (Eq, Ord, Read, Show, Generic, Typeable) deriving (Eq, Ord, Read, Show, Generic, Typeable)
deriveJSON defaultOptions deriveJSON defaultOptions

View File

@ -3,6 +3,7 @@ module Auth.LDAP
, campusLogin , campusLogin
, CampusUserException(..) , CampusUserException(..)
, campusUser, campusUser' , campusUser, campusUser'
, campusUserMatr, campusUserMatr'
, CampusMessage(..) , CampusMessage(..)
, ldapUserPrincipalName, ldapUserEmail, ldapUserDisplayName , ldapUserPrincipalName, ldapUserEmail, ldapUserDisplayName
, ldapUserMatriculation, ldapUserFirstName, ldapUserSurname , ldapUserMatriculation, ldapUserFirstName, ldapUserSurname
@ -43,7 +44,7 @@ data CampusMessage = MsgCampusIdentPlaceholder
findUser :: LdapConf -> Ldap -> Text -> [Ldap.Attr] -> IO [Ldap.SearchEntry] findUser :: LdapConf -> Ldap -> Text -> [Ldap.Attr] -> IO [Ldap.SearchEntry]
findUser LdapConf{..} ldap ident retAttrs = fromMaybe [] <$> findM (assertM (not . null) . lift . flip (Ldap.search ldap ldapBase userSearchSettings) retAttrs) userFilters findUser conf@LdapConf{..} ldap ident retAttrs = fromMaybe [] <$> findM (assertM (not . null) . lift . flip (Ldap.search ldap ldapBase $ userSearchSettings conf) retAttrs) userFilters
where where
userFilters = userFilters =
[ ldapUserPrincipalName Ldap.:= Text.encodeUtf8 ident [ ldapUserPrincipalName Ldap.:= Text.encodeUtf8 ident
@ -54,14 +55,24 @@ findUser LdapConf{..} ldap ident retAttrs = fromMaybe [] <$> findM (assertM (not
, ldapUserEmail' <- toList ldapUserEmail , ldapUserEmail' <- toList ldapUserEmail
] ++ ] ++
[ ldapUserDisplayName Ldap.:= Text.encodeUtf8 ident [ ldapUserDisplayName Ldap.:= Text.encodeUtf8 ident
, ldapUserMatriculation Ldap.:= Text.encodeUtf8 ident
] ]
userSearchSettings = mconcat
[ Ldap.scope ldapScope findUserMatr :: LdapConf -> Ldap -> Text -> [Ldap.Attr] -> IO [Ldap.SearchEntry]
, Ldap.size 2 findUserMatr conf@LdapConf{..} ldap userMatr retAttrs = fromMaybe [] <$> findM (assertM (not . null) . lift . flip (Ldap.search ldap ldapBase $ userSearchSettings conf) retAttrs) userFilters
, Ldap.time ldapSearchTimeout where
, Ldap.derefAliases Ldap.DerefAlways userFilters =
[ ldapUserMatriculation Ldap.:= Text.encodeUtf8 userMatr
] ]
userSearchSettings :: LdapConf -> Ldap.Mod Ldap.Search
userSearchSettings LdapConf{..} = mconcat
[ Ldap.scope ldapScope
, Ldap.size 2
, Ldap.time ldapSearchTimeout
, Ldap.derefAliases Ldap.DerefAlways
]
ldapUserPrincipalName, ldapUserDisplayName, ldapUserMatriculation, ldapUserFirstName, ldapUserSurname, ldapUserTitle, ldapUserStudyFeatures, ldapUserFieldName, ldapUserSchoolAssociation, ldapSex, ldapUserSubTermsSemester :: Ldap.Attr ldapUserPrincipalName, ldapUserDisplayName, ldapUserMatriculation, ldapUserFirstName, ldapUserSurname, ldapUserTitle, ldapUserStudyFeatures, ldapUserFieldName, ldapUserSchoolAssociation, ldapSex, ldapUserSubTermsSemester :: Ldap.Attr
ldapUserPrincipalName = Ldap.Attr "userPrincipalName" ldapUserPrincipalName = Ldap.Attr "userPrincipalName"
ldapUserDisplayName = Ldap.Attr "displayName" ldapUserDisplayName = Ldap.Attr "displayName"
@ -99,13 +110,7 @@ campusUser conf@LdapConf{..} pool Creds{..} = liftIO . (`catches` errHandlers) $
results <- case lookup "DN" credsExtra of results <- case lookup "DN" credsExtra of
Just userDN -> do Just userDN -> do
let userFilter = Ldap.Present ldapUserPrincipalName let userFilter = Ldap.Present ldapUserPrincipalName
userSearchSettings = mconcat Ldap.search ldap (Ldap.Dn userDN) (userSearchSettings conf) userFilter []
[ Ldap.scope Ldap.BaseObject
, Ldap.size 2
, Ldap.time ldapSearchTimeout
, Ldap.derefAliases Ldap.DerefAlways
]
Ldap.search ldap (Ldap.Dn userDN) userSearchSettings userFilter []
Nothing -> do Nothing -> do
findUser conf ldap credsIdent [] findUser conf ldap credsIdent []
case results of case results of
@ -123,6 +128,26 @@ campusUser' conf pool User{userIdent}
= runMaybeT . catchIfMaybeT (is _CampusUserNoResult) $ campusUser conf pool (Creds apLdap (CI.original userIdent) []) = runMaybeT . catchIfMaybeT (is _CampusUserNoResult) $ campusUser conf pool (Creds apLdap (CI.original userIdent) [])
campusUserMatr :: MonadUnliftIO m => LdapConf -> LdapPool -> UserMatriculation -> m (Ldap.AttrList [])
campusUserMatr conf@LdapConf{..} pool userMatr = liftIO . (`catches` errHandlers) $ either (throwM . CampusUserLdapError) return <=< withLdap pool $ \ldap -> do
Ldap.bind ldap ldapDn ldapPassword
results <- findUserMatr conf ldap userMatr []
case results of
[] -> throwM CampusUserNoResult
[Ldap.SearchEntry _ attrs] -> return attrs
_otherwise -> throwM CampusUserAmbiguous
where
errHandlers = [ Exc.Handler $ \LineTooLong -> throwM CampusUserLineTooLong
, Exc.Handler $ \(HostNotResolved host) -> throwM $ CampusUserHostNotResolved host
, Exc.Handler $ \(HostCannotConnect host excs) -> throwM $ CampusUserHostCannotConnect host excs
]
campusUserMatr' :: (MonadCatch m, MonadUnliftIO m) => LdapConf -> LdapPool -> UserMatriculation -> m (Maybe (Ldap.AttrList []))
campusUserMatr' conf pool
= runMaybeT . catchIfMaybeT (is _CampusUserNoResult) . campusUserMatr conf pool
campusForm :: ( RenderMessage (HandlerSite m) FormMessage campusForm :: ( RenderMessage (HandlerSite m) FormMessage
, RenderMessage (HandlerSite m) CampusMessage , RenderMessage (HandlerSite m) CampusMessage
, MonadHandler m , MonadHandler m

View File

@ -23,7 +23,7 @@ import Auth.Dummy
import qualified Network.Wai as W (pathInfo) import qualified Network.Wai as W (pathInfo)
import qualified Yesod.Core.Unsafe as Unsafe import qualified Yesod.Core.Unsafe as Unsafe
import Data.CaseInsensitive (original, mk) import qualified Data.CaseInsensitive as CI
import Data.ByteArray (convert) import Data.ByteArray (convert)
import Crypto.Hash (Digest, SHAKE256, SHAKE128) import Crypto.Hash (Digest, SHAKE256, SHAKE128)
@ -45,6 +45,7 @@ import qualified Data.Set as Set
import Data.Map (Map, (!?)) import Data.Map (Map, (!?))
import qualified Data.Map as Map import qualified Data.Map as Map
import qualified Data.HashSet as HashSet import qualified Data.HashSet as HashSet
import qualified Data.List.NonEmpty as NonEmpty
import Data.List (nubBy, (!!), findIndex, inits) import Data.List (nubBy, (!!), findIndex, inits)
import qualified Data.List as List import qualified Data.List as List
@ -1781,7 +1782,7 @@ instance YesodBreadcrumbs UniWorX where
breadcrumb SchoolListR = i18nCrumb MsgMenuSchoolList $ Just AdminR breadcrumb SchoolListR = i18nCrumb MsgMenuSchoolList $ Just AdminR
breadcrumb (SchoolR ssh SchoolEditR) = maybeT (i18nCrumb MsgBreadcrumbSchool $ Just SchoolListR) $ do breadcrumb (SchoolR ssh SchoolEditR) = maybeT (i18nCrumb MsgBreadcrumbSchool $ Just SchoolListR) $ do
School{..} <- MaybeT . runDB $ get ssh School{..} <- MaybeT . runDB $ get ssh
return (original schoolName, Just SchoolListR) return (CI.original schoolName, Just SchoolListR)
breadcrumb SchoolNewR = i18nCrumb MsgMenuSchoolNew $ Just SchoolListR breadcrumb SchoolNewR = i18nCrumb MsgMenuSchoolNew $ Just SchoolListR
breadcrumb (ExamOfficeR EOExamsR) = i18nCrumb MsgMenuExamOfficeExams Nothing breadcrumb (ExamOfficeR EOExamsR) = i18nCrumb MsgMenuExamOfficeExams Nothing
@ -1821,13 +1822,13 @@ instance YesodBreadcrumbs UniWorX where
guardM . lift . runDB $ guardM . lift . runDB $
(&&) <$> fmap isJust (get ssh) (&&) <$> fmap isJust (get ssh)
<*> fmap isJust (get tid) <*> fmap isJust (get tid)
return (original $ unSchoolKey ssh, Just $ TermCourseListR tid) return (CI.original $ unSchoolKey ssh, Just $ TermCourseListR tid)
breadcrumb AllocationListR = i18nCrumb MsgAllocationListTitle $ Just HomeR breadcrumb AllocationListR = i18nCrumb MsgAllocationListTitle $ Just HomeR
breadcrumb (AllocationR tid ssh ash AShowR) = maybeT (i18nCrumb MsgBreadcrumbAllocation $ Just AllocationListR) $ do breadcrumb (AllocationR tid ssh ash AShowR) = maybeT (i18nCrumb MsgBreadcrumbAllocation $ Just AllocationListR) $ do
mr <- getMessageRender mr <- getMessageRender
Entity _ Allocation{allocationName} <- MaybeT . runDB . getBy $ TermSchoolAllocationShort tid ssh ash Entity _ Allocation{allocationName} <- MaybeT . runDB . getBy $ TermSchoolAllocationShort tid ssh ash
return ([st|#{allocationName} (#{mr (ShortTermIdentifier (unTermKey tid))}, #{original (unSchoolKey ssh)})|], Just $ AllocationListR) return ([st|#{allocationName} (#{mr (ShortTermIdentifier (unTermKey tid))}, #{CI.original (unSchoolKey ssh)})|], Just $ AllocationListR)
breadcrumb (AllocationR tid ssh ash ARegisterR) = i18nCrumb MsgBreadcrumbAllocationRegister . Just $ AllocationR tid ssh ash AShowR breadcrumb (AllocationR tid ssh ash ARegisterR) = i18nCrumb MsgBreadcrumbAllocationRegister . Just $ AllocationR tid ssh ash AShowR
breadcrumb (AllocationR tid ssh ash (AApplyR cID)) = maybeT (i18nCrumb MsgBreadcrumbCourse . Just $ AllocationR tid ssh ash AShowR) $ do breadcrumb (AllocationR tid ssh ash (AApplyR cID)) = maybeT (i18nCrumb MsgBreadcrumbCourse . Just $ AllocationR tid ssh ash AShowR) $ do
cid <- decrypt cID cid <- decrypt cID
@ -1835,13 +1836,13 @@ instance YesodBreadcrumbs UniWorX where
aid <- MaybeT . getKeyBy $ TermSchoolAllocationShort tid ssh ash aid <- MaybeT . getKeyBy $ TermSchoolAllocationShort tid ssh ash
guardM . lift $ exists [ AllocationCourseAllocation ==. aid, AllocationCourseCourse ==. cid ] guardM . lift $ exists [ AllocationCourseAllocation ==. aid, AllocationCourseCourse ==. cid ]
MaybeT $ get cid MaybeT $ get cid
return (original courseName, Just $ AllocationR tid ssh ash AShowR) return (CI.original courseName, Just $ AllocationR tid ssh ash AShowR)
breadcrumb CourseListR = i18nCrumb MsgMenuCourseList Nothing breadcrumb CourseListR = i18nCrumb MsgMenuCourseList Nothing
breadcrumb CourseNewR = i18nCrumb MsgMenuCourseNew $ Just CourseListR breadcrumb CourseNewR = i18nCrumb MsgMenuCourseNew $ Just CourseListR
breadcrumb (CourseR tid ssh csh CShowR) = maybeT (i18nCrumb MsgBreadcrumbCourse . Just $ TermSchoolCourseListR tid ssh) $ do breadcrumb (CourseR tid ssh csh CShowR) = maybeT (i18nCrumb MsgBreadcrumbCourse . Just $ TermSchoolCourseListR tid ssh) $ do
guardM . lift . runDB . existsBy $ TermSchoolCourseShort tid ssh csh guardM . lift . runDB . existsBy $ TermSchoolCourseShort tid ssh csh
return (original csh, Just $ TermSchoolCourseListR tid ssh) return (CI.original csh, Just $ TermSchoolCourseListR tid ssh)
breadcrumb (CourseR tid ssh csh CEditR) = i18nCrumb MsgMenuCourseEdit . Just $ CourseR tid ssh csh CShowR breadcrumb (CourseR tid ssh csh CEditR) = i18nCrumb MsgMenuCourseEdit . Just $ CourseR tid ssh csh CShowR
breadcrumb (CourseR tid ssh csh CUsersR) = i18nCrumb MsgMenuCourseMembers . Just $ CourseR tid ssh csh CShowR breadcrumb (CourseR tid ssh csh CUsersR) = i18nCrumb MsgMenuCourseMembers . Just $ CourseR tid ssh csh CShowR
breadcrumb (CourseR tid ssh csh CAddUserR) = i18nCrumb MsgMenuCourseAddMembers . Just $ CourseR tid ssh csh CUsersR breadcrumb (CourseR tid ssh csh CAddUserR) = i18nCrumb MsgMenuCourseAddMembers . Just $ CourseR tid ssh csh CUsersR
@ -1899,7 +1900,7 @@ instance YesodBreadcrumbs UniWorX where
breadcrumb (CourseR tid ssh csh (ExamR examn sRoute)) = case sRoute of breadcrumb (CourseR tid ssh csh (ExamR examn sRoute)) = case sRoute of
EShowR -> maybeT (i18nCrumb MsgBreadcrumbExam . Just $ CourseR tid ssh csh CExamListR) $ do EShowR -> maybeT (i18nCrumb MsgBreadcrumbExam . Just $ CourseR tid ssh csh CExamListR) $ do
guardM . hasReadAccessTo $ CExamR tid ssh csh examn EShowR guardM . hasReadAccessTo $ CExamR tid ssh csh examn EShowR
return (original examn, Just $ CourseR tid ssh csh CExamListR) return (CI.original examn, Just $ CourseR tid ssh csh CExamListR)
EEditR -> i18nCrumb MsgMenuExamEdit . Just $ CExamR tid ssh csh examn EShowR EEditR -> i18nCrumb MsgMenuExamEdit . Just $ CExamR tid ssh csh examn EShowR
EUsersR -> i18nCrumb MsgMenuExamUsers . Just $ CExamR tid ssh csh examn EShowR EUsersR -> i18nCrumb MsgMenuExamUsers . Just $ CExamR tid ssh csh examn EShowR
EAddUserR -> i18nCrumb MsgMenuExamAddMembers . Just $ CExamR tid ssh csh examn EUsersR EAddUserR -> i18nCrumb MsgMenuExamAddMembers . Just $ CExamR tid ssh csh examn EUsersR
@ -1912,7 +1913,7 @@ instance YesodBreadcrumbs UniWorX where
breadcrumb (CourseR tid ssh csh (TutorialR tutn sRoute)) = case sRoute of breadcrumb (CourseR tid ssh csh (TutorialR tutn sRoute)) = case sRoute of
TUsersR -> maybeT (i18nCrumb MsgBreadcrumbTutorial . Just $ CourseR tid ssh csh CTutorialListR) $ do TUsersR -> maybeT (i18nCrumb MsgBreadcrumbTutorial . Just $ CourseR tid ssh csh CTutorialListR) $ do
guardM . hasReadAccessTo $ CTutorialR tid ssh csh tutn TUsersR guardM . hasReadAccessTo $ CTutorialR tid ssh csh tutn TUsersR
return (original tutn, Just $ CourseR tid ssh csh CTutorialListR) return (CI.original tutn, Just $ CourseR tid ssh csh CTutorialListR)
TEditR -> i18nCrumb MsgMenuTutorialEdit . Just $ CTutorialR tid ssh csh tutn TUsersR TEditR -> i18nCrumb MsgMenuTutorialEdit . Just $ CTutorialR tid ssh csh tutn TUsersR
TDeleteR -> i18nCrumb MsgMenuTutorialDelete . Just $ CTutorialR tid ssh csh tutn TUsersR TDeleteR -> i18nCrumb MsgMenuTutorialDelete . Just $ CTutorialR tid ssh csh tutn TUsersR
TCommR -> i18nCrumb MsgMenuTutorialComm . Just $ CTutorialR tid ssh csh tutn TUsersR TCommR -> i18nCrumb MsgMenuTutorialComm . Just $ CTutorialR tid ssh csh tutn TUsersR
@ -1922,7 +1923,7 @@ instance YesodBreadcrumbs UniWorX where
breadcrumb (CourseR tid ssh csh (SheetR shn sRoute)) = case sRoute of breadcrumb (CourseR tid ssh csh (SheetR shn sRoute)) = case sRoute of
SShowR -> maybeT (i18nCrumb MsgBreadcrumbSheet . Just $ CourseR tid ssh csh SheetListR) $ do SShowR -> maybeT (i18nCrumb MsgBreadcrumbSheet . Just $ CourseR tid ssh csh SheetListR) $ do
guardM . hasReadAccessTo $ CSheetR tid ssh csh shn SShowR guardM . hasReadAccessTo $ CSheetR tid ssh csh shn SShowR
return (original shn, Just $ CourseR tid ssh csh SheetListR) return (CI.original shn, Just $ CourseR tid ssh csh SheetListR)
SEditR -> i18nCrumb MsgMenuSheetEdit . Just $ CSheetR tid ssh csh shn SShowR SEditR -> i18nCrumb MsgMenuSheetEdit . Just $ CSheetR tid ssh csh shn SShowR
SDelR -> i18nCrumb MsgMenuSheetDelete . Just $ CSheetR tid ssh csh shn SShowR SDelR -> i18nCrumb MsgMenuSheetDelete . Just $ CSheetR tid ssh csh shn SShowR
SSubsR -> i18nCrumb MsgMenuSubmissions . Just $ CSheetR tid ssh csh shn SShowR SSubsR -> i18nCrumb MsgMenuSubmissions . Just $ CSheetR tid ssh csh shn SShowR
@ -1955,7 +1956,7 @@ instance YesodBreadcrumbs UniWorX where
breadcrumb (CourseR tid ssh csh (MaterialR mnm sRoute)) = case sRoute of breadcrumb (CourseR tid ssh csh (MaterialR mnm sRoute)) = case sRoute of
MShowR -> maybeT (i18nCrumb MsgBreadcrumbMaterial . Just $ CourseR tid ssh csh MaterialListR) $ do MShowR -> maybeT (i18nCrumb MsgBreadcrumbMaterial . Just $ CourseR tid ssh csh MaterialListR) $ do
guardM . hasReadAccessTo $ CMaterialR tid ssh csh mnm MShowR guardM . hasReadAccessTo $ CMaterialR tid ssh csh mnm MShowR
return (original mnm, Just $ CourseR tid ssh csh MaterialListR) return (CI.original mnm, Just $ CourseR tid ssh csh MaterialListR)
MEditR -> i18nCrumb MsgMenuMaterialEdit . Just $ CMaterialR tid ssh csh mnm MShowR MEditR -> i18nCrumb MsgMenuMaterialEdit . Just $ CMaterialR tid ssh csh mnm MShowR
MDelR -> i18nCrumb MsgMenuMaterialDelete . Just $ CMaterialR tid ssh csh mnm MShowR MDelR -> i18nCrumb MsgMenuMaterialDelete . Just $ CMaterialR tid ssh csh mnm MShowR
MArchiveR -> i18nCrumb MsgBreadcrumbMaterialArchive . Just $ CMaterialR tid ssh csh mnm MShowR MArchiveR -> i18nCrumb MsgBreadcrumbMaterialArchive . Just $ CMaterialR tid ssh csh mnm MShowR
@ -2688,8 +2689,8 @@ pageActions (CourseR tid ssh csh SheetListR) =
, menuItemLabel = MsgMenuCorrectionsOwn , menuItemLabel = MsgMenuCorrectionsOwn
, menuItemIcon = Nothing , menuItemIcon = Nothing
, menuItemRoute = SomeRoute (CorrectionsR, [ ("corrections-term" , termToText $ unTermKey tid) , menuItemRoute = SomeRoute (CorrectionsR, [ ("corrections-term" , termToText $ unTermKey tid)
, ("corrections-school", original $ unSchoolKey ssh) , ("corrections-school", CI.original $ unSchoolKey ssh)
, ("corrections-course", original csh) , ("corrections-course", CI.original csh)
]) ])
, menuItemModal = False , menuItemModal = False
, menuItemAccessCallback' = do , menuItemAccessCallback' = do
@ -2915,9 +2916,9 @@ pageActions (CSheetR tid ssh csh shn SShowR) =
, menuItemLabel = MsgMenuCorrectionsOwn , menuItemLabel = MsgMenuCorrectionsOwn
, menuItemIcon = Nothing , menuItemIcon = Nothing
, menuItemRoute = SomeRoute (CorrectionsR, [ ("corrections-term" , termToText $ unTermKey tid) , menuItemRoute = SomeRoute (CorrectionsR, [ ("corrections-term" , termToText $ unTermKey tid)
, ("corrections-school", original $ unSchoolKey ssh) , ("corrections-school", CI.original $ unSchoolKey ssh)
, ("corrections-course", original csh) , ("corrections-course", CI.original csh)
, ("corrections-sheet" , original shn) , ("corrections-sheet" , CI.original shn)
]) ])
, menuItemModal = False , menuItemModal = False
, menuItemAccessCallback' = (== Authorized) <$> evalAccessCorrector tid ssh csh , menuItemAccessCallback' = (== Authorized) <$> evalAccessCorrector tid ssh csh
@ -3324,7 +3325,7 @@ routeNormalizers =
caseChanged :: (Eq a, Show a) => CI a -> CI a -> MaybeT (WriterT Any DB) () caseChanged :: (Eq a, Show a) => CI a -> CI a -> MaybeT (WriterT Any DB) ()
caseChanged a b caseChanged a b
| ((/=) `on` original) a b = do | ((/=) `on` CI.original) a b = do
$logDebugS "routeNormalizers" [st|#{tshow a} /= #{tshow b}|] $logDebugS "routeNormalizers" [st|#{tshow a} /= #{tshow b}|]
tell $ Any True tell $ Any True
| otherwise = return () | otherwise = return ()
@ -3416,7 +3417,8 @@ instance YesodPersistRunner UniWorX where
return . (, cleanup) $ DBRunner (\act -> $logDebugS "YesodPersist" "runDBRunner" >> runDBRunner act) return . (, cleanup) $ DBRunner (\act -> $logDebugS "YesodPersist" "runDBRunner" >> runDBRunner act)
data CampusUserConversionException data CampusUserConversionException
= CampusUserInvalidEmail = CampusUserInvalidIdent
| CampusUserInvalidEmail
| CampusUserInvalidDisplayName | CampusUserInvalidDisplayName
| CampusUserInvalidGivenName | CampusUserInvalidGivenName
| CampusUserInvalidSurname | CampusUserInvalidSurname
@ -3430,12 +3432,36 @@ instance Exception CampusUserConversionException
embedRenderMessage ''UniWorX ''CampusUserConversionException id embedRenderMessage ''UniWorX ''CampusUserConversionException id
upsertCampusUser :: Ldap.AttrList [] -> Creds UniWorX -> DB (Entity User) data UpsertCampusUserMode
upsertCampusUser ldapData Creds{..} = do = UpsertCampusUser
| UpsertCampusUserDummy { upsertCampusUserIdent :: UserIdent }
| UpsertCampusUserOther { uspertCampusUserIdent :: UserIdent }
deriving (Eq, Ord, Read, Show, Generic, Typeable)
makeLenses_ ''UpsertCampusUserMode
makePrisms ''UpsertCampusUserMode
_upsertCampusUserMode :: Traversal' (Creds UniWorX) UpsertCampusUserMode
_upsertCampusUserMode mMode cs@Creds{..}
| credsPlugin == "dummy" = setMode <$> mMode (UpsertCampusUserDummy $ CI.mk credsIdent)
| credsPlugin `elem` others = setMode <$> mMode (UpsertCampusUserOther $ CI.mk credsIdent)
| otherwise = setMode <$> mMode UpsertCampusUser
where
setMode UpsertCampusUser
= cs{ credsPlugin = "LDAP" }
setMode (UpsertCampusUserDummy ident)
= cs{ credsPlugin = "dummy", credsIdent = CI.original ident }
setMode (UpsertCampusUserOther ident)
= cs{ credsPlugin = bool (NonEmpty.head others) credsPlugin (credsPlugin `elem` others), credsIdent = CI.original ident }
others = "PWHash" :| []
upsertCampusUser :: UpsertCampusUserMode -> Ldap.AttrList [] -> DB (Entity User)
upsertCampusUser plugin ldapData = do
now <- liftIO getCurrentTime now <- liftIO getCurrentTime
UserDefaultConf{..} <- getsYesod $ view _appUserDefaults UserDefaultConf{..} <- getsYesod $ view _appUserDefaults
let let
userIdent'' = fold [ v | (k, v) <- ldapData, k == ldapUserPrincipalName ]
userMatrikelnummer' = fold [ v | (k, v) <- ldapData, k == ldapUserMatriculation ] userMatrikelnummer' = fold [ v | (k, v) <- ldapData, k == ldapUserMatriculation ]
userEmail' = fold $ do userEmail' = fold $ do
k' <- toList ldapUserEmail k' <- toList ldapUserEmail
@ -3449,13 +3475,23 @@ upsertCampusUser ldapData Creds{..} = do
userSex' = fold [ v | (k, v) <- ldapData, k == ldapSex ] userSex' = fold [ v | (k, v) <- ldapData, k == ldapSex ]
userAuthentication userAuthentication
| isPWHash = error "PWHash should only work for users that are already known" | is _UpsertCampusUserOther plugin
= error "PWHash should only work for users that are already known"
| otherwise = AuthLDAP | otherwise = AuthLDAP
userLastAuthentication = now <$ guard (not isDummy) userLastAuthentication = now <$ guard (isn't _UpsertCampusUserDummy plugin)
userIdent <- if
| [bs] <- userIdent''
, Right userIdent' <- CI.mk <$> Text.decodeUtf8' bs
, hasn't _upsertCampusUserIdent plugin || has (_upsertCampusUserIdent . only userIdent') plugin
-> return userIdent'
| Just userIdent' <- plugin ^? _upsertCampusUserIdent
-> return userIdent'
| otherwise
-> throwM CampusUserInvalidIdent
userEmail <- if userEmail <- if
| userEmail : _ <- mapMaybe (assertM (elem '@') . either (const Nothing) Just . Text.decodeUtf8') userEmail' | userEmail : _ <- mapMaybe (assertM (elem '@') . either (const Nothing) Just . Text.decodeUtf8') userEmail'
-> return $ mk userEmail -> return $ CI.mk userEmail
| otherwise | otherwise
-> throwM CampusUserInvalidEmail -> throwM CampusUserInvalidEmail
userDisplayName' <- if userDisplayName' <- if
@ -3505,8 +3541,7 @@ upsertCampusUser ldapData Creds{..} = do
let let
newUser = User newUser = User
{ userIdent = mk credsIdent { userMaxFavourites = userDefaultMaxFavourites
, userMaxFavourites = userDefaultMaxFavourites
, userMaxFavouriteTerms = userDefaultMaxFavouriteTerms , userMaxFavouriteTerms = userDefaultMaxFavouriteTerms
, userTheme = userDefaultTheme , userTheme = userDefaultTheme
, userDateTimeFormat = userDefaultDateTimeFormat , userDateTimeFormat = userDefaultDateTimeFormat
@ -3534,9 +3569,9 @@ upsertCampusUser ldapData Creds{..} = do
, UserSex =. userSex , UserSex =. userSex
, UserLastLdapSynchronisation =. Just now , UserLastLdapSynchronisation =. Just now
] ++ ] ++
[ UserLastAuthentication =. Just now | not isDummy ] [ UserLastAuthentication =. Just now | isn't _UpsertCampusUserDummy plugin ]
user@(Entity userId userRec) <- upsertBy (UniqueAuthentication $ mk credsIdent) newUser userUpdate user@(Entity userId userRec) <- upsertBy (UniqueAuthentication userIdent) newUser userUpdate
unless (validDisplayName userTitle userFirstName userSurname $ userDisplayName userRec) $ unless (validDisplayName userTitle userFirstName userSurname $ userDisplayName userRec) $
update userId [ UserDisplayName =. userDisplayName' ] update userId [ UserDisplayName =. userDisplayName' ]
@ -3549,7 +3584,7 @@ upsertCampusUser ldapData Creds{..} = do
Right str <- return $ Text.decodeUtf8' v' Right str <- return $ Text.decodeUtf8' v'
return str return str
termNames = nubBy ((==) `on` mk) $ do termNames = nubBy ((==) `on` CI.mk) $ do
(k, v) <- ldapData (k, v) <- ldapData
guard $ k == ldapUserFieldName guard $ k == ldapUserFieldName
v' <- v v' <- v
@ -3620,7 +3655,7 @@ upsertCampusUser ldapData Creds{..} = do
| otherwise -> do | otherwise -> do
$logDebugS "Campus" [st|Ignoring subterm #{tshow subterm}|] $logDebugS "Campus" [st|Ignoring subterm #{tshow subterm}|]
assimilateSubTerms subterms unusedFeats assimilateSubTerms subterms unusedFeats
$logDebugS "Campus" [st|Terms for #{credsIdent}: #{tshow (sts, fs')}|] $logDebugS "Campus" [st|Terms for #{userIdent}: #{tshow (sts, fs')}|]
(fs, studyFieldParentCandidates) <- runWriterT $ assimilateSubTerms sts fs' (fs, studyFieldParentCandidates) <- runWriterT $ assimilateSubTerms sts fs'
let let
@ -3726,8 +3761,6 @@ upsertCampusUser ldapData Creds{..} = do
return user return user
where where
insertMaybe key val = get key >>= maybe (insert_ val) (\_ -> return ()) insertMaybe key val = get key >>= maybe (insert_ val) (\_ -> return ())
isDummy = credsPlugin == "dummy"
isPWHash = credsPlugin == "PWHash"
associateUserSchoolsByTerms :: UserId -> DB () associateUserSchoolsByTerms :: UserId -> DB ()
associateUserSchoolsByTerms uid = do associateUserSchoolsByTerms uid = do
@ -3803,18 +3836,18 @@ instance YesodAuth UniWorX where
setTitleI MsgLoginTitle setTitleI MsgLoginTitle
$(widgetFile "login") $(widgetFile "login")
authenticate Creds{..} = liftHandler . runDB $ do authenticate creds@Creds{..} = liftHandler . runDB $ do
now <- liftIO getCurrentTime now <- liftIO getCurrentTime
let let
userIdent = mk credsIdent uAuth = UniqueAuthentication $ CI.mk credsIdent
uAuth = UniqueAuthentication userIdent upsertMode = creds ^? _upsertCampusUserMode
isDummy = credsPlugin == "dummy" isDummy = is (_Just . _UpsertCampusUserDummy) upsertMode
isPWHash = credsPlugin == "PWHash" isOther = is (_Just . _UpsertCampusUserOther) upsertMode
excRecovery res excRecovery res
| isDummy || isPWHash | isDummy || isOther
= do = do
case res of case res of
UserError err -> addMessageI Error err UserError err -> addMessageI Error err
@ -3858,12 +3891,12 @@ instance YesodAuth UniWorX where
UniWorX{ appSettings' = AppSettings{ appUserDefaults = UserDefaultConf{..}, ..}, .. } <- getYesod UniWorX{ appSettings' = AppSettings{ appUserDefaults = UserDefaultConf{..}, ..}, .. } <- getYesod
flip catches excHandlers $ case (,) <$> appLdapConf <*> appLdapPool of flip catches excHandlers $ case (,) <$> appLdapConf <*> appLdapPool of
Just (ldapConf, ldapPool) -> do Just (ldapConf, ldapPool)
let userCreds = Creds credsPlugin (original userIdent) credsExtra | Just upsertMode' <- upsertMode -> do
ldapData <- campusUser ldapConf ldapPool userCreds ldapData <- campusUser ldapConf ldapPool Creds{..}
$logDebugS "LDAP" $ "Successful LDAP lookup: " <> tshow ldapData $logDebugS "LDAP" $ "Successful LDAP lookup: " <> tshow ldapData
Authenticated . entityKey <$> upsertCampusUser ldapData userCreds Authenticated . entityKey <$> upsertCampusUser upsertMode' ldapData
Nothing _other
-> acceptExisting -> acceptExisting
authPlugins (UniWorX{ appSettings' = AppSettings{..}, appLdapPool }) = catMaybes authPlugins (UniWorX{ appSettings' = AppSettings{..}, appLdapPool }) = catMaybes

View File

@ -8,6 +8,7 @@ import Import hiding ((<.), (.>))
import Handler.Utils import Handler.Utils
import Handler.Utils.Exam import Handler.Utils.Exam
import Handler.Utils.Users
import Handler.Utils.Csv import Handler.Utils.Csv
import Handler.ExamOffice.Exam (examCloseWidget) import Handler.ExamOffice.Exam (examCloseWidget)
@ -372,7 +373,7 @@ data ExamUserCsvAction
deriving (Eq, Ord, Read, Show, Generic, Typeable) deriving (Eq, Ord, Read, Show, Generic, Typeable)
deriveJSON defaultOptions deriveJSON defaultOptions
{ constructorTagModifier = over Text.packed $ Text.intercalate "-" . map Text.toLower . drop 3 . dropEnd 1 . splitCamel { constructorTagModifier = over Text.packed $ Text.intercalate "-" . map Text.toLower . drop 3 . dropEnd 1 . splitCamel
, fieldLabelModifier = camelToPathPiece' 3 , fieldLabelModifier = camelToPathPiece' 4
, sumEncoding = TaggedObject "action" "data" , sumEncoding = TaggedObject "action" "data"
} ''ExamUserCsvAction } ''ExamUserCsvAction
@ -624,7 +625,7 @@ postEUsersR tid ssh csh examn = do
<*> preview (resultCourseNote . _entityVal . _courseUserNoteNote) <*> preview (resultCourseNote . _entityVal . _courseUserNoteNote)
dbtCsvDecode = Just DBTCsvDecode dbtCsvDecode = Just DBTCsvDecode
{ dbtCsvRowKey = \csv -> do { dbtCsvRowKey = \csv -> do
uid <- lift $ view _2 <$> guessUser csv uid <- lift $ view _2 <$> guessUser' csv
fmap E.Value . MaybeT . getKeyBy $ UniqueExamRegistration eid uid fmap E.Value . MaybeT . getKeyBy $ UniqueExamRegistration eid uid
, dbtCsvComputeActions = \case , dbtCsvComputeActions = \case
DBCsvDiffMissing{dbCsvOldKey} DBCsvDiffMissing{dbCsvOldKey}
@ -632,7 +633,7 @@ postEUsersR tid ssh csh examn = do
DBCsvDiffNew{dbCsvNewKey = Just _} DBCsvDiffNew{dbCsvNewKey = Just _}
-> error "An UniqueExamRegistration could be found, but the ExamRegistrationKey is not among the existing keys" -> error "An UniqueExamRegistration could be found, but the ExamRegistrationKey is not among the existing keys"
DBCsvDiffNew{dbCsvNewKey = Nothing, dbCsvNew} -> do DBCsvDiffNew{dbCsvNewKey = Nothing, dbCsvNew} -> do
(isPart, uid) <- lift $ guessUser dbCsvNew (isPart, uid) <- lift $ guessUser' dbCsvNew
if if
| isPart -> do | isPart -> do
yieldM $ ExamUserCsvRegisterData <$> pure uid <*> lookupOccurrence dbCsvNew yieldM $ ExamUserCsvRegisterData <$> pure uid <*> lookupOccurrence dbCsvNew
@ -930,37 +931,17 @@ postEUsersR tid ssh csh examn = do
registeredUserName existing (E.Value -> registration) = nameWidget userDisplayName userSurname registeredUserName existing (E.Value -> registration) = nameWidget userDisplayName userSurname
where where
Entity _ User{..} = view resultUser $ existing ! registration Entity _ User{..} = view resultUser $ existing ! registration
guessUser :: ExamUserTableCsv -> DB (Bool, UserId) guessUser' :: ExamUserTableCsv -> DB (Bool, UserId)
guessUser ExamUserTableCsv{..} = $cachedHereBinary (csvEUserMatriculation, csvEUserName, csvEUserSurname) $ do guessUser' ExamUserTableCsv{..} = do
users <- E.select . E.from $ \user -> do let criteria = Set.fromList $ catMaybes
E.where_ . E.or $ catMaybes [ GuessUserMatrikelnummer <$> csvEUserMatriculation
[ (user E.^. UserMatrikelnummer E.==.) . E.val . Just <$> csvEUserMatriculation , GuessUserDisplayName <$> csvEUserName
, (user E.^. UserDisplayName `E.hasInfix`) . E.val <$> csvEUserName , GuessUserSurname <$> csvEUserSurname
, (user E.^. UserSurname `E.hasInfix`) . E.val <$> csvEUserSurname , GuessUserFirstName <$> csvEUserFirstName
, (user E.^. UserFirstName `E.hasInfix`) . E.val <$> csvEUserFirstName
]
let isCourseParticipant = E.exists . E.from $ \courseParticipant ->
E.where_ $ courseParticipant E.^. CourseParticipantCourse E.==. E.val examCourse
E.&&. courseParticipant E.^. CourseParticipantUser E.==. user E.^. UserId
return (isCourseParticipant, user)
let users' = reverse $ sortBy closeness users
closeness :: (E.Value Bool, Entity User) -> (E.Value Bool, Entity User) -> Ordering
closeness = mconcat $ catMaybes
[ pure $ comparing (preview $ _2 . _entityVal . _userMatrikelnummer . only csvEUserMatriculation)
, pure $ comparing (view _1)
, csvEUserSurname <&> \surn -> comparing (preview $ _2 . _entityVal . _userSurname . to CI.mk . only (CI.mk surn))
, csvEUserFirstName <&> \firstn -> comparing (preview $ _2 . _entityVal . _userFirstName . to CI.mk . only (CI.mk firstn))
, csvEUserName <&> \dispn -> comparing (preview $ _2 . _entityVal . _userDisplayName . to CI.mk . only (CI.mk dispn))
] ]
case users' of pid <- maybe (throwM ExamUserCsvExceptionNoMatchingUser) return =<< guessUser criteria
[(E.Value isPart, Entity uid _)] (,) <$> existsBy (UniqueParticipant pid examCourse) <*> pure pid
-> return (isPart, uid)
(x@(E.Value isPart, Entity uid _) : x' : _)
| GT <- x `closeness` x'
-> return (isPart, uid)
_other
-> throwM ExamUserCsvExceptionNoMatchingUser
lookupOccurrence :: ExamUserTableCsv -> DB (Maybe ExamOccurrenceId) lookupOccurrence :: ExamUserTableCsv -> DB (Maybe ExamOccurrenceId)
lookupOccurrence ExamUserTableCsv{..} = $cachedHereBinary (CI.foldedCase <$> csvEUserOccurrence) . for csvEUserOccurrence $ \occName -> do lookupOccurrence ExamUserTableCsv{..} = $cachedHereBinary (CI.foldedCase <$> csvEUserOccurrence) . for csvEUserOccurrence $ \occName -> do
@ -971,7 +952,7 @@ postEUsersR tid ssh csh examn = do
lookupStudyFeatures :: ExamUserTableCsv -> DB (Maybe StudyFeaturesId) lookupStudyFeatures :: ExamUserTableCsv -> DB (Maybe StudyFeaturesId)
lookupStudyFeatures csv@ExamUserTableCsv{..} = do lookupStudyFeatures csv@ExamUserTableCsv{..} = do
uid <- view _2 <$> guessUser csv uid <- view _2 <$> guessUser' csv
oldFeatures <- getBy $ UniqueParticipant uid examCourse oldFeatures <- getBy $ UniqueParticipant uid examCourse
studyFeatures <- E.select . E.from $ \(studyFeatures `E.InnerJoin` studyDegree `E.InnerJoin` studyTerms) -> studyFeatures <- E.select . E.from $ \(studyFeatures `E.InnerJoin` studyDegree `E.InnerJoin` studyTerms) ->
E.distinctOnOrderBy [ E.asc (studyFeatures E.^. StudyFeaturesField) E.distinctOnOrderBy [ E.asc (studyFeatures E.^. StudyFeaturesField)

View File

@ -140,15 +140,15 @@ resultSynchronised :: Traversal' ExamUserTableData (UserDisplayName, UserSurname
resultSynchronised = _dbrOutput . _9 . traverse resultSynchronised = _dbrOutput . _9 . traverse
data ExamUserTableCsv = ExamUserTableCsv data ExamUserTableCsv = ExamUserTableCsv
{ csvEUserSurname :: Text { csvEUserSurname :: Text
, csvEUserFirstName :: Text , csvEUserFirstName :: Text
, csvEUserName :: Text , csvEUserName :: Text
, csvEUserMatriculation :: Maybe Text , csvEUserMatriculation :: Maybe Text
, csvEUserField :: Maybe Text , csvEUserField :: Maybe Text
, csvEUserDegree :: Maybe Text , csvEUserDegree :: Maybe Text
, csvEUserSemester :: Maybe Int , csvEUserSemester :: Maybe Int
, csvEUserOccurrenceStart :: Maybe ZonedTime , csvEUserOccurrenceStart :: Maybe ZonedTime
, csvEUserExamResult :: ExamResultPassedGrade , csvEUserExamResult :: ExamResultPassedGrade
} }
deriving (Generic) deriving (Generic)
makeLenses_ ''ExamUserTableCsv makeLenses_ ''ExamUserTableCsv
@ -396,7 +396,7 @@ postEGradesR tid ssh csh examn = do
dbtIdent = "exam-results" dbtIdent = "exam-results"
dbtCsvEncode = Just DBTCsvEncode dbtCsvEncode = Just DBTCsvEncode
{ dbtCsvExportForm = ExamUserCsvExportData { dbtCsvExportForm = ExamUserCsvExportData
<$> apopt checkBoxField (fslI MsgExamUserMarkSynchronisedCsv) (Just True) <$> apopt checkBoxField (fslI MsgExamUserMarkSynchronisedCsv & setTooltip MsgExamUserMarkSynchronisedCsvTip) (Just False)
, dbtCsvDoEncode = \ExamUserCsvExportData{..} -> C.mapM $ \(E.Value k, row) -> do , dbtCsvDoEncode = \ExamUserCsvExportData{..} -> C.mapM $ \(E.Value k, row) -> do
when csvEUserMarkSynchronised $ markSynced k when csvEUserMarkSynchronised $ markSynced k
return $ ExamUserTableCsv return $ ExamUserTableCsv

View File

@ -4,7 +4,15 @@ module Handler.ExamOffice.ExternalExam
import Import import Import
import Handler.Utils.ExternalExam.Users
getEEGradesR, postEEGradesR :: TermId -> SchoolId -> CourseName -> ExamName -> Handler Html getEEGradesR, postEEGradesR :: TermId -> SchoolId -> CourseName -> ExamName -> Handler Html
getEEGradesR = postEEGradesR getEEGradesR = postEEGradesR
postEEGradesR = error "Not implemented" postEEGradesR tid ssh coursen examn = do
(_, table) <- runDB $ do
eExam <- getBy404 $ UniqueExternalExam tid ssh coursen examn
makeExternalExamUsersTable EEUMGrades eExam
siteLayoutMsg (MsgExternalExamGrades coursen examn) $ do
setTitleI MsgBreadcrumbExternalExamGrades
table

View File

@ -4,7 +4,15 @@ module Handler.ExternalExam.Users
import Import import Import
import Handler.Utils.ExternalExam.Users
getEEUsersR, postEEUsersR :: TermId -> SchoolId -> CourseName -> ExamName -> Handler Html getEEUsersR, postEEUsersR :: TermId -> SchoolId -> CourseName -> ExamName -> Handler Html
getEEUsersR = postEEUsersR getEEUsersR = postEEUsersR
postEEUsersR = error "Not implemented" postEEUsersR tid ssh coursen examn = do
(_, table) <- runDB $ do
eExam <- getBy404 $ UniqueExternalExam tid ssh coursen examn
makeExternalExamUsersTable EEUMUsers eExam
siteLayoutMsg (MsgExternalExamUsers coursen examn) $ do
setTitleI MsgBreadcrumbExternalExamUsers
table

View File

@ -1,5 +1,6 @@
module Handler.Utils.ExamOffice.ExternalExam module Handler.Utils.ExamOffice.ExternalExam
( examOfficeExternalExamResultAuth ( resultIsSynced
, examOfficeExternalExamResultAuth
) where ) where
import Import.NoFoundation import Import.NoFoundation
@ -7,6 +8,27 @@ import Import.NoFoundation
import qualified Database.Esqueleto as E import qualified Database.Esqueleto as E
resultIsSynced :: E.SqlExpr (E.Value UserId) -- ^ office
-> E.SqlExpr (Entity ExternalExamResult)
-> E.SqlExpr (E.Value Bool)
resultIsSynced authId eexamResult = (hasSchool E.&&. allSchools) E.||. (E.not_ hasSchool E.&&. anySync)
where
anySync = E.exists . E.from $ \synced ->
E.where_ $ synced E.^. ExamOfficeExternalResultSyncedResult E.==. eexamResult E.^. ExternalExamResultId
E.&&. synced E.^. ExamOfficeExternalResultSyncedTime E.>=. eexamResult E.^. ExternalExamResultLastChanged
hasSchool = E.exists . E.from $ \userFunction ->
E.where_ $ userFunction E.^. UserFunctionUser E.==. authId
E.&&. userFunction E.^. UserFunctionFunction E.==. E.val SchoolExamOffice
allSchools = E.not_ . E.exists . E.from $ \userFunction -> do
E.where_ $ userFunction E.^. UserFunctionUser E.==. authId
E.&&. userFunction E.^. UserFunctionFunction E.==. E.val SchoolExamOffice
E.where_ . E.not_ . E.exists . E.from $ \synced ->
E.where_ $ synced E.^. ExamOfficeExternalResultSyncedSchool E.==. E.just (userFunction E.^. UserFunctionSchool)
E.&&. synced E.^. ExamOfficeExternalResultSyncedResult E.==. eexamResult E.^. ExternalExamResultId
E.&&. synced E.^. ExamOfficeExternalResultSyncedTime E.>=. eexamResult E.^. ExternalExamResultLastChanged
examOfficeExternalExamResultAuth :: E.SqlExpr (E.Value UserId) -- ^ office examOfficeExternalExamResultAuth :: E.SqlExpr (E.Value UserId) -- ^ office
-> E.SqlExpr (Entity ExternalExamResult) -> E.SqlExpr (Entity ExternalExamResult)
-> E.SqlExpr (E.Value Bool) -> E.SqlExpr (E.Value Bool)

View File

@ -0,0 +1,430 @@
{-# OPTIONS_GHC -fno-warn-redundant-constraints #-}
module Handler.Utils.ExternalExam.Users where
import Import hiding ((.:))
import Handler.Utils
import Handler.Utils.Csv
import Handler.Utils.Users
import qualified Data.Map as Map
import qualified Data.Set as Set
import qualified Colonnade
import qualified Database.Esqueleto as E
import qualified Database.Esqueleto.Utils as E
import Data.Csv ((.:))
import qualified Data.Csv as Csv
import qualified Handler.Utils.ExamOffice.ExternalExam as ExternalExam
import qualified Data.Text as Text
import qualified Data.Text.Lens as Text
import qualified Data.Conduit.List as C
data ExternalExamUserMode = EEUMUsers | EEUMGrades
deriving (Eq, Ord, Read, Show, Bounded, Enum, Generic, Typeable)
instance Universe ExternalExamUserMode
instance Finite ExternalExamUserMode
nullaryPathPiece ''ExternalExamUserMode $ camelToPathPiece' 1
type ExternalExamUserTableExpr = E.SqlExpr (Entity ExternalExamResult)
`E.InnerJoin` E.SqlExpr (Entity User)
type ExternalExamUserTableData = DBRow ( Entity ExternalExamResult
, Entity User
, Bool
, [(UserDisplayName, UserSurname, UTCTime, Set SchoolShorthand)]
)
queryUser :: Getter ExternalExamUserTableExpr (E.SqlExpr (Entity User))
queryUser = to $(E.sqlIJproj 2 2)
queryResult :: Getter ExternalExamUserTableExpr (E.SqlExpr (Entity ExternalExamResult))
queryResult = to $(E.sqlIJproj 2 1)
queryIsSynced :: E.SqlExpr (E.Value UserId) -> Getter ExternalExamUserTableExpr (E.SqlExpr (E.Value Bool))
queryIsSynced authId = to $ ExternalExam.resultIsSynced authId <$> view queryResult
resultUser :: Lens' ExternalExamUserTableData (Entity User)
resultUser = _dbrOutput . _2
resultResult :: Lens' ExternalExamUserTableData (Entity ExternalExamResult)
resultResult = _dbrOutput . _1
resultIsSynced :: Lens' ExternalExamUserTableData Bool
resultIsSynced = _dbrOutput . _3
resultSynchronised :: Traversal' ExternalExamUserTableData (UserDisplayName, UserSurname, UTCTime, Set SchoolShorthand)
resultSynchronised = _dbrOutput . _4 . traverse
data ExternalExamUserTableCsv = ExternalExamUserTableCsv
{ csvEUserSurname :: Maybe Text
, csvEUserFirstName :: Maybe Text
, csvEUserName :: Maybe Text
, csvEUserMatriculation :: Maybe Text
, csvEUserOccurrenceStart :: ZonedTime
, csvEUserExamResult :: ExamResultPassedGrade
} deriving (Generic)
makeLenses_ ''ExternalExamUserTableCsv
externalExamUserTableCsvOptions :: Csv.Options
externalExamUserTableCsvOptions = Csv.defaultOptions { Csv.fieldLabelModifier = camelToPathPiece' 3 }
instance ToNamedRecord ExternalExamUserTableCsv where
toNamedRecord = Csv.genericToNamedRecord externalExamUserTableCsvOptions
instance DefaultOrdered ExternalExamUserTableCsv where
headerOrder = Csv.genericHeaderOrder externalExamUserTableCsvOptions
instance FromNamedRecord ExternalExamUserTableCsv where
parseNamedRecord csv
= ExternalExamUserTableCsv
<$> csv .:?? "surname"
<*> csv .:?? "first-name"
<*> csv .:?? "name"
<*> csv .:?? "matriculation"
<*> csv .: "occurrence-start"
<*> csv .: "exam-result"
instance CsvColumnsExplained ExternalExamUserTableCsv where
csvColumnsExplanations = genericCsvColumnsExplanations externalExamUserTableCsvOptions $ Map.fromList
[ ('csvEUserSurname , MsgCsvColumnExamUserSurname )
, ('csvEUserFirstName , MsgCsvColumnExamUserFirstName )
, ('csvEUserName , MsgCsvColumnExamUserName )
, ('csvEUserMatriculation , MsgCsvColumnExamUserMatriculation )
, ('csvEUserOccurrenceStart , MsgCsvColumnExamOfficeExamUserOccurrenceStart )
, ('csvEUserExamResult , MsgCsvColumnExamUserResult )
]
data ExternalExamUserAction
= ExternalExamUserMarkSynchronised
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic, Typeable)
instance Universe ExternalExamUserAction
instance Finite ExternalExamUserAction
nullaryPathPiece ''ExternalExamUserAction $ camelToPathPiece' 3
embedRenderMessage ''UniWorX ''ExternalExamUserAction id
data ExternalExamUserActionData
= ExternalExamUserMarkSynchronisedData
newtype ExternalExamUserCsvExportDataGrades = ExternalExamUserCsvExportDataGrades
{ csvEEUserMarkSynchronised :: Bool
} deriving (Eq, Ord, Read, Show, Generic, Typeable)
data ExamUserCsvException
= ExamUserCsvExceptionNoMatchingUser
deriving (Show, Generic, Typeable)
instance Exception ExamUserCsvException
embedRenderMessage ''UniWorX ''ExamUserCsvException id
data ExternalExamUserCsvActionClass
= ExternalExamUserCsvRegister
| ExternalExamUserCsvDeregister
| ExternalExamUserCsvSetTime
| ExternalExamUserCsvSetResult
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable)
embedRenderMessage ''UniWorX ''ExternalExamUserCsvActionClass id
data ExternalExamUserCsvAction
= ExternalExamUserCsvRegisterData
{ externalExamUserCsvActUser :: UserId
, externalExamUserCsvActTime :: UTCTime
, externalExamUserCsvActResult :: ExamResultPassedGrade
}
| ExternalExamUserCsvSetTimeData
{ externalExamUserCsvActRegistration :: ExternalExamResultId
, externalExamUserCsvActTime :: UTCTime
}
| ExternalExamUserCsvSetResultData
{ externalExamUserCsvActRegistration :: ExternalExamResultId
, externalExamUserCsvActResult :: ExamResultPassedGrade
}
| ExternalExamUserCsvDeregisterData
{ externalExamUserCsvActRegistration :: ExternalExamResultId
}
deriving (Eq, Ord, Read, Show, Generic, Typeable)
deriveJSON defaultOptions
{ constructorTagModifier = over Text.packed $ Text.intercalate "-" . map Text.toLower . drop 4 . dropEnd 1 . splitCamel
, fieldLabelModifier = camelToPathPiece' 5
, sumEncoding = TaggedObject "action" "data"
} ''ExternalExamUserCsvAction
makeExternalExamUsersTable :: ExternalExamUserMode
-> Entity ExternalExam
-> DB (FormResult (ExternalExamUserActionData, Set ExternalExamResultId), Widget)
makeExternalExamUsersTable mode (Entity eeId ExternalExam{..}) = do
let tid = externalExamTerm
ssh = externalExamSchool
coursen = externalExamCourseName
examn = externalExamExamName
uid <- requireAuthId
isLecturer <- hasReadAccessTo $ EExamR tid ssh coursen examn EEUsersR
currentRoute <- fromMaybe (error "makeExternalExamUsersTable called from 404-handler") <$> getCurrentRoute
let
resultView :: ExamResultGrade -> ExamResultPassedGrade
resultView = fmap $ bool (Left . view passingGrade) Right externalExamShowGrades
dbtSQLQuery = runReaderT $ do
result <- view queryResult
user <- view queryUser
isSynced <- view . queryIsSynced $ E.val uid
lift $ do
E.on $ result E.^. ExternalExamResultUser E.==. user E.^. UserId
E.where_ $ result E.^. ExternalExamResultExam E.==. E.val eeId
unless (isLecturer || mode == EEUMUsers) $ do
E.where_ $ ExternalExam.examOfficeExternalExamResultAuth (E.val uid) result
return (result, user, isSynced)
dbtRowKey = views queryResult (E.^. ExternalExamResultId)
dbtProj :: DBRow _ -> MaybeT (YesodDB UniWorX) ExternalExamUserTableData
dbtProj = runReaderT $ (asks . set _dbrOutput) <=< magnify _dbrOutput $
(,,,)
<$> view _1 <*> view _2 <*> view (_3 . _Value)
<*> getSynchronised
where
getSynchronised :: ReaderT _ (MaybeT (YesodDB UniWorX)) [(UserDisplayName, UserSurname, UTCTime, Set SchoolShorthand)]
getSynchronised = do
resId <- view $ _1 . _entityKey
syncs <- lift . lift . E.select . E.from $ \(examOfficeExternalResultSynced `E.InnerJoin` user) -> do
E.on $ examOfficeExternalResultSynced E.^. ExamOfficeExternalResultSyncedOffice E.==. user E.^. UserId
E.where_ $ examOfficeExternalResultSynced E.^. ExamOfficeExternalResultSyncedResult E.==. E.val resId
return ( examOfficeExternalResultSynced E.^. ExamOfficeExternalResultSyncedOffice
, ( user E.^. UserDisplayName
, user E.^. UserSurname
, examOfficeExternalResultSynced E.^. ExamOfficeExternalResultSyncedTime
, examOfficeExternalResultSynced E.^. ExamOfficeExternalResultSyncedSchool
)
)
let syncs' = Map.fromListWith
(\(dn, sn, t, sshs) (_, _, _, sshs') -> (dn, sn, t, Set.union sshs sshs'))
[ ((officeId, t), (dn, sn, t, maybe Set.empty Set.singleton ssh'))
| (E.Value officeId, (E.Value dn, E.Value sn, E.Value t, fmap unSchoolKey . E.unValue -> ssh')) <- syncs
]
return $ Map.elems syncs'
colSynced = Colonnade.singleton (fromSortable . Sortable (Just "is-synced") $ i18nCell MsgExamUserSynchronised) $ \x -> cell . flip runReaderT x $ do
syncs <- asks $ sortOn (Down . view _3) . toListOf resultSynchronised
lastChange <- view $ resultResult . _entityVal . _externalExamResultLastChanged
user <- view $ resultUser . _entityVal
isSynced <- view resultIsSynced
let
hasSyncs = has folded syncs
syncs' = [ Right sync | sync@(_, _, t, _) <- syncs, t > lastChange]
++ [ Left lastChange ]
++ [ Right sync | sync@(_, _, t, _) <- syncs, t <= lastChange]
syncIcon :: Widget
syncIcon
| not isSynced
, not hasSyncs
= mempty
| not isSynced
= toWidget iconNotOK
| otherwise
= toWidget iconOK
syncsModal :: Widget
syncsModal = $(widgetFile "exam-office/exam-result-synced")
lift $ bool id (flip modal $ Right syncsModal) hasSyncs syncIcon
dbtColonnade :: Colonnade Sortable _ _
dbtColonnade = mconcat
[ case mode of
EEUMGrades -> mconcat
[ dbSelect (applying _2) id $ return . view (resultResult . _entityKey)
, colSynced
]
_other -> mempty
, colUserDisplayName (resultUser . _entityVal . $(multifocusL 2) _userDisplayName _userSurname)
, colUserMatriculation (resultUser . _entityVal . _userMatrikelnummer)
, Colonnade.singleton (fromSortable . Sortable (Just "occurrence-start") $ i18nCell MsgExamTime) $ \x -> cell . flip runReaderT x $ do
t <- view $ resultResult . _entityVal . _externalExamResultTime
lift $ formatTimeW SelFormatDateTime t
, colExamResult externalExamShowGrades (resultResult . _entityVal . _externalExamResultResult)
]
dbtSorting = mconcat
[ sortUserName' (queryUser . to ((,) <$> (E.^. UserDisplayName) <*> (E.^. UserSurname)))
, sortUserMatriculation (queryUser . to (E.^. UserMatrikelnummer))
, sortOccurrenceStart (queryResult . to (E.^. ExternalExamResultTime))
, maybeOpticSortColumn (sortExamResult externalExamShowGrades) (queryResult . to (E.^. ExternalExamResultResult))
, singletonMap "is-synced" . SortColumn $ view (queryIsSynced $ E.val uid)
]
dbtFilter = mconcat
[ fltrUserName' (queryUser . to (E.^. UserDisplayName))
, fltrUserMatriculation (queryUser . to (E.^. UserMatrikelnummer))
, fltrExamResultPoints externalExamShowGrades (queryResult . to (E.^. ExternalExamResultResult))
, singletonMap "is-synced" . FilterColumn $ E.mkExactFilter (view . queryIsSynced $ E.val uid)
]
dbtFilterUI = mconcat
[ fltrUserNameUI'
, fltrUserMatriculationUI
, fltrExamResultPointsUI externalExamShowGrades
, case mode of
EEUMGrades ->
\mPrev -> prismAForm (singletonFilter "is-synced" . maybePrism _PathPiece) mPrev $ aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgExamUserSynchronised)
_other -> mempty
]
dbtStyle = def { dbsFilterLayout = defaultDBSFilterLayout }
dbtParams = DBParamsForm
{ dbParamsFormMethod = POST
, dbParamsFormAction = Just $ SomeRoute currentRoute
, dbParamsFormAttrs = []
, dbParamsFormSubmit = case mode of
EEUMGrades -> FormSubmit
_other -> FormNoSubmit
, dbParamsFormAdditional = case mode of
EEUMGrades -> \csrf -> do
let
actionMap :: Map ExternalExamUserAction (AForm Handler ExternalExamUserActionData)
actionMap = Map.fromList
[ ( ExternalExamUserMarkSynchronised
, pure ExternalExamUserMarkSynchronisedData
)
]
(res, formWgt) <- multiActionM actionMap (fslI MsgAction) Nothing csrf
let formRes = (, mempty) . First . Just <$> res
return (formRes, formWgt)
_other -> \csrf -> return (FormMissing, toWidget csrf)
, dbParamsFormEvaluate = liftHandler . runFormPost
, dbParamsFormResult = id
, dbParamsFormIdent = def
}
dbtIdent = mode
dbtCsvEncode = Nothing
dbtCsvDecode
| mode == EEUMUsers = Just DBTCsvDecode
{ dbtCsvRowKey = \csv -> do
pid <- lift $ guessUser' csv
fmap E.Value . MaybeT . getKeyBy $ UniqueExternalExamResult eeId pid
, dbtCsvComputeActions = \case
DBCsvDiffMissing{dbCsvOldKey}
-> yield . ExternalExamUserCsvDeregisterData $ E.unValue dbCsvOldKey
DBCsvDiffNew{dbCsvNewKey = Just _}
-> error "An UniqueExternalExamResult could be found, but the ExternalExamResultKey is not among the existing keys"
DBCsvDiffNew{dbCsvNewKey = Nothing, dbCsvNew} -> do
pid <- lift $ guessUser' dbCsvNew
let ExternalExamUserTableCsv{..} = dbCsvNew
yield $ ExternalExamUserCsvRegisterData pid (zonedTimeToUTC csvEUserOccurrenceStart) csvEUserExamResult
DBCsvDiffExisting{..} -> do
let ExternalExamUserTableCsv{..} = dbCsvNew
when (zonedTimeToUTC csvEUserOccurrenceStart /= dbCsvOld ^. resultResult . _entityVal . _externalExamResultTime) $
yield $ ExternalExamUserCsvSetTimeData (E.unValue dbCsvOldKey) (zonedTimeToUTC csvEUserOccurrenceStart)
when (csvEUserExamResult /= dbCsvOld ^. resultResult . _entityVal . _externalExamResultResult . to resultView) $
yield $ ExternalExamUserCsvSetResultData (E.unValue dbCsvOldKey) csvEUserExamResult
, dbtCsvClassifyAction = \case
ExternalExamUserCsvRegisterData{} -> ExternalExamUserCsvRegister
ExternalExamUserCsvSetTimeData{} -> ExternalExamUserCsvSetTime
ExternalExamUserCsvSetResultData{} -> ExternalExamUserCsvSetResult
ExternalExamUserCsvDeregisterData{} -> ExternalExamUserCsvDeregister
, dbtCsvCoarsenActionClass = \case
ExternalExamUserCsvRegister -> DBCsvActionNew
ExternalExamUserCsvDeregister -> DBCsvActionMissing
_other -> DBCsvActionExisting
, dbtCsvExecuteActions = do
C.mapM_ $ \case
ExternalExamUserCsvRegisterData{..} -> do
now <- liftIO getCurrentTime
let res' = either (review passingGrade) id <$> externalExamUserCsvActResult
insert_ ExternalExamResult
{ externalExamResultExam = eeId
, externalExamResultUser = externalExamUserCsvActUser
, externalExamResultTime = externalExamUserCsvActTime
, externalExamResultResult = res'
, externalExamResultLastChanged = now
}
audit $ TransactionExternalExamResultEdit eeId externalExamUserCsvActUser
ExternalExamUserCsvSetTimeData{..} -> do
now <- liftIO getCurrentTime
ExternalExamResult{..} <- updateGet externalExamUserCsvActRegistration
[ ExternalExamResultTime =. externalExamUserCsvActTime
, ExternalExamResultLastChanged =. now
]
audit $ TransactionExternalExamResultEdit eeId externalExamResultUser
ExternalExamUserCsvSetResultData{..} -> do
now <- liftIO getCurrentTime
let res' = either (review passingGrade) id <$> externalExamUserCsvActResult
ExternalExamResult{..} <- updateGet externalExamUserCsvActRegistration
[ ExternalExamResultResult =. res'
, ExternalExamResultLastChanged =. now
]
audit $ TransactionExternalExamResultEdit eeId externalExamResultUser
ExternalExamUserCsvDeregisterData{..} -> do
ExternalExamResult{..} <- getJust externalExamUserCsvActRegistration
delete externalExamUserCsvActRegistration
audit $ TransactionExternalExamResultDelete eeId externalExamResultUser
return $ EExamR tid ssh coursen examn EEUsersR
, dbtCsvRenderKey = \(registeredUserName -> registeredUserName') -> \case
ExternalExamUserCsvRegisterData{..} -> do
User{..} <- liftHandler . runDB $ getJust externalExamUserCsvActUser
[whamlet|
$newline never
^{nameWidget userDisplayName userSurname}
, ^{formatTimeW SelFormatDateTime externalExamUserCsvActTime}
, _{externalExamUserCsvActResult}
|]
ExternalExamUserCsvSetTimeData{..} ->
[whamlet|
$newline never
^{registeredUserName' externalExamUserCsvActRegistration}
, ^{formatTimeW SelFormatDateTime externalExamUserCsvActTime}
|]
ExternalExamUserCsvSetResultData{..} ->
[whamlet|
$newline never
^{registeredUserName' externalExamUserCsvActRegistration}
, _{externalExamUserCsvActResult}
|]
ExternalExamUserCsvDeregisterData{..} ->
registeredUserName' externalExamUserCsvActRegistration
, dbtCsvRenderActionClass = i18n
, dbtCsvRenderException = ap getMessageRender . pure :: ExamUserCsvException -> DB Text
}
| otherwise = Nothing
where
registeredUserName :: Map (E.Value ExternalExamResultId) ExternalExamUserTableData -> ExternalExamResultId -> Widget
registeredUserName existing (E.Value -> registration) = nameWidget userDisplayName userSurname
where
Entity _ User{..} = view resultUser $ existing Map.! registration
guessUser' :: ExternalExamUserTableCsv -> DB UserId
guessUser' ExternalExamUserTableCsv{..} = do
let criteria = Set.fromList $ catMaybes
[ GuessUserMatrikelnummer <$> csvEUserMatriculation
, GuessUserDisplayName <$> csvEUserName
, GuessUserSurname <$> csvEUserSurname
, GuessUserFirstName <$> csvEUserFirstName
]
maybe (throwM ExamUserCsvExceptionNoMatchingUser) return =<< guessUser criteria
externalExamUsersDBTableValidator = def
& defaultSorting (bool id (SortAscBy "is-synced" :) (mode == EEUMGrades) [SortAscBy "user-name"])
& defaultPagesize PagesizeAll
postprocess :: FormResult (First ExternalExamUserActionData, DBFormResult ExternalExamResultId Bool ExternalExamUserTableData) -> FormResult (ExternalExamUserActionData, Set ExternalExamResultId)
postprocess inp = do
(First (Just act), regMap) <- inp
let regSet = Map.keysSet . Map.filter id $ getDBFormResult (const False) regMap
return (act, regSet)
over _1 postprocess <$> dbTable externalExamUsersDBTableValidator DBTable{..}

View File

@ -2,9 +2,12 @@ module Handler.Utils.Users
( computeUserAuthenticationDigest ( computeUserAuthenticationDigest
, Digest, SHA3_256 , Digest, SHA3_256
, constEq , constEq
, GuessUserInfo(..)
, guessUser
) where ) where
import Import import Import
import Auth.LDAP (campusUserMatr')
import Crypto.Hash (Digest, SHA3_256, hashlazy) import Crypto.Hash (Digest, SHA3_256, hashlazy)
@ -12,6 +15,74 @@ import Data.ByteArray (constEq)
import qualified Data.Aeson as JSON import qualified Data.Aeson as JSON
import qualified Data.Set as Set
import qualified Data.CaseInsensitive as CI
import qualified Database.Esqueleto as E
import qualified Database.Esqueleto.Utils as E
computeUserAuthenticationDigest :: AuthenticationMode -> Digest SHA3_256 computeUserAuthenticationDigest :: AuthenticationMode -> Digest SHA3_256
computeUserAuthenticationDigest = hashlazy . JSON.encode computeUserAuthenticationDigest = hashlazy . JSON.encode
data GuessUserInfo
= GuessUserMatrikelnummer { guessUserMatrikelnummer :: UserMatriculation }
| GuessUserDisplayName { guessUserDisplayName :: UserDisplayName }
| GuessUserSurname { guessUserSurname :: UserSurname }
| GuessUserFirstName { guessUserFirstName :: UserFirstName }
deriving (Eq, Ord, Read, Show, Generic, Typeable)
instance Binary GuessUserInfo
makeLenses_ ''GuessUserInfo
guessUser :: Set GuessUserInfo -> DB (Maybe UserId)
guessUser (Set.toList -> criteria) = $cachedHereBinary criteria $ go False
where
toSql user = \case
GuessUserMatrikelnummer userMatriculation' -> user E.^. UserMatrikelnummer E.==. E.val (Just userMatriculation')
GuessUserDisplayName userDisplayName' -> user E.^. UserDisplayName `E.hasInfix` E.val userDisplayName'
GuessUserSurname userSurname' -> user E.^. UserSurname `E.hasInfix` E.val userSurname'
GuessUserFirstName userFirstName' -> user E.^. UserFirstName `E.hasInfix` E.val userFirstName'
go didLdap = do
let retrieveUsers = E.select . E.from $ \user -> do
E.where_ . E.or $ map (toSql user) criteria
return user
users <- retrieveUsers
let users' = reverse $ sortBy closeness users
matchesMatriculation :: Entity User -> Maybe Bool
matchesMatriculation = preview $ _entityVal . _userMatrikelnummer . to (\userMatr -> all ((== userMatr) . Just) $ criteria ^.. folded . _guessUserMatrikelnummer)
closeness :: Entity User -> Entity User -> Ordering
closeness = mconcat $ concat
[ pure $ comparing (fmap Down . matchesMatriculation)
, (criteria ^.. folded . _guessUserSurname) <&> \surn -> comparing (preview $ _entityVal . _userSurname . to CI.mk . only (CI.mk surn))
, (criteria ^.. folded . _guessUserFirstName) <&> \firstn -> comparing (preview $ _entityVal . _userFirstName . to CI.mk . only (CI.mk firstn))
, (criteria ^.. folded . _guessUserDisplayName) <&> \dispn -> comparing (preview $ _entityVal . _userDisplayName . to CI.mk . only (CI.mk dispn))
]
doLdap userMatr = do
app <- getYesod
let ldap = (,) <$> app ^. _appLdapConf <*> app ^. _appLdapPool
fmap (fmap entityKey . join) . for ldap $ \(ldapConf, ldapPool) -> do
ldapData <- campusUserMatr' ldapConf ldapPool userMatr
for ldapData $ upsertCampusUser UpsertCampusUser
case users' of
x@(Entity pid _) : xs
| [] <- xs
, fromMaybe False (matchesMatriculation x) || didLdap
-> return $ Just pid
| x' : _ <- xs
, fromMaybe False (matchesMatriculation x) || didLdap
, GT <- x `closeness` x'
-> return $ Just pid
| not didLdap
, userMatr : userMatrs' <- criteria ^.. folded . _guessUserMatrikelnummer
, all (== userMatr) userMatrs'
-> doLdap userMatr >>= maybe (go True) (return . Just)
_other
-> return Nothing

View File

@ -6,7 +6,6 @@ module Jobs.Handler.SynchroniseLdap
import Import import Import
import qualified Data.Conduit.List as C import qualified Data.Conduit.List as C
import qualified Data.CaseInsensitive as CI
import Auth.LDAP import Auth.LDAP
@ -48,11 +47,7 @@ dispatchJobSynchroniseLdapUser jUser = do
$logInfoS "SynchroniseLdap" [st|Synchronising #{userIdent}|] $logInfoS "SynchroniseLdap" [st|Synchronising #{userIdent}|]
ldapAttrs <- MaybeT $ campusUser' ldapConf ldapPool user ldapAttrs <- MaybeT $ campusUser' ldapConf ldapPool user
void . lift $ upsertCampusUser ldapAttrs Creds void . lift $ upsertCampusUser UpsertCampusUser ldapAttrs
{ credsIdent = CI.original userIdent
, credsPlugin = "dummy"
, credsExtra = []
}
Nothing -> Nothing ->
throwM SynchroniseLdapNoLdap throwM SynchroniseLdapNoLdap
where where

View File

@ -223,6 +223,7 @@ makeLenses_ ''SessionFile
makeLenses_ ''ExternalExamOfficeSchool makeLenses_ ''ExternalExamOfficeSchool
makeLenses_ ''ExternalExamStaff makeLenses_ ''ExternalExamStaff
makeLenses_ ''ExternalExamResult
-- makeClassy_ ''Load -- makeClassy_ ''Load