diff --git a/frontend/src/app.sass b/frontend/src/app.sass
index 9b842f8a2..b4acef15e 100644
--- a/frontend/src/app.sass
+++ b/frontend/src/app.sass
@@ -249,6 +249,13 @@ button,
box-shadow: 0 0 0 0.25rem rgba(50, 115, 220, 0.25)
outline: 0
+ .buttongroup > &
+ min-width: 0
+
+.buttongroup
+ display: grid
+ grid: min-content / auto-flow 1fr
+
input[type="submit"][disabled],
input[type="button"][disabled],
button[disabled],
diff --git a/messages/uniworx/de-de-formal.msg b/messages/uniworx/de-de-formal.msg
index d7f784150..ebff2b149 100644
--- a/messages/uniworx/de-de-formal.msg
+++ b/messages/uniworx/de-de-formal.msg
@@ -2285,6 +2285,8 @@ ExamAutoOccurrenceParticipantsAssigned num@Int64: Verteilungstabelle erfolgreich
TitleExamAutoOccurrence tid@TermId ssh@SchoolId csh@CourseShorthand examn@ExamName: #{tid} - #{ssh} - #{csh} #{examn}: Automatische Raum-/Terminverteilung
BtnExamAutoOccurrenceCalculate: Verteilungstabelle berechnen
BtnExamAutoOccurrenceAccept: Verteilung akzeptieren
+BtnExamAutoOccurrenceNudgeUp: +
+BtnExamAutoOccurrenceNudgeDown: -
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 345fe2d08..b26b66919 100644
--- a/messages/uniworx/en-eu.msg
+++ b/messages/uniworx/en-eu.msg
@@ -2284,6 +2284,8 @@ ExamAutoOccurrenceParticipantsAssigned num: Occurrence/room assignment rule save
TitleExamAutoOccurrence tid ssh csh examn: #{tid} - #{ssh} - #{csh} #{examn}: Automatic occurrence/room distribution
BtnExamAutoOccurrenceCalculate: Calculate assignment rules
BtnExamAutoOccurrenceAccept: Accept assignments
+BtnExamAutoOccurrenceNudgeUp: +
+BtnExamAutoOccurrenceNudgeDown: -
ExamRoomMappingSurname: Surnames starting with
ExamRoomMappingMatriculation: Matriculation numbers ending in
ExamRoomLoad: Utilisation
\ No newline at end of file
diff --git a/src/Handler/Exam/AutoOccurrence.hs b/src/Handler/Exam/AutoOccurrence.hs
index 925df22fd..222fd7896 100644
--- a/src/Handler/Exam/AutoOccurrence.hs
+++ b/src/Handler/Exam/AutoOccurrence.hs
@@ -18,7 +18,9 @@ import Database.Persist.Sql (updateWhereCount)
newtype ExamAutoOccurrenceCalculateForm = ExamAutoOccurrenceCalculateForm
{ eaofConfig :: ExamAutoOccurrenceConfig
} deriving stock (Eq, Ord, Read, Show, Generic, Typeable)
- deriving newtype (Default)
+ deriving newtype (Default, FromJSON, ToJSON)
+
+makeLenses_ ''ExamAutoOccurrenceCalculateForm
data ExamAutoOccurrenceAcceptForm = ExamAutoOccurrenceAcceptForm
{ eaofMapping :: Maybe (ExamOccurrenceMapping ExamOccurrenceId)
@@ -32,6 +34,7 @@ deriveJSON defaultOptions
data ExamAutoOccurrenceButton
= BtnExamAutoOccurrenceCalculate
| BtnExamAutoOccurrenceAccept
+ | BtnExamAutoOccurrenceNudgeUp | BtnExamAutoOccurrenceNudgeDown
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable)
instance Universe ExamAutoOccurrenceButton
instance Finite ExamAutoOccurrenceButton
@@ -40,6 +43,8 @@ nullaryPathPiece ''ExamAutoOccurrenceButton $ camelToPathPiece' 4
embedRenderMessage ''UniWorX ''ExamAutoOccurrenceButton id
instance Button UniWorX ExamAutoOccurrenceButton where
+ btnClasses BtnExamAutoOccurrenceNudgeUp = [BCIsButton]
+ btnClasses BtnExamAutoOccurrenceNudgeDown = [BCIsButton]
btnClasses _ = [BCIsButton, BCPrimary]
@@ -51,6 +56,23 @@ examAutoOccurrenceCalculateForm ExamAutoOccurrenceCalculateForm{ eaofConfig }
(set _eaocMinimizeRooms <$> apopt checkBoxField (fslI MsgExamAutoOccurrenceMinimizeRooms & setTooltip MsgExamAutoOccurrenceMinimizeRoomsTip) (Just $ eaofConfig ^. _eaocMinimizeRooms))
<*> pure def
+examAutoOccurrenceNudgeForm :: ExamOccurrenceId -> Maybe ExamAutoOccurrenceCalculateForm -> Form ExamAutoOccurrenceCalculateForm
+examAutoOccurrenceNudgeForm occId protoForm html = do
+ cID <- encrypt occId
+ (btnRes, wgt) <- identifyForm (FIDExamAutoOccurrenceNudge $ ciphertext cID) (buttonForm' [BtnExamAutoOccurrenceNudgeUp, BtnExamAutoOccurrenceNudgeDown]) html
+ oldDataRes <- globalPostParamField PostExamAutoOccurrencePrevious secretJsonField
+ oldDataId <- newIdent
+
+ let protoForm' = fromMaybe def $ oldDataRes <|> protoForm
+ genForm btn = protoForm' & _eaofConfig . _eaocNudge %~ Map.insertWith (+) occId n
+ where n = case btn of
+ BtnExamAutoOccurrenceNudgeUp -> 1
+ BtnExamAutoOccurrenceNudgeDown -> -1
+ _other -> 0
+ res = genForm <$> btnRes
+ oldDataView = fieldView (secretJsonField :: Field Handler _) oldDataId (toPathPiece PostExamAutoOccurrencePrevious) [] (Right . fromMaybe protoForm' $ formResult' res) False
+ return (res, wgt <> oldDataView)
+
examAutoOccurrenceAcceptForm :: Maybe ExamAutoOccurrenceAcceptForm -> Form ExamAutoOccurrenceAcceptForm
examAutoOccurrenceAcceptForm confirmData = identifyForm FIDExamAutoOccurrenceConfirm $ \html -> do
(confirmDataRes, confirmDataView) <- mreq secretJsonField "" confirmData
@@ -78,7 +100,14 @@ postEAutoOccurrenceR tid ssh csh examn = do
((calculateRes, _), _) <- runFormPost $ examAutoOccurrenceCalculateForm def
- calcResult <- formResultMaybe calculateRes $ \ExamAutoOccurrenceCalculateForm{..} -> runDB $ do
+ nudgeRes <- sequence . flip Map.fromSet (setOf (folded . _entityKey) occurrences) $ \occId ->
+ runFormPost $ examAutoOccurrenceNudgeForm occId (formResult' calculateRes)
+
+ let calculateRes' = asum $
+ [ calculateRes
+ ] ++ toListOf (folded . _1 . _1) nudgeRes
+
+ 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
@@ -114,6 +143,14 @@ postEAutoOccurrenceR tid ssh csh examn = do
addMessageI Success $ MsgExamAutoOccurrenceParticipantsAssigned assignedCount
redirect $ CExamR tid ssh csh examn EUsersR
+ let nudgeWgt = nudgeRes <&> \((_, nudgeView), nudgeEncoding) ->
+ wrapForm nudgeView def
+ { formAction = Just . SomeRoute $ CExamR tid ssh csh examn EAutoOccurrenceR
+ , formEncoding = nudgeEncoding
+ , formSubmit = FormNoSubmit
+ , formAttrs = [("class", "buttongroup")]
+ }
+
ExamAutoOccurrenceAcceptForm{..} <- maybe (redirect $ CExamR tid ssh csh examn EUsersR) return calcResult
let heading = MsgTitleExamAutoOccurrence tid ssh csh examn
diff --git a/src/Handler/Utils/Exam.hs b/src/Handler/Utils/Exam.hs
index 205e0f94c..129d11b7e 100644
--- a/src/Handler/Utils/Exam.hs
+++ b/src/Handler/Utils/Exam.hs
@@ -1,11 +1,13 @@
+{-# OPTIONS_GHC -fno-warn-deprecations #-}
+
module Handler.Utils.Exam
( fetchExamAux
, fetchExam, fetchExamId, fetchCourseIdExamId, fetchCourseIdExam
, examBonus, examBonusPossible, examBonusAchieved
, examResultBonus, examGrade
, ExamAutoOccurrenceConfig
- , eaocMinimizeRooms, eaocFinenessCost
- , _eaocMinimizeRooms, _eaocFinenessCost
+ , eaocMinimizeRooms, eaocFinenessCost, eaocNudge, eaocNudgeSize
+ , _eaocMinimizeRooms, _eaocFinenessCost, _eaocNudge, _eaocNudgeSize
, examAutoOccurrence
) where
@@ -192,15 +194,23 @@ examGrade Exam{..} mBonus (otoList -> results)
data ExamAutoOccurrenceConfig = ExamAutoOccurrenceConfig
{ eaocMinimizeRooms :: Bool
, eaocFinenessCost :: Rational -- ^ Cost factor incentivising shorter common prefixes on breaks between rooms
+ , eaocNudge :: Map ExamOccurrenceId Integer
+ , eaocNudgeSize :: Rational
} deriving (Eq, Ord, Read, Show, Generic, Typeable)
instance Default ExamAutoOccurrenceConfig where
def = ExamAutoOccurrenceConfig
{ eaocMinimizeRooms = False
- , eaocFinenessCost = 0.1
+ , eaocFinenessCost = 0.2
+ , eaocNudge = Map.empty
+ , eaocNudgeSize = 0.05
}
makeLenses_ ''ExamAutoOccurrenceConfig
+
+deriveJSON defaultOptions
+ { fieldLabelModifier = camelToPathPiece' 1
+ } ''ExamAutoOccurrenceConfig
examAutoOccurrence :: forall seed.
@@ -283,9 +293,10 @@ examAutoOccurrence (hash -> seed) rule ExamAutoOccurrenceConfig{..} occurrences
distribute :: forall wordId lineId cost.
_
- => [(wordId, Natural)]
- -> [(lineId, Natural)]
- -> (wordId -> wordId -> Extended Rational)
+ => [(wordId, Natural)] -- ^ Word sizes (in order)
+ -> [(lineId, Natural)] -- ^ Line sizes (in order)
+ -> (lineId -> Integer) -- ^ Nudge
+ -> (wordId -> wordId -> Extended Rational) -- ^ Break cost
-> 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
@@ -294,7 +305,7 @@ examAutoOccurrence (hash -> seed) rule ExamAutoOccurrenceConfig{..} occurrences
-- Return a cost scaled to item-size squared
--
-- See