Merge branch 'fradrive/cr3'

This commit is contained in:
Steffen Jost 2024-06-10 18:42:46 +02:00
commit ab2e81f34d
24 changed files with 240 additions and 165 deletions

View File

@ -4,8 +4,8 @@
#messages or constructors that are used all over the code #messages or constructors that are used all over the code
Logo !ident-ok: Uni2work Logo !ident-ok: FRADrive
EmailInvitationWarning: Diese Adresse konnte keinem Uni2work-Benutzer/keiner Uni2work-Benutzerin zugeordnet werden. Es wird eine Einladung per E-Mail versandt. EmailInvitationWarning: Diese Adresse konnte keinem FRADrive-Benutzer/-Benutzerin zugeordnet werden. Es wird eine Einladung per E-Mail versandt.
BoolIrrelevant !ident-ok: — BoolIrrelevant !ident-ok: —
FieldPrimary: Hauptfach FieldPrimary: Hauptfach
FieldSecondary: Nebenfach FieldSecondary: Nebenfach
@ -15,6 +15,7 @@ WeekDay: Wochentag
LdapIdentificationOrEmail: Fraport AG-Kennung / E-Mail-Adresse LdapIdentificationOrEmail: Fraport AG-Kennung / E-Mail-Adresse
Months num@Int64: #{num} #{pluralDE num "Monat" "Monate"} Months num@Int64: #{num} #{pluralDE num "Monat" "Monate"}
Days num@Int64: #{num} #{pluralDE num "Tag" "Tage"} Days num@Int64: #{num} #{pluralDE num "Tag" "Tage"}
NoAutomaticUpdateTip: Dieser Wert wurde manuell editiert und wird daher nicht mehr automatisch aktualisiert.
ClusterVolatileQuickActionsEnabled: Schnellzugriffsmenü aktiv ClusterVolatileQuickActionsEnabled: Schnellzugriffsmenü aktiv

View File

@ -4,8 +4,8 @@
#messages or constructors that are used all over the Code #messages or constructors that are used all over the Code
Logo: Uni2work Logo: FRADrive
EmailInvitationWarning: This address could not be matched to any Uni2work user. An invitation will be sent via email. EmailInvitationWarning: This address could not be matched to any FRADrive user. An invitation will be sent via email.
BoolIrrelevant: — BoolIrrelevant: —
FieldPrimary: Major FieldPrimary: Major
FieldSecondary: Minor FieldSecondary: Minor
@ -15,6 +15,7 @@ WeekDay: Day of the week
LdapIdentificationOrEmail: Fraport AG-Kennung / email address LdapIdentificationOrEmail: Fraport AG-Kennung / email address
Months num: #{num} #{pluralEN num "Month" "Months"} Months num: #{num} #{pluralEN num "Month" "Months"}
Days num: #{num} #{pluralEN num "Day" "Days"} Days num: #{num} #{pluralEN num "Day" "Days"}
NoAutomaticUpdateTip: This value receives no automatic updates, since it has been edited manually.
ClusterVolatileQuickActionsEnabled: Quick actions enabled ClusterVolatileQuickActionsEnabled: Quick actions enabled

View File

@ -11,15 +11,8 @@ Company
prefersPostal Bool default=false -- new company users prefers letters by post instead of email prefersPostal Bool default=false -- new company users prefers letters by post instead of email
postAddress StoredMarkup Maybe -- default company postal address, including company name postAddress StoredMarkup Maybe -- default company postal address, including company name
email UserEmail Maybe -- Case-insensitive generic company eMail address email UserEmail Maybe -- Case-insensitive generic company eMail address
UniqueCompanyName name -- UniqueCompanyName name -- Should be Unique in AVS, but we do not yet need to enforce it
-- UniqueCompanyShorthand shorthand -- unnecessary, since it is the primary key already -- UniqueCompanyShorthand shorthand -- unnecessary, since it is the primary key already
UniqueCompanyAvsId avsId UniqueCompanyAvsId avsId -- Should be the key, is not for historical reasons and for convenience in URLs and columns
Primary shorthand -- newtype Key Company = CompanyKey { unCompanyKey :: CompanyShorthand } Primary shorthand -- newtype Key Company = CompanyKey { unCompanyKey :: CompanyShorthand }
deriving Ord Eq Show Generic Binary deriving Ord Eq Show Generic Binary
-- -- TODO: a way to populate this table (manually)
-- CompanySynonym
-- synonym CompanyName
-- canonical CompanyShorthand OnDeleteCascade OnUpdateCascade -- DEPRECATED: should be CompanyId
-- UniqueCompanySynonym synonym
-- deriving Ord Eq Show Generic

View File

