fix(avs): fix #124 implement automatic avs driving licence synchronisation

This commit is contained in:
Steffen Jost 2024-08-12 18:01:04 +02:00
parent e551fadd29
commit cc5da9a2a9
7 changed files with 166 additions and 42 deletions

View File

@ -15,7 +15,7 @@ module Database.Esqueleto.Utils
, (=?.), (?=.) , (=?.), (?=.)
, (=~.), (~=.) , (=~.), (~=.)
, (>~.), (<~.) , (>~.), (<~.)
, (~.), (~*.) , (~.), (~*.), (!~.), (!~*.)
, or, and , or, and
, any, all , any, all
, not__, parens , not__, parens
@ -165,7 +165,7 @@ infixl 4 <~.
(<~.) :: PersistField typ => E.SqlExpr (E.Value typ) -> E.SqlExpr (E.Value (Maybe typ)) -> E.SqlExpr (E.Value Bool) (<~.) :: PersistField typ => E.SqlExpr (E.Value typ) -> E.SqlExpr (E.Value (Maybe typ)) -> E.SqlExpr (E.Value Bool)
(<~.) a b = E.isNothing b E.||. (E.just a E.<. b) (<~.) a b = E.isNothing b E.||. (E.just a E.<. b)
infixr 2 ~., ~*. infixr 2 ~., ~*., !~., !~*.
-- | PostgreSQL regular expression match, case sensitive. Works, but may throw SQL error for unblanced parenthesis, etc. Not suitable for dbTable filters -- | PostgreSQL regular expression match, case sensitive. Works, but may throw SQL error for unblanced parenthesis, etc. Not suitable for dbTable filters
(~.) :: E.SqlString s => E.SqlExpr (E.Value s) -> E.SqlExpr (E.Value s) -> E.SqlExpr (E.Value Bool) (~.) :: E.SqlString s => E.SqlExpr (E.Value s) -> E.SqlExpr (E.Value s) -> E.SqlExpr (E.Value Bool)
@ -175,6 +175,14 @@ infixr 2 ~., ~*.
(~*.) :: E.SqlString s => E.SqlExpr (E.Value s) -> E.SqlExpr (E.Value s) -> E.SqlExpr (E.Value Bool) (~*.) :: E.SqlString s => E.SqlExpr (E.Value s) -> E.SqlExpr (E.Value s) -> E.SqlExpr (E.Value Bool)
(~*.) = E.unsafeSqlBinOp " ~* " (~*.) = E.unsafeSqlBinOp " ~* "
-- | PostgreSQL regular expression does not match, case sensitive. Works, but may throw SQL error for unblanced parenthesis, etc. Not suitable for dbTable filters
(!~.) :: E.SqlString s => E.SqlExpr (E.Value s) -> E.SqlExpr (E.Value s) -> E.SqlExpr (E.Value Bool)
(!~.) = E.unsafeSqlBinOp " !~ "
-- | PostgreSQL regular expression does not match, case insensitive. Works, but may throw SQL errors
(!~*.) :: E.SqlString s => E.SqlExpr (E.Value s) -> E.SqlExpr (E.Value s) -> E.SqlExpr (E.Value Bool)
(!~*.) = E.unsafeSqlBinOp " !~* "
-- | Negation of `isNothing` which is missing -- | Negation of `isNothing` which is missing
isJust :: PersistField typ => E.SqlExpr (E.Value (Maybe typ)) -> E.SqlExpr (E.Value Bool) isJust :: PersistField typ => E.SqlExpr (E.Value (Maybe typ)) -> E.SqlExpr (E.Value Bool)

View File

@ -476,6 +476,7 @@ getProblemAvsSynchR = do
formResult tres1up $ procRes AvsLicenceVorfeld formResult tres1up $ procRes AvsLicenceVorfeld
formResult tres0 $ procRes AvsNoLicence formResult tres0 $ procRes AvsNoLicence
AvsLicenceSynchConf{..} <- getsYesod $ view _appAvsLicenceSynchConf
siteLayoutMsg MsgAvsTitleLicenceSynch $ do siteLayoutMsg MsgAvsTitleLicenceSynch $ do
setTitleI MsgAvsTitleLicenceSynch setTitleI MsgAvsTitleLicenceSynch
$(i18nWidgetFile "avs-synchronisation") $(i18nWidgetFile "avs-synchronisation")

