feat(workflows): node messages

This commit is contained in:
Gregor Kleen 2020-10-28 17:16:42 +01:00
parent 5b897c7a42
commit 6a7a892c74
7 changed files with 84 additions and 29 deletions

View File

@ -2,6 +2,7 @@ module Handler.Utils.Workflow.Workflow
( ensureScope ( ensureScope
, followEdge , followEdge
, followAutomaticEdges, WorkflowAutomaticEdgeException(..) , followAutomaticEdges, WorkflowAutomaticEdgeException(..)
, checkWorkflowRestriction
) where ) where
import Import import Import
@ -68,16 +69,22 @@ followAutomaticEdges WorkflowGraph{..} = go []
(nodeLbl, WGN{..}) <- Map.toList wgNodes (nodeLbl, WGN{..}) <- Map.toList wgNodes
(edgeLbl, WorkflowGraphEdgeAutomatic{..}) <- Map.toList wgnEdges (edgeLbl, WorkflowGraphEdgeAutomatic{..}) <- Map.toList wgnEdges
guard $ wgeSource == cState guard $ wgeSource == cState
whenIsJust wgeRestriction $ guard . checkRestriction whenIsJust wgeRestriction $ guard . checkWorkflowRestriction history
return (edgeLbl, nodeLbl) return (edgeLbl, nodeLbl)
checkRestriction :: PredDNF WorkflowGraphEdgeAutomaticRestriction -> Bool
checkRestriction dnf = maybe False (ofoldr1 (||)) . fromNullable $ map evalConj dnf'
where
evalConj = maybe True (ofoldr1 (&&)) . fromNullable . map evalPred
evalPred PLVariable{ plVar = WorkflowGraphEdgeAutomaticRestrictionPayloadFilled{..} } = wgearPayloadFilled `Set.member` filledPayloads
evalPred PLNegated{ plVar = WorkflowGraphEdgeAutomaticRestrictionPayloadFilled{..} } = wgearPayloadFilled `Set.notMember` filledPayloads
evalPred PLVariable{ plVar = WorkflowGraphEdgeAutomaticRestrictionPreviousNode{..} } = wgearPreviousNode == cState
evalPred PLNegated{ plVar = WorkflowGraphEdgeAutomaticRestrictionPreviousNode{..} } = wgearPreviousNode /= cState
dnf' = map (Set.toList . toNullable) . Set.toList $ dnfTerms dnf
filledPayloads = Map.keysSet . Map.filter (not . Set.null) $ workflowStateCurrentPayloads history filledPayloads = Map.keysSet . Map.filter (not . Set.null) $ workflowStateCurrentPayloads history
edgeDecisionInput = (cState, filledPayloads) edgeDecisionInput = (cState, filledPayloads)
checkWorkflowRestriction :: WorkflowState FileReference UserId
-> PredDNF WorkflowGraphRestriction
-> Bool
checkWorkflowRestriction history dnf = maybe False (ofoldr1 (||)) . fromNullable $ map evalConj dnf'
where
evalConj = maybe True (ofoldr1 (&&)) . fromNullable . map evalPred
evalPred PLVariable{ plVar = WorkflowGraphRestrictionPayloadFilled{..} } = wgrPayloadFilled `Set.member` filledPayloads
evalPred PLNegated{ plVar = WorkflowGraphRestrictionPayloadFilled{..} } = wgrPayloadFilled `Set.notMember` filledPayloads
evalPred PLVariable{ plVar = WorkflowGraphRestrictionPreviousNode{..} } = wgrPreviousNode == cState
evalPred PLNegated{ plVar = WorkflowGraphRestrictionPreviousNode{..} } = wgrPreviousNode /= cState
dnf' = map (Set.toList . toNullable) . Set.toList $ dnfTerms dnf
filledPayloads = Map.keysSet . Map.filter (not . Set.null) $ workflowStateCurrentPayloads history
cState = wpTo $ last history

View File