@ -93,7 +93,7 @@ validateAvsQueryPerson = do
is _Just avsPersonQueryInternalPersonalNo || is _Just avsPersonQueryInternalPersonalNo ||
is _Just avsPersonQueryVersionNo is _Just avsPersonQueryVersionNo
makeAvsStatusForm :: Maybe AvsQueryStatus -> Form AvsQueryStatus makeAvsStatusForm :: Maybe AvsPersonId -> Form AvsQueryStatus
makeAvsStatusForm tmpl = identifyForm FIDAvsQueryStatus . validateForm validateAvsQueryStatus $ \html -> makeAvsStatusForm tmpl = identifyForm FIDAvsQueryStatus . validateForm validateAvsQueryStatus $ \html ->
flip (renderAForm FormStandard) html $ flip (renderAForm FormStandard) html $
parseAvsIds <$> areq textField (fslI MsgAvsPersonId) (unparseAvsIds <$> tmpl) parseAvsIds <$> areq textField (fslI MsgAvsPersonId) (unparseAvsIds <$> tmpl)
@ -103,15 +103,15 @@ makeAvsStatusForm tmpl = identifyForm FIDAvsQueryStatus . validateForm validateA
where where
nonemptys = filter (not . Text.null) $ Text.strip <$> Text.split (==',') txt nonemptys = filter (not . Text.null) $ Text.strip <$> Text.split (==',') txt
ids = mapMaybe readMay nonemptys ids = mapMaybe readMay nonemptys
unparseAvsIds :: AvsQueryStatus -> Text unparseAvsIds :: AvsPersonId -> Text
unparseAvsIds (AvsQueryStatus ids) = Text.intercalate ", " $ tshow <$> Set.toAscList ids unparseAvsIds = tshow . avsPersonId
validateAvsQueryStatus :: FormValidator AvsQueryStatus Handler () validateAvsQueryStatus :: FormValidator AvsQueryStatus Handler ()
validateAvsQueryStatus = do validateAvsQueryStatus = do
AvsQueryStatus ids <- State.get AvsQueryStatus ids <- State.get
guardValidation (MsgAvsQueryStatusInvalid $ tshow ids) $ not (null ids) guardValidation (MsgAvsQueryStatusInvalid $ tshow ids) $ not (null ids)
makeAvsContactForm :: Maybe AvsQueryContact -> Form AvsQueryContact makeAvsContactForm :: Maybe AvsPersonId -> Form AvsQueryContact
makeAvsContactForm tmpl = identifyForm FIDAvsQueryContact . validateForm validateAvsQueryContact $ \html -> makeAvsContactForm tmpl = identifyForm FIDAvsQueryContact . validateForm validateAvsQueryContact $ \html ->
flip (renderAForm FormStandard) html $ flip (renderAForm FormStandard) html $
parseAvsIds <$> areq textField (fslI MsgAvsPersonId) (unparseAvsIds <$> tmpl) -- consider using cfAnySeparatedSet here parseAvsIds <$> areq textField (fslI MsgAvsPersonId) (unparseAvsIds <$> tmpl) -- consider using cfAnySeparatedSet here
@ -121,8 +121,9 @@ makeAvsContactForm tmpl = identifyForm FIDAvsQueryContact . validateForm validat
where where
nonemptys = filter (not . Text.null) $ Text.strip <$> Text.split (==',') txt nonemptys = filter (not . Text.null) $ Text.strip <$> Text.split (==',') txt
ids = mapMaybe (fmap AvsObjPersonId . readMay) nonemptys ids = mapMaybe (fmap AvsObjPersonId . readMay) nonemptys
unparseAvsIds :: AvsQueryContact -> Text unparseAvsIds :: AvsPersonId -> Text
unparseAvsIds (AvsQueryContact ids) = Text.intercalate ", " $ tshow <$> Set.toAscList ids unparseAvsIds = tshow . avsPersonId
--unparseAvsIds (AvsQueryContact ids) = Text.intercalate ", " $ tshow <$> Set.toAscList ids
validateAvsQueryContact :: FormValidator AvsQueryContact Handler () validateAvsQueryContact :: FormValidator AvsQueryContact Handler ()
validateAvsQueryContact = do validateAvsQueryContact = do
@ -161,19 +162,26 @@ postAdminAvsR = do
((presult, pwidget), penctype) <- runFormPost $ makeAvsPersonForm Nothing ((presult, pwidget), penctype) <- runFormPost $ makeAvsPersonForm Nothing
let procFormPerson fr = do let procFormPerson :: AvsQueryPerson -> Handler (Maybe (Maybe Widget, Maybe AvsPersonId))
procFormPerson fr = do
addMessage Info $ text2Html $ "Query: " <> tshow (toJSON fr) addMessage Info $ text2Html $ "Query: " <> tshow (toJSON fr)
tryShow $ do try (avsQuery fr) >>= \case
AvsResponsePerson pns <- avsQuery fr Left err -> return $ Just (Just $ exceptionWgt err, Nothing)
return [whamlet| Right (AvsResponsePerson pns) -> do
<ul> let mapid = case Set.toList pns of
$forall p <- pns [AvsDataPerson{avsPersonPersonID=apid}] -> Just apid
<li>^{jsonWidget p} _ -> Nothing
|] -- <li>#{decodeUtf8 (Pretty.encodePretty (toJSON p))} wgt = [whamlet|
mbPerson <- formResultMaybe presult (Just <<$>> procFormPerson) <ul>
$forall p <- pns
<li>^{jsonWidget p}
|] -- <li>#{decodeUtf8 (Pretty.encodePretty (toJSON p))}
return $ Just (toMaybe (notNull pns) wgt, mapid)
(mbPerson,mapid) <- fromMaybe (Nothing,Nothing) <$> formResultMaybe presult procFormPerson
((sresult, swidget), senctype) <- runFormPost $ makeAvsStatusForm Nothing ((sresult', swidget), senctype) <- runFormPost $ makeAvsStatusForm mapid
let procFormStatus fr = do let sresult = sresult' <|> maybe FormMissing (FormSuccess . AvsQueryStatus . Set.singleton) mapid -- use unique AvsId from PersonSearch for convenience, if form was empty
procFormStatus fr = do
addMessage Info $ text2Html $ "Status Query: " <> tshow (toJSON fr) addMessage Info $ text2Html $ "Status Query: " <> tshow (toJSON fr)
tryShow $ do tryShow $ do
AvsResponseStatus pns <- avsQuery fr AvsResponseStatus pns <- avsQuery fr
@ -184,8 +192,9 @@ postAdminAvsR = do
|] |]
mbStatus <- formResultMaybe sresult (Just <<$>> procFormStatus) mbStatus <- formResultMaybe sresult (Just <<$>> procFormStatus)
((cresult, cwidget), cenctype) <- runFormPost $ makeAvsContactForm Nothing ((cresult', cwidget), cenctype) <- runFormPost $ makeAvsContactForm mapid
let procFormContact fr = do let cresult = cresult' <|> maybe FormMissing (FormSuccess . AvsQueryContact . Set.singleton . AvsObjPersonId) mapid -- use unique AvsId from PersonSearch for convenience, if form was empty
procFormContact fr = do
addMessage Info $ text2Html $ "Contact Query: " <> tshow (toJSON fr) addMessage Info $ text2Html $ "Contact Query: " <> tshow (toJSON fr)
tryShow $ do tryShow $ do
AvsResponseContact pns <- avsQuery fr AvsResponseContact pns <- avsQuery fr

View File

@ -440,7 +440,7 @@ mkLmsTable isAdmin (Entity qid quali) acts cols psValidator = do
dbtSQLQuery = lmsTableQuery now qid dbtSQLQuery = lmsTableQuery now qid
dbtRowKey = queryUser >>> (E.^. UserId) dbtRowKey = queryUser >>> (E.^. UserId)
dbtProj = dbtProjSimple $ \(qualUsr, usr, lmsUsr, qUsrBlock, printAcks, validQ) -> do dbtProj = dbtProjSimple $ \(qualUsr, usr, lmsUsr, qUsrBlock, printAcks, validQ) -> do
cmpUsr <- selectList [UserCompanyUser ==. entityKey usr] [Asc UserCompanyCompany] cmpUsr <- selectList [UserCompanyUser ==. entityKey usr] [Desc UserCompanyPriority, Asc UserCompanyCompany, LimitTo 1]
return (qualUsr, usr, lmsUsr, qUsrBlock, printAcks, cmpUsr, validQ) return (qualUsr, usr, lmsUsr, qUsrBlock, printAcks, cmpUsr, validQ)
dbtColonnade = cols cmpMap dbtColonnade = cols cmpMap
dbtSorting = mconcat dbtSorting = mconcat
@ -619,7 +619,7 @@ postLmsR sid qsh = do
[ guardMonoid isAdmin $ dbSelect (applying _2) id (return . view (resultUser . _entityKey)) [ guardMonoid isAdmin $ dbSelect (applying _2) id (return . view (resultUser . _entityKey))
, colUserNameModalHdrAdmin MsgLmsUser AdminUserR , colUserNameModalHdrAdmin MsgLmsUser AdminUserR
, colUserEmail , colUserEmail
, sortable (Just "user-company") (i18nCell MsgTableCompanies) $ \( view resultCompanyUser -> cmps) -> , sortable (Just "user-company") (i18nCell MsgTableCompany) $ \( view resultCompanyUser -> cmps) ->
let cs = [ companyCell (unCompanyKey cmpId) cmpName cmpSpr let cs = [ companyCell (unCompanyKey cmpId) cmpName cmpSpr
| Entity _ UserCompany{userCompanyCompany=cmpId, userCompanySupervisor=cmpSpr} <- cmps | Entity _ UserCompany{userCompanyCompany=cmpId, userCompanySupervisor=cmpSpr} <- cmps
, let cmpName = maybe (unCompanyKey cmpId) companyName $ Map.lookup cmpId cmpMap , let cmpName = maybe (unCompanyKey cmpId) companyName $ Map.lookup cmpId cmpMap

View File

@ -21,6 +21,7 @@ import Import
import Handler.Utils import Handler.Utils
import Handler.Utils.Profile import Handler.Utils.Profile
import Handler.Utils.Users import Handler.Utils.Users
import Handler.Utils.Company
import Utils.Print (validCmdArgument) import Utils.Print (validCmdArgument)
@ -587,9 +588,7 @@ makeProfileData :: Entity User -> DB Widget
makeProfileData usrEnt@(Entity uid User{..}) = do makeProfileData usrEnt@(Entity uid User{..}) = do
now <- liftIO getCurrentTime now <- liftIO getCurrentTime
avsId <- entityVal <<$>> getBy (UniqueUserAvsUser uid) avsId <- entityVal <<$>> getBy (UniqueUserAvsUser uid)
(actualPrefersPostal, actualPostAddress, actualDisplayEmail) <- getPostalPreferenceAndAddress' usrEnt (actualPrefersPostal, (actualPostAddress, postalAutomatic), (actualDisplayEmail, emailAutomatic)) <- getPostalPreferenceAndAddress' usrEnt
let postalAutomatic = isJust actualPostAddress && isNothing userPostAddress -- address is either from company or department
emailAutomatic = isJust actualDisplayEmail && not (validEmail' userDisplayEmail)
functions <- Map.fromListWith Set.union . map (\(Entity _ UserFunction{..}) -> (userFunctionFunction, Set.singleton userFunctionSchool)) <$> selectList [UserFunctionUser ==. uid] [] functions <- Map.fromListWith Set.union . map (\(Entity _ UserFunction{..}) -> (userFunctionFunction, Set.singleton userFunctionSchool)) <$> selectList [UserFunctionUser ==. uid] []
lecture_corrector <- E.select $ E.distinct $ E.from $ \(sheet `E.InnerJoin` corrector `E.InnerJoin` course) -> do lecture_corrector <- E.select $ E.distinct $ E.from $ \(sheet `E.InnerJoin` corrector `E.InnerJoin` course) -> do
E.on $ sheet E.^. SheetCourse E.==. course E.^. CourseId E.on $ sheet E.^. SheetCourse E.==. course E.^. CourseId
@ -601,14 +600,7 @@ makeProfileData usrEnt@(Entity uid User{..}) = do
E.on $ studyfeat E.^. StudyFeaturesDegree E.==. studydegree E.^. StudyDegreeId E.on $ studyfeat E.^. StudyFeaturesDegree E.==. studydegree E.^. StudyDegreeId
E.where_ $ studyfeat E.^. StudyFeaturesUser E.==. E.val uid E.where_ $ studyfeat E.^. StudyFeaturesUser E.==. E.val uid
return (studyfeat, studydegree, studyterms) return (studyfeat, studydegree, studyterms)
companies' <- E.select $ E.from $ \(usrComp `E.InnerJoin` comp) -> do companies <- wgtCompanies uid
E.on $ usrComp E.^. UserCompanyCompany E.==. comp E.^. CompanyId
E.where_ $ usrComp E.^. UserCompanyUser E.==. E.val uid
E.orderBy [E.asc (comp E.^. CompanyName)] -- E.desc (usrComp E.^. UserCompanySupervisor),
return (comp E.^. CompanyName, usrComp E.^. UserCompanySupervisor)
let companies = intersperse (text2markup ", ") $
(\(E.Value cmpName, E.Value cmpSpr) -> text2markup (CI.original cmpName) <> bool mempty icnSuper cmpSpr) <$> companies'
icnSuper = text2markup " " <> icon IconSupervisor
supervisors' <- E.select $ E.from $ \(spvr `E.InnerJoin` usrSpvr) -> do supervisors' <- E.select $ E.from $ \(spvr `E.InnerJoin` usrSpvr) -> do
E.on $ spvr E.^. UserSupervisorSupervisor E.==. usrSpvr E.^. UserId E.on $ spvr E.^. UserSupervisorSupervisor E.==. usrSpvr E.^. UserId
E.where_ $ spvr E.^. UserSupervisorUser E.==. E.val uid E.where_ $ spvr E.^. UserSupervisorUser E.==. E.val uid
@ -645,7 +637,6 @@ makeProfileData usrEnt@(Entity uid User{..}) = do
showAdminInfo <- pure (mCRoute == Just (AdminUserR cID)) `or2M` hasReadAccessTo (AdminUserR cID) showAdminInfo <- pure (mCRoute == Just (AdminUserR cID)) `or2M` hasReadAccessTo (AdminUserR cID)
tooltipAvsPersNo <- messageI Info MsgAvsPersonNoNotId tooltipAvsPersNo <- messageI Info MsgAvsPersonNoNotId
tooltipInvalidEmail <- messageI Error MsgInvalidEmailAddress tooltipInvalidEmail <- messageI Error MsgInvalidEmailAddress
let profileRemarks = $(i18nWidgetFile "profile-remarks") let profileRemarks = $(i18nWidgetFile "profile-remarks")
return $(widgetFile "profileData") return $(widgetFile "profileData")

View File

@ -378,7 +378,7 @@ mkQualificationTable isAdmin (Entity qid quali) acts cols psValidator = do
-- E.where_ $ usrComp E.^. UserCompanyUser E.==. E.val (entityKey usr) -- E.where_ $ usrComp E.^. UserCompanyUser E.==. E.val (entityKey usr)
-- E.orderBy [E.asc (comp E.^. CompanyName)] -- E.orderBy [E.asc (comp E.^. CompanyName)]
-- return (comp E.^. CompanyName, comp E.^. CompanyAvsId, usrComp E.^. UserCompanySupervisor) -- return (comp E.^. CompanyName, comp E.^. CompanyAvsId, usrComp E.^. UserCompanySupervisor)
cmpUsr <- selectList [UserCompanyUser ==. entityKey usr] [Asc UserCompanyCompany] cmpUsr <- selectList [UserCompanyUser ==. entityKey usr] [Desc UserCompanyPriority, Asc UserCompanyCompany, LimitTo 1]
return (qualUsr, usr, lmsUsr, qUsrBlock, cmpUsr) return (qualUsr, usr, lmsUsr, qUsrBlock, cmpUsr)
dbtColonnade = cols cmpMap dbtColonnade = cols cmpMap
dbtSorting = mconcat dbtSorting = mconcat
@ -578,7 +578,7 @@ postQualificationR sid qsh = do
[ dbSelect (applying _2) id (return . view (hasEntity . _entityKey)) [ dbSelect (applying _2) id (return . view (hasEntity . _entityKey))
, colUserNameModalHdr MsgLmsUser linkUserName , colUserNameModalHdr MsgLmsUser linkUserName
, colUserEmail , colUserEmail
, sortable (Just "user-company") (i18nCell MsgTableCompanies) $ \( view resultCompanyUser -> cmps) -> , sortable (Just "user-company") (i18nCell MsgTableCompany) $ \( view resultCompanyUser -> cmps) ->
let cs = [ companyCell (unCompanyKey cmpId) cmpName cmpSpr let cs = [ companyCell (unCompanyKey cmpId) cmpName cmpSpr
| Entity _ UserCompany{userCompanyCompany=cmpId, userCompanySupervisor=cmpSpr} <- cmps | Entity _ UserCompany{userCompanyCompany=cmpId, userCompanySupervisor=cmpSpr} <- cmps
, let cmpName = maybe (unCompanyKey cmpId) companyName $ Map.lookup cmpId cmpMap , let cmpName = maybe (unCompanyKey cmpId) companyName $ Map.lookup cmpId cmpMap

View File

@ -17,6 +17,7 @@ import Handler.Utils
import Handler.Utils.Users import Handler.Utils.Users
import Handler.Utils.Invitations import Handler.Utils.Invitations
import Handler.Utils.Avs import Handler.Utils.Avs
import Handler.Utils.Company
import qualified Auth.LDAP as Auth import qualified Auth.LDAP as Auth
@ -107,19 +108,11 @@ postUsersR = do
(AdminUserR <$> encrypt uid) (AdminUserR <$> encrypt uid)
(nameWidget userDisplayName userSurname) (nameWidget userDisplayName userSurname)
, sortable (Just "matriculation") (i18nCell MsgTableMatrikelNr) $ \DBRow{ dbrOutput = entUsr } -> cellHasMatrikelnummerLinkedAdmin entUsr , sortable (Just "matriculation") (i18nCell MsgTableMatrikelNr) $ \DBRow{ dbrOutput = entUsr } -> cellHasMatrikelnummerLinkedAdmin entUsr
, sortable (Just "user-company") (i18nCell MsgTableCompanies) $ \DBRow{ dbrOutput = Entity uid _ } -> flip (set' cellContents) mempty $ do -- why does sqlCell not work here? Mismatch "YesodDB UniWorX" and "RWST (Maybe (Env,FileEnv), UniWorX, [Lang]) Enctype Ints (HandlerFor UniWorX" , sortable (Just "user-company") (i18nCell MsgTableCompanies) $ \DBRow{ dbrOutput = Entity uid _ } -> flip (set' cellContents) mempty $ liftHandler $ runDB $ -- why does sqlCell not work here? Mismatch "YesodDB UniWorX" and "RWST (Maybe (Env,FileEnv), UniWorX, [Lang]) Enctype Ints (HandlerFor UniWorX"
companies' <- liftHandler . runDB . E.select $ E.from $ \(usrComp `E.InnerJoin` comp) -> do maybeMonoid <$> wgtCompanies uid
E.on $ usrComp E.^. UserCompanyCompany E.==. comp E.^. CompanyId , sortable (Just "personal-number") (i18nCell MsgCompanyPersonalNumber) $ \DBRow{ dbrOutput = Entity uid User{..} } -> anchorCellM
E.where_ $ usrComp E.^. UserCompanyUser E.==. E.val uid (AdminUserR <$> encrypt uid)
E.orderBy [E.asc (comp E.^. CompanyName)] (toWgt userCompanyPersonalNumber)
return (comp E.^. CompanyShorthand, comp E.^. CompanyName, usrComp E.^. UserCompanySupervisor)
let icnSuper = toWidget $ text2markup " " <> icon IconSupervisor
companies =
(\(E.Value cmpSh, E.Value cmpName, E.Value cmpSpr) -> simpleLink (citext2widget cmpName) (FirmUsersR cmpSh) <> bool mempty icnSuper cmpSpr) <$> companies'
pure $ intercalate (text2widget "; ") companies
-- , sortable (Just "personal-number") (i18nCell MsgCompanyPersonalNumber) $ \DBRow{ dbrOutput = Entity uid User{..} } -> anchorCellM
-- (AdminUserR <$> encrypt uid)
-- (toWgt userCompanyPersonalNumber)
, sortable (Just "personal-number") (i18nCell MsgCompanyPersonalNumber) $ \DBRow{ dbrOutput = Entity _uid User{..} } -> cellMaybe textCell userCompanyPersonalNumber , sortable (Just "personal-number") (i18nCell MsgCompanyPersonalNumber) $ \DBRow{ dbrOutput = Entity _uid User{..} } -> cellMaybe textCell userCompanyPersonalNumber
, sortable (Just "company-department") (i18nCell MsgCompanyDepartment) $ \DBRow{ dbrOutput = Entity _uid User{..} } -> cellMaybe textCell userCompanyDepartment , sortable (Just "company-department") (i18nCell MsgCompanyDepartment) $ \DBRow{ dbrOutput = Entity _uid User{..} } -> cellMaybe textCell userCompanyDepartment
-- , sortable (Just "last-name") (i18nCell MsgName) $ \DBRow{ dbrOutput = Entity uid User{..} } -> anchorCellM -- , sortable (Just "last-name") (i18nCell MsgName) $ \DBRow{ dbrOutput = Entity uid User{..} } -> anchorCellM

View File

@ -191,3 +191,11 @@ msgAdminProblem AdminProblemNewlyUnsupervised{adminProblemCompanyOld=comp, admin
SomeMessages [SomeMessage MsgAdminProblemNewlyUnsupervised, text2message ": ", maybe (text2message "???") company2msg comp, text2message " -> ", company2msg newComp] SomeMessages [SomeMessage MsgAdminProblemNewlyUnsupervised, text2message ": ", maybe (text2message "???") company2msg comp, text2message " -> ", company2msg newComp]
msgAdminProblem AdminProblemUnknown{adminProblemText=err} = return $ msgAdminProblem AdminProblemUnknown{adminProblemText=err} = return $
someMessages ["Problem: ", err] someMessages ["Problem: ", err]
updateAutomatic :: Bool -> Widget
-- updateAutomatic = iconTooltip [whamlet|_{MsgNoAutomaticUpdateTip}|] (Just IconLocked)
updateAutomatic True = mempty
updateAutomatic False = do
msg <- messageIconI Warning IconLocked MsgNoAutomaticUpdateTip
messageTooltip msg

View File

@ -340,17 +340,25 @@ updateAvsUserByADC (AvsDataContact apid newAvsPersonInfo newAvsFirmInfo) = runMa
, CheckUpdate UserDisplayName _avsInfoDisplayName , CheckUpdate UserDisplayName _avsInfoDisplayName
, CheckUpdate UserBirthday _avsInfoDateOfBirth , CheckUpdate UserBirthday _avsInfoDateOfBirth
, CheckUpdate UserMobile _avsInfoPersonMobilePhoneNo , CheckUpdate UserMobile _avsInfoPersonMobilePhoneNo
, CheckUpdate UserMatrikelnummer $ _avsInfoPersonNo . re _Just -- Maybe im User, aber nicht im AvsInfo; also: `re _Just` work like `to Just` , CheckUpdate UserMatrikelnummer $ _avsInfoPersonNo . re _Just -- Maybe im User, aber nicht im AvsInfo; also: `re _Just` work like `to Just`
, CheckUpdate UserCompanyPersonalNumber $ _avsInfoInternalPersonalNo . _Just . _avsInternalPersonalNo . re _Just -- Maybe im User und im AvsInfo , CheckUpdate UserCompanyPersonalNumber $ _avsInfoInternalPersonalNo . _Just . _avsInternalPersonalNo . re _Just -- Maybe im User und im AvsInfo
] ]
em_p_up = mkUpdate' usr newAvsPersonInfo oldAvsPersonInfo $ apiEmail = _avsInfoPersonEMail . to (fromMaybe mempty) . from _CI
CheckUpdate UserDisplayEmail $ _avsInfoPersonEMail . to (fromMaybe mempty) . from _CI -- Maybe im AvsInfo, aber nicht im User afiEmail = _avsFirmPrimaryEmail . to (fromMaybe mempty) . from _CI
em_f_up = mkUpdate' usr newAvsFirmInfo oldAvsFirmInfo $ -- Email updates erfolgen nur, wenn identisch. Für Firmen-Email leer lassen. em_p_up = mkUpdate' usr newAvsPersonInfo oldAvsPersonInfo $ CheckUpdate UserDisplayEmail apiEmail -- Maybe im AvsInfo, aber nicht im User
CheckUpdate UserDisplayEmail $ _avsFirmPrimaryEmail . to (fromMaybe mempty) . from _CI em_f_up = mkUpdate' usr newAvsFirmInfo oldAvsFirmInfo $ CheckUpdate UserDisplayEmail afiEmail -- Email updates erfolgen nur, wenn identisch. Für Firmen-Email leer lassen.
eml_up = em_p_up <|> em_f_up -- ensure that only one email update is produced; there is no Eq instance for the Update type eml_up -- Ensure that only one email update is produced; there is no Eq instance for the Update type
frm_up = mkUpdate' usr newAvsFirmInfo oldAvsFirmInfo $ -- Legacy, if company postal is stored in user; should no longer be true for new users, | isJust em_f_up, mempty == newAvsFirmInfo ^. afiEmail -- Was some FirmEmail, but this is no longer the case; update to PersonalEmail, if possible
CheckUpdate UserPostAddress _avsFirmPostAddress -- since company address should now be referenced with UserCompany instead = mkUpdate' usr newAvsPersonInfo Nothing $ CheckUpdate UserDisplayEmail apiEmail
pin_up = mkUpdate' usr newAvsCardNo oldAvsCardNo $ -- Maybe update PDF pin to latest card | isJust em_f_up -- Update FirmEmail
= em_f_up
| isJust em_p_up, mempty == newAvsPersonInfo ^. apiEmail -- Was PersonalEmai, but this is no longer the case; update to FirmEmail, if possible
= mkUpdate' usr newAvsFirmInfo Nothing $ CheckUpdate UserDisplayEmail afiEmail
| otherwise -- Maybe update PersonalEmail
= em_p_up
frm_up = mkUpdate' usr newAvsFirmInfo oldAvsFirmInfo $ -- Legacy, if company postal is stored in user; should no longer be true for new users,
CheckUpdate UserPostAddress _avsFirmPostAddress -- since company address should now be referenced with UserCompany instead
pin_up = mkUpdate' usr newAvsCardNo oldAvsCardNo $ -- Maybe update PDF pin to latest card
CheckUpdate UserPinPassword $ to $ fmap avsFullCardNo2pin -- _Just . to avsFullCardNo2pin . re _Just CheckUpdate UserPinPassword $ to $ fmap avsFullCardNo2pin -- _Just . to avsFullCardNo2pin . re _Just
usr_up1 = eml_up `mcons` (frm_up `mcons` (pin_up `mcons` per_ups)) usr_up1 = eml_up `mcons` (frm_up `mcons` (pin_up `mcons` per_ups))
avs_ups = ((UserAvsNoPerson =.) <$> readMay (avsInfoPersonNo newAvsPersonInfo)) `mcons` avs_ups = ((UserAvsNoPerson =.) <$> readMay (avsInfoPersonNo newAvsPersonInfo)) `mcons`
@ -557,15 +565,17 @@ repsertSuperiorSupervisor cid afi uid =
-- | Query DB from given AvsFirmInfo. Guarantees that all Uniqueness-Constraints are checked. Highly unlikely that Nothing is returned, since all AvsResponseContact always contains an AvsFirmInfo -- | Query DB from given AvsFirmInfo. Guarantees that all Uniqueness-Constraints are checked. Highly unlikely that Nothing is returned, since all AvsResponseContact always contains an AvsFirmInfo
getAvsCompany :: AvsFirmInfo -> DB (Maybe (Entity Company)) getAvsCompany :: AvsFirmInfo -> DB (Maybe (Entity Company))
getAvsCompany afi = getAvsCompany afi =
let compName :: CompanyName let compName :: CompanyName
compName = afi ^. _avsFirmFirm . from _CI compName = afi ^. _avsFirmFirm . from _CI
compShorthand :: CompanyShorthand compShorthand :: CompanyShorthand
compShorthand = afi ^. _avsFirmAbbreviation . from _CI compShorthand = afi ^. _avsFirmAbbreviation . from _CI
compAvsId = afi ^. _avsFirmFirmNo compAvsId = afi ^. _avsFirmFirmNo
in firstJustM $ in firstJustM $ -- legacy treatment, only use UniqueCompnayAvsId in the future
bcons (compAvsId > 0) guardMonoid (compAvsId > 0)
( getBy $ UniqueCompanyAvsId compAvsId ) [ getBy $ UniqueCompanyAvsId compAvsId
[ getBy $ UniqueCompanyName compName , getEntity $ CompanyKey $ compShorthand <> "-" <> ciShow compAvsId
] <>
[ getByFilter [CompanyName ==. compName]
, getEntity $ CompanyKey compShorthand , getEntity $ CompanyKey compShorthand
] ]
@ -575,17 +585,20 @@ upsertAvsCompany newAvsFirmInfo mbOldAvsFirmInfo = do
mbFirmEnt <- getAvsCompany newAvsFirmInfo -- primarily by AvsId, then Shorthand, then name mbFirmEnt <- getAvsCompany newAvsFirmInfo -- primarily by AvsId, then Shorthand, then name
$logInfoS "AVS" [st|upsertAvsCompany: old #{tshow mbFirmEnt} new #{tshow newAvsFirmInfo}|] $logInfoS "AVS" [st|upsertAvsCompany: old #{tshow mbFirmEnt} new #{tshow newAvsFirmInfo}|]
case (mbFirmEnt, mbOldAvsFirmInfo) of case (mbFirmEnt, mbOldAvsFirmInfo) of
(Nothing, _) -> do -- insert new company, neither AvsId, Shorthand or Name are known to exist (Nothing, _) -> do -- insert new company, neither AvsId nor Shorthand exist in DB
let upd = flip updateRecord newAvsFirmInfo afn <- if 0 < newAvsFirmInfo ^. _avsFirmFirmNo
then return $ newAvsFirmInfo ^. _avsFirmFirmNo
else maybe (-1) (pred . companyAvsId . entityVal) <$> selectMaybe [CompanyAvsId <. 0] [Asc CompanyAvsId]
let upd = flip updateRecord newAvsFirmInfo
dmy = Company -- mostly dummy, values are actually prodcued through firmInfo2company below for consistency dmy = Company -- mostly dummy, values are actually prodcued through firmInfo2company below for consistency
{ companyName = newAvsFirmInfo ^. _avsFirmFirm . from _CI { companyName = newAvsFirmInfo ^. _avsFirmFirm . from _CI
, companyShorthand = newAvsFirmInfo ^. _avsFirmAbbreviation . from _CI , companyShorthand = newAvsFirmInfo ^. _avsFirmAbbreviation . from _CI
, companyAvsId = newAvsFirmInfo ^. _avsFirmFirmNo , companyAvsId = afn
, companyPrefersPostal = True , companyPrefersPostal = True
, companyPostAddress = newAvsFirmInfo ^. _avsFirmPostAddress , companyPostAddress = newAvsFirmInfo ^. _avsFirmPostAddress
, companyEmail = newAvsFirmInfo ^? _avsFirmPrimaryEmail . _Just . from _CI , companyEmail = newAvsFirmInfo ^? _avsFirmPrimaryEmail . _Just . from _CI
} }
cmp = foldl' upd dmy $ firmInfo2key : firmInfo2companyUniques <> firmInfo2company cmp = foldl' upd dmy $ firmInfo2key : firmInfo2companyNo : firmInfo2company
$logInfoS "AVS" $ "Insert new company: " <> tshow cmp $logInfoS "AVS" $ "Insert new company: " <> tshow cmp
newCmp <- insertEntity cmp newCmp <- insertEntity cmp
reportAdminProblem $ AdminProblemNewCompany $ entityKey newCmp reportAdminProblem $ AdminProblemNewCompany $ entityKey newCmp
@ -595,30 +608,33 @@ upsertAvsCompany newAvsFirmInfo mbOldAvsFirmInfo = do
(Just Entity{entityKey=firmid, entityVal=firm}, oldAvsFirmInfo) -> do -- possibly update existing company, if isJust oldAvsFirmInfo and changed occurred (Just Entity{entityKey=firmid, entityVal=firm}, oldAvsFirmInfo) -> do -- possibly update existing company, if isJust oldAvsFirmInfo and changed occurred
let cmp_ups = mapMaybe (mkUpdate' firm newAvsFirmInfo oldAvsFirmInfo) firmInfo2company let cmp_ups = mapMaybe (mkUpdate' firm newAvsFirmInfo oldAvsFirmInfo) firmInfo2company
key_ups = mkUpdate' firm newAvsFirmInfo oldAvsFirmInfo firmInfo2key key_ups = mkUpdate' firm newAvsFirmInfo oldAvsFirmInfo firmInfo2key
uniq_ups <- maybeMapM (mkUpdateCheckUnique' firm newAvsFirmInfo oldAvsFirmInfo) firmInfo2companyUniques uniq_ups <- mkUpdateCheckUnique' firm newAvsFirmInfo oldAvsFirmInfo firmInfo2companyNo
$logInfoS "AVS" [st|Update company #{companyShorthand firm}: #{tshow (length cmp_ups)}, #{tshow (length key_ups)}, #{tshow (length uniq_ups)} for #{tshow newAvsFirmInfo}|] $logInfoS "AVS" [st|Update company #{companyShorthand firm}: #{tshow (length cmp_ups)}, #{tshow (length key_ups)}, #{tshow (length uniq_ups)} for #{tshow oldAvsFirmInfo}|]
res_cmp <- updateGetEntity firmid $ cmp_ups <> uniq_ups res_cmp <- updateGetEntity firmid $ mcons uniq_ups cmp_ups
case key_ups of let cmp_id = res_cmp ^. _entityVal . _companyAvsId
Nothing -> do res_cmp2 <- case key_ups of
$logInfoS "AVS" "Update new company completed." Just key_up | cmp_id > 0 -> do
return res_cmp $logInfoS "AVS" $ "Updating CompanyShorthand from " <> ciOriginal (companyShorthand firm) <> " to " <> avsFirmAbbreviation newAvsFirmInfo <> " for AvsNo " <> tshow cmp_id
Just key_up -> do let uniq_cmp = UniqueCompanyAvsId cmp_id
let compId = res_cmp ^. _entityVal . _companyAvsId cmp_key = newAvsFirmInfo ^. _avsFirmAbbreviation . from _CI
uniq_cmp = if compId > 0 then UniqueCompanyAvsId compId alt_key = cmp_key <> "-" <> ciShow cmp_id
else UniqueCompanyName $ res_cmp ^. _entityVal . _companyName key_ok <- notExists [CompanyShorthand ==. cmp_key]
updateBy uniq_cmp [key_up] -- this is ok, since we have OnUpdateCascade on all CompanyId entries alt_ok <- notExists [CompanyShorthand ==. alt_key]
$logInfoS "AVS" "Update new company completed." if | key_ok -> updateBy uniq_cmp [key_up] -- this is ok, since we have OnUpdateCascade on all CompanyId entries
| alt_ok -> updateBy uniq_cmp [CompanyShorthand =. alt_key]
| otherwise -> $logInfoS "AVS" $ "Update company shorthand failed for " <> ciOriginal cmp_key <> " and " <> ciOriginal alt_key
maybeM (return res_cmp) return $ getBy uniq_cmp maybeM (return res_cmp) return $ getBy uniq_cmp
_otherwise -> return res_cmp
$logInfoS "AVS" "Update company completed."
return res_cmp2
where where
firmInfo2key = firmInfo2key =
CheckUpdate CompanyShorthand $ _avsFirmAbbreviation . from _CI -- Updating primary key works in principle thanks to OnUpdateCascade, but fails due to update get CheckUpdate CompanyShorthand $ _avsFirmAbbreviation . from _CI -- Updating primary key works in principle thanks to OnUpdateCascade, but fails due to update get
firmInfo2companyUniques = firmInfo2companyNo =
[ CheckUpdate CompanyName $ _avsFirmFirm . from _CI -- Updating unique turned out to be problematic, who would have thought! CheckUpdate CompanyAvsId _avsFirmFirmNo -- Updating a unique needs special considerations; AVS does not update FirmNo, but for legacy reasons we might have companies without a number
, CheckUpdate CompanyAvsId _avsFirmFirmNo -- Updating unique turned out to be problematic, who would have thought!
]
firmInfo2company = firmInfo2company =
[ CheckUpdate CompanyPostAddress _avsFirmPostAddress [ CheckUpdate CompanyName $ _avsFirmFirm . from _CI
, CheckUpdate CompanyPostAddress _avsFirmPostAddress
, CheckUpdate CompanyEmail $ _avsFirmPrimaryEmail . _Just . from _CI . re _Just , CheckUpdate CompanyEmail $ _avsFirmPrimaryEmail . _Just . from _CI . re _Just
-- , CheckUpdate CompanyPrefersPostal _avsFirmPrefersPostal -- Guessing here is not useful, since postal preference is ignored anyway when there is only one option available -- , CheckUpdate CompanyPrefersPostal _avsFirmPrefersPostal -- Guessing here is not useful, since postal preference is ignored anyway when there is only one option available
] ]

View File

@ -13,17 +13,43 @@ import Import
-- import qualified Data.Text as Text -- import qualified Data.Text as Text
import Database.Persist.Postgresql import Database.Persist.Postgresql
-- import Database.Esqueleto.Experimental ((:&)(..)) import Database.Esqueleto.Experimental ((:&)(..))
import qualified Database.Esqueleto.Experimental as E -- needs TypeApplications Lang-Pragma import qualified Database.Esqueleto.Experimental as E -- needs TypeApplications Lang-Pragma
import qualified Database.Esqueleto.Utils as E import qualified Database.Esqueleto.Utils as E
import qualified Database.Esqueleto.PostgreSQL as E import qualified Database.Esqueleto.PostgreSQL as E
import Handler.Utils.Users import Handler.Utils.Users
import Handler.Utils.Widgets
company2msg :: CompanyId -> SomeMessage UniWorX company2msg :: CompanyId -> SomeMessage UniWorX
company2msg = text2message . ciOriginal . unCompanyKey company2msg = text2message . ciOriginal . unCompanyKey
wgtCompanies :: UserId -> DB (Maybe Widget)
wgtCompanies = \uid -> do
companies <- E.select $ do
(usrComp :& comp) <- E.from $ E.table @UserCompany `E.innerJoin` E.table @Company
`E.on` (\(usrComp :& comp) -> usrComp E.^. UserCompanyCompany E.==. comp E.^. CompanyId)
E.where_ $ usrComp E.^. UserCompanyUser E.==. E.val uid
E.orderBy [E.asc (comp E.^. CompanyName)]
return (comp E.^. CompanyShorthand, comp E.^. CompanyName, usrComp E.^. UserCompanySupervisor, usrComp E.^. UserCompanyPriority)
let (mPri, topCmp, otherCmp) = procCmp mPri companies
resWgt =
[whamlet|
$forall c <- topCmp
<p>
^{c}
$forall c <- otherCmp
<p>
#{c}
|]
return $ toMaybe (notNull topCmp) resWgt
where
procCmp _ [] = (0, [],[])
procCmp maxPri ((E.Value cmpSh, E.Value cmpName, E.Value cmpSpr, E.Value cmpPrio) : cs) =
let cmpWgt = companyWidget (cmpSh, cmpName, cmpSpr)
isTop = cmpPrio >= maxPri
(accPri,accTop,accRem) = procCmp maxPri cs
in (max cmpPrio accPri, bool accTop (cmpWgt : accTop) isTop, bool (cmpName : accRem) accRem isTop) -- lazy evaluation after repmin example
-- TODO: use this function in company view Handler.Firm #157 -- TODO: use this function in company view Handler.Firm #157
-- | add all company supervisors for a given users -- | add all company supervisors for a given users

View File

@ -356,10 +356,11 @@ courseCell Course{..} = anchorCell link name `mappend` desc
^{modal "Beschreibung" (Right $ toWidget descr)} ^{modal "Beschreibung" (Right $ toWidget descr)}
|] |]
-- also see Handler.Utils.Widgets.companyWidget
companyCell :: IsDBTable m a => CompanyShorthand -> CompanyName -> Bool -> DBCell m a companyCell :: IsDBTable m a => CompanyShorthand -> CompanyName -> Bool -> DBCell m a
companyCell csh cname isSupervisor = anchorCell link name companyCell csh cname isSupervisor = anchorCell curl name
where where
link = FirmUsersR csh curl = FirmUsersR csh
corg = ciOriginal cname corg = ciOriginal cname
name name
| isSupervisor = text2markup (corg <> " ") <> icon IconSupervisor | isSupervisor = text2markup (corg <> " ") <> icon IconSupervisor

View File

@ -18,6 +18,7 @@ module Handler.Utils.Users
, getUserPrimaryCompany, getUserPrimaryCompanyAddress , getUserPrimaryCompany, getUserPrimaryCompanyAddress
, getUserEmail , getUserEmail
, getEmailAddress, getJustEmailAddress , getEmailAddress, getJustEmailAddress
, getUserEmailAutomatic
, getEmailAddressFor, getJustEmailAddressFor , getEmailAddressFor, getJustEmailAddressFor
, getPostalAddress, getPostalAddress' , getPostalAddress, getPostalAddress'
, getPostalPreferenceAndAddress, getPostalPreferenceAndAddress' , getPostalPreferenceAndAddress, getPostalPreferenceAndAddress'
@ -102,13 +103,13 @@ getPostalPreferenceAndAddress usr = do
-- | result (True, Nothing, Nothing) indicates that neither userEmail nor userPostAddress is known -- | result (True, Nothing, Nothing) indicates that neither userEmail nor userPostAddress is known
-- primed variant returns storedMarkup without prefixed userDisplayName -- primed variant returns storedMarkup without prefixed userDisplayName
getPostalPreferenceAndAddress' :: Entity User -> DB (Bool, Maybe StoredMarkup, Maybe UserEmail) getPostalPreferenceAndAddress' :: Entity User -> DB (Bool, (Maybe StoredMarkup, Bool), (Maybe UserEmail, Bool))
getPostalPreferenceAndAddress' usr = do getPostalPreferenceAndAddress' usr = do
pa <- getPostalAddress' usr pa <- getPostalAddress' usr
em <- getUserEmail usr em <- getUserEmailAutomatic usr
let usrPrefPost = usr ^. _entityVal . _userPrefersPostal let usrPrefPost = usr ^. _entityVal . _userPrefersPostal
finalPref = (usrPrefPost && isJust pa) || isNothing em finalPref = (usrPrefPost && isJust (fst pa)) || isNothing (fst em)
-- finalPref = isJust pa && (usrPrefPost || isNothing em) -- finalPref = isJust (fst pa) && (usrPrefPost || isNothing (fst em))
return (finalPref, pa, em) return (finalPref, pa, em)
getEmailAddressFor :: UserId -> DB (Maybe Address) getEmailAddressFor :: UserId -> DB (Maybe Address)
@ -133,6 +134,21 @@ getUserEmail Entity{entityKey=uid, entityVal=User{userDisplayEmail, userEmail}}
compEmailMb <- getUserPrimaryCompanyAddress uid companyEmail compEmailMb <- getUserPrimaryCompanyAddress uid companyEmail
return $ pickValidEmail' $ mcons compEmailMb [userEmail] return $ pickValidEmail' $ mcons compEmailMb [userEmail]
-- like `getUserEmail`, but also checks whether the Email will be update automatically
getUserEmailAutomatic :: Entity User -> DB (Maybe UserEmail, Bool)
getUserEmailAutomatic Entity{entityKey=uid, entityVal=User{userDisplayEmail, userEmail}}
| validEmail' userDisplayEmail
= do
muavs <- getBy $ UniqueUserAvsUser uid
let auto = userDisplayEmail == muavs ^. _Just . _userAvsLastFirmInfo . _Just . _avsFirmPrimaryEmail . _Just . from _CI -- Recall: _Just on Nothing yields mempty here
|| userDisplayEmail == muavs ^. _Just . _userAvsLastPersonInfo . _Just . _avsInfoPersonEMail . _Just . from _CI
return (Just userDisplayEmail, auto)
| otherwise
= getUserPrimaryCompanyAddress uid companyEmail >>= \case
Just compEmail | validEmail' compEmail -> return (Just compEmail, True )
Nothing | validEmail' userEmail -> return (Just userEmail, False)
_ -> return (Nothing , False)
-- address is prefixed with userDisplayName -- address is prefixed with userDisplayName
getPostalAddress :: Entity User -> DB (Maybe [Text]) getPostalAddress :: Entity User -> DB (Maybe [Text])
getPostalAddress Entity{entityKey=uid, entityVal=User{..}} getPostalAddress Entity{entityKey=uid, entityVal=User{..}}
@ -151,22 +167,25 @@ getPostalAddress Entity{entityKey=uid, entityVal=User{..}}
where where
prefixMarkupName = return . Just . (userDisplayName :) . html2textlines prefixMarkupName = return . Just . (userDisplayName :) . html2textlines
-- primed variant returns storedMarkup without prefixed userDisplayName -- primed variant returns storedMarkup without prefixed userDisplayName and whether updates are automatic
getPostalAddress' :: Entity User -> DB (Maybe StoredMarkup) getPostalAddress' :: Entity User -> DB (Maybe StoredMarkup, Bool)
getPostalAddress' Entity{entityKey=uid, entityVal=User{..}} getPostalAddress' Entity{entityKey=uid, entityVal=User{..}}
| res@(Just _) <- userPostAddress | res@(Just upo) <- userPostAddress
= return res = do
muavs <- getBy $ UniqueUserAvsUser uid
let auto = upo == muavs ^. _Just . _userAvsLastFirmInfo . _Just . _avsFirmPostAddress . _Just -- Recall: _Just on Nothing yields mempty here
return (res, auto)
| otherwise | otherwise
= do = do
getUserPrimaryCompanyAddress uid companyPostAddress >>= \case getUserPrimaryCompanyAddress uid companyPostAddress >>= \case
res@(Just _) res@(Just _)
-> return res -> return (res, True)
Nothing Nothing
| Just abt <- userCompanyDepartment | Just abt <- userCompanyDepartment
-> return $ Just $ plaintextToStoredMarkup $ textUnlines $ -> return $ (,True) $ Just $ plaintextToStoredMarkup $ textUnlines $
if | "BVD" `isPrefixOf` abt -> [userDisplayName, abt, "Bodenverkehrsdienste"] if | "BVD" `isPrefixOf` abt -> [userDisplayName, abt, "Bodenverkehrsdienste"]
| otherwise -> [userDisplayName, abt, "Hausbriefkasten" ] | otherwise -> [userDisplayName, abt, "Hausbriefkasten" ]
| otherwise -> return Nothing | otherwise -> return (Nothing, True)
-- | Consider using Handler.Utils.Avs.updateReceivers instead -- | Consider using Handler.Utils.Avs.updateReceivers instead
-- Return Entity User and all Supervisors with rerouteNotifications as well as -- Return Entity User and all Supervisors with rerouteNotifications as well as

View File

@ -14,6 +14,7 @@ import Handler.Utils.DateTime
import qualified Data.Char as Char import qualified Data.Char as Char
import qualified Data.HashMap.Strict as Aeson -- ON UPDATE replace with: import qualified Data.Aeson.KeyMap as Aeson import qualified Data.HashMap.Strict as Aeson -- ON UPDATE replace with: import qualified Data.Aeson.KeyMap as Aeson
import Data.Scientific
--------- ---------
-- Simple utilities for consistent display -- Simple utilities for consistent display
@ -131,6 +132,16 @@ modalAccess wdgtNo wdgtYes writeAccess route = do
then modal wdgtYes (Left $ SomeRoute route) then modal wdgtYes (Left $ SomeRoute route)
else wdgtNo else wdgtNo
-- also see Handler.Utils.Table.Cells.companyCell
companyWidget :: (CompanyShorthand, CompanyName, Bool) -> Widget
companyWidget (csh, cname, isSupervisor) = simpleLink (toWgt name) curl
where
curl = FirmUsersR csh
corg = ciOriginal cname
name
| isSupervisor = text2markup (corg <> " ") <> icon IconSupervisor
| otherwise = text2markup corg
---------- ----------
-- HEAT -- -- HEAT --
---------- ----------
@ -253,7 +264,9 @@ jsonWidget x = jsonWidgetAux $ toJSON x
jsonWidgetAux Null = [whamlet|Null|] jsonWidgetAux Null = [whamlet|Null|]
jsonWidgetAux (Bool b) = toWidget $ boolSymbol b jsonWidgetAux (Bool b) = toWidget $ boolSymbol b
jsonWidgetAux (String s) = [whamlet|#{s}|] jsonWidgetAux (String s) = [whamlet|#{s}|]
jsonWidgetAux (Number n) = [whamlet|#{show n}|] jsonWidgetAux (Number n)
| isInteger n = [whamlet|#{formatScientific Fixed (Just 0) n}|]
| otherwise = [whamlet|#{formatScientific Generic Nothing n}|]
jsonWidgetAux (Array l) jsonWidgetAux (Array l)
| 1 >= length l = foldMap jsonWidgetAux l -- empty arrays don't show | 1 >= length l = foldMap jsonWidgetAux l -- empty arrays don't show
| otherwise = | otherwise =

View File

@ -710,6 +710,10 @@ bcons :: Bool -> a -> [a] -> [a]
bcons False _ = id bcons False _ = id
bcons True x = (x:) bcons True x = (x:)
bsnoc :: Bool -> a -> [a] -> [a]
bsnoc False _ xs = xs
bsnoc True x xs = xs ++ [x]
-- | Merge/Add any attribute-value pair to an existing list of such pairs. -- | Merge/Add any attribute-value pair to an existing list of such pairs.
-- If the attribute exists, the new valu will be prepended, separated by a single empty space -- If the attribute exists, the new valu will be prepended, separated by a single empty space
insertAttr :: Text -> Text -> [(Text,Text)] -> [(Text,Text)] insertAttr :: Text -> Text -> [(Text,Text)] -> [(Text,Text)]

View File

@ -102,7 +102,7 @@ mkAvsQuery _ _ _ = AvsQuery
AvsQueryPerson{avsPersonQueryCardNo=Just (AvsCardNo "00006666"), avsPersonQueryVersionNo=Just "4"} -> AvsResponsePerson $ sumpfi1 <> sumpfi2 <> sumpfi3 AvsQueryPerson{avsPersonQueryCardNo=Just (AvsCardNo "00006666"), avsPersonQueryVersionNo=Just "4"} -> AvsResponsePerson $ sumpfi1 <> sumpfi2 <> sumpfi3
AvsQueryPerson{avsPersonQueryCardNo=Just (AvsCardNo "00007777"), avsPersonQueryVersionNo=Just "4"} -> AvsResponsePerson $ sumpfi1 <> sumpfi2 <> sumpfi3 AvsQueryPerson{avsPersonQueryCardNo=Just (AvsCardNo "00007777"), avsPersonQueryVersionNo=Just "4"} -> AvsResponsePerson $ sumpfi1 <> sumpfi2 <> sumpfi3
AvsQueryPerson{avsPersonQueryCardNo=Just (AvsCardNo "00008888"), avsPersonQueryVersionNo=Just "4"} -> AvsResponsePerson $ sumpfi1 <> sumpfi2 <> sumpfi3 AvsQueryPerson{avsPersonQueryCardNo=Just (AvsCardNo "00008888"), avsPersonQueryVersionNo=Just "4"} -> AvsResponsePerson $ sumpfi1 <> sumpfi2 <> sumpfi3
_ -> AvsResponsePerson mempty _ -> AvsResponsePerson steffen
fakeStatus :: AvsQueryStatus -> AvsResponseStatus fakeStatus :: AvsQueryStatus -> AvsResponseStatus
fakeStatus (AvsQueryStatus (Set.toList -> (api:_))) = AvsResponseStatus $ Set.singleton $ AvsStatusPerson api $ Set.fromList fakeStatus (AvsQueryStatus (Set.toList -> (api:_))) = AvsResponseStatus $ Set.singleton $ AvsStatusPerson api $ Set.fromList

View File

@ -82,6 +82,9 @@ getEntity404 :: (PersistStoreRead backend, PersistRecordBackend record backend,
=> Key record -> ReaderT backend m (Entity record) => Key record -> ReaderT backend m (Entity record)
getEntity404 k = Entity k <$> get404 k getEntity404 k = Entity k <$> get404 k
notExists :: (PersistRecordBackend record backend, PersistQueryRead backend, MonadIO m) => [Filter record] -> ReaderT backend m Bool
notExists = fmap not . exists
existsBy :: (PersistRecordBackend record backend, PersistUniqueRead backend, MonadIO m) existsBy :: (PersistRecordBackend record backend, PersistUniqueRead backend, MonadIO m)
=> Unique record -> ReaderT backend m Bool => Unique record -> ReaderT backend m Bool
existsBy = fmap (is _Just) . getKeyBy existsBy = fmap (is _Just) . getKeyBy
@ -108,6 +111,7 @@ existsKey404 :: (PersistRecordBackend record backend, PersistQueryRead backend,
existsKey404 = bool notFound (return ()) <=< existsKey existsKey404 = bool notFound (return ()) <=< existsKey
-- | given filter criteria like `selectList` this function returns Just if and only if there is precisely one result -- | given filter criteria like `selectList` this function returns Just if and only if there is precisely one result
-- getByPeseudoUnique
getByFilter :: (PersistRecordBackend record backend, PersistQueryRead backend, MonadIO m) getByFilter :: (PersistRecordBackend record backend, PersistQueryRead backend, MonadIO m)
=> [Filter record] -> ReaderT backend m (Maybe (Entity record)) => [Filter record] -> ReaderT backend m (Maybe (Entity record))
getByFilter crit = getByFilter crit =
@ -368,7 +372,6 @@ updateRecord ent new (CheckUpdate up l) =
-- | like mkUpdate' but only returns the update if the new value would be unique -- | like mkUpdate' but only returns the update if the new value would be unique
-- mkUpdateCheckUnique' :: PersistEntity record => record -> iraw -> Maybe iraw -> CheckUpdate record iraw -> DB (Maybe (Update record)) -- mkUpdateCheckUnique' :: PersistEntity record => record -> iraw -> Maybe iraw -> CheckUpdate record iraw -> DB (Maybe (Update record))
mkUpdateCheckUnique' :: (MonadIO m, PersistQueryRead backend, PersistEntity record, PersistEntityBackend record ~ BaseBackend backend) mkUpdateCheckUnique' :: (MonadIO m, PersistQueryRead backend, PersistEntity record, PersistEntityBackend record ~ BaseBackend backend)
=> record -> a -> Maybe a -> CheckUpdate record a -> ReaderT backend m (Maybe (Update record)) => record -> a -> Maybe a -> CheckUpdate record a -> ReaderT backend m (Maybe (Update record))

View File

@ -118,7 +118,7 @@ data Icon
| IconCompany | IconCompany
| IconEdit | IconEdit
| IconUserEdit | IconUserEdit
| IconMagic -- indicates automatic updates -- | IconMagic -- indicates automatic updates
deriving (Eq, Ord, Enum, Bounded, Show, Read, Generic) deriving (Eq, Ord, Enum, Bounded, Show, Read, Generic)
deriving anyclass (Universe, Finite, NFData) deriving anyclass (Universe, Finite, NFData)
@ -215,7 +215,7 @@ iconText = \case
IconCompany -> "building" IconCompany -> "building"
IconEdit -> "edit" IconEdit -> "edit"
IconUserEdit -> "user-edit" IconUserEdit -> "user-edit"
IconMagic -> "wand-magic" -- IconMagic -> "wand-magic"
nullaryPathPiece ''Icon $ camelToPathPiece' 1 nullaryPathPiece ''Icon $ camelToPathPiece' 1
deriveLift ''Icon deriveLift ''Icon
@ -298,10 +298,11 @@ isNew :: Bool -> Markup
isNew True = icon IconNew isNew True = icon IconNew
isNew False = mempty isNew False = mempty
-- ^ Maybe display an icon that denotes that something™ is automagically updated or derived -- DEPRECATED by Handler.Utils.updateAutomatic, which includes a helpful tooltip
isAutomatic :: Bool -> Markup -- Maybe display an icon that denotes that something™ is NOT automagically updated or derived, but had been edited
isAutomatic True = icon IconMagic -- isAutomatic :: Bool -> Markup
isAutomatic False = mempty -- isAutomatic True = mempty -- icon IconMagic
-- isAutomatic False = icon IconLocked -- IconEdit
boolSymbol :: Bool -> Markup boolSymbol :: Bool -> Markup
boolSymbol True = icon IconOK boolSymbol True = icon IconOK

View File

@ -44,6 +44,10 @@ $# SPDX-License-Identifier: AGPL-3.0-or-later
<p> <p>
Unverarbeitete Antwort: # Unverarbeitete Antwort: #
^{answer} ^{answer}
$maybe apid <- mapid
<p>
Einzelne erhaltene AVS PersonId #{show apid} wurde auch gleich
in die Status und Contact Abfragen eingesetzt.
<section> <section>
<p> <p>

View File

@ -7,21 +7,17 @@ $# SPDX-License-Identifier: AGPL-3.0-or-later
<section> <section>
<h2>Hinweise <h2>Hinweise
<ul> <ul>
<li>
Sichern Sie bitte Ihre Daten! Die Uni2work Datenbank wird täglich gesichert;
dennoch können wir Probleme noch nicht gänzlich ausschließen.
<li> <li>
Nicht aufgeführt sind Zeitstempel mit Benutzerinformationen, z.B. bei der Editierung und Korrektur von Übungen, Kursleiterschaft, Raumbuchungen, etc. Nicht aufgeführt sind Zeitstempel mit Benutzerinformationen, z.B. bei der Editierung und Korrektur von Übungen, Kursleiterschaft, Raumbuchungen, etc.
<li> <li>
<p> <p>
Sie können die Sie können die
<a href=@{HelpR}> <a href=@{HelpR}>
Löschung Ihre Daten über eine Supportanfrage beantragen Löschung Ihrer Daten über eine Supportanfrage beantragen
. Ihre Daten werden dann nach Ablauf einer Frist gelöscht. . Ihre Daten werden dann nach Ablauf einer Frist gelöscht.
Daten, welche keiner gesetzlichen Aufbewahrungsfrist unterliegen Daten, welche keiner gesetzlichen Aufbewahrungsfrist unterliegen
(z.B. Klausurnoten) verbleiben im System bis zur Ablauf der Aufbewahrungsfrist. verbleiben im System bis zur Ablauf der Aufbewahrungsfrist.
<p> <p>
Benutzerdaten bleiben prinzipiell so lange gespeichert, Benutzerdaten bleiben prinzipiell so lange gespeichert,
bis ein Bereichsadministrator über die Exmatrikulation informiert wurde. bis der Account nach einer angemessenen Zeitverzögerung nach Ablauf aller Qualifikation automatisch gelöscht wurde.
Dann wird der Account mit einer angemessenen zeitverzögerung gelöscht.
Anonymisierte Prüfungsnoten verbleiben aus statistischen Gründen dauerhaft im System. Anonymisierte Prüfungsnoten verbleiben aus statistischen Gründen dauerhaft im System.

View File

@ -7,9 +7,6 @@ $# SPDX-License-Identifier: AGPL-3.0-or-later
<section> <section>
<h2>Remarks <h2>Remarks
<ul> <ul>
<li>
Back up your data! Uni2work's database is backed up daily but we can
nontheless not guarantee that there will be no problems.
<li> <li>
Timestamps with user information (e.g. editing of corrections, submission groups, rooms, ...) are not shown here. Timestamps with user information (e.g. editing of corrections, submission groups, rooms, ...) are not shown here.
<li> <li>
@ -19,11 +16,10 @@ $# SPDX-License-Identifier: AGPL-3.0-or-later
a support request a support request
. .
Your data will then be deleted after a suitable time period has passed. Your data will then be deleted after a suitable time period has passed.
Data that falls under legal retention periods (e.g. exam results) remian Data that falls under legal retention periods remain
in the system until their retention period has passed. in the system until their retention period has passed.
<p> <p>
User data remains in the system (in principle) until a department User data remains in the system until
administrator has been informed of exmatriculation. a suitable time period has passed after the expiry all qualifications and the account is automatically deleted.
After a suitable time period has passed the account is deleted. Anonymised online exam results remain in the system indefinitely for
Anonymised exam results remain in the system indefinitely for
statistical purposes. statistical purposes.

View File

@ -162,7 +162,7 @@ $endif$
\opening{$en-opening$} \opening{$en-opening$}
$endif$ $endif$
\begin{textblock}{65}(84,232)%hpos,vpos \begin{textblock}{65}(92,236)%hpos,vpos Werte in mm
\textcolor{black!39}{ \textcolor{black!39}{
\begin{labeling}{Password:}%Achtung! Die Position des Logins muss sprachunabhängig immer an der gleichen Position sein, sonst kannn die Rückmeldung der Druckerei den Ident nicht mehr identifizieren! \begin{labeling}{Password:}%Achtung! Die Position des Logins muss sprachunabhängig immer an der gleichen Position sein, sonst kannn die Rückmeldung der Druckerei den Ident nicht mehr identifizieren!
$if(is-de)$ $if(is-de)$
@ -192,7 +192,7 @@ $endif$
$endif$ $endif$
$if(notice)$ $if(notice)$
\begin{textblock}{170}(20,258)%hpos,vpos \begin{textblock}{170}(20,262)%hpos,vpos Werte in mm
\scriptsize \scriptsize
\textbf{Hinweise für den Schulungsteilnehmer:} \textbf{Hinweise für den Schulungsteilnehmer:}
\newline \newline

View File

@ -13,10 +13,10 @@ de-opening: Liebe Fahrberechtigungsinhaber,
en-opening: Dear driver, en-opening: Dear driver,
de-closing: | de-closing: |
Mit freundlichen Grüßen, Mit freundlichen Grüßen,
Ihre Fraport Fahrerausbildung Ihre Fahrerausbildung
en-closing: | en-closing: |
With kind regards, With kind regards,
Your Fraport Driver Training Your Driver Training
encludes: encludes:
hyperrefoptions: hidelinks hyperrefoptions: hidelinks

View File

@ -37,7 +37,7 @@ $# SPDX-License-Identifier: AGPL-3.0-or-later
<dt .deflist__dt> <dt .deflist__dt>
_{MsgTableMatrikelNr} _{MsgTableMatrikelNr}
<dd .deflist__dd> <dd .deflist__dd>
#{matnr} ^{modalAccess (text2widget matnr) (text2widget matnr) False (AdminAvsUserR cID)}
$maybe sex <- userSex $maybe sex <- userSex
<dt .deflist__dt> <dt .deflist__dt>
_{MsgTableSex} _{MsgTableSex}
@ -56,9 +56,9 @@ $# SPDX-License-Identifier: AGPL-3.0-or-later
#{iconLetterOrEmail userPrefersPostal} #{iconLetterOrEmail userPrefersPostal}
$maybe addr <- actualPostAddress $maybe addr <- actualPostAddress
<dt .deflist__dt> <dt .deflist__dt>
_{MsgAdminUserPostAddress} _{MsgAdminUserPostAddress} #
^{updateAutomatic postalAutomatic}
<dd .deflist__dd> <dd .deflist__dd>
#{isAutomatic postalAutomatic} #
#{addr} #{addr}
$if (not postalAutomatic) $if (not postalAutomatic)
$maybe postUpdate <- userPostLastUpdate $maybe postUpdate <- userPostLastUpdate
@ -67,10 +67,10 @@ $# SPDX-License-Identifier: AGPL-3.0-or-later
<dd .deflist__dd> <dd .deflist__dd>
^{formatTimeW SelFormatDateTime postUpdate} ^{formatTimeW SelFormatDateTime postUpdate}
<dt .deflist__dt> <dt .deflist__dt>
_{MsgUserDisplayEmail} _{MsgUserDisplayEmail} #
^{updateAutomatic emailAutomatic}
<dd .deflist__dd .email> <dd .deflist__dd .email>
$maybe primaryEmail <- actualDisplayEmail $maybe primaryEmail <- actualDisplayEmail
#{isAutomatic emailAutomatic} #
#{mailtoHtml primaryEmail} #{mailtoHtml primaryEmail}
$nothing $nothing
^{messageTooltip tooltipInvalidEmail} # ^{messageTooltip tooltipInvalidEmail} #
@ -109,11 +109,11 @@ $# SPDX-License-Identifier: AGPL-3.0-or-later
_{MsgCompanyPersonalNumber} _{MsgCompanyPersonalNumber}
<dd .deflist__dd> <dd .deflist__dd>
#{companyPersonalNumber} #{companyPersonalNumber}
$if not $ null companies $maybe compWgt <- companies
<dt .deflist__dt> <dt .deflist__dt>
_{MsgCompany} _{MsgCompany}
<dd .deflist__dd> <dd .deflist__dd>
^{toWgt (mconcat companies)} ^{compWgt}
$if numSupervisors > 0 $if numSupervisors > 0
<dt .deflist__dt>_{MsgProfileSupervisor} <dt .deflist__dt>_{MsgProfileSupervisor}
$if numSupervisors > 3 $if numSupervisors > 3