diff --git a/src/Database/Esqueleto/Utils.hs b/src/Database/Esqueleto/Utils.hs index 236463675..2d823de2b 100644 --- a/src/Database/Esqueleto/Utils.hs +++ b/src/Database/Esqueleto/Utils.hs @@ -430,6 +430,7 @@ bool onFalse onTrue val = E.case_ (E.else_ onFalse) -- called see greatest and least within postgresql +-- TODO: this is buggy! Both return always the first argument if any argument is NULL! max, min :: PersistField a => E.SqlExpr (E.Value a) -> E.SqlExpr (E.Value a) @@ -437,7 +438,7 @@ max, min :: PersistField a max a b = bool a b $ b E.>. a min a b = bool a b $ b E.<. a --- these alternatives for max/min ought to be more efficient +-- these alternatives for max/min ought to be more efficient; note that NULL is avoided by greatest/least greatest :: PersistField a => E.SqlExpr (E.Value a) -> E.SqlExpr (E.Value a) -> E.SqlExpr (E.Value a) greatest a b = E.unsafeSqlFunction "GREATEST" $ E.toArgList (a,b) diff --git a/src/Handler/Utils/Users.hs b/src/Handler/Utils/Users.hs index 0fe21a898..c94dda85b 100644 --- a/src/Handler/Utils/Users.hs +++ b/src/Handler/Utils/Users.hs @@ -273,6 +273,7 @@ data UserAssimilateExceptionReason | UserAssimilatePersonalisedSheetFileDifferentContent (Entity PersonalisedSheetFile) (Entity PersonalisedSheetFile) | UserAssimilateTutorialParticipantCollidingRegGroups (Entity TutorialParticipant) (Entity TutorialParticipant) | UserAssimilateCouldNotDetermineUserIdents + | UserAssimilateConflictingLmsQualifications (Set.Set QualificationId) deriving (Eq, Ord, Show, Generic, Typeable) assimilateUser :: UserId -- ^ @newUserId@ @@ -846,7 +847,21 @@ assimilateUser newUserId oldUserId = mapReaderT execWriterT $ do delete oldSFId in runConduit $ getStudyFeatures .| C.mapM_ upsertStudyFeatures - -- lms + -- Qualifications and ongoing LMS + -- LmsUser: insertSelectWithConflict impossible due to 2 simultaneous uniqueness constraints; UniqueLmsIdent requires proper update, prohibits insert and then delete + -- updateWhere [ LmsUserUser ==. oldUserId ] [ LmsUserUser =. newUserId ] -- might fail due to UniqueLmsQualficationUuser + oldLms <- selectList [ LmsUserUser ==. oldUserId ] [ Asc LmsUserQualification ] + newLms <- selectList [ LmsUserUser ==. newUserId ] [ Asc LmsUserQualification ] + let projQ = lmsUserQualification . entityVal + oldQs = Set.fromList (projQ <$> oldLms) + newQs = Set.fromList (projQ <$> newLms) + qConflicts = oldQs `Set.intersection` newQs + qResolvable = Set.fromList [ lmsUserQualification | Entity _ LmsUser{..} <- oldLms, isJust lmsUserEnded, lmsUserQualification `Set.member` qConflicts ] + qProblems = qConflicts `Set.difference` qResolvable + unless (Set.null qProblems) $ tellError $ UserAssimilateConflictingLmsQualifications qProblems + unless (Set.null qResolvable) $ deleteWhere [ LmsUserUser ==. oldUserId, LmsUserQualification <-. Set.toList qResolvable ] -- delete conflicting and finished LMS, which are still within auditDuration + updateWhere [ LmsUserUser ==. oldUserId ] [ LmsUserUser =. newUserId ] + updateWhere [ QualificationEditUser ==. oldUserId ] [ QualificationEditUser =. newUserId ] E.insertSelectWithConflict UniqueQualificationUser (E.from $ \qualificationUser -> do @@ -860,15 +875,52 @@ assimilateUser newUserId oldUserId = mapReaderT execWriterT $ do E.<&> (qualificationUser E.^. QualificationUserBlockedDue) ) (\current excluded -> - [ QualificationUserValidUntil E.=. combineWith current excluded E.max QualificationUserValidUntil - , QualificationUserLastRefresh E.=. combineWith current excluded E.max QualificationUserLastRefresh - , QualificationUserFirstHeld E.=. combineWith current excluded E.min QualificationUserFirstHeld - , QualificationUserBlockedDue E.=. combineWith current excluded E.max QualificationUserBlockedDue + [ QualificationUserValidUntil E.=. combineWith current excluded E.greatest QualificationUserValidUntil + , QualificationUserLastRefresh E.=. combineWith current excluded E.greatest QualificationUserLastRefresh + , QualificationUserFirstHeld E.=. combineWith current excluded E.least QualificationUserFirstHeld + , QualificationUserBlockedDue E.=. combineWith current excluded E.greatest QualificationUserBlockedDue -- Tested: PostgreSQL GREATEST/LEAST ignores NULL values ] ) - -- TODO: LmsUser! deleteWhere [ QualificationUserUser ==. oldUserId ] + -- Supervision is fully merged + E.insertSelectWithConflict + UniqueUserSupervisor + (E.from $ \userSupervisor -> do + E.where_ $ userSupervisor E.^. UserSupervisorSupervisor E.==. E.val oldUserId + return $ UserSupervisor + E.<# E.val newUserId + E.<&> (userSupervisor E.^. UserSupervisorUser) + E.<&> (userSupervisor E.^. UserSupervisorRerouteNotifications) + ) + (\current excluded -> [ UserSupervisorRerouteNotifications E.=. (current E.^. UserSupervisorRerouteNotifications E.||. excluded E.^. UserSupervisorRerouteNotifications) ] ) + deleteWhere [ UserSupervisorSupervisor ==. oldUserId] + + E.insertSelectWithConflict + UniqueUserSupervisor + (E.from $ \userSupervisor -> do + E.where_ $ userSupervisor E.^. UserSupervisorUser E.==. E.val oldUserId + return $ UserSupervisor + E.<# (userSupervisor E.^. UserSupervisorSupervisor) + E.<&> E.val newUserId + E.<&> (userSupervisor E.^. UserSupervisorRerouteNotifications) + ) + (\current excluded -> [ UserSupervisorRerouteNotifications E.=. (current E.^. UserSupervisorRerouteNotifications E.||. excluded E.^. UserSupervisorRerouteNotifications) ] ) + deleteWhere [ UserSupervisorUser ==. oldUserId] + + -- Companies, in conflict, keep the newUser-Company as is + E.insertSelectWithConflict + UniqueCompanyUser + (E.from $ \userCompany -> do + E.where_ $ userCompany E.^. UserCompanyUser E.==. E.val oldUserId + return $ UserCompany + E.<# (userCompany E.^. UserCompanyCompany) + E.<&> E.val newUserId + E.<&> (userCompany E.^. UserCompanySupervisor) + ) + (\current _excluded -> [ UserCompanySupervisor E.=. (current E.^. UserCompanySupervisor)] ) + deleteWhere [ UserCompanyUser ==. oldUserId] + userIdents <- E.select . E.from $ \user -> do E.where_ $ user E.^. UserId `E.in_` E.valList [newUserId, oldUserId] return ( user E.^. UserId