@ -18,6 +18,7 @@ import Handler.Utils.Workflow.Workflow
import qualified Data.Map as Map import qualified Data.Map as Map
import qualified Data.Set as Set import qualified Data.Set as Set
import qualified Data.Sequence as Seq
import qualified Control.Monad.State.Class as State import qualified Control.Monad.State.Class as State
import Control.Monad.Trans.RWS.Strict (RWST, execRWST) import Control.Monad.Trans.RWS.Strict (RWST, execRWST)
@ -51,6 +52,7 @@ data WorkflowHistoryItem = WorkflowHistoryItem
data WorkflowCurrentState = WorkflowCurrentState data WorkflowCurrentState = WorkflowCurrentState
{ wcsState :: Maybe Text { wcsState :: Maybe Text
, wcsMessages :: Set Message
, wcsPayload :: [(Text, ([WorkflowFieldPayloadW Void (Maybe (Entity User))], Maybe (Route UniWorX)))] , wcsPayload :: [(Text, ([WorkflowFieldPayloadW Void (Maybe (Entity User))], Maybe (Route UniWorX)))]
} }
@ -73,11 +75,12 @@ workflowR wwId = do
rScope <- maybeT notFound . toRouteWorkflowScope $ _DBWorkflowScope # workflowWorkflowScope rScope <- maybeT notFound . toRouteWorkflowScope $ _DBWorkflowScope # workflowWorkflowScope
mEdgeForm <- workflowEdgeForm (Right wwId) Nothing mEdgeForm <- workflowEdgeForm (Right wwId) Nothing
let canonRoute = _WorkflowScopeRoute # (rScope, WorkflowWorkflowR cID WWWorkflowR) let canonRoute = _WorkflowScopeRoute # (rScope, WorkflowWorkflowR cID WWWorkflowR)
wGraph = _DBWorkflowGraph # workflowWorkflowGraph
mEdge <- for mEdgeForm $ \edgeForm -> do mEdge <- for mEdgeForm $ \edgeForm -> do
((edgeRes, edgeView), edgeEnc) <- liftHandler . runFormPost $ renderAForm FormStandard edgeForm ((edgeRes, edgeView), edgeEnc) <- liftHandler . runFormPost $ renderAForm FormStandard edgeForm
edgeAct <- formResultMaybe edgeRes $ \edgeRes' -> do edgeAct <- formResultMaybe edgeRes $ \edgeRes' -> do
nState <- followEdge (_DBWorkflowGraph # workflowWorkflowGraph) edgeRes' . Just $ _DBWorkflowState # workflowWorkflowState nState <- followEdge wGraph edgeRes' . Just $ _DBWorkflowState # workflowWorkflowState
update wwId [ WorkflowWorkflowState =. view _DBWorkflowState nState ] update wwId [ WorkflowWorkflowState =. view _DBWorkflowState nState ]
@ -95,10 +98,10 @@ workflowR wwId = do
) )
=> WorkflowStateIndex => WorkflowStateIndex
-> Maybe WorkflowGraphNodeLabel -> Maybe WorkflowGraphNodeLabel
-> Map WorkflowPayloadLabel (Set (WorkflowFieldPayloadW FileReference UserId)) -> [WorkflowAction FileReference UserId]
-> WorkflowAction FileReference UserId -> WorkflowAction FileReference UserId
-> RWST () (Maybe (Last WorkflowCurrentState), [WorkflowHistoryItem]) (Map WorkflowPayloadLabel (Set (WorkflowFieldPayloadW FileReference UserId))) (SqlPersistT m) () -> RWST () (Maybe (Last WorkflowCurrentState), [WorkflowHistoryItem]) (Map WorkflowPayloadLabel (Set (WorkflowFieldPayloadW FileReference UserId))) (SqlPersistT m) ()
go stIx wpFrom currentPayload act@WorkflowAction{..} = maybeT (return ()) $ do go stIx wpFrom history@(workflowStateCurrentPayloads -> currentPayload) act@WorkflowAction{..} = maybeT (return ()) $ do
mAuthId <- maybeAuthId mAuthId <- maybeAuthId
guardM . lift . lift . hoist liftHandler $ mayViewWorkflowAction mAuthId wwId act guardM . lift . lift . hoist liftHandler $ mayViewWorkflowAction mAuthId wwId act
@ -183,17 +186,28 @@ workflowR wwId = do
whiPayloadChanges <- renderPayload payloadChanges whiPayloadChanges <- renderPayload payloadChanges
wcsPayload <- renderPayload currentPayload wcsPayload <- renderPayload currentPayload
wcsMessages <- do
let msgs = maybe Set.empty wgnMessages $ Map.lookup wpTo wgNodes
flip foldMapM msgs $ \WorkflowNodeMessage{..} -> lift . maybeT (return Set.empty) . fmap Set.singleton $ do
guardM $ anyM (otoList wnmViewers) hasWorkflowRole'
history' <- hoistMaybe . fromNullable $ Seq.fromList history
whenIsJust wnmRestriction $ guard . checkWorkflowRestriction history'
let messageStatus = wnmStatus
messageIcon = Nothing
messageContent <- selectLanguageI18n wnmContent
return Message{..}
tell ( Just $ Last WorkflowCurrentState{..} tell ( Just $ Last WorkflowCurrentState{..}
, pure WorkflowHistoryItem{..} , pure WorkflowHistoryItem{..}
) )
WorkflowGraph{..} = _DBWorkflowGraph # workflowWorkflowGraph WorkflowGraph{..} = wGraph
wState = otoList $ review _DBWorkflowState workflowWorkflowState wState = otoList $ review _DBWorkflowState workflowWorkflowState
in fmap (over _2 (sortOn (Down . whiTime) . reverse) . view _2) . (\act -> execRWST act () Map.empty) $ sequence_ in fmap (over _2 (sortOn (Down . whiTime) . reverse) . view _2) . (\act -> execRWST act () Map.empty) $ sequence_
[ go stIx fromSt payload act [ go stIx fromSt payload act
| fromSt <- Nothing : map (Just . wpTo) wState | fromSt <- Nothing : map (Just . wpTo) wState
| act <- wState | act <- wState
| stIx <- [minBound..] | stIx <- [minBound..]
| payload <- map workflowStateCurrentPayloads . tailEx $ inits wState | payload <- tailEx $ inits wState
] ]
return (mEdge, rScope, (workflowState, workflowHistory)) return (mEdge, rScope, (workflowState, workflowHistory))

