diff --git a/ghci.sh b/ghci.sh
index ab5cf41bd..750d384b8 100755
--- a/ghci.sh
+++ b/ghci.sh
@@ -20,4 +20,4 @@ if [[ -d .stack-work-ghci ]]; then
trap move-back EXIT
fi
-stack ghci --flag uniworx:dev --flag uniworx:library-only ${@:-uniworx:lib}
+stack ghci --flag uniworx:dev --flag uniworx:library-only --ghci-options -fobject-code ${@:-uniworx:lib}
diff --git a/messages/uniworx/de-de-formal.msg b/messages/uniworx/de-de-formal.msg
index 81bbd36ff..f31a52272 100644
--- a/messages/uniworx/de-de-formal.msg
+++ b/messages/uniworx/de-de-formal.msg
@@ -1267,6 +1267,7 @@ BreadcrumbExternalExamGrades: Prüfungsleistungen
BreadcrumbExternalExamStaffInvite: Einladung zum Prüfer
BreadcrumbParticipantsList: Kursteilnehmerlisten
BreadcrumbParticipants: Kursteilnehmerliste
+BreadcrumbExamAutoOccurrence: Automatische Raumverteilung
ExternalExamEdit coursen@CourseName examn@ExamName: Bearbeiten: #{coursen}, #{examn}
ExternalExamGrades coursen@CourseName examn@ExamName: Prüfungsleistungen: #{coursen}, #{examn}
@@ -1574,7 +1575,7 @@ ExamBonusRoundNonPositive: Vielfaches, auf das gerundet werden soll, muss positi
ExamBonusRoundTip: Bonuspunkte werden kaufmännisch auf ein Vielfaches der angegeben Zahl gerundet.
ExamAutomaticOccurrenceAssignment: Termin- bzw. Raumzuteilung
-ExamAutomaticOccurrenceAssignmentTip: Sollen Prüfungsteilnehmer automatisch auf die zur Verfügung stehenden Räume bzw. Termine verteilt werden, sich selbstständig einen Raum bzw. Termin aussuchen dürfen oder manuell durch Kursverwalter zugeteilt werden? Manuelle Umverteilung bzw. vorheriges Festlegen von Zuteilungen einzelner Teilnehmer ist trotzdem möglich.
+ExamAutomaticOccurrenceAssignmentTip: Sollen Prüfungsteilnehmer automatisch auf die zur Verfügung stehenden Räume bzw. Termine verteilt werden, sich selbstständig einen Raum bzw. Termin aussuchen dürfen oder manuell durch Kursverwalter zugeteilt werden? Die automatische Verteilung muss von einem Kursverwalter ausgelöst werden und geschieht nicht mit Ablauf einer Frist o.Ä.. Manuelle Umverteilung bzw. vorheriges Festlegen von Zuteilungen einzelner Teilnehmer ist somit immer möglich.
ExamOccurrenceRule: Verfahren
ExamOccurrenceRuleParticipant: Termin- bzw. Raumzuteilungsverfahren
ExamRoomManual': Keine automatische bzw. selbstständige Zuteilung
@@ -2270,4 +2271,15 @@ ExternalExamUserMustBeStaff: Sie selbst müssen stets assoziierte Person sein, f
ExternalExamCourseExists: Der angegebene Kurs existiert im System. Prüfungen sollten daher direkt beim Kurs (statt extern) hinterlegt werden.
ExternalExamExists coursen@CourseName examn@ExamName: Prüfung „#{examn}“ für Kurs „#{coursen}“ existiert bereits.
ExternalExamCreated coursen@CourseName examn@ExamName: Prüfung „#{examn}“ für Kurs „#{coursen}“ erfolgreich angelegt.
-ExternalExamEdited coursen@CourseName examn@ExamName: Prüfung „#{examn}“ für Kurs „#{coursen}“ erfolgreich bearbeitet.
\ No newline at end of file
+ExternalExamEdited coursen@CourseName examn@ExamName: Prüfung „#{examn}“ für Kurs „#{coursen}“ erfolgreich bearbeitet.
+
+ExamAutoOccurrenceMinimizeRooms: Verwendete Räume minimieren
+ExamAutoOccurrenceMinimizeRoomsTip: Soll, für die Aufteilung, die Liste an Räumen zunächst reduziert werden, sodass nur so wenige Räume verwendet werden, wie nötig (größte zuerst)?
+ExamAutoOccurrenceOccurrencesChangedInFlight: Raumliste wurde verändert
+ExamAutoOccurrenceParticipantsAssigned num@Int64: Verteilungstabelle erfolgreich gespeichert und #{num} Teilnehmer zugewiesen
+TitleExamAutoOccurrence tid@TermId ssh@SchoolId csh@CourseShorthand examn@ExamName: #{tid}-#{ssh}-#{csh} #{examn}: Automatische Raumverteilung
+BtnExamAutoOccurrenceCalculate: Verteilung berechnen
+BtnExamAutoOccurrenceAccept: Verteilung akzeptieren
+ExamRoomMappingSurname: Nachnamen beginnend mit
+ExamRoomMappingMatriculation: Matrikelnummern endend in
+ExamRoomLoad: Auslastung
\ No newline at end of file
diff --git a/messages/uniworx/en-eu.msg b/messages/uniworx/en-eu.msg
index cfac175fd..1f697b94d 100644
--- a/messages/uniworx/en-eu.msg
+++ b/messages/uniworx/en-eu.msg
@@ -1572,7 +1572,7 @@ ExamBonusRoundNonPositive: Rounding multiple must be positive and greater than z
ExamBonusRoundTip: Bonus points are rounded commercially to a multiple of the given number
ExamAutomaticOccurrenceAssignment: Selection of occurrences/rooms for/by participants
-ExamAutomaticOccurrenceAssignmentTip: Should exam participants be distributed automatically among the configured occurrences/rooms, should they instead be permitted to autonomously choose an occurrence/a room, or should they be assigned to occurrences/rooms manually by course administrators? Manipulation of the distribution and manually assigning participants remains possible.
+ExamAutomaticOccurrenceAssignmentTip: Should exam participants be distributed automatically among the configured occurrences/rooms, should they instead be permitted to autonomously choose an occurrence/room, or should they be assigned to occurrences/rooms manually by course administrators? Automatic distribution needs to be triggered by a course administrator. It is not done automatically at a predefined time. Thus manipulation of the distribution and manually assigning participants remains possible.
ExamOccurrenceRule: Procedure
ExamOccurrenceRuleParticipant: Occurrence/room assignment procedure
ExamRoomManual': No automatic or autonomous assignment
diff --git a/models/exams.model b/models/exams.model
index 5baa6e711..2bdc42cda 100644
--- a/models/exams.model
+++ b/models/exams.model
@@ -4,6 +4,7 @@ Exam
gradingRule ExamGradingRule Maybe
bonusRule ExamBonusRule Maybe
occurrenceRule ExamOccurrenceRule
+ examOccurrenceMapping (ExamOccurrenceMapping ExamOccurrenceName) Maybe
visibleFrom UTCTime Maybe
registerFrom UTCTime Maybe
registerTo UTCTime Maybe
diff --git a/package.yaml b/package.yaml
index f8d437c20..86e2ff3c8 100644
--- a/package.yaml
+++ b/package.yaml
@@ -138,6 +138,7 @@ dependencies:
- prometheus-metrics-ghc
- wai-middleware-prometheus
- extended-reals
+ - rfc5051
other-extensions:
- GeneralizedNewtypeDeriving
@@ -182,6 +183,8 @@ default-extensions:
- DeriveGeneric
- DeriveLift
- DeriveFunctor
+ - DeriveFoldable
+ - DeriveTraversable
- DerivingStrategies
- DerivingVia
- DataKinds
diff --git a/routes b/routes
index 8bf60981c..52f9bad23 100644
--- a/routes
+++ b/routes
@@ -188,6 +188,7 @@
/register ERegisterR POST !timeANDcourse-registeredAND¬exam-registered !timeANDexam-registeredAND¬exam-result
/register/#ExamOccurrenceName ERegisterOccR POST !exam-occurrence-registrationANDtimeANDcapacityANDcourse-registeredAND¬exam-occurrence-registered !exam-occurrence-registrationANDtimeANDexam-occurrence-registeredAND¬exam-result
/grades EGradesR GET POST !exam-office
+ /assign-occurrences EAutoOccurrenceR POST
/apps CApplicationsR GET POST
!/apps/files CAppsFilesR GET
/apps/#CryptoFileNameCourseApplication CourseApplicationR:
diff --git a/src/Database/Persist/Class/Instances.hs b/src/Database/Persist/Class/Instances.hs
index 8fc9eb20b..193ea1f16 100644
--- a/src/Database/Persist/Class/Instances.hs
+++ b/src/Database/Persist/Class/Instances.hs
@@ -1,5 +1,5 @@
{-# OPTIONS_GHC -fno-warn-orphans #-}
-{-# LANGUAGE UndecidableInstances #-}
+{-# LANGUAGE UndecidableInstances, GeneralizedNewtypeDeriving #-}
module Database.Persist.Class.Instances
(
@@ -10,12 +10,15 @@ import ClassyPrelude
import Database.Persist.Class
import Database.Persist.Types (HaskellName, DBName, PersistValue)
import Database.Persist.Types.Instances ()
+import Database.Persist.Sql
import Data.Binary (Binary)
import qualified Data.Binary as Binary
import qualified Data.Map as Map
+import Data.Aeson (ToJSONKey, FromJSONKey)
+
instance PersistEntity record => Hashable (Key record) where
hashWithSalt s = hashWithSalt s . toPersistValue
@@ -34,3 +37,7 @@ uniqueToMap = fmap Map.fromList $ zip <$> persistUniqueToFieldNames <*> persistU
instance PersistEntity record => Eq (Unique record) where
(==) = (==) `on` uniqueToMap
+
+
+deriving newtype instance ToJSONKey (BackendKey SqlBackend)
+deriving newtype instance FromJSONKey (BackendKey SqlBackend)
diff --git a/src/Foundation.hs b/src/Foundation.hs
index a4d40b60e..184cedf7c 100644
--- a/src/Foundation.hs
+++ b/src/Foundation.hs
@@ -1923,6 +1923,7 @@ instance YesodBreadcrumbs UniWorX where
EInviteR -> i18nCrumb MsgBreadcrumbExamParticipantInvite . Just $ CExamR tid ssh csh examn EShowR
ERegisterR -> i18nCrumb MsgBreadcrumbExamRegister . Just $ CExamR tid ssh csh examn EShowR
ERegisterOccR _occn -> i18nCrumb MsgBreadcrumbExamRegister . Just $ CExamR tid ssh csh examn EShowR
+ EAutoOccurrenceR -> i18nCrumb MsgBreadcrumbExamAutoOccurrence . Just $ CExamR tid ssh csh examn EUsersR
breadcrumb (CourseR tid ssh csh (TutorialR tutn sRoute)) = case sRoute of
TUsersR -> maybeT (i18nCrumb MsgBreadcrumbTutorial . Just $ CourseR tid ssh csh CTutorialListR) $ do
diff --git a/src/Handler/Exam.hs b/src/Handler/Exam.hs
index 6580c90f4..ca916130c 100644
--- a/src/Handler/Exam.hs
+++ b/src/Handler/Exam.hs
@@ -11,3 +11,4 @@ import Handler.Exam.Edit as Handler.Exam
import Handler.Exam.Show as Handler.Exam
import Handler.Exam.Users as Handler.Exam
import Handler.Exam.AddUser as Handler.Exam
+import Handler.Exam.AutoOccurrence as Handler.Exam
diff --git a/src/Handler/Exam/AutoOccurrence.hs b/src/Handler/Exam/AutoOccurrence.hs
new file mode 100644
index 000000000..908449351
--- /dev/null
+++ b/src/Handler/Exam/AutoOccurrence.hs
@@ -0,0 +1,137 @@
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+
+module Handler.Exam.AutoOccurrence
+ ( examAutoOccurrenceCalculateWidget
+ , postEAutoOccurrenceR
+ ) where
+
+import Import
+import Handler.Utils
+import Handler.Utils.Exam
+
+import qualified Data.Map as Map
+
+import qualified Database.Esqueleto as E
+import Database.Persist.Sql (updateWhereCount)
+
+
+newtype ExamAutoOccurrenceCalculateForm = ExamAutoOccurrenceCalculateForm
+ { eaofConfig :: ExamAutoOccurrenceConfig
+ } deriving stock (Eq, Ord, Read, Show, Generic, Typeable)
+ deriving newtype (Default)
+
+data ExamAutoOccurrenceAcceptForm = ExamAutoOccurrenceAcceptForm
+ { eaofMapping :: Maybe (ExamOccurrenceMapping ExamOccurrenceId)
+ , eaofAssignment :: Map UserId (Maybe ExamOccurrenceId)
+ } deriving (Eq, Ord, Read, Show, Generic, Typeable)
+
+deriveJSON defaultOptions
+ { fieldLabelModifier = camelToPathPiece' 1
+ } ''ExamAutoOccurrenceAcceptForm
+
+data ExamAutoOccurrenceButton
+ = BtnExamAutoOccurrenceCalculate
+ | BtnExamAutoOccurrenceAccept
+ deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable)
+instance Universe ExamAutoOccurrenceButton
+instance Finite ExamAutoOccurrenceButton
+
+nullaryPathPiece ''ExamAutoOccurrenceButton $ camelToPathPiece' 4
+
+embedRenderMessage ''UniWorX ''ExamAutoOccurrenceButton id
+instance Button UniWorX ExamAutoOccurrenceButton where
+ btnClasses _ = [BCIsButton, BCPrimary]
+
+
+examAutoOccurrenceCalculateForm :: ExamAutoOccurrenceCalculateForm -> Form ExamAutoOccurrenceCalculateForm
+examAutoOccurrenceCalculateForm ExamAutoOccurrenceCalculateForm{ eaofConfig }
+ = identifyForm FIDExamAutoOccurrenceCalculate . renderAForm FormStandard $ ExamAutoOccurrenceCalculateForm <$> eaocForm
+ where
+ eaocForm =
+ (set _eaocMinimizeRooms <$> apopt checkBoxField (fslI MsgExamAutoOccurrenceMinimizeRooms & setTooltip MsgExamAutoOccurrenceMinimizeRoomsTip) (Just $ eaofConfig ^. _eaocMinimizeRooms))
+ <*> pure def
+
+examAutoOccurrenceAcceptForm :: Maybe ExamAutoOccurrenceAcceptForm -> Form ExamAutoOccurrenceAcceptForm
+examAutoOccurrenceAcceptForm confirmData = identifyForm FIDExamAutoOccurrenceConfirm $ \html -> do
+ (confirmDataRes, confirmDataView) <- mreq secretJsonField "" confirmData
+ (acceptRes, acceptView) <- buttonForm' [BtnExamAutoOccurrenceAccept] mempty
+ return (acceptRes *> confirmDataRes, toWidget html <> fvInput confirmDataView <> acceptView)
+
+
+examAutoOccurrenceCalculateWidget :: TermId -> SchoolId -> CourseShorthand -> ExamName -> Widget
+examAutoOccurrenceCalculateWidget tid ssh csh examn = do
+ (formView, formEncoding) <- liftHandler . generateFormPost $ examAutoOccurrenceCalculateForm def
+
+ wrapForm' BtnExamAutoOccurrenceCalculate formView def
+ { formAction = Just . SomeRoute $ CExamR tid ssh csh examn EAutoOccurrenceR
+ , formEncoding
+ }
+
+
+postEAutoOccurrenceR :: TermId -> SchoolId -> CourseShorthand -> ExamName -> Handler Html
+postEAutoOccurrenceR tid ssh csh examn = do
+ (Entity eId Exam{ examOccurrenceRule }, occurrences) <- runDB $ do
+ exam@(Entity eId _) <- fetchExam tid ssh csh examn
+ occurrences <- selectList [ ExamOccurrenceExam ==. eId ] [ Asc ExamOccurrenceName ]
+ return (exam, occurrences)
+
+
+ ((calculateRes, _), _) <- runFormPost $ examAutoOccurrenceCalculateForm def
+
+ calcResult <- formResultMaybe calculateRes $ \ExamAutoOccurrenceCalculateForm{..} -> runDB $ do
+ participants <- E.select . E.from $ \(registration `E.InnerJoin` user) -> do
+ E.on $ registration E.^. ExamRegistrationUser E.==. user E.^. UserId
+ E.where_ $ registration E.^. ExamRegistrationExam E.==. E.val eId
+ return (user, registration)
+ let participants' = Map.fromList $ do
+ (Entity uid userRec, Entity _ ExamRegistration{..}) <- participants
+ return (uid, (userRec, examRegistrationOccurrence))
+ occurrences' = Map.fromList $ map (\(Entity eoId ExamOccurrence{..}) -> (eoId, examOccurrenceCapacity)) occurrences
+ (eaofMapping, eaofAssignment) = examAutoOccurrence eId examOccurrenceRule eaofConfig occurrences' participants'
+ return $ Just ExamAutoOccurrenceAcceptForm{..}
+
+ ((confirmRes, confirmView), confirmEncoding) <- runFormPost $ examAutoOccurrenceAcceptForm calcResult
+ let confirmWidget = wrapForm confirmView def
+ { formAction = Just . SomeRoute $ CExamR tid ssh csh examn EAutoOccurrenceR
+ , formEncoding = confirmEncoding
+ , formSubmit = FormNoSubmit
+ }
+
+ formResult confirmRes $ \ExamAutoOccurrenceAcceptForm{..} -> do
+ Sum assignedCount <- runDB $ do
+ let eaofMapping'' :: Maybe (Maybe (ExamOccurrenceMapping ExamOccurrenceName))
+ eaofMapping'' = (<$> eaofMapping) . traverseExamOccurrenceMapping $ \eoId -> case filter ((== eoId) . entityKey) occurrences of
+ [Entity _ ExamOccurrence{..}] -> Just examOccurrenceName
+ _other -> Nothing
+ eaofMapping' <- case eaofMapping'' of
+ Nothing -> return Nothing
+ Just Nothing -> invalidArgsI [MsgExamAutoOccurrenceOccurrencesChangedInFlight]
+ Just (Just x ) -> return $ Just x
+ update eId [ ExamExamOccurrenceMapping =. eaofMapping' ]
+ fmap fold . iforM eaofAssignment $ \pid occ -> case occ of
+ Just _ -> Sum <$> updateWhereCount [ ExamRegistrationExam ==. eId, ExamRegistrationUser ==. pid, ExamRegistrationOccurrence ==. Nothing ] [ ExamRegistrationOccurrence =. occ ]
+ Nothing -> return mempty
+ addMessageI Success $ MsgExamAutoOccurrenceParticipantsAssigned assignedCount
+ redirect $ CExamR tid ssh csh examn EUsersR
+
+ ExamAutoOccurrenceAcceptForm{..} <- maybe (redirect $ CExamR tid ssh csh examn EUsersR) return calcResult
+
+ let heading = MsgTitleExamAutoOccurrence tid ssh csh examn
+ mappingWgt
+ = let occLoads :: Map ExamOccurrenceId Natural
+ occLoads = Map.fromListWith (+) . mapMaybe (\(_, mOcc) -> (, 1) <$> mOcc) $ Map.toList eaofAssignment
+
+ occLoad = fromMaybe 0 . flip Map.lookup occLoads
+
+ occMappingRule = examOccurrenceMappingRule <$> eaofMapping
+
+ loadProp curr max'
+ | max' /= 0 = MsgProportion (toMessage curr) (toMessage max') (toRational curr / toRational max')
+ | otherwise = MsgProportionNoRatio (toMessage curr) (toMessage max')
+
+ occMapping occId = examOccurrenceMappingDescriptionWidget <$> occMappingRule <*> (Map.lookup occId . examOccurrenceMappingMapping =<< eaofMapping)
+ in $(widgetFile "widgets/exam-occurrence-mapping")
+
+ siteLayoutMsg heading $ do
+ setTitleI heading
+ $(widgetFile "exam/auto-occurrence-confirm")
diff --git a/src/Handler/Exam/Edit.hs b/src/Handler/Exam/Edit.hs
index 52d90559f..ae40a86c3 100644
--- a/src/Handler/Exam/Edit.hs
+++ b/src/Handler/Exam/Edit.hs
@@ -35,6 +35,7 @@ postEEditR tid ssh csh examn = do
, examGradingRule = efGradingRule
, examBonusRule = efBonusRule
, examOccurrenceRule = efOccurrenceRule
+ , examExamOccurrenceMapping = examExamOccurrenceMapping oldExam
, examVisibleFrom = efVisibleFrom
, examRegisterFrom = efRegisterFrom
, examRegisterTo = efRegisterTo
diff --git a/src/Handler/Exam/New.hs b/src/Handler/Exam/New.hs
index d4e6582a7..7cbfdb32d 100644
--- a/src/Handler/Exam/New.hs
+++ b/src/Handler/Exam/New.hs
@@ -32,6 +32,7 @@ postCExamNewR tid ssh csh = do
, examGradingRule = efGradingRule
, examBonusRule = efBonusRule
, examOccurrenceRule = efOccurrenceRule
+ , examExamOccurrenceMapping = Nothing
, examVisibleFrom = efVisibleFrom
, examRegisterFrom = efRegisterFrom
, examRegisterTo = efRegisterTo
diff --git a/src/Handler/Exam/Show.hs b/src/Handler/Exam/Show.hs
index e072b9e71..1bb67c713 100644
--- a/src/Handler/Exam/Show.hs
+++ b/src/Handler/Exam/Show.hs
@@ -145,6 +145,7 @@ getEShowR tid ssh csh examn = do
showAchievedPoints = not $ null results
showOccurrenceRegisterColumn = occurrenceAssignmentsShown || (mayRegister && examOccurrenceRule == ExamRoomFifo)
markUnregisteredOccurrences mOcc = occurrenceAssignmentsShown && hasRegistration && isn't _Just (registerWidget mOcc)
+ showOccurrenceMappingColumn = examOccurrenceRuleAutomatic examOccurrenceRule && occurrenceAssignmentsShown && is _Just examExamOccurrenceMapping
let heading = prependCourseTitle tid ssh csh $ CI.original examName
@@ -161,4 +162,7 @@ getEShowR tid ssh csh examn = do
examBonusW :: ExamBonusRule -> Widget
examBonusW bonusRule = $(widgetFile "widgets/bonusRule")
+
+ occurrenceMapping :: ExamOccurrenceName -> Maybe Widget
+ occurrenceMapping occName = examOccurrenceMappingDescriptionWidget <$> fmap examOccurrenceMappingRule examExamOccurrenceMapping <*> (fmap examOccurrenceMappingMapping examExamOccurrenceMapping >>= Map.lookup occName)
$(widgetFile "exam-show")
diff --git a/src/Handler/Exam/Users.hs b/src/Handler/Exam/Users.hs
index 39624ab04..eee9a53b0 100644
--- a/src/Handler/Exam/Users.hs
+++ b/src/Handler/Exam/Users.hs
@@ -11,6 +11,8 @@ import Handler.Utils.Exam
import Handler.Utils.Users
import Handler.Utils.Csv
+import Handler.Exam.AutoOccurrence (examAutoOccurrenceCalculateWidget)
+
import Handler.ExamOffice.Exam (examCloseWidget)
import qualified Database.Esqueleto as E
@@ -390,7 +392,7 @@ embedRenderMessage ''UniWorX ''ExamUserCsvException id
getEUsersR, postEUsersR :: TermId -> SchoolId -> CourseShorthand -> ExamName -> Handler Html
getEUsersR = postEUsersR
postEUsersR tid ssh csh examn = do
- (((Any computedValues, registrationResult), examUsersTable), Entity eId examVal, bonus) <- runDB $ do
+ (((Any computedValues, registrationResult), examUsersTable), Entity eId examVal@Exam{..}, bonus) <- runDB $ do
exam@(Entity eid examVal@Exam{..}) <- fetchExam tid ssh csh examn
examParts <- selectList [ExamPartExam ==. eid] [Asc ExamPartName]
bonus <- examBonus exam
diff --git a/src/Handler/Utils/Exam.hs b/src/Handler/Utils/Exam.hs
index f4d1d32d9..9ccb492d4 100644
--- a/src/Handler/Utils/Exam.hs
+++ b/src/Handler/Utils/Exam.hs
@@ -1,12 +1,17 @@
+{-# OPTIONS_GHC -fno-warn-deprecations #-}
+
module Handler.Utils.Exam
( fetchExamAux
, fetchExam, fetchExamId, fetchCourseIdExamId, fetchCourseIdExam
, examBonus, examBonusPossible, examBonusAchieved
, examResultBonus, examGrade
+ , ExamAutoOccurrenceConfig
+ , eaocMinimizeRooms, eaocFinenessCost
+ , _eaocMinimizeRooms, _eaocFinenessCost
, examAutoOccurrence
) where
-import Import.NoFoundation hiding (distribute)
+import Import.NoFoundation
import Database.Persist.Sql (SqlBackendCanRead)
import qualified Database.Esqueleto as E
@@ -26,7 +31,7 @@ import qualified Data.CaseInsensitive as CI
import Control.Monad.Trans.Random.Lazy (evalRand)
import System.Random (mkStdGen)
-import Control.Monad.Random.Class (weightedMay)
+import Control.Monad.Random.Class (weighted)
import Control.Monad.ST (ST, runST)
import Data.Array (Array)
@@ -40,9 +45,10 @@ import qualified Data.List as List
import Data.ExtendedReal
-import qualified Data.Text as Text
import qualified Data.Char as Char
+import qualified Data.RFC5051 as RFC5051
+
fetchExamAux :: ( SqlBackendCanRead backend
, E.SqlSelect b a
@@ -184,17 +190,32 @@ examGrade Exam{..} mBonus (otoList -> results)
where
lowerBounds :: [(ExamGrade, Points)]
lowerBounds = zip [Grade40, Grade37 ..] examGradingKey'
+
+data ExamAutoOccurrenceConfig = ExamAutoOccurrenceConfig
+ { eaocMinimizeRooms :: Bool
+ , eaocFinenessCost :: Rational -- ^ Cost factor incentivising shorter common prefixes on breaks between rooms
+ } deriving (Eq, Ord, Read, Show, Generic, Typeable)
+
+instance Default ExamAutoOccurrenceConfig where
+ def = ExamAutoOccurrenceConfig
+ { eaocMinimizeRooms = False
+ , eaocFinenessCost = 0.2
+ }
+
+makeLenses_ ''ExamAutoOccurrenceConfig
examAutoOccurrence :: forall seed.
Hashable seed
=> seed
-> ExamOccurrenceRule
+ -> ExamAutoOccurrenceConfig
-> Map ExamOccurrenceId Natural
-> Map UserId (User, Maybe ExamOccurrenceId)
-> (Maybe (ExamOccurrenceMapping ExamOccurrenceId), Map UserId (Maybe ExamOccurrenceId))
-examAutoOccurrence (hash -> seed) rule occurrences users
+examAutoOccurrence (hash -> seed) rule ExamAutoOccurrenceConfig{..} occurrences users
| sum occurrences < usersCount
+ || sum occurrences <= 0
|| Map.null users
= nullResult
| otherwise
@@ -203,8 +224,8 @@ examAutoOccurrence (hash -> seed) rule occurrences users
-> ( Nothing
, flip Map.mapWithKey users $ \uid (_, mOcc)
-> let randomOcc = flip evalRand (mkStdGen $ hashWithSalt seed uid) $
- weightedMay $ over _2 fromIntegral <$> occurrences'
- in mOcc <|> randomOcc
+ weighted $ over _2 fromIntegral <$> occurrences'
+ in Just $ fromMaybe randomOcc mOcc
)
_ | Just (postprocess -> (resMapping, result)) <- bestOption
-> ( Just $ ExamOccurrenceMapping rule resMapping
@@ -221,38 +242,21 @@ examAutoOccurrence (hash -> seed) rule occurrences users
users' = case rule of
ExamRoomSurname
-> Map.fromListWith Set.union
- [ (map CI.mk $ unpack userSurname', Set.singleton uid)
+ [ (map CI.mk $ unpack userSurname, Set.singleton uid)
| (uid, (User{..}, Nothing)) <- Map.toList users
- , let userSurname' = Text.filter Char.isLetter userSurname
- , not $ null userSurname'
+ , not $ null userSurname
]
ExamRoomMatriculation
-> let matrUsers
= Map.fromListWith Set.union
[ (map CI.mk $ unpack matriculation', Set.singleton uid)
| (uid, (User{..}, Nothing)) <- Map.toList users
- , let Just matriculation' = Text.filter Char.isDigit <$> userMatrikelnummer
+ , let Just matriculation' = userMatrikelnummer
, not $ null matriculation'
]
in Map.mapKeysWith Set.union (take . F.minimum . Set.map length $ Map.keysSet matrUsers) matrUsers
_ -> Map.singleton [] $ Map.keysSet users
- usersGroups :: Natural -- ^ fineness
- -> Map [CI Char] (Set UserId)
- -- ^ Partition users into monotonously finer
- usersGroups (fromIntegral -> c) = Map.mapKeysWith Set.union restr users'
- where
- restr = case rule of
- ExamRoomSurname
- -> take c
- ExamRoomMatriculation
- -> reverse . take c . reverse
- _other
- -> id
-
- maximumFineness :: Natural
- -- ^ Fineness at which `usersGroups` becomes constant
- maximumFineness = fromIntegral . F.maximum . Set.map length $ Map.keysSet users'
occurrences' :: [(ExamOccurrenceId, Natural)]
-- ^ Minimise number of occurrences used
@@ -262,6 +266,8 @@ examAutoOccurrence (hash -> seed) rule occurrences users
-- If a single occurrence can accomodate all participants, pick the one with
-- the least capacity
occurrences'
+ | not eaocMinimizeRooms
+ = Map.toList occurrences
| Just largeEnoughs <- fromNullable . filter ((>= usersCount) . view _2) $ Map.toList occurrences
= pure $ minimumBy (comparing $ view _2) largeEnoughs
| otherwise
@@ -278,24 +284,12 @@ examAutoOccurrence (hash -> seed) rule occurrences users
, occ : accOccs
)
- largestOccurrence :: Num a => a
- largestOccurrence = fromIntegral . maximum . mapNonNull (view _2) $ impureNonNull occurrences'
-
- finenessCost :: Natural -> Natural
- finenessCost x = round (finenessConst * largestOccurrence) * fromIntegral (length occurrences') * x * x
- where
- finenessConst :: Rational
- -- ^ Cost (scaled to proportion of occurrence) of having higher fineness
- finenessConst = 1 % 5 -- TODO: tweak
-
-
distribute :: forall wordId lineId cost.
- ( Num cost
- , Ord wordId, Ord lineId
- )
+ _
=> [(wordId, Natural)]
-> [(lineId, Natural)]
- -> Maybe (cost, Map lineId (Set wordId))
+ -> (wordId -> wordId -> Extended Rational)
+ -> Maybe (cost, [(lineId, [wordId])])
-- ^ Distribute the given items (@wordId@s) with associated size in
-- contiguous blocks into the given buckets (@lineId@s) such that they are
-- filled as evenly as possible (proportionally)
@@ -303,8 +297,8 @@ examAutoOccurrence (hash -> seed) rule occurrences users
-- Return a cost scaled to item-size squared
--
-- See
| + _{MsgExamRoomName} + | + _{MsgExamRoomLoad} + $maybe rule <- occMappingRule + $case rule + $of ExamRoomSurname + | + _{MsgExamRoomMappingSurname} + $of ExamRoomMatriculation + | + _{MsgExamRoomMappingMatriculation} + $of _ + | + | + _{MsgExamRoom} + | + _{MsgExamRoomTime} + | + _{MsgExamRoomDescription} + |
|---|---|---|---|---|---|---|---|
| + _{examOccurrenceName} + | + _{loadProp (occLoad occId) examOccurrenceCapacity} + $maybe mappingWgt <- occMapping occId + | + ^{mappingWgt} + | + #{examOccurrenceRoom} + | + ^{formatTimeRangeW SelFormatDateTime examOccurrenceStart examOccurrenceEnd} + | + $maybe desc <- examOccurrenceDescription + #{desc} diff --git a/test/Database.hs b/test/Database.hs index 8335ef6ac..9038f14cb 100755 --- a/test/Database.hs +++ b/test/Database.hs @@ -505,6 +505,7 @@ fillDb = do , examGradingRule = Nothing , examBonusRule = Nothing , examOccurrenceRule = ExamRoomManual + , examExamOccurrenceMapping = Nothing , examVisibleFrom = Just now , examRegisterFrom = Just now , examRegisterTo = Just $ addUTCTime (14 * nominalDay) now |