View File

@ -419,6 +419,19 @@ determineCrontab = execWriterT $ do
, cronNotAfter = Right CronNotScheduled -- maximal delay of an execution, before it is skipped entirely , cronNotAfter = Right CronNotScheduled -- maximal delay of an execution, before it is skipped entirely
} }
when (notNull (avsLicenceSynchTimes appAvsLicenceSynchConf)) $ tell $ HashMap.singleton
(JobCtlQueue JobSynchroniseAvsLicences)
Cron
{ cronInitial = CronAsap
, cronRateLimit = 10 -- minimal time between two executions, before the second job is skipped
, cronNotAfter = Right CronNotScheduled -- maximal delay of an execution, before it is skipped entirely
, cronRepeat = CronRepeatScheduled $ cronCalendarAny { cronDayOfWeek = CronMatchSome . impureNonNull . Set.fromList $ [1..5] --weekdays only
, cronHour = CronMatchSome . impureNonNull . Set.fromList $ avsLicenceSynchTimes appAvsLicenceSynchConf
, cronMinute = cronMatchOne 1
, cronSecond = cronMatchOne 3
}
}
let let
correctorNotifications :: Map (UserId, SheetId) (Max UTCTime) -> WriterT (Crontab JobCtl) (ReaderT SqlReadBackend (HandlerFor UniWorX)) () correctorNotifications :: Map (UserId, SheetId) (Max UTCTime) -> WriterT (Crontab JobCtl) (ReaderT SqlReadBackend (HandlerFor UniWorX)) ()
correctorNotifications = (tell .) . Map.foldMapWithKey $ \(nUser, nSheet) (Max time) -> HashMap.singleton correctorNotifications = (tell .) . Map.foldMapWithKey $ \(nUser, nSheet) (Max time) -> HashMap.singleton

View File