View File

@ -68,7 +68,7 @@ predNFAesonOptions = defaultOptions
} }
workflowGraphAesonOptions, workflowGraphEdgeAesonOptions, workflowGraphNodeAesonOptions, workflowActionAesonOptions, workflowPayloadViewAesonOptions, workflowNodeViewAesonOptions :: Options workflowGraphAesonOptions, workflowGraphEdgeAesonOptions, workflowGraphNodeAesonOptions, workflowActionAesonOptions, workflowPayloadViewAesonOptions, workflowNodeViewAesonOptions, workflowNodeMessageAesonOptions :: Options
workflowGraphAesonOptions = defaultOptions workflowGraphAesonOptions = defaultOptions
{ fieldLabelModifier = camelToPathPiece' 1 { fieldLabelModifier = camelToPathPiece' 1
} }
@ -89,3 +89,6 @@ workflowPayloadViewAesonOptions = defaultOptions
workflowNodeViewAesonOptions = defaultOptions workflowNodeViewAesonOptions = defaultOptions
{ fieldLabelModifier = camelToPathPiece' 1 { fieldLabelModifier = camelToPathPiece' 1
} }
workflowNodeMessageAesonOptions = defaultOptions
{ fieldLabelModifier = camelToPathPiece' 1
}

View File

@ -5,9 +5,10 @@ module Model.Types.Workflow
, WorkflowGraphNodeLabel , WorkflowGraphNodeLabel
, WorkflowGraphNode(..) , WorkflowGraphNode(..)
, WorkflowNodeView(..) , WorkflowNodeView(..)
, WorkflowNodeMessage(..)
, WorkflowGraphEdgeLabel , WorkflowGraphEdgeLabel
, WorkflowGraphEdge(..) , WorkflowGraphEdge(..)
, WorkflowGraphEdgeAutomaticRestriction(..) , WorkflowGraphRestriction(..)
, WorkflowGraphEdgeFormOrder , WorkflowGraphEdgeFormOrder
, WorkflowGraphEdgeForm(..) , WorkflowGraphEdgeForm(..)
, WorkflowRole(..) , WorkflowRole(..)
@ -78,6 +79,7 @@ newtype WorkflowGraphNodeLabel = WorkflowGraphNodeLabel { unWorkflowGraphNodeLab
data WorkflowGraphNode fileid userid = WGN data WorkflowGraphNode fileid userid = WGN
{ wgnFinal :: Bool { wgnFinal :: Bool
, wgnViewers :: Maybe (WorkflowNodeView userid) , wgnViewers :: Maybe (WorkflowNodeView userid)
, wgnMessages :: Set (WorkflowNodeMessage userid)
, wgnEdges :: Map WorkflowGraphEdgeLabel (WorkflowGraphEdge fileid userid) , wgnEdges :: Map WorkflowGraphEdgeLabel (WorkflowGraphEdge fileid userid)
, wgnPayloadView :: Map WorkflowPayloadLabel (WorkflowPayloadView userid) , wgnPayloadView :: Map WorkflowPayloadLabel (WorkflowPayloadView userid)
} }
@ -92,15 +94,22 @@ data WorkflowNodeView userid = WorkflowNodeView
, wnvDisplayLabel :: I18nText , wnvDisplayLabel :: I18nText
} deriving (Eq, Ord, Read, Show, Data, Generic, Typeable) } deriving (Eq, Ord, Read, Show, Data, Generic, Typeable)
data WorkflowNodeMessage userid = WorkflowNodeMessage
{ wnmViewers :: NonNull (Set (WorkflowRole userid))
, wnmRestriction :: Maybe (PredDNF WorkflowGraphRestriction)
, wnmStatus :: MessageStatus
, wnmContent :: I18nHtml
} deriving (Eq, Ord, Read, Show, Generic, Typeable)
----- WORKFLOW GRAPH: EDGES ----- ----- WORKFLOW GRAPH: EDGES -----
newtype WorkflowGraphEdgeLabel = WorkflowGraphEdgeLabel { unWorkflowGraphEdgeLabel :: CI Text } newtype WorkflowGraphEdgeLabel = WorkflowGraphEdgeLabel { unWorkflowGraphEdgeLabel :: CI Text }
deriving stock (Eq, Ord, Read, Show, Data, Generic, Typeable) deriving stock (Eq, Ord, Read, Show, Data, Generic, Typeable)
deriving newtype (IsString, ToJSON, ToJSONKey, FromJSON, FromJSONKey, PathPiece, PersistField, PersistFieldSql, Binary) deriving newtype (IsString, ToJSON, ToJSONKey, FromJSON, FromJSONKey, PathPiece, PersistField, PersistFieldSql, Binary)
data WorkflowGraphEdgeAutomaticRestriction data WorkflowGraphRestriction
= WorkflowGraphEdgeAutomaticRestrictionPayloadFilled { wgearPayloadFilled :: WorkflowPayloadLabel } = WorkflowGraphRestrictionPayloadFilled { wgrPayloadFilled :: WorkflowPayloadLabel }
| WorkflowGraphEdgeAutomaticRestrictionPreviousNode { wgearPreviousNode :: WorkflowGraphNodeLabel } | WorkflowGraphRestrictionPreviousNode { wgrPreviousNode :: WorkflowGraphNodeLabel }
deriving (Eq, Ord, Read, Show, Generic, Typeable) deriving (Eq, Ord, Read, Show, Generic, Typeable)
data WorkflowGraphEdge fileid userid data WorkflowGraphEdge fileid userid
@ -113,7 +122,7 @@ data WorkflowGraphEdge fileid userid
} }
| WorkflowGraphEdgeAutomatic | WorkflowGraphEdgeAutomatic
{ wgeSource :: WorkflowGraphNodeLabel { wgeSource :: WorkflowGraphNodeLabel
, wgeRestriction :: Maybe (PredDNF WorkflowGraphEdgeAutomaticRestriction) , wgeRestriction :: Maybe (PredDNF WorkflowGraphRestriction)
} }
| WorkflowGraphEdgeInitial | WorkflowGraphEdgeInitial
{ wgeActors :: Set (WorkflowRole userid) { wgeActors :: Set (WorkflowRole userid)
@ -601,15 +610,18 @@ deriveJSON defaultOptions
} ''WorkflowRole } ''WorkflowRole
deriveToJSON workflowNodeViewAesonOptions ''WorkflowNodeView deriveToJSON workflowNodeViewAesonOptions ''WorkflowNodeView
deriveToJSON workflowNodeMessageAesonOptions ''WorkflowNodeMessage
deriveToJSON workflowPayloadViewAesonOptions ''WorkflowPayloadView deriveToJSON workflowPayloadViewAesonOptions ''WorkflowPayloadView
pathPieceJSON ''WorkflowFieldPayload' pathPieceJSON ''WorkflowFieldPayload'
pathPieceJSON ''WorkflowPayloadField' pathPieceJSON ''WorkflowPayloadField'
deriveJSON defaultOptions deriveJSON defaultOptions
{ fieldLabelModifier = camelToPathPiece' 1 { fieldLabelModifier = camelToPathPiece' 1
, constructorTagModifier = camelToPathPiece' 5 , constructorTagModifier = camelToPathPiece' 3
} ''WorkflowGraphEdgeAutomaticRestriction } ''WorkflowGraphRestriction
instance (FromJSON userid, Ord userid) => FromJSON (WorkflowNodeMessage userid) where
parseJSON = genericParseJSON workflowNodeMessageAesonOptions
instance (FromJSON userid, Ord userid) => FromJSON (WorkflowNodeView userid) where instance (FromJSON userid, Ord userid) => FromJSON (WorkflowNodeView userid) where
parseJSON = genericParseJSON workflowNodeViewAesonOptions parseJSON = genericParseJSON workflowNodeViewAesonOptions
instance (FromJSON userid, Ord userid) => FromJSON (WorkflowPayloadView userid) where instance (FromJSON userid, Ord userid) => FromJSON (WorkflowPayloadView userid) where

View File

@ -30,12 +30,10 @@ import Text.HTML.SanitizeXSS (sanitizeBalance)
data MessageStatus = Error | Warning | Info | Success data MessageStatus = Error | Warning | Info | Success
deriving (Eq, Ord, Enum, Bounded, Show, Read, Lift) deriving (Eq, Ord, Enum, Bounded, Show, Read, Lift, Generic, Typeable)
deriving anyclass (Universe, Finite)
instance Default MessageStatus where
instance Universe MessageStatus
instance Finite MessageStatus
instance Default MessageStatus where
def = Info def = Info
deriveJSON defaultOptions deriveJSON defaultOptions
@ -113,7 +111,7 @@ instance FromJSON Message where
parseJSON = withObject "Message" $ \o -> do parseJSON = withObject "Message" $ \o -> do
messageStatus <- o .: "status" messageStatus <- o .: "status"
messageContent <- preEscapedText . sanitizeBalance <$> o .: "content" messageContent <- preEscapedText . sanitizeBalance <$> o .: "content"
messageIcon <- o .: "icon" messageIcon <- o .:? "icon"
return Message{..} return Message{..}
statusToUrgencyClass :: MessageStatus -> Text statusToUrgencyClass :: MessageStatus -> Text

View File

@ -14,6 +14,10 @@ $maybe WorkflowCurrentState{..} <- workflowState
$nothing $nothing
<span .workflow-state--state-special> <span .workflow-state--state-special>
_{MsgWorkflowWorkflowWorkflowStateStateHidden} _{MsgWorkflowWorkflowWorkflowStateStateHidden}
$forall msg <- wcsMessages
^{notification NotificationBroad msg}
$if not (onull wcsPayload) $if not (onull wcsPayload)
<div .workflow-payload> <div .workflow-payload>
<div .workflow-payload--label> <div .workflow-payload--label>

17
testdata/theses.yaml vendored
View File

@ -79,6 +79,7 @@ nodes:
- *betreuer - *betreuer
- {"tag": "initiator"} - {"tag": "initiator"}
display-label: "Notizen" display-label: "Notizen"
messages: []
final: false final: false
edges: edges:
"antrag als pruefungsamt": "antrag als pruefungsamt":
@ -352,6 +353,7 @@ nodes:
- *hochschullehrer - *hochschullehrer
- *betreuer - *betreuer
payload-view: *payload-view payload-view: *payload-view
messages: []
final: false final: false
edges: edges:
"antrag bestaetigen als hochschullehrer": "antrag bestaetigen als hochschullehrer":
@ -381,6 +383,7 @@ nodes:
- *hochschullehrer - *hochschullehrer
- *betreuer - *betreuer
payload-view: *payload-view payload-view: *payload-view
messages: []
final: false final: false
edges: edges:
"antrag bestaetigen als student": "antrag bestaetigen als student":
@ -409,6 +412,13 @@ nodes:
- *hochschullehrer - *hochschullehrer
- *betreuer - *betreuer
payload-view: *payload-view payload-view: *payload-view
messages:
- viewers:
- *hochschullehrer
- *betreuer
restriction: null
status: info
content: "Es muss zunächst „Anmeldetag“ eingetragen werden, damit der Antrag weiter von der Prüfungsverwaltung bearbeitet werden kann."
final: false final: false
edges: edges:
"antrag bestaetigen als student": "antrag bestaetigen als student":
@ -437,6 +447,7 @@ nodes:
- *hochschullehrer - *hochschullehrer
- *betreuer - *betreuer
payload-view: *payload-view payload-view: *payload-view
messages: []
final: false final: false
edges: edges:
"anmeldetag ist eingetragen": "anmeldetag ist eingetragen":
@ -457,6 +468,7 @@ nodes:
- *betreuer - *betreuer
- *student - *student
payload-view: *payload-view payload-view: *payload-view
messages: []
final: false final: false
edges: edges:
"anmelden, bestaetigt student&hochschullehrer, anmeldetag": "anmelden, bestaetigt student&hochschullehrer, anmeldetag":
@ -516,6 +528,7 @@ nodes:
- *betreuer - *betreuer
- *student - *student
payload-view: *payload-view payload-view: *payload-view
messages: []
final: false final: false
edges: {} edges: {}
"abgegeben": "abgegeben":
@ -527,6 +540,7 @@ nodes:
- *betreuer - *betreuer
- *student - *student
payload-view: *payload-view payload-view: *payload-view
messages: []
final: false final: false
edges: {} edges: {}
"benotet": "benotet":
@ -538,6 +552,7 @@ nodes:
- *betreuer - *betreuer
- *student - *student
payload-view: *payload-view payload-view: *payload-view
messages: []
final: false final: false
edges: {} edges: {}
"abgebrochen": "abgebrochen":
@ -549,6 +564,7 @@ nodes:
- *betreuer - *betreuer
- *student - *student
payload-view: *payload-view payload-view: *payload-view
messages: []
final: false final: false
edges: {} edges: {}
"fertig": "fertig":
@ -557,5 +573,6 @@ nodes:
viewers: viewers:
- *pruefungsamt - *pruefungsamt
payload-view: *payload-view payload-view: *payload-view
messages: []
final: true final: true
edges: {} edges: {}