@ -140,37 +140,43 @@ dispatchJobSynchroniseAvsQueue = JobHandlerException $ do
dispatchJobSynchroniseAvsLicences :: JobHandler UniWorX dispatchJobSynchroniseAvsLicences :: JobHandler UniWorX
-- dispatchJobSynchroniseAvsLicences = error "TODO" -- dispatchJobSynchroniseAvsLicences = error "TODO"
dispatchJobSynchroniseAvsLicences = JobHandlerException $ do -- when (synchLevel > 0) $ do dispatchJobSynchroniseAvsLicences = JobHandlerException $ do -- when (synchLevel > 0) $ do
let synchLevel = 0 -- SynchLevel corresponds to tables of ProblemAvsSynchR: 4=top grant R, 3= reduce R->F, 2= grant F, 1= revoke F AvsLicenceSynchConf
-- TODO: turn level into a setting { avsLicenceSynchLevel = synchLevel -- SynchLevel corresponds to tables of ProblemAvsSynchR: 4=top grant R, 3= reduce R->F, 2= grant F, 1= revoke F
-- TODO: enable a cron job by setting , avsLicenceSynchReasonFilter = reasonFilter
, avsLicenceSynchMaxChanges = maxChanges
} <- getsYesod $ view _appAvsLicenceSynchConf
let -- TODO: enable a cron job by setting
procLic :: AvsLicence -> Bool -> Set AvsPersonId -> Handler () procLic :: AvsLicence -> Bool -> Set AvsPersonId -> Handler ()
procLic aLic up apids procLic aLic up apids
| n <- Set.size apids, n > 0 = do | n <- Set.size apids, n > 0 =
let subtype = Text.cons (bool '↧' '↥' up) $ Text.singleton $ licence2char aLic let subtype = Text.cons (bool '↧' '↥' up) $ Text.singleton $ licence2char aLic
logit errm = runDB $ logInterface' "AVS" subtype False (isJust errm) (Just n) (fromMaybe "Automatic synch" errm) logit errm = runDB $ logInterface' "AVS" subtype False (isJust errm) (Just n) (fromMaybe "Automatic synch" errm)
catchAllAvs = flip catch (\err -> logit (Just $ tshow (err :: SomeException)) >> return (-1)) catchAllAvs = flip catch (\err -> logit (Just $ tshow (err :: SomeException)) >> return (-1))
oks <- catchAllAvs $ setLicencesAvs $ Set.map (AvsPersonLicence aLic) apids in if NTop (Just n) <= NTop maxChanges
when (oks > 0) $ logit $ toMaybe (oks /= n) [st|Only #{tshow oks}/#{tshow n} licence changes accepted by AVS|] then do
oks <- catchAllAvs $ setLicencesAvs $ Set.map (AvsPersonLicence aLic) apids
when (oks > 0) $ logit $ toMaybe (oks /= n) [st|Only #{tshow oks}/#{tshow n} licence changes accepted by AVS|]
else
logit $ Just $ [st|Too many changes at once. Consider increasing avs-licence-synch-max-changes #{tshow maxChanges}|]
| otherwise = return () | otherwise = return ()
now <- liftIO getCurrentTime
(AvsLicenceDifferences{..}, rsChanged) <- retrieveDifferingLicences (AvsLicenceDifferences{..}, rsChanged) <- retrieveDifferingLicences
-- for synchLevel < 5 prevent automatic changes to users blocked with a reason mentioning "Firm" and currently being associatd with multiple companies -- prevent automatic changes to users blocked with certain reasons and with currently being associated with multiple companies
multiFirmBlocks <- if synchLevel >= 5 multiFirmBlocks <- ifNothingM reasonFilter mempty $ \reasons -> do
then return mempty now <- liftIO getCurrentTime
else do firmBlocks <- runDBRead $ E.select $ do
firmBlocks <- runDBRead $ E.select $ do (uavs :& _qualUser :& qblock) <- E.from $ E.table @UserAvs
(uavs :& _qualUser :& qblock) <- E.from $ E.table @UserAvs `E.innerJoin` E.table @QualificationUser `E.on` (\(uavs :& qualUser) -> uavs E.^. UserAvsUser E.==. qualUser E.^. QualificationUserUser)
`E.innerJoin` E.table @QualificationUser `E.on` (\(uavs :& qualUser) -> uavs E.^. UserAvsUser E.==. qualUser E.^. QualificationUserUser) `E.innerJoin` E.table @QualificationUserBlock `E.on` (\(_uavs :& qualUser :& qblock) ->
`E.innerJoin` E.table @QualificationUserBlock `E.on` (\(_uavs :& qualUser :& qblock) -> qualUser E.^. QualificationUserId E.==. qblock E.^. QualificationUserBlockQualificationUser
qualUser E.^. QualificationUserId E.==. qblock E.^. QualificationUserBlockQualificationUser E.&&. qblock `isLatestBlockBefore'` E.val now)
E.&&. qblock `isLatestBlockBefore'` E.val now) E.where_ $ (qblock E.^. QualificationUserBlockReason E.~*. E.val reasons)
E.where_ $ (E.val ("Firm"::Text) `E.isInfixOf` qblock E.^. QualificationUserBlockReason) E.&&. uavs E.^. UserAvsPersonId `E.in_` E.vals (avsLicenceDiffRevokeAll `Set.union` avsLicenceDiffRevokeRollfeld)
E.&&. uavs E.^. UserAvsPersonId `E.in_` E.vals (avsLicenceDiffRevokeAll `Set.union` avsLicenceDiffRevokeRollfeld) E.&&. E.not_ (qblock E.^. QualificationUserBlockUnblock)
E.&&. E.not_ (qblock E.^. QualificationUserBlockUnblock) return $ uavs E.^. UserAvsPersonId
return $ uavs E.^. UserAvsPersonId firmBlockData <- lookupAvsUsers $ Set.fromList $ map E.unValue firmBlocks -- may throw, but we need to abort then
firmBlockData <- lookupAvsUsers $ Set.fromList $ map E.unValue firmBlocks -- may throw, but we need to abort then return $ Map.keysSet $ Map.filter hasMultipleFirms firmBlockData
return $ Map.keysSet $ Map.filter hasMultipleFirms firmBlockData
let fltrIds let fltrIds
| synchLevel >= 5 = id | synchLevel >= 5 = id

View File

@ -102,6 +102,8 @@ data AppSettings = AppSettings
-- ^ Configuration settings for accessing the LDAP-directory -- ^ Configuration settings for accessing the LDAP-directory
, appAvsConf :: Maybe AvsConf , appAvsConf :: Maybe AvsConf
-- ^ Configuration settings for accessing AVS Server (= Ausweis Verwaltungs System) -- ^ Configuration settings for accessing AVS Server (= Ausweis Verwaltungs System)
, appAvsLicenceSynchConf :: AvsLicenceSynchConf
-- ^ Configuration settings for automatically synching driving licences with AVS
, appLprConf :: LprConf , appLprConf :: LprConf
-- ^ Configuration settings for accessing a printer queue via lpr for letter mailing -- ^ Configuration settings for accessing a printer queue via lpr for letter mailing
, appSmtpConf :: Maybe SmtpConf , appSmtpConf :: Maybe SmtpConf
@ -335,6 +337,21 @@ data AvsConf = AvsConf
, avsCacheExpiry :: DiffTime -- Seconds, only for non-licence related queries , avsCacheExpiry :: DiffTime -- Seconds, only for non-licence related queries
} deriving (Show) } deriving (Show)
data AvsLicenceSynchConf = AvsLicenceSynchConf
{ avsLicenceSynchTimes :: [Natural] -- hours, when a synch should occur
, avsLicenceSynchLevel :: Int -- 0: No synch, 1: revoke Vorfeld, 2: Grant Vorfeld, 3: Downgrade to Vorfeld, 4: Grant Rollfeld
, avsLicenceSynchReasonFilter :: Maybe Text -- regular expression matched case-insensitive against latest block/grant reason, preventing automatic synch to users with this reason AND being associated with multiple companies
, avsLicenceSynchMaxChanges :: Maybe Int -- abort synch for group, if there are too many changes overall
} deriving (Show)
instance Default AvsLicenceSynchConf where
def = AvsLicenceSynchConf
{ avsLicenceSynchTimes = []
, avsLicenceSynchLevel = 0
, avsLicenceSynchReasonFilter = Nothing
, avsLicenceSynchMaxChanges = Nothing
}
data LprConf = LprConf data LprConf = LprConf
{ lprHost :: String { lprHost :: String
, lprPort :: Int , lprPort :: Int
@ -542,6 +559,16 @@ instance FromJSON AvsConf where
makeLenses_ ''AvsConf makeLenses_ ''AvsConf
instance FromJSON AvsLicenceSynchConf where
parseJSON = withObject "AvsLicenceSynch" $ \o -> do
avsLicenceSynchTimes <- o .: "times"
avsLicenceSynchLevel <- o .: "level"
avsLicenceSynchReasonFilter <- o .:? "reason-filter"
avsLicenceSynchMaxChanges <- o .:? "max-changes"
return AvsLicenceSynchConf{..}
makeLenses_ ''AvsLicenceSynchConf
instance FromJSON LprConf where instance FromJSON LprConf where
parseJSON = withObject "LprConf" $ \o -> do parseJSON = withObject "LprConf" $ \o -> do
lprHost <- o .: "host" lprHost <- o .: "host"
@ -640,6 +667,7 @@ instance FromJSON AppSettings where
appLdapConf <- P.fromList . mapMaybe (assertM nonEmptyHost) <$> o .:? "ldap" .!= [] appLdapConf <- P.fromList . mapMaybe (assertM nonEmptyHost) <$> o .:? "ldap" .!= []
appLmsConf <- o .: "lms-direct" appLmsConf <- o .: "lms-direct"
appAvsConf <- assertM (not . null . avsPass) <$> o .:? "avs" appAvsConf <- assertM (not . null . avsPass) <$> o .:? "avs"
appAvsLicenceSynchConf <- o .:? "avs-licence-synch" .!= def
appLprConf <- o .: "lpr" appLprConf <- o .: "lpr"
appSmtpConf <- assertM (not . null . smtpHost) <$> o .:? "smtp" appSmtpConf <- assertM (not . null . smtpHost) <$> o .:? "smtp"
let validMemcachedConf MemcachedConf{memcachedConnectInfo = Memcached.ConnectInfo{..}} = and let validMemcachedConf MemcachedConf{memcachedConnectInfo = Memcached.ConnectInfo{..}} = and

View File

@ -47,3 +47,37 @@ $# SPDX-License-Identifier: AGPL-3.0-or-later
<p> <p>
^{tb0} ^{tb0}
$if notNull avsLicenceSynchTimes
<section>
<h2>
Automatische AVS Fahrlizen Sychronisation
<p>
<dl .deflist>
<dt .deflist__dt>
Uhrzeiten Synchronisation
<dd .deflist__dd>
Werktags, weniger Minuten nach folgenden vollen Stunden: #{tshow avsLicenceSynchTimes}
<dt .deflist__dt>
Synchronisationslevel
<dd .deflist__dd>
#{avsLicenceSynchLevel} #
$case avsLicenceSynchLevel
$of 1
Nur Vorfeld-Fahrberechtigungen entziehen
$of 2
Vorfeld-Fahrberechtigungen entziehen und gewähren
$of 3
Vorfeld-Fahrberechtigungen entziehen und gewähren, #
so wie Rollfeld-Fahrberechtigungen zu Vorfeld-Fahrberechtigungen herabstufen
$of _
Vorfeld- und Rollfeld-Fahrberechtigungen entziehen und gewähren
$maybe reasons <- avsLicenceSynchReasonFilter
<dt .deflist__dt>
Ausnahmen
<dd .deflist__dd>
Keine automatische Synchronisation, wenn die Begründung des letzten Un-/Blocks zu diesen regulären Ausdruck passt: #{reasons}
$maybe maxChange <- avsLicenceSynchMaxChanges
<dt .deflist__dt>
Maximal Änderungen
<dd .deflist__dd>
Keine Synchronisation durchführen, wenn es mehr als #{maxChange} Änderungen pro Level wären

View File

@ -46,3 +46,37 @@ $# SPDX-License-Identifier: AGPL-3.0-or-later
No valid driving licence in FRADrive, but having any driving licence in AVS (maneuvering or apron) No valid driving licence in FRADrive, but having any driving licence in AVS (maneuvering or apron)
<p> <p>
^{tb0} ^{tb0}
$if notNull avsLicenceSynchTimes
<section>
<h2>
Automatic AVS licence sychronisation
<p>
<dl .deflist>
<dt .deflist__dt>
Synchronisation times
<dd .deflist__dd>
Synchronize on weekdays, few minutes after each full hour: #{tshow avsLicenceSynchTimes}
<dt .deflist__dt>
Synchronisation level
<dd .deflist__dd>
#{avsLicenceSynchLevel} #
$case avsLicenceSynchLevel
$of 1
Revoke apron driving licences only
$of 2
Grant and revoke apron driving licences only
$of 3
Grant and revoke apron driving licences and downgrade maneuvering area licences to apron driving licences
$of _
Grant and revoke all driving licences automatically
$maybe reasons <- avsLicenceSynchReasonFilter
<dt .deflist__dt>
Exemptions
<dd .deflist__dd>
Do not synchronize changes where the last un-/block reason matches #{reasons}
$maybe maxChange <- avsLicenceSynchMaxChanges
<dt .deflist__dt>
Max changes
<dd .deflist__dd>
Do not synchronize a licence if the number of changes exceeds #{maxChange}