Merge branch 'master' into fradrive/driving-course-participants

This commit is contained in:
Sarah Vaupel 2022-12-13 22:23:19 +01:00
commit 5a2d2247ad
337 changed files with 1338 additions and 2268 deletions

View File

@ -2,6 +2,28 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
## [26.6.6](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v26.6.5...v26.6.6) (2022-12-12)
## [26.6.5](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v26.6.4...v26.6.5) (2022-12-05)
## [26.6.4](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v26.6.3...v26.6.4) (2022-12-02)
## [26.6.3](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v26.6.2...v26.6.3) (2022-11-30)
### Bug Fixes
* **avs:** normalize internal personal numbers between LDAP and AVS ([b20008d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b20008d3bcb730ff76a76ce2928364e6ce9e7c35))
## [26.6.2](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v26.6.1...v26.6.2) (2022-11-29)
## [26.6.1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v26.6.0...v26.6.1) (2022-11-28)
### Bug Fixes
* **lms:** filtering qualifications by supervisor works properly now ([15f7a75](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/15f7a7576ab48a362a479f43034510b4e80bb1b2))
## [26.6.0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v26.5.14...v26.6.0) (2022-11-18)

View File

@ -276,6 +276,7 @@ user-defaults:
show-sex: false
exam-office-get-synced: true
exam-office-get-labels: true
prefers-postal: true
instance-id: "_env:INSTANCE_ID:instance"

View File

@ -79,7 +79,7 @@ StudyFeatureInferenceNoNameConflicts: Keine Konflikte beobachtet
StudyFeatureInferenceNameConflictsHeading: Studiengangseinträge mit beobachteten Konflikten
AdminHeading !ident-ok: Administration
AdminPageEmpty: Diese Seite soll eine Übersichtsseite für Administrator:innen werden. Aktuell finden sich hier nur Links zu wichtigen Administrator-Funktionalitäten.
BearerTokenImpersonate: Auftreten als
BearerTokenImpersonateNone: Keine Änderung
BearerTokenImpersonateSingle: Einzelner Benutzer/Einzelne Benutzerin
@ -94,3 +94,22 @@ BearerTokenArchiveName !ident-ok: tokens.zip
TestDownloadDirect: Direkte Generierung
TestDownloadInTransaction: Generierung während Datenbank-Transaktion
TestDownloadFromDatabase: Generierung während Download aus Datenbank
ProblemsHeading: Problemübersicht
ProblemsHeadingDrivers: Fahrberechtigungen
ProblemsAvsProblem: Synchronisation mit AVS/MoBaKo komplett fehlgeschlagen
ProblemsDriverSynch n@Int: #{tshow n} Diskrepanzen zwischen AVS und FRADrive
ProblemsDriverSynch0: Alle Sperrungen von Fahrberechtigungen sind im AVS eingetragen
ProblemsDriverSynch1: Alle gültigen Vorfeld-Fahrberechtigungen 'F' sind im AVS eingetragen
ProblemsDriverSynch2: Alle gültigen Rollfeld-Fahrberechtigungen 'R' sind im AVS eingetragen
ProblemsRDriversHaveFs: Alle Inhaber einer Rollfeld-Fahrberechtigung besitzen auch eine gültige Vorfeld-Fahrberechtigung
ProblemsDriversHaveAvsIds: Alle Inhaber einer Fahrberechtigung konnten einer AVS Identifikationsnummer zugeordnet werden
ProblemsHeadingUsers: Allgemein
ProblemsUsersAreReachable: Für alle Benutzer ist eine E-Mail oder postalische Adresse bekannt
ProblemsNoStalePrintJobs n@Integer: Alle Briefversandaufträge der vergangenen #{show n} Tage wurden von der Druckerei bestätigt
ProblemsUnreachableHeading: Unerreichbare Benutzer
ProblemsUnreachableBody: Benutzer ohne E-Mail oder Postadresse, welche z.B. bei ablaufenden Berechtigungen nicht benachrichtigt werden können:
ProblemsRWithoutFHeading: Fahrer mit R ohne F
ProblemsRWithoutFBody: Diese Fahrer sind wegen einer ungültigen Vorfeld-Fahrberechtigung komplett gesperrt, obwohl eine gültige Rollfeld-Fahrberechtigung besteht:
ProblemsNoAvsIdHeading: Fahrer ohne AVS-Id
ProblemsNoAvsIdBody: Fahrer mit gültiger Fahrberechtigung in FRADrive, welche trotzdem nicht fahren dürfen, da die Fahrberechtigung aufgrund einer unbekannten AVS Id nicht an die Ausweisstelle übermittelt werden konnte:

View File

@ -79,7 +79,6 @@ StudyFeatureInferenceNoNameConflicts: No observed conflicts
StudyFeatureInferenceNameConflictsHeading: Fields of study with observed conflicts
AdminHeading: Administration
AdminPageEmpty: This page shall provide an overview for administrators in the future. For now there are only links to important administrator-functions.
BearerTokenImpersonate: Impersonate
BearerTokenImpersonateNone: No one
@ -95,3 +94,22 @@ BearerTokenArchiveName: tokens.zip
TestDownloadDirect: Direct generation
TestDownloadInTransaction: Generate during database transaction
TestDownloadFromDatabase: Generate while streaming from database
ProblemsHeading: Overview Problems
ProblemsHeadingDrivers: Driving Licences
ProblemsAvsProblem: Synchronisation with AVS/MoBaKo failed entirely
ProblemsDriverSynch n: #{tshow n} mismatches between AVS and FRADrive
ProblemsDriverSynch0: All revocations of driving licences were successfully registered with AVS
ProblemsDriverSynch1: All valid apron driving licences 'F' were successfully registered with AVS
ProblemsDriverSynch2: All valid maneuvering area driving licences 'R' were successfully registered with AVS
ProblemsRDriversHaveFs: All driving licence 'R' holders also have a valid 'F' licence
ProblemsDriversHaveAvsIds: All driving licence holder could be matched with their AVS id
ProblemsHeadingUsers: Miscellaneous
ProblemsUsersAreReachable: Either Email or postal address is known for all users
ProblemsNoStalePrintJobs n: All requests for letter mailing within the last #{show n} days were acknowledged as printed by the airport printing center
ProblemsUnreachableHeading: Unreachable Users
ProblemsUnreachableBody: Users without Email nor postal address, who thus cannot be notified about expiring qualifications:
ProblemsRWithoutFHeading: Drivers having 'R' but not 'F'
ProblemsRWithoutFBody: Drivers without apron driving licence are prohibited from driving, even if they own a valid maneuvering driving licence:
ProblemsNoAvsIdHeading: Drivers without AVS id
ProblemsNoAvsIdBody: Drivers having a valid apron driving licence within FRADrive only, but who may not drive since a missing AVS id prevents communication of the driving licence to AVS:

View File

@ -28,7 +28,6 @@ UnauthorizedExamExamOffice: Es existieren keine Prüfungsergebnisse für Nutzer:
UnauthorizedSchoolExamOffice: Sie sind nicht mit Prüfungsverwaltung für dieses Institut beauftragt.
UnauthorizedSystemExamOffice: Sie sind nicht mit systemweiter Prüfungsverwaltung beauftragt.
UnauthorizedSystemPrinter: Sie sind nicht mit systemweitem Druck und Briefversand beauftragt.
UnauthorizedSystemSap: Sie sind nicht mit der systemweitem SAP Schnittstellenverwaltung beauftragt.
UnauthorizedExternalExamExamOffice: Es existieren keine Prüfungsergebnisse für Nutzer:innen, für die Sie mit der Prüfungsverwaltung beauftragt sind.
UnauthorizedEvaluation: Sie sind nicht mit der Kursumfragenverwaltung beauftragt.
UnauthorizedSchoolLecturer: Sie sind nicht als Veranstalter:in für dieses Institut eingetragen.

View File

@ -29,7 +29,6 @@ UnauthorizedExamExamOffice: You are not part of the appropriate exam office for
UnauthorizedSchoolExamOffice: You are not part of an exam office for this school.
UnauthorizedSystemExamOffice: You are not charged with system wide exam administration.
UnauthorizedSystemPrinter: You are not charged with system wide letter printing.
UnauthorizedSystemSap: You are not charged with system wide SAP administration.
UnauthorizedExternalExamExamOffice: You are not part of the appropriate exam office for any of the participants of this exam.
UnauthorizedSchoolLecturer: You are no lecturer for this department.
UnauthorizedLecturer: You are no administrator for this course.

View File

@ -10,4 +10,6 @@ AvsLastName: Nachname
AvsInternalPersonalNo: Personalnummer (nur Fraport AG)
AvsVersionNo: Versionsnummer
AvsQueryEmpty: Bitte mindestens ein Anfragefeld ausfüllen!
AvsQueryStatusInvalid t@Text: Nur numerische IDs eingeben, durch Komma getrennt! Erhalten: #{show t}
AvsQueryStatusInvalid t@Text: Nur numerische IDs eingeben, durch Komma getrennt! Erhalten: #{show t}
AvsLicence: Fahrberechtigung
AvsPersonNoNotId: AVS Personennummer dient zur menschlichen Kommunikation mit der Ausweisstelle und darf nicht verwechselt werden mit der maschinell verwendeten AVS Personen Id

View File

@ -10,4 +10,6 @@ AvsLastName: Last name
AvsInternalPersonalNo: Personnel number (Fraport AG only)
AvsVersionNo: Version number
AvsQueryEmpty: At least one query field must be filled!
AvsQueryStatusInvalid t: Numeric IDs only, comma seperated! #{show t}
AvsQueryStatusInvalid t: Numeric IDs only, comma seperated! #{show t}
AvsLicence: Driving Licence
AvsPersonNoNotId: AVS person number is used in human communication only and must not be mistaken for the AVS personen id used in machine communications

View File

@ -18,5 +18,4 @@ BothSubmissions: Abgabe direkt in Uni2work oder extern mit Pseudonym
SystemExamOffice: Prüfungsverwaltung
SystemFaculty: Fakultätsmitglied
SystemStudent: Student:in
SystemPrinter: Drucker:in
SystemSap: SAP Verwalter:in
SystemPrinter: Drucker:in

View File

@ -18,5 +18,4 @@ BothSubmissions: Submission either directly in Uni2work or externally via pseudo
SystemExamOffice: Exam office
SystemFaculty: Faculty member
SystemStudent: Student
SystemPrinter: Printing staff
SystemSap: SAP Administrator
SystemPrinter: Printing staff

View File

@ -14,9 +14,9 @@ TableQualificationCountActive: Active
TableQualificationCountActiveTooltip: Number of currently valid qualification holders
TableQualificationCountTotal: Total
TableQualificationIsAvsLicence: AVS Driving License
TableQualificationIsAvsLicenceTooltip: Is this Qualification synchronized with AVS? Only applies to qualification holders having an AVS PersonID.
TableQualificationIsAvsLicenceTooltip: Under which name is this qualification synchronized with AVS, if any? Only applies to qualification holders having an AVS PersonID.
TableQualificationSapExport: Sent to SAP
TableQualificationSapExportTooltip: Is this Qualification transmitted to SAP? Only applies to qualification holder having a Fraport Personnelnumber.
TableQualificationSapExportTooltip: Is this qualification transmitted to SAP? Only applies to qualification holder having a Fraport AG personnel number.
LmsQualificationValidUntil: Valid until
TableQualificationLastRefresh: Last renewed
TableQualificationFirstHeld: First held

View File

@ -14,7 +14,6 @@ AuthTagAdmin: Nutzer:in ist Administrator:in
AuthTagExamOffice: Nutzer:in ist mit Prüfungsverwaltung beauftragt
AuthTagSystemExamOffice: Nutzer:in ist mit systemweiter Prüfungsverwaltung beauftragt
AuthTagSystemPrinter: Nutzer:in ist mit systemweiten Druck von Briefen beauftragt
AuthTagSystemSap: Nutzer:in ist mit systemweiter SAP Schnittstellen-Administration beauftragt
AuthTagEvaluation: Nutzer:in ist mit Kursumfragenverwaltung beauftragt
AuthTagToken: Nutzer:in präsentiert Authorisierungs-Token
AuthTagNoEscalation: Nutzer-Rechte werden nicht auf fremde Institute ausgeweitet

View File

@ -14,7 +14,6 @@ AuthTagAdmin: User is administrator
AuthTagExamOffice: User is part of an exam office
AuthTagSystemExamOffice: User is charged with system wide exam administration
AuthTagSystemPrinter: User is responsible for system wide letter printing
AuthTagSystemSap: User is responsible for system wide SAP interface administration
AuthTagEvaluation: User is charged with course evaluation
AuthTagToken: User is presenting an authorisation-token
AuthTagNoEscalation: User permissions are not being expanded to other departments

View File

@ -27,7 +27,11 @@ ProfileCorrectorRemark: Die oberhalb angezeigte Tabelle zeigt nur prinzipielle E
ProfileCorrections: Auflistung aller zugewiesenen Korrekturen
Remarks: Hinweise
ProfileSupervisor: Übergeordnete Ansprechpartner
ProfileSupervisee: Ist Ansprechpartner für
UserTelephone: Telefon
UserMobile: Mobiltelefon
Company: Firmenzugehörigkeit
CompanyPersonalNumber: Personalnummer (nur Fraport AG)
CompanyDepartment: Abteilung

View File

@ -27,7 +27,11 @@ ProfileCorrectorRemark: The table above only shows registration as a corrector i
ProfileCorrections: List of all assigned corrections
Remarks: Remarks
ProfileSupervisor: Supervised by
ProfileSupervisee: Supervises
UserTelephone: Phone
UserMobile: Mobile
Company: Company affilitaion
CompanyPersonalNumber: Personnel number (Fraport AG only)
CompanyDepartment: Department

View File

@ -28,7 +28,7 @@ TermLectureStartTooltip: Muss am oder nach dem Beginn liegen
TermLectureEndTooltip: Muss am oder vor dem Ende liegen
TermActive: Aktiv
TermActiveTooltip: Zeitraum in dem Lehrende Kurse anlegen dürfen; kann auf angegebene Lehrende eingeschränkt werden
TermActiveForPlaceholder: Email (optional)
TermActiveForPlaceholder: E-Mail (optional)
NumCourses num@Int64: #{num} #{pluralDE num "Kurs" "Kurse"}
TermsHeading: Semesterübersicht
TermEditHeading: Semester editieren/anlegen

View File

@ -28,7 +28,7 @@ TermLectureStartTooltip: Must be on or after starting day
TermLectureEndTooltip: Must be before or on ending day
TermActive: Active
TermActiveTooltip: Timeframe when lecturers may add courses; maybe restricted for specified lecturers
TermActiveForPlaceholder: E-Mail (optional)
TermActiveForPlaceholder: Email (optional)
NumCourses num: #{num} #{pluralEN num "course" "courses"}
TermsHeading: Semesters
TermEditHeading: Edit semester

View File

@ -13,7 +13,7 @@ AdminUserAuth: Authentifizierung
AdminUserMatriculation: Matrikelnummer
AdminUserSex: Geschlecht
AdminUserTelephone: Telefonnummer
AdminUserMobile: Mobiltelefonmummer
AdminUserMobile: Mobiltelefonnummer
AdminUserFPersonalNumber: Personalnummer (nur Fraport AG)
AdminUserFDepartment: Abteilung
AdminUserPostAddress: Postalische Anschrift

View File

@ -72,3 +72,5 @@ TableExamOfficeLabel: Label-Name
TableExamOfficeLabelStatus: Label-Farbe
TableExamOfficeLabelPriority: Label-Priorität
TableQualifications: Qualifikationen
TableCompany: Firma
TableSupervisor: Ansprechpartner

View File

@ -72,3 +72,5 @@ TableExamOfficeLabel: Label name
TableExamOfficeLabelStatus: Label colour
TableExamOfficeLabelPriority: Label priority
TableQualifications: Qualifications
TableCompany: Company
TableSupervisor: Supervisor

View File

@ -14,8 +14,9 @@
UserAvs
personId AvsPersonId -- unique identifier for user throughout avs
personId AvsPersonId -- unique identifier for user throughout avs; newtype for Int
user UserId
noPerson Int default=0 -- only needed for manual communication with personnel from Ausweisverwaltungsstelle
UniqueUserAvsUser user
UniqueUserAvsId personId
deriving Generic

View File

@ -15,8 +15,8 @@ Qualification
-- elearningOnly Bool -- successful E-learing automatically increases validity. NO!
-- refreshInvitation StoredMarkup -- hard-coded I18N-MSGs used instead, but displayed on qualification page NO!
-- expiryNotification StoredMarkup Maybe -- configurable user-profile-notifcations are used instead NO!
avsLicence AvsLicence Maybe -- if set, is synchronized to Avs as a driving licence
sapId Text Maybe -- if set, all QualificationUsers with userCompanyPersonalNumber are transmitted via SAP interface under this id
avsLicence AvsLicence Maybe -- if set, valid QualificationUsers are synchronized to AVS as a driving licence
sapId Text Maybe -- if set, valid QualificationUsers with userCompanyPersonalNumber are transmitted via SAP interface under this id
SchoolQualificationShort school shorthand -- must be unique per school and shorthand
SchoolQualificationName school name -- must be unique per school and name
-- across all schools, only one qualification may be a driving licence:

View File

@ -1,3 +1,3 @@
{
"version": "26.6.0"
"version": "26.6.6"
}

View File

@ -1,3 +1,3 @@
{
"version": "26.6.0"
"version": "26.6.6"
}

2
package-lock.json generated
View File

@ -1,6 +1,6 @@
{
"name": "uni2work",
"version": "26.6.0",
"version": "26.6.6",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@ -1,6 +1,6 @@
{
"name": "uni2work",
"version": "26.6.0",
"version": "26.6.6",
"description": "",
"keywords": [],
"author": "",

View File

@ -1,5 +1,5 @@
name: uniworx
version: 26.6.0
version: 26.6.6
dependencies:
- base
- yesod

Binary file not shown.

BIN
resources/FraportIcons.zip Normal file

Binary file not shown.

Binary file not shown.

13
routes
View File

@ -68,6 +68,9 @@
/admin/crontab AdminCrontabR GET
/admin/avs AdminAvsR GET POST
/admin/ldap AdminLdapR GET POST
/admin/problems/no-contact ProblemUnreachableR GET
/admin/problems/no-avs-id ProblemWithoutAvsId GET
/admin/problems/r-without-f ProblemFbutNoR GET
/print PrintCenterR GET POST !system-printer
/print/acknowledge/#Day/#Int/#Int PrintAckR GET POST !system-printer
@ -102,8 +105,8 @@
/user/lang LangR POST !free
/user/storage-key StorageKeyR POST !free
/for/#CryptoUUIDUser/user ForProfileR GET POST !supervisor
/for/#CryptoUUIDUser/user/profile ForProfileDataR GET !supervisor
/for/#CryptoUUIDUser/user ForProfileR GET POST !supervisor !self
/for/#CryptoUUIDUser/user/profile ForProfileDataR GET !supervisor !self
/exam-office ExamOfficeR !exam-office:
@ -254,12 +257,12 @@
-- !/*{CI FilePath} CryptoFileNameDispatchR GET !free -- Disabled until preliminary check for valid cID exists
-- for users
/qualification QualificationAllR GET !free -- TODO repurpose
/qualification/#SchoolId QualificationSchoolR GET !free -- TODO repurpose
/qualification QualificationAllR GET -- TODO repurpose
/qualification/#SchoolId QualificationSchoolR GET -- TODO repurpose
/qualification/#SchoolId/#QualificationShorthand QualificationR GET -- TODO repurpose
-- SAP export
/qualifications/sap/direct QualificationSAPDirectR GET !system-sap
/qualifications/sap/direct QualificationSAPDirectR GET !token
-- OSIS CSV Export Demo
/lms LmsAllR GET POST !free -- TODO verify that this is ok
/lms/#SchoolId LmsSchoolR GET !free -- TODO verify that this is ok

View File

@ -146,7 +146,7 @@ campusUserReTest' :: (MonadMask m, MonadLogger m, MonadUnliftIO m) => Failover (
campusUserReTest' pool doTest mode User{userIdent}
= runMaybeT . catchIfMaybeT (is _CampusUserNoResult) $ campusUserReTest pool doTest mode (Creds apLdap (CI.original userIdent) [])
campusUser :: (MonadUnliftIO m, MonadMask m, MonadLogger m) => Failover (LdapConf, LdapPool) -> FailoverMode -> Creds site -> m (Ldap.AttrList [])
campusUser :: (MonadMask m, MonadUnliftIO m, MonadLogger m) => Failover (LdapConf, LdapPool) -> FailoverMode -> Creds site -> m (Ldap.AttrList [])
campusUser pool mode creds = throwLeft =<< campusUserWith withLdapFailover pool mode creds
campusUser' :: (MonadMask m, MonadUnliftIO m, MonadLogger m) => Failover (LdapConf, LdapPool) -> FailoverMode -> User -> m (Maybe (Ldap.AttrList []))

View File

@ -99,7 +99,7 @@ instance PersistField CalendarDiffDays where
coerceICcd :: Integer -> CDDdb
coerceICcd = fromIntegral
-- placement in Utils impossivle due to cyclic dependencies
-- placement in Utils impossible due to cyclic dependencies
-- Data.Tuple.Extra is not yet a dependency
-- both = join (***) is still too cryptic for me
both :: (a -> b) -> (a, a) -> (b, b)

View File

@ -7,7 +7,7 @@
module Database.Esqueleto.Utils
( true, false
, justVal, justValList
, justVal, justValList, toValues
, isJust, alt
, isInfixOf, hasInfix
, strConcat, substring
@ -50,7 +50,9 @@ import Data.Universe
import qualified Data.Set as Set
import qualified Data.List as List
import qualified Data.Foldable as F
import Data.List.NonEmpty (NonEmpty(..))
import qualified Database.Esqueleto.Legacy as E
import qualified Database.Esqueleto.Experimental as Ex
import qualified Database.Esqueleto.PostgreSQL as E
import qualified Database.Esqueleto.Internal.Internal as E
import Database.Esqueleto.Utils.TH
@ -97,10 +99,15 @@ false = E.val False
-- infinity = unsafeSqlValue "'infinity'"
justVal :: PersistField typ => typ -> E.SqlExpr (E.Value (Maybe typ))
justVal = E.val . Just
-- justVal = E.val . Just
justVal = E.just . E.val
justValList :: PersistField typ => [typ] -> E.SqlExpr (E.ValueList (Maybe typ))
justValList = E.valList . map Just
-- justValList = E.valList . map Just
justValList = E.justList . E.valList
toValues :: PersistField typ => NonEmpty typ -> Ex.From (Ex.SqlExpr (Ex.Value typ)) -- E.From invalid here, requires Esqueleto.Experimental
toValues = E.values . fmap Ex.val
infixl 4 =?.
(=?.) :: PersistField typ => E.SqlExpr (E.Value typ) -> E.SqlExpr (E.Value (Maybe typ)) -> E.SqlExpr (E.Value Bool)

View File

@ -568,15 +568,6 @@ tagAccessPredicate AuthSystemPrinter = cacheAPSystemFunction SystemPrinter (Just
isPrinter <- lift $ exists [UserSystemFunctionUser ==. authId, UserSystemFunctionFunction ==. SystemPrinter, UserSystemFunctionIsOptOut ==. False]
guardMExceptT isPrinter $ unauthorizedI MsgUnauthorizedSystemPrinter
return Authorized
tagAccessPredicate AuthSystemSap = cacheAPSystemFunction SystemSap (Just $ Right diffHour) $ \mAuthId' _ _ sapList -> if
| maybe True (`Set.notMember` sapList) mAuthId' -> Right $ if
| is _Nothing mAuthId' -> return AuthenticationRequired
| otherwise -> unauthorizedI MsgUnauthorizedSystemSap
| otherwise -> Left $ APDB $ \_ _ mAuthId _ _ -> $cachedHereBinary mAuthId . exceptT return return $ do
authId <- maybeExceptT AuthenticationRequired $ return mAuthId
isPrinter <- lift $ exists [UserSystemFunctionUser ==. authId, UserSystemFunctionFunction ==. SystemSap, UserSystemFunctionIsOptOut ==. False]
guardMExceptT isPrinter $ unauthorizedI MsgUnauthorizedSystemSap
return Authorized
tagAccessPredicate AuthStudent = cacheAPSystemFunction SystemStudent (Just $ Right diffHour) $ \mAuthId' _ _ studentList -> if
| maybe True (`Set.notMember` studentList) mAuthId' -> Right $ if
| is _Nothing mAuthId' -> return AuthenticationRequired
@ -1505,6 +1496,8 @@ tagAccessPredicate AuthSelf = APDB $ \_ _ mAuthId route _ -> exceptT return retu
UserNotificationR cID -> return $ Left cID
UserPasswordR cID -> return $ Left cID
CourseR _ _ _ (CUserR cID) -> return $ Left cID
ForProfileR cID -> return $ Left cID
ForProfileDataR cID -> return $ Left cID
_other -> throwError =<< $unsupportedAuthPredicate AuthSelf route
referencedUser <- case referencedUser' of
Right uid -> return uid

View File

@ -106,14 +106,17 @@ breadcrumb (UserPasswordR cID) = useRunDB $ do
breadcrumb AdminNewFunctionaryInviteR = i18nCrumb MsgMenuLecturerInvite $ Just UsersR
breadcrumb AdminFunctionaryInviteR = i18nCrumb MsgBreadcrumbFunctionaryInvite Nothing
breadcrumb AdminR = i18nCrumb MsgAdminHeading Nothing
breadcrumb AdminTestR = i18nCrumb MsgMenuAdminTest $ Just AdminR
breadcrumb AdminTestPdfR = i18nCrumb MsgMenuAdminTest $ Just AdminTestR
breadcrumb AdminErrMsgR = i18nCrumb MsgMenuAdminErrMsg $ Just AdminR
breadcrumb AdminTokensR = i18nCrumb MsgMenuAdminTokens $ Just AdminR
breadcrumb AdminCrontabR = i18nCrumb MsgBreadcrumbAdminCrontab $ Just AdminR
breadcrumb AdminAvsR = i18nCrumb MsgMenuAvs $ Just AdminR
breadcrumb AdminLdapR = i18nCrumb MsgMenuLdap $ Just AdminR
breadcrumb AdminR = i18nCrumb MsgAdminHeading Nothing
breadcrumb AdminTestR = i18nCrumb MsgMenuAdminTest $ Just AdminR
breadcrumb AdminTestPdfR = i18nCrumb MsgMenuAdminTest $ Just AdminTestR
breadcrumb AdminErrMsgR = i18nCrumb MsgMenuAdminErrMsg $ Just AdminR
breadcrumb AdminTokensR = i18nCrumb MsgMenuAdminTokens $ Just AdminR
breadcrumb AdminCrontabR = i18nCrumb MsgBreadcrumbAdminCrontab $ Just AdminR
breadcrumb AdminAvsR = i18nCrumb MsgMenuAvs $ Just AdminR
breadcrumb AdminLdapR = i18nCrumb MsgMenuLdap $ Just AdminR
breadcrumb ProblemUnreachableR = i18nCrumb MsgProblemsUnreachableHeading $Just AdminR
breadcrumb ProblemWithoutAvsId = i18nCrumb MsgProblemsNoAvsIdHeading $ Just AdminR
breadcrumb ProblemFbutNoR = i18nCrumb MsgProblemsRWithoutFHeading $ Just AdminR
breadcrumb PrintCenterR = i18nCrumb MsgMenuApc Nothing
breadcrumb PrintSendR = i18nCrumb MsgMenuPrintSend $ Just PrintCenterR
@ -732,6 +735,14 @@ defaultLinks = fmap catMaybes . mapM runMaybeT $ -- Define the menu items of the
, navIcon = IconMenuAdmin
, navChildren =
[ NavLink
{ navLabel = MsgProblemsHeading
, navRoute = AdminR
, navAccess' = NavAccessTrue
, navType = NavTypeLink { navModal = False }
, navQuick' = mempty
, navForceActive = False
}
, NavLink
{ navLabel = MsgMenuUsers
, navRoute = UsersR
, navAccess' = NavAccessTrue

View File

@ -4,7 +4,8 @@
module Foundation.Yesod.Auth
( authenticate
, upsertCampusUser, upsertCampusUserByCn
, ldapLookupAndUpsert
, upsertCampusUser
, decodeUserTest
, CampusUserConversionException(..)
, campusUserFailoverMode, updateUserLanguage
@ -106,10 +107,10 @@ authenticate creds@Creds{..} = liftHandler . runDB . withReaderT projectBackend
| not isDummy -> res <$ update uid [ UserLastAuthentication =. Just now ]
_other -> return res
$logDebugS "auth" $ tshow Creds{..}
UniWorX{..} <- getYesod
$logDebugS "auth" $ tshow Creds{..}
ldapPool' <- getsYesod $ view _appLdapPool
flip catches excHandlers $ case appLdapPool of
flip catches excHandlers $ case ldapPool' of
Just ldapPool
| Just upsertMode' <- upsertMode -> do
ldapData <- campusUser ldapPool campusUserFailoverMode Creds{..}
@ -152,14 +153,25 @@ _upsertCampusUserMode mMode cs@Creds{..}
defaultOther = apHash
ldapLookupAndUpsert :: forall m. (MonadHandler m, HandlerSite m ~ UniWorX, MonadMask m, MonadUnliftIO m) => Text -> SqlPersistT m (Entity User)
ldapLookupAndUpsert ident =
getsYesod (view _appLdapPool) >>= \case
Nothing -> throwM $ CampusUserLdapError $ LdapHostNotResolved "No LDAP configuration in Foundation."
Just ldapPool ->
campusUser'' ldapPool campusUserFailoverMode ident >>= \case
Nothing -> throwM CampusUserNoResult
Just ldapResponse -> upsertCampusUser UpsertCampusUserGuessUser ldapResponse
{- THIS FUNCION JUST DECODES, BUT IT DOES NOT QUERY LDAP!
upsertCampusUserByCn :: forall m.
( MonadHandler m, HandlerSite m ~ UniWorX
, MonadThrow m
)
=> Text -> SqlPersistT m (Entity User)
upsertCampusUserByCn persNo = upsertCampusUser UpsertCampusUserGuessUser [(ldapPrimaryKey,[Text.encodeUtf8 persNo])]
-}
-- | Upsert User DB according to given LDAP data (does not query LDAP itself)
upsertCampusUser :: forall m.
( MonadHandler m, HandlerSite m ~ UniWorX
, MonadThrow m
@ -208,7 +220,7 @@ decodeUserTest mbIdent ldapData = do
decodeUser :: (MonadThrow m) => UTCTime -> UserDefaultConf -> UpsertCampusUserMode -> Ldap.AttrList [] -> m (User,_)
decodeUser now UserDefaultConf{..} upsertMode ldapData = do
decodeUser now UserDefaultConf{..} upsertMode ldapData = do
let
userTelephone = decodeLdap ldapUserTelephone
userMobile = decodeLdap ldapUserMobile
@ -279,7 +291,7 @@ decodeUser now UserDefaultConf{..} upsertMode ldapData = do
, userMatrikelnummer = Nothing -- not known from LDAP, must be derived from REST interface to AVS TODO
, userPostAddress = Nothing -- not known from LDAP, must be derived from REST interface to AVS TODO
, userPinPassword = Nothing -- must be derived via AVS
, userPrefersPostal = False
, userPrefersPostal = userDefaultPrefersPostal
, ..
}
userUpdate = [

View File

@ -8,6 +8,22 @@ module Handler.Admin
import Import
-- import Data.Either
import qualified Data.Set as Set
-- import qualified Data.Text.Lazy.Encoding as LBS
-- import qualified Control.Monad.Catch as Catch
-- import Servant.Client (ClientError(..), ResponseF(..))
-- import Text.Blaze.Html (preEscapedToHtml)
import Database.Esqueleto.Experimental ((:&)(..))
import qualified Database.Esqueleto.Experimental as E
import qualified Database.Esqueleto.Utils as E
import Handler.Utils.DateTime
import Handler.Utils.Avs
import Handler.Utils.Widgets
import Handler.Admin.Test as Handler.Admin
import Handler.Admin.ErrorMessage as Handler.Admin
import Handler.Admin.Tokens as Handler.Admin
@ -15,8 +31,188 @@ import Handler.Admin.Crontab as Handler.Admin
import Handler.Admin.Avs as Handler.Admin
import Handler.Admin.Ldap as Handler.Admin
getAdminR :: Handler Html
getAdminR =
siteLayoutMsg MsgAdminHeading $ do
setTitleI MsgAdminHeading
i18n MsgAdminPageEmpty
getAdminR = do
now <- liftIO getCurrentTime
let nowaday = utctDay now
cutOffPrintDays = 7
cutOffPrintJob = addLocalDays (-cutOffPrintDays) now
(usersAreReachable, driversHaveAvsIds, rDriversHaveFs, noStalePrintJobs) <- runDB $ (,,,)
<$> areAllUsersReachable
<*> allDriversHaveAvsId nowaday
<*> allRDriversHaveFs nowaday
<*> (not <$> exists [PrintJobAcknowledged ==. Nothing, PrintJobCreated <=. cutOffPrintJob])
diffLics <- try retrieveDifferingLicences <&> \case
-- (Left (UnsupportedContentType "text/html" resp)) -> Left $ text2widget "Html received"
(Left e) -> Left $ text2widget $ tshow (e :: SomeException)
(Right (to0, to1, to2)) -> Right (Set.size to0, Set.size to1, Set.size to2)
-- let procDiffLics (to0, to1, to2) = Right (Set.size to0, Set.size to1, Set.size to2)
-- diffLics <- (procDiffLics <$> retrieveDifferingLicences) `catches`
-- [ Catch.Handler (\case (UnsupportedContentType "text/html;charset=utf-8" Response{responseBody})
-- -> return $ Left $ toWidget $ preEscapedToHtml $ fromRight "Response UTF8-decoding error" $ LBS.decodeUtf8' responseBody
-- ex -> return $ Left $ text2widget $ tshow ex)
-- , Catch.Handler (\(ex::SomeException) -> return $ Left $ text2widget $ tshow ex)
-- ]
-- we abuse messageTooltip for colored icons here
msgSuccessTooltip <- messageI Success MsgMessageSuccess
msgWarningTooltip <- messageI Warning MsgMessageWarning
msgErrorTooltip <- messageI Error MsgMessageError
let flagError = messageTooltip . bool msgErrorTooltip msgSuccessTooltip
flagWarning = messageTooltip . bool msgWarningTooltip msgSuccessTooltip
flagNonZero :: Int -> Widget
flagNonZero n | n <= 0 = flagError True
| otherwise = messageTooltip =<< handlerToWidget (messageI Error (MsgProblemsDriverSynch n))
siteLayoutMsg MsgProblemsHeading $ do
setTitleI MsgProblemsHeading
$(widgetFile "admin-problems")
getProblemUnreachableR :: Handler Html
getProblemUnreachableR = do
unreachables <- runDB $ E.select retrieveUnreachableUsers
siteLayoutMsg MsgProblemsUnreachableHeading $ do
setTitleI MsgProblemsUnreachableHeading
[whamlet|
<section>
_{MsgProblemsUnreachableBody}
<ul>
$forall usr <- unreachables
<li>
^{linkUserWidget ForProfileR usr}
|]
getProblemFbutNoR :: Handler Html
getProblemFbutNoR = do
now <- liftIO getCurrentTime
let nowaday = utctDay now
rnofs <- runDB $ E.select $ retrieveDriversRWithoutF nowaday
siteLayoutMsg MsgProblemsRWithoutFHeading $ do
setTitleI MsgProblemsRWithoutFHeading
[whamlet|
<section>
_{MsgProblemsRWithoutFBody}
<ul>
$forall usr <- rnofs
<li>
^{linkUserWidget AdminUserR usr}
|]
getProblemWithoutAvsId :: Handler Html
getProblemWithoutAvsId = do
now <- liftIO getCurrentTime
let nowaday = utctDay now
rnofs <- runDB $ E.select $ retrieveDriversWithoutAvsId nowaday
siteLayoutMsg MsgProblemsNoAvsIdHeading $ do
setTitleI MsgProblemsNoAvsIdHeading
[whamlet|
<section>
_{MsgProblemsNoAvsIdBody}
<ul>
$forall usr <- rnofs
<li>
^{linkUserWidget AdminUserR usr}
|]
{-
mkUnreachableUsersTable = do
let dbtSQLQuery user -> do
E.where_ $ E.isNothing (user E.^. UserPostAddress)
E.&&. E.not_ ((user E.^. UserEmail) `E.like` E.val "%@%.%")
pure user
dbtRowKey = (E.^. UserId)
dbtProj = dbtProjFilteredPostId -- TODO: still don't understand the choices here
dbtColonnade =
-}
areAllUsersReachable :: DB Bool
-- areAllUsersReachable = isNothing <$> E.selectOne retrieveUnreachableUsers
areAllUsersReachable = E.selectNotExists retrieveUnreachableUsers
retrieveUnreachableUsers :: E.SqlQuery (E.SqlExpr (Entity User))
retrieveUnreachableUsers = do
user <- E.from $ E.table @User
E.where_ $ E.isNothing (user E.^. UserPostAddress)
E.&&. E.not_ ((user E.^. UserEmail) `E.like` E.val "%@%.%")
return user
allDriversHaveAvsId :: Day -> DB Bool
-- allDriversHaveAvsId = fmap isNothing . E.selectOne . retrieveDriversWithoutAvsId
allDriversHaveAvsId = E.selectNotExists . retrieveDriversWithoutAvsId
qIsValid :: E.SqlExpr (Entity QualificationUser) -> Day -> E.SqlExpr (E.Value Bool)
qIsValid qualUsr nowaday =
E.isNothing (qualUsr E.^. QualificationUserBlockedDue) -- not blocked
E.&&. -- currently valid
(E.val nowaday `E.between`
( qualUsr E.^. QualificationUserFirstHeld
, qualUsr E.^. QualificationUserValidUntil))
{-
-- | Returns users more than once if they own multiple avs-related valid licences, but no AvsID is known
retrieveDriversWithoutAvsId' :: Day -> E.SqlQuery (E.SqlExpr (Entity User))
retrieveDriversWithoutAvsId' nowaday = do
(usr :& qualUsr :& qual) <- E.from $ E.table @User
`E.innerJoin` E.table @QualificationUser
`E.on` (\(usr :& qualUsr) -> usr E.^. UserId E.==. qualUsr E.^. QualificationUserUser)
`E.innerJoin` E.table @Qualification
`E.on` (\(_usr :& qualUsr :& qual) -> qual E.^. QualificationId E.==. qualUsr E.^. QualificationUserQualification)
E.where_ $ -- is avs licence
E.isJust (qual E.^. QualificationAvsLicence)
E.&&. (qualUsr `qIsValid` nowaday)
E.&&. -- AvsId is unknown
E.notExists (do
avsUsr <- E.from $ E.table @UserAvs
E.where_ $ avsUsr E.^. UserAvsUser E.==. usr E.^. UserId
)
return usr
-}
-- | Returns users at most once, even if they own multiple avs-related licences, but no AvsID is known
retrieveDriversWithoutAvsId :: Day -> E.SqlQuery (E.SqlExpr (Entity User))
retrieveDriversWithoutAvsId nowaday = do
usr <- E.from $ E.table @User
E.where_ $
E.exists (do -- a valid avs licence
(qual :& qualUsr) <- E.from (E.table @Qualification
`E.innerJoin` E.table @QualificationUser
`E.on` (\(qual :& qualUsr) -> qual E.^. QualificationId E.==. qualUsr E.^. QualificationUserQualification))
E.where_ $ -- is avs licence
E.isJust (qual E.^. QualificationAvsLicence)
E.&&. (qualUsr `qIsValid` nowaday) -- currently valid
E.&&. -- matches user
(qualUsr E.^. QualificationUserUser E.==. usr E.^. UserId)
)
E.&&.
E.notExists (do -- a known AvsId
avsUsr <- E.from $ E.table @UserAvs
E.where_ $ avsUsr E.^. UserAvsUser E.==. usr E.^. UserId
)
return usr
allRDriversHaveFs :: Day -> DB Bool
-- allRDriversHaveFs = fmap isNothing . E.selectOne . retrieveDriversRWithoutF
allRDriversHaveFs = E.selectNotExists . retrieveDriversRWithoutF
-- | Returns users at most once, even if they own multiple avs-related licences, but no AvsID is known
retrieveDriversRWithoutF :: Day -> E.SqlQuery (E.SqlExpr (Entity User))
retrieveDriversRWithoutF nowaday = do
usr <- E.from $ E.table @User
let hasValidQual lic = do
(qual :& qualUsr) <- E.from (E.table @Qualification
`E.innerJoin` E.table @QualificationUser
`E.on` (\(qual :& qualUsr) -> qual E.^. QualificationId E.==. qualUsr E.^. QualificationUserQualification))
E.where_ $ (qual E.^. QualificationAvsLicence E.==. E.justVal lic) -- matches licence
E.&&. (qualUsr E.^. QualificationUserUser E.==. usr E.^. UserId) -- matches user
E.&&. (qualUsr `qIsValid` nowaday) -- currently valid
E.where_ $ E.exists (hasValidQual AvsLicenceRollfeld)
E.&&. E.notExists (hasValidQual AvsLicenceVorfeld)
return usr
{-
getAdjustLicences :: SchoolId -> QualificationShortand -> Handler Html
-}

View File

@ -4,7 +4,7 @@
module Handler.Admin.Avs
( getAdminAvsR
, postAdminAvsR
, postAdminAvsR
) where
import Import
@ -14,23 +14,45 @@ import qualified Data.Text as Text
import qualified Data.Set as Set
import Handler.Utils
import Handler.Utils.Avs
import Utils.Avs
-- Button needed only here
data ButtonAvsTest = BtnCheckLicences | BtnSynchLicences
deriving (Enum, Eq, Ord, Bounded, Read, Show, Generic, Typeable)
instance Universe ButtonAvsTest
instance Finite ButtonAvsTest
nullaryPathPiece ''ButtonAvsTest camelToPathPiece
instance Button UniWorX ButtonAvsTest where
btnLabel BtnCheckLicences = "Check all licences" -- could be msg
btnLabel BtnSynchLicences = "Synchronize all licences" -- could be msg
btnClasses BtnCheckLicences = [BCIsButton, BCPrimary]
btnClasses BtnSynchLicences = [BCIsButton, BCDanger]
-- END Button
avsCardNoField :: Monad m => RenderMessage (HandlerSite m) FormMessage => Field m AvsCardNo
avsCardNoField = convertField AvsCardNo avsCardNo textField
avsInternalPersonalNoField :: Monad m => RenderMessage (HandlerSite m) FormMessage => Field m AvsInternalPersonalNo
avsInternalPersonalNoField = convertField mkAvsInternalPersonalNo avsInternalPersonalNo textField
makeAvsPersonForm :: Maybe AvsQueryPerson -> Form AvsQueryPerson
makeAvsPersonForm tmpl = identifyForm FIDAvsQueryPerson . validateForm validateAvsQueryPerson $ \html ->
flip (renderAForm FormStandard) html $ AvsQueryPerson
makeAvsPersonForm tmpl = identifyForm FIDAvsQueryPerson . validateForm validateAvsQueryPerson $ \html ->
flip (renderAForm FormStandard) html $ AvsQueryPerson
<$> aopt avsCardNoField (fslI MsgAvsCardNo) (avsPersonQueryCardNo <$> tmpl)
<*> aopt textField (fslI MsgAvsVersionNo) (avsPersonQueryVersionNo <$> tmpl)
<*> aopt textField (fslI MsgAvsFirstName) (avsPersonQueryFirstName <$> tmpl)
<*> aopt textField (fslI MsgAvsLastName) (avsPersonQueryLastName <$> tmpl)
<*> aopt textField (fslI MsgAvsInternalPersonalNo) (avsPersonQueryInternalPersonalNo <$> tmpl)
<*> aopt textField (fslI MsgAvsVersionNo) (avsPersonQueryVersionNo <$> tmpl)
<*> aopt avsInternalPersonalNoField
(fslI MsgAvsInternalPersonalNo) (avsPersonQueryInternalPersonalNo <$> tmpl)
validateAvsQueryPerson :: FormValidator AvsQueryPerson Handler ()
validateAvsQueryPerson = do
validateAvsQueryPerson = do
AvsQueryPerson{..} <- State.get
guardValidation MsgAvsQueryEmpty $
is _Just avsPersonQueryCardNo ||
@ -40,23 +62,34 @@ validateAvsQueryPerson = do
is _Just avsPersonQueryVersionNo
makeAvsStatusForm :: Maybe AvsQueryStatus -> Form AvsQueryStatus
makeAvsStatusForm tmpl = identifyForm FIDAvsQueryStatus . validateForm validateAvsQueryStatus $ \html ->
makeAvsStatusForm tmpl = identifyForm FIDAvsQueryStatus . validateForm validateAvsQueryStatus $ \html ->
flip (renderAForm FormStandard) html $
parseAvsIds <$> areq textField (fslI MsgAvsPersonId) (unparseAvsIds <$> tmpl)
where
parseAvsIds :: Text -> AvsQueryStatus
parseAvsIds txt = AvsQueryStatus $ Set.fromList ids
where
nonemptys = filter (not . Text.null) $ Text.strip <$> Text.split (==',') txt
where
nonemptys = filter (not . Text.null) $ Text.strip <$> Text.split (==',') txt
ids = catMaybes $ readMay <$> nonemptys
unparseAvsIds :: AvsQueryStatus -> Text
unparseAvsIds (AvsQueryStatus ids) = Text.intercalate ", " $ tshow <$> Set.toAscList ids
unparseAvsIds :: AvsQueryStatus -> Text
unparseAvsIds (AvsQueryStatus ids) = Text.intercalate ", " $ tshow <$> Set.toAscList ids
validateAvsQueryStatus :: FormValidator AvsQueryStatus Handler ()
validateAvsQueryStatus = do
validateAvsQueryStatus = do
AvsQueryStatus ids <- State.get
guardValidation (MsgAvsQueryStatusInvalid $ tshow ids) $ not (null ids)
avsLicenceOptions :: OptionList AvsLicence
avsLicenceOptions = mkOptionList
[ Option
{ optionDisplay = Text.singleton $ licence2char l
, optionInternalValue = l
, optionExternalValue = toJsonText l
}
| l <- universeF
]
getAdminAvsR, postAdminAvsR :: Handler Html
getAdminAvsR = postAdminAvsR
postAdminAvsR = do
@ -66,31 +99,136 @@ postAdminAvsR = do
Just AvsQuery{..} -> do
((presult, pwidget), penctype) <- runFormPost $ makeAvsPersonForm Nothing
let procFormPerson fr = do
let procFormPerson fr = do
addMessage Info $ text2Html $ "Query: " <> tshow (toJSON fr)
res <- avsQueryPerson fr
case res of
Left err -> return . Just $ tshow err
Right jsn -> return . Just $ tshow jsn
case res of
Left err -> return . Just $ tshow err
Right jsn -> return . Just $ Text.replace "},Avs" "},\n Avs" $ tshow jsn
mbPerson <- formResultMaybe presult procFormPerson
((sresult, swidget), senctype) <- runFormPost $ makeAvsStatusForm Nothing
let procFormStatus fr = do
let procFormStatus fr = do
addMessage Info $ text2Html $ "Query: " <> tshow (toJSON fr)
res <- avsQueryStatus fr
case res of
Left err -> return . Just $ tshow err
Right jsn -> return . Just $ tshow jsn
case res of
Left err -> return . Just $ tshow err
Right jsn -> return . Just $ Text.replace "},Avs" "},\n Avs" $ tshow jsn
mbStatus <- formResultMaybe sresult procFormStatus
((crUsrRes, crUsrWgt), crUsrEnctype) <- runFormPost $ identifyForm FIDAvsCreateUser $ \html ->
flip (renderAForm FormStandard) html $ areq textField (fslI MsgAvsCardNo) Nothing
let procFormCrUsr fr = do
-- addMessage Info $ text2Html $ "Query: " <> tshow (toJSON fr)
res <- try $ upsertAvsUser fr
case res of
(Right (Just uid)) -> do
uuid :: CryptoUUIDUser <- encrypt uid
return $ Just [whamlet|<h2>Success:</h2> <a href=@{ForProfileR uuid}>User created or updated.|]
(Right Nothing) ->
return $ Just [whamlet|<h2>Warning:</h2> No user found.|]
(Left e) -> do
let msg = tshow (e :: SomeException)
return $ Just [whamlet|<h2>Error:</h2> #{msg}|]
mbCrUser <- formResultMaybe crUsrRes procFormCrUsr
((getLicRes, getLicWgt), getLicEnctype) <- runFormPost $ identifyForm FIDAvsQueryLicence $ \html ->
flip (renderAForm FormStandard) html $ (,,) <$> aopt intField (fslI $ text2message "Min AvsPersonId") Nothing
<*> aopt intField (fslI $ text2message "Max AvsPersonId") Nothing
<*> aopt (selectField $ return avsLicenceOptions) (fslI MsgAvsLicence) Nothing
let procFormGetLic fr = do
res <- avsQueryGetAllLicences
case res of
(Right (AvsResponseGetLicences lics)) -> do
let flics = Set.toList $ Set.filter lfltr lics
lfltr = case fr of -- not pretty, but it'll do
(Just idmin, Just idmax, Just lic) -> \AvsPersonLicence{..} -> (avsLicenceRampLicence == lic) && (avsLicencePersonID `inBetween` (AvsPersonId idmin, AvsPersonId idmax))
(Just idmin, Nothing, Just lic) -> \AvsPersonLicence{..} -> (avsLicenceRampLicence == lic) && (avsLicencePersonID == AvsPersonId idmin)
(Nothing , Just idmax, Just lic) -> \AvsPersonLicence{..} -> (avsLicenceRampLicence == lic) && (avsLicencePersonID == AvsPersonId idmax)
(Nothing , Nothing, Just lic) -> \AvsPersonLicence{..} -> avsLicenceRampLicence == lic
(Just idmin, Just idmax, Nothing ) -> (`inBetween` (AvsPersonId idmin, AvsPersonId idmax)) . avsLicencePersonID
(Just idmin, Nothing, Nothing ) -> (== AvsPersonId idmin) . avsLicencePersonID
(Nothing , Just idmax, Nothing ) -> (== AvsPersonId idmax) . avsLicencePersonID
(Nothing , Nothing, Nothing ) -> const True
addMessage Info $ text2Html $ "Query returned " <> tshow (length flics) <> " licences."
return $ Just [whamlet|
<h2>Success:</h2>
<ul>
$forall AvsPersonLicence{..} <- flics
<li> #{tshow avsLicencePersonID}: #{licence2char avsLicenceRampLicence}
|]
(Left err) -> do
let msg = tshow err
return $ Just [whamlet|<h2>Error:</h2> #{msg}|]
mbGetLic <- formResultMaybe getLicRes procFormGetLic
((setLicRes, setLicWgt), setLicEnctype) <- runFormPost $ identifyForm FIDAvsSetLicence $ \html ->
flip (renderAForm FormStandard) html $ (,) <$> areq intField (fslI MsgAvsPersonId) Nothing
<*> areq (selectField $ return avsLicenceOptions) (fslI MsgAvsLicence) (Just AvsLicenceVorfeld)
let procFormSetLic (aid, lic) = do
let req = Set.singleton $ AvsPersonLicence { avsLicenceRampLicence = lic, avsLicencePersonID = AvsPersonId aid }
addMessage Info $ text2Html $ "See log for detailed errors. Query: " <> tshow (toJSON $ AvsQuerySetLicences req)
res <- try $ setLicencesAvs req
case res of
(Right True) ->
return $ Just [whamlet|<h2>Success:</h2> Licence #{tshow (licence2char lic)} set for #{tshow aid}.|]
(Right False) ->
return $ Just [whamlet|<h2>Error:</h2> Licence could not be set for #{tshow aid}.|]
(Left e) -> do
let msg = tshow (e :: SomeException)
return $ Just [whamlet|<h2>Error:</h2> #{msg}|]
mbSetLic <- formResultMaybe setLicRes procFormSetLic
((qryLicRes, qryLicWgt), qryLicEnctype) <- runFormPost $ identifyForm FIDAvsQueryLicenceDiffs (buttonForm :: Form ButtonAvsTest)
let procFormQryLic btn = case btn of
BtnCheckLicences -> do
res <- try $ do
allLicences <- throwLeftM avsQueryGetAllLicences
computeDifferingLicences allLicences
case res of
(Right diffs) -> do
let showLics l = Text.intercalate ", " $ fmap (tshow . avsLicencePersonID) $ Set.toList $ Set.filter ((l ==) . avsLicenceRampLicence) diffs
r_grant = showLics AvsLicenceRollfeld
f_set = showLics AvsLicenceVorfeld
revoke = showLics AvsNoLicence
return $ Just [whamlet|
<h2>Licence check differences:
<h3>Grant R:
<p>
#{r_grant}
<h3>Set to F:
<p>
#{f_set}
<h3>Revoke licence:
<p>
#{revoke}
|]
(Left e) -> do
let msg = tshow (e :: SomeException)
return $ Just [whamlet|<h2>Licence check error:</h2> #{msg}|]
BtnSynchLicences -> do
res <- try synchAvsLicences
case res of
(Right True) ->
return $ Just [whamlet|<h2>Success:</h2> Licences sychronized.|]
(Right False) ->
return $ Just [whamlet|<h2>Error:</h2> Licences could not be synchronized, see error log.|]
(Left e) -> do
let msg = tshow (e :: SomeException)
return $ Just [whamlet|<h2>Licence synchronisation error:</h2> #{msg}|]
mbQryLic <- formResultMaybe qryLicRes procFormQryLic
actionUrl <- fromMaybe AdminAvsR <$> getCurrentRoute
siteLayoutMsg MsgMenuAvs $ do
setTitleI MsgMenuAvs
let personForm = wrapForm pwidget def
{ formAction = Just $ SomeRoute actionUrl
, formEncoding = penctype
}
statusForm = wrapForm swidget def
{ formAction = Just $ SomeRoute actionUrl
, formEncoding = senctype
}
let wrapFormHere fw fe = wrapForm fw def { formAction = Just $ SomeRoute actionUrl, formEncoding = fe }
personForm = wrapFormHere pwidget penctype
statusForm = wrapFormHere swidget senctype
crUsrForm = wrapFormHere crUsrWgt crUsrEnctype
getLicForm = wrapFormHere getLicWgt getLicEnctype
setLicForm = wrapFormHere setLicWgt setLicEnctype
qryLicForm = wrapForm qryLicWgt def { formAction = Just $ SomeRoute actionUrl, formEncoding = qryLicEnctype, formSubmit = FormNoSubmit }
-- TODO: use i18nWidgetFile instead if this is to become permanent
$(widgetFile "avs")
$(widgetFile "avs")

View File

@ -10,67 +10,46 @@ module Handler.Admin.Ldap
) where
import Import
import qualified Control.Monad.State.Class as State
-- import qualified Control.Monad.State.Class as State
-- import Data.Aeson (encode)
import qualified Data.CaseInsensitive as CI
import qualified Data.Text as Text
import qualified Data.Text.Encoding as Text
-- import qualified Data.Set as Set
import Foundation.Yesod.Auth (decodeUserTest)
import Foundation.Yesod.Auth (decodeUserTest,ldapLookupAndUpsert,campusUserFailoverMode,CampusUserConversionException())
import Handler.Utils
import qualified Ldap.Client as Ldap
import Auth.LDAP
data LdapQueryPerson = LdapQueryPerson
{ ldapQueryIdent :: Maybe Text
-- , ldapQueryName :: Maybe Text
, ldapQueryPNum :: Maybe Text
}
deriving (Eq, Ord, Read, Show, Generic, Typeable)
makeLdapPersonForm :: Maybe LdapQueryPerson -> Form LdapQueryPerson
makeLdapPersonForm tmpl = validateForm validateLdapQueryPerson $ \html ->
flip (renderAForm FormStandard) html $ LdapQueryPerson
<$> aopt textField (fslI MsgAdminUserIdent) (ldapQueryIdent <$> tmpl)
-- <*> aopt textField (fslI MsgAdminUserSurname) (ldapQueryName <$> tmpl)
<*> aopt textField (fslI MsgAdminUserFPersonalNumber) (ldapQueryPNum <$> tmpl)
validateLdapQueryPerson :: FormValidator LdapQueryPerson Handler ()
validateLdapQueryPerson = do
LdapQueryPerson{..} <- State.get
guardValidation MsgAvsQueryEmpty $
is _Just ldapQueryIdent ||
-- is _Just ldapQueryName ||
is _Just ldapQueryPNum
getAdminLdapR, postAdminLdapR :: Handler Html
getAdminLdapR = postAdminLdapR
postAdminLdapR = do
((presult, pwidget), penctype) <- runFormPost $ makeLdapPersonForm Nothing
let procFormPerson :: LdapQueryPerson -> Handler (Maybe (Ldap.AttrList []))
procFormPerson LdapQueryPerson{..} = do
ldapPool' <- getsYesod $ view _appLdapPool
if isNothing ldapPool'
then addMessage Warning $ text2Html "LDAP Configuration missing."
else addMessage Info $ text2Html "Input for LDAP test received."
fmap join . for ldapPool' $ \ldapPool -> do
ldapData <- if | Just lqi <- ldapQueryIdent -> campusUser'' ldapPool FailoverUnlimited lqi
| Just lqn <- ldapQueryPNum -> campusUserMatr' ldapPool FailoverUnlimited lqn
| otherwise -> addMessageI Error MsgAvsQueryEmpty >> pure Nothing
decodedErr <- decodeUserTest (CI.mk <$> ldapQueryIdent) $ concat ldapData
whenIsLeft decodedErr $ addMessageI Error
return ldapData
((presult, pwidget), penctype) <- runFormPost $ identifyForm ("adminLdapLookup"::Text) $ \html ->
flip (renderAForm FormStandard) html $ areq textField (fslI MsgAdminUserIdent) Nothing
let procFormPerson :: Text -> Handler (Maybe (Ldap.AttrList []))
procFormPerson lid = do
ldapPool' <- getsYesod $ view _appLdapPool
case ldapPool' of
Nothing -> addMessage Error (text2Html "LDAP Configuration missing.") >> return Nothing
Just ldapPool -> do
addMessage Info $ text2Html "Input for LDAP test received."
ldapData <- campusUser'' ldapPool campusUserFailoverMode lid
decodedErr <- decodeUserTest (pure $ CI.mk lid) $ concat ldapData
whenIsLeft decodedErr $ addMessageI Error
return ldapData
mbLdapData <- formResultMaybe presult procFormPerson
((uresult, uwidget), uenctype) <- runFormPost $ identifyForm ("adminLdapUpsert"::Text) $ \html ->
flip (renderAForm FormStandard) html $ areq textField (fslI MsgAdminUserIdent) Nothing
let procFormUpsert :: Text -> Handler (Maybe (Either CampusUserConversionException (Entity User)))
procFormUpsert lid = pure <$> runDB (try $ ldapLookupAndUpsert lid)
mbLdapUpsert <- formResultMaybe uresult procFormUpsert
actionUrl <- fromMaybe AdminLdapR <$> getCurrentRoute
siteLayoutMsg MsgMenuLdap $ do
setTitleI MsgMenuLdap
@ -78,7 +57,10 @@ postAdminLdapR = do
{ formAction = Just $ SomeRoute actionUrl
, formEncoding = penctype
}
upsertForm = wrapForm uwidget def
{ formAction = Just $ SomeRoute actionUrl
, formEncoding = uenctype
}
presentUtf8 lv = Text.intercalate ", " (either tshow id . Text.decodeUtf8' <$> lv)
presentLatin1 lv = Text.intercalate ", " ( Text.decodeLatin1 <$> lv)

View File

@ -83,8 +83,9 @@ postLmsAllR = do
FormMissing -> return ()
_other -> addMessage Warning "Kein korrekter LMS Knopf erkannt"
isAdmin <- hasReadAccessTo AdminR
lmsTable <- runDB $ do
view _2 <$> mkLmsAllTable
view _2 <$> mkLmsAllTable isAdmin
siteLayoutMsg MsgMenuQualifications $ do
setTitleI MsgMenuQualifications
$(widgetFile "lms-all")
@ -100,9 +101,10 @@ resultAllQualificationTotal :: Lens' AllQualificationTableData Word64
resultAllQualificationTotal = _dbrOutput . _3 . _unValue
mkLmsAllTable :: DB (Any, Widget)
mkLmsAllTable = do
mkLmsAllTable :: Bool -> DB (Any, Widget)
mkLmsAllTable isAdmin = do
now <- liftIO getCurrentTime
let
resultDBTable = DBTable{..}
where
@ -139,7 +141,12 @@ mkLmsAllTable = do
, sortable Nothing (i18nCell MsgTableQualificationIsAvsLicence & cellTooltip MsgTableQualificationIsAvsLicenceTooltip)
$ \(view (resultAllQualification . _qualificationAvsLicence) -> licence) -> maybeCell licence $ textCell . T.singleton . licence2char
, sortable Nothing (i18nCell MsgTableQualificationSapExport & cellTooltip MsgTableQualificationSapExportTooltip)
$ \(view (resultAllQualification . _qualificationSapId) -> sapid) -> tickmarkCell $ isJust sapid
$ \(view (resultAllQualification . _qualificationSapId) -> mbSapId) ->
let icn = IconOK -- change icon here, if desired
in case mbSapId of
Nothing -> mempty
Just sapId | isAdmin -> cellTooltipIcon (Just icn) (text2message sapId) mempty
Just _ -> iconCell icn
, sortable Nothing (i18nCell MsgTableQualificationCountActive & cellTooltip MsgTableQualificationCountActiveTooltip)
$ \(view resultAllQualificationActive -> n) -> wgtCell $ word2widget n
, sortable Nothing (i18nCell MsgTableQualificationCountTotal) $ wgtCell . word2widget . view resultAllQualificationTotal
@ -553,7 +560,7 @@ postLmsR sid qsh = do
where
-- i18nLms :: (RenderMessage UniWorX msg, IsDBTable m a) => msg -> DBCell m a
i18nLms msg = cell [whamlet|LMS #|] <> i18nCell msg
psValidator = def -- TODO: hier einen Filter für Schützlinge einbauen
psValidator = def & forceFilter "may-access" (Any True)
tbl <- mkLmsTable qent acts (const E.true) colChoices psValidator
return (tbl, qent)

View File

@ -559,17 +559,42 @@ getForProfileDataR cID = do
makeProfileData :: Entity User -> DB Widget
makeProfileData (Entity uid User{..}) = do
avsId <- entityVal <<$>> getBy (UniqueUserAvsUser uid)
-- avsCards <- maybe (pure mempty) (\a -> selectList [UserAvsCardPersonId ==. userAvsPersonId a] []) avsId
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
E.on $ sheet E.^. SheetCourse E.==. course E.^. CourseId
E.on $ sheet E.^. SheetId E.==. corrector E.^. SheetCorrectorSheet
E.where_ $ corrector E.^. SheetCorrectorUser E.==. E.val uid
return (course E.^. CourseTerm, course E.^. CourseSchool, course E.^. CourseShorthand)
studies <- E.select $ E.from $ \(studydegree `E.InnerJoin` studyfeat `E.InnerJoin` studyterms) -> do
E.where_ $ studyfeat E.^. StudyFeaturesUser E.==. E.val uid
studies <- E.select $ E.from $ \(studydegree `E.InnerJoin` studyfeat `E.InnerJoin` studyterms) -> do
E.on $ studyfeat E.^. StudyFeaturesField E.==. studyterms E.^. StudyTermsId
E.on $ studyfeat E.^. StudyFeaturesDegree E.==. studydegree E.^. StudyDegreeId
E.where_ $ studyfeat E.^. StudyFeaturesUser E.==. E.val uid
return (studyfeat, studydegree, studyterms)
companies' <- E.select $ E.from $ \(usrComp `E.InnerJoin` comp) -> do
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
E.on $ spvr E.^. UserSupervisorSupervisor E.==. usrSpvr E.^. UserId
E.where_ $ spvr E.^. UserSupervisorUser E.==. E.val uid
E.orderBy [E.asc (usrSpvr E.^. UserDisplayName)]
return (usrSpvr, spvr E.^. UserSupervisorRerouteNotifications)
let supervisors = intersperse (text2widget ", ") $
(\(usr, E.Value reroutCom) -> linkUserWidget ForProfileDataR usr <> bool mempty icnReroute reroutCom) <$> supervisors'
icnReroute = text2widget " " <> toWgt (icon IconLetter)
supervisees' <- E.select $ E.from $ \(spvr `E.InnerJoin` usrSpvr) -> do
E.on $ spvr E.^. UserSupervisorUser E.==. usrSpvr E.^. UserId
E.where_ $ spvr E.^. UserSupervisorSupervisor E.==. E.val uid
return (usrSpvr, spvr E.^. UserSupervisorRerouteNotifications)
let supervisees = intersperse (text2widget ", ") $
(\(usr, E.Value reroutCom) -> linkUserWidget ForProfileDataR usr <> bool mempty icnReroute reroutCom) <$> supervisees'
-- icnReroute = text2widget " " <> toWgt (icon IconLetter)
--Tables
(hasRows, ownedCoursesTable) <- mkOwnedCoursesTable uid -- Tabelle mit eigenen Kursen
enrolledCoursesTable <- mkEnrolledCoursesTable uid -- Tabelle mit allen Teilnehmer: Kurs (link), Datum
@ -584,7 +609,8 @@ makeProfileData (Entity uid User{..}) = do
cID <- encrypt uid
mCRoute <- getCurrentRoute
showAdminInfo <- pure (mCRoute == Just (AdminUserR cID)) `or2M` hasReadAccessTo (AdminUserR cID)
showAdminInfo <- pure (mCRoute == Just (AdminUserR cID)) `or2M` hasReadAccessTo (AdminUserR cID)
tooltipAvsPersNo <- messageI Info MsgAvsPersonNoNotId
let profileRemarks = $(i18nWidgetFile "profile-remarks")
return $(widgetFile "profileData")
@ -908,7 +934,6 @@ mkCorrectionsTable =
in dbTableWidget' validator DBTable{..}
-- | Table listing all qualifications that the given user is enrolled in
mkQualificationsTable :: UserId -> DB Widget
mkQualificationsTable =

View File

@ -16,6 +16,7 @@ import Handler.Utils.Csv
-- import qualified Data.CaseInsensitive as CI
import qualified Data.Csv as Csv
import Database.Esqueleto.Experimental ((:&)(..))
import qualified Database.Esqueleto.Experimental as Ex -- needs TypeApplications Lang-Pragma
-- import qualified Database.Esqueleto.Legacy as E
import qualified Database.Esqueleto.Utils as E
@ -69,12 +70,12 @@ sapRes2csv l = [ res | (Ex.Value (Just persNo), Ex.Value firstHeld, Ex.Value val
getQualificationSAPDirectR :: Handler TypedContent
getQualificationSAPDirectR = do
qualUsers <- runDB $ Ex.select $ do
(qual Ex.:& qualUser Ex.:& user) <-
(qual :& qualUser :& user) <-
Ex.from $ Ex.table @Qualification
`Ex.innerJoin` Ex.table @QualificationUser
`Ex.on` (\(qual Ex.:& qualUser) -> qual Ex.^. QualificationId Ex.==. qualUser Ex.^. QualificationUserQualification)
`Ex.on` (\(qual :& qualUser) -> qual Ex.^. QualificationId Ex.==. qualUser Ex.^. QualificationUserQualification)
`Ex.innerJoin` Ex.table @User
`Ex.on` (\(_ Ex.:& qualUser Ex.:& user) -> qualUser Ex.^. QualificationUserUser Ex.==. user Ex.^. UserId)
`Ex.on` (\(_ :& qualUser :& user) -> qualUser Ex.^. QualificationUserUser Ex.==. user Ex.^. UserId)
Ex.where_ $ E.isJust (qual Ex.^. QualificationSapId)
Ex.&&. E.isJust (user Ex.^. UserCompanyPersonalNumber)
return

View File

@ -85,6 +85,16 @@ postUsersR = do
-- , sortable (Just "matriculation") (i18nCell MsgTableMatrikelNr) $ \DBRow{ dbrOutput = Entity uid User{..} } -> anchorCellM
-- (AdminUserR <$> encrypt uid)
-- (toWgt userMatrikelnummer)
, sortable (Just "user-company") (i18nCell MsgTableCompany) $ \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"
companies' <- liftHandler . runDB . E.select $ E.from $ \(usrComp `E.InnerJoin` comp) -> do
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)]
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
pure $ toWgt $ mconcat companies
, sortable (Just "personal-number") (i18nCell MsgCompanyPersonalNumber) $ \DBRow{ dbrOutput = Entity uid User{..} } -> anchorCellM
(AdminUserR <$> encrypt uid)
(toWgt userCompanyPersonalNumber)
@ -92,6 +102,16 @@ postUsersR = do
-- , sortable (Just "last-name") (i18nCell MsgName) $ \DBRow{ dbrOutput = Entity uid User{..} } -> anchorCellM
-- (AdminUserR <$> encrypt uid)
-- (toWidget . display $ last $ impureNonNull $ words $ userDisplayName)
, sortable (Just "user-supervisor") (i18nCell MsgTableSupervisor) $ \DBRow{ dbrOutput = Entity uid _ } -> flip (set' cellContents) mempty $ do
supervisors' <- liftHandler . runDB . E.select $ E.from $ \(spvr `E.InnerJoin` usrSpvr) -> do
E.on $ spvr E.^. UserSupervisorSupervisor E.==. usrSpvr E.^. UserId
E.where_ $ spvr E.^. UserSupervisorUser E.==. E.val uid
E.orderBy [E.asc (usrSpvr E.^. UserDisplayName)]
return (usrSpvr, spvr E.^. UserSupervisorRerouteNotifications)
let supervisors = intersperse (text2widget ", ") $
(\(usr, E.Value reroutCom) -> linkUserWidget ForProfileDataR usr <> bool mempty icnReroute reroutCom) <$> supervisors'
icnReroute = text2widget " " <> toWgt (icon IconLetter)
pure $ mconcat supervisors
, sortable (Just "auth-ldap") (i18nCell MsgAuthMode) $ \DBRow{ dbrOutput = Entity _ User{..} } -> i18nCell userAuthentication
, sortable (Just "ldap-sync") (i18nCell MsgLdapSynced) $ \DBRow{ dbrOutput = Entity _ User{..} } -> maybe mempty dateTimeCell userLastLdapSynchronisation
, flip foldMap universeF $ \function ->
@ -171,6 +191,20 @@ postUsersR = do
, ( "ldap-sync"
, SortColumn $ \user -> user E.^. UserLastLdapSynchronisation
)
, ( "user-company"
, SortColumn $ \user -> E.subSelect $ E.from $ \(usrComp `E.InnerJoin` comp) -> do
E.on $ usrComp E.^. UserCompanyCompany E.==. comp E.^. CompanyId
E.where_ $ usrComp E.^. UserCompanyUser E.==. user E.^. UserId
E.orderBy [E.asc (comp E.^. CompanyName)]
return (comp E.^. CompanyName)
)
, ( "user-supervisor"
, SortColumn $ \user -> E.subSelect $ E.from $ \(spvr `E.InnerJoin` usrSpvr) -> do
E.on $ spvr E.^. UserSupervisorSupervisor E.==. usrSpvr E.^. UserId
E.where_ $ spvr E.^. UserSupervisorUser E.==. user E.^. UserId
E.orderBy [E.asc (usrSpvr E.^. UserDisplayName)]
return (usrSpvr E.^. UserDisplayName)
)
]
, dbtFilter = Map.fromList -- OverloadedLists does not work with the templates
[ ( "user-search", FilterColumn $ \user (criteria :: Set.Set Text) ->
@ -185,7 +219,7 @@ postUsersR = do
, ( "user-email", FilterColumn $ \user criterion -> case getLast (criterion :: Last Text) of
Nothing -> E.val True :: E.SqlExpr (E.Value Bool)
Just needle -> (E.castString (user E.^. UserEmail) `E.ilike` (E.%) E.++. E.val needle E.++. (E.%))
E.||. (E.castString (user E.^. UserDisplayEmail) `E.ilike` (E.%) E.++. E.val needle E.++. (E.%))
E.||. (E.castString (user E.^. UserDisplayEmail) `E.ilike` (E.%) E.++. E.val needle E.++. (E.%))
)
-- , ( "matriculation", FilterColumn $ \user (criteria :: Set.Set Text) -> if
-- | Set.null criteria -> E.true -- TODO: why can this be eFalse and work still?
@ -217,14 +251,32 @@ postUsersR = do
in E.maybe E.true (E.<=. E.val minTime) $ user E.^. UserLastLdapSynchronisation
| otherwise -> E.val True :: E.SqlExpr (E.Value Bool)
)
, ( "user-company", FilterColumn $ \user criteria -> if
| Set.null (criteria :: Set.Set Text) -> E.val True :: E.SqlExpr (E.Value Bool)
| otherwise ->
E.exists . E.from $ \(ucomp `E.InnerJoin` comp) -> do
E.on $ ucomp E.^. UserCompanyCompany E.==. comp E.^. CompanyId
E.where_ $ (ucomp E.^. UserCompanyUser E.==. user E.^.UserId)
E.&&. E.any (E.hasInfix (comp E.^. CompanyName)) (E.val <$> Set.toList criteria)
)
, ( "user-supervisor", FilterColumn $ \user criteria -> if
| Set.null (criteria :: Set.Set Text) -> E.val True :: E.SqlExpr (E.Value Bool)
| otherwise ->
E.exists . E.from $ \(spvr `E.InnerJoin` usrSpvr) -> do
E.on $ spvr E.^. UserSupervisorSupervisor E.==. usrSpvr E.^. UserId
E.where_ $ (spvr E.^. UserSupervisorUser E.==. user E.^.UserId)
E.&&. E.any (E.hasInfix (usrSpvr E.^. UserDisplayName)) (E.val <$> Set.toList criteria)
)
]
, dbtFilterUI = \mPrev -> mconcat
[ prismAForm (singletonFilter "user-search") mPrev $ aopt textField (fslI MsgName)
, prismAForm (singletonFilter "user-ident") mPrev $ aopt textField (fslI MsgAdminUserIdent)
, prismAForm (singletonFilter "user-email") mPrev $ aopt textField (fslI MsgAdminUserEmail)
-- , prismAForm (singletonFilter "matriculation") mPrev $ aopt matriculationField (fslI MsgTableMatrikelNr)
, prismAForm (singletonFilter "user-email") mPrev $ aopt textField (fslI MsgAdminUserEmail)
-- , prismAForm (singletonFilter "matriculation") mPrev $ aopt matriculationField (fslI MsgTableMatrikelNr)
, prismAForm (singletonFilter "personal-number" ) mPrev $ aopt textField (fslI MsgCompanyPersonalNumber)
, prismAForm (singletonFilter "company-department" ) mPrev $ aopt textField (fslI MsgCompanyDepartment)
, prismAForm (singletonFilter "user-company") mPrev $ aopt textField (fslI MsgTableCompany)
, prismAForm (singletonFilter "user-supervisor") mPrev $ aopt textField (fslI MsgTableSupervisor)
, prismAForm (singletonFilter "auth-ldap" . maybePrism _PathPiece) mPrev $ aopt (lift `hoistField` selectFieldList [(MsgAuthPWHash "", False), (MsgAuthLDAP, True)]) (fslI MsgAuthMode)
, prismAForm (singletonFilter "school") mPrev $ aopt (lift `hoistField` selectFieldList schoolOptions) (fslI MsgCourseSchool)
, prismAForm (singletonFilter "ldap-sync" . maybePrism _PathPiece) mPrev $ aopt utcTimeField (fslI MsgLdapSyncedBefore)
@ -317,7 +369,7 @@ nullaryPathPiece ''UserAssimilateButton $ camelToPathPiece' 2
embedRenderMessage ''UniWorX ''UserAssimilateButton id
getAdminUserR, postAdminUserR :: CryptoUUIDUser -> Handler Html
getAdminUserR = postAdminUserR
@ -337,7 +389,7 @@ postAdminUserR uuid = do
return (school, userFunction E.?. UserFunctionFunction, isAdmin)
systemFunctionsF <- Set.fromList . map (userSystemFunctionFunction . entityVal) <$> selectList [UserSystemFunctionUser ==. uid, UserSystemFunctionIsOptOut ==. False] []
let systemFunctions = (`Set.member` systemFunctionsF)
let systemFunctions = (`Set.member` systemFunctionsF)
return ( user
, setOf (folded . filtered (view $ _3 . _Value) . _1 . _entityKey) schools

View File

@ -36,9 +36,9 @@ data AdminUserForm = AdminUserForm
}
data AuthenticationKind = AuthKindLDAP | AuthKindPWHash | AuthKindNoLogin
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable)
instance Universe AuthenticationKind
instance Finite AuthenticationKind
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable, Universe, Finite)
--instance Universe AuthenticationKind
--instance Finite AuthenticationKind
embedRenderMessage ''UniWorX ''AuthenticationKind id
nullaryPathPiece ''AuthenticationKind $ camelToPathPiece' 2

View File

@ -4,14 +4,16 @@
{-# LANGUAGE TypeApplications #-}
{-# OPTIONS_GHC -fno-warn-unused-imports #-}
module Handler.Utils.Avs
( upsertAvsUser, upsertAvsUserById, upsertAvsUserByCard
, getLicence, getLicenceDB
-- , getLicence, getLicenceDB, getLicenceByAvsId -- not supported by interface
, setLicence, setLicenceAvs, setLicencesAvs
, checkLicences
, retrieveDifferingLicences, computeDifferingLicences
, synchAvsLicences
, lookupAvsUser, lookupAvsUsers
, AvsException(..)
, updateReceivers
) where
import Import
@ -26,14 +28,14 @@ import qualified Data.Map as Map
import qualified Data.CaseInsensitive as CI
-- import Auth.LDAP (ldapUserPrincipalName)
import Foundation.Yesod.Auth (upsertCampusUserByCn,CampusUserConversionException())
import Foundation.Yesod.Auth (ldapLookupAndUpsert, CampusUserConversionException())
import Handler.Utils.Company
import Handler.Users.Add
import Database.Esqueleto.Experimental ((:&))
import Database.Esqueleto.Experimental ((:&)(..))
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
--------------------
@ -43,7 +45,7 @@ import qualified Database.Esqueleto.Utils as E
data AvsException
= AvsInterfaceUnavailable -- Interface to AVS was not configured at startup or does not respond
| AvsUserUnassociated UserId -- Manipulating AVS Data for a user that is not linked to AVS yet
| AvsUserUnknownByAvs AvsPersonId -- AvsPersionId not (or no longer) found in AVS DB
| AvsUserUnknownByAvs AvsPersonId -- AvsPersonId not (or no longer) found in AVS DB
| AvsUserAmbiguous -- Multiple matching existing users found in our DB
| AvsPersonSearchEmpty -- AvsPersonSearch returned empty result
| AvsPersonSearchAmbiguous -- AvsPersonSearch returned more than one result
@ -68,6 +70,7 @@ instance Exception AvsException
-}
{- AVS interface only allows collecting all licences at once, thus getLicence should be avoided, see getLicenceByAvsId including a workaround
-- Do we need this?
-- getLicence :: UserId -> Handler (Maybe AvsLicence) -- with runDB
getLicence :: ( MonadThrow m, MonadHandler m, HandlerSite m ~ UniWorX, WithRunDB SqlReadBackend (HandlerFor UniWorX) m ) => UserId -> m (Maybe AvsLicence)
@ -86,96 +89,169 @@ getLicenceDB uid = do
let ulicence = Set.lookupMax $ Set.filter ((userAvsPersonId ==) . avsLicencePersonID) licences
return (avsLicenceRampLicence <$> ulicence)
setLicence :: UserId -> AvsLicence -> DB ()
-- | Should be avoided, since all licences must be requested at once.
getLicenceByAvsId :: (MonadThrow m, MonadHandler m, HandlerSite m ~ UniWorX) =>
Set AvsPersonId -> m (Set AvsPersonLicence)
getLicenceByAvsId aids = do
AvsQuery{..} <- maybeThrowM AvsInterfaceUnavailable $ liftHandler $ getsYesod $ view _appAvsQuery
AvsResponseGetLicences licences <- throwLeftM avsQueryGetAllLicences
return $ Set.filter (\x -> avsLicencePersonID x `Set.member` aids) licences
-}
-- setLicence :: (MonadHandler m, MonadThrow m, HandlerSite m ~ UniWorX) => UserId -> AvsLicence -> m Bool
setLicence :: (PersistUniqueRead backend, MonadThrow m,
MonadHandler m, HandlerSite m ~ UniWorX,
BaseBackend backend ~ SqlBackend) =>
UserId -> AvsLicence -> ReaderT backend m Bool
setLicence uid lic = do
Entity _ UserAvs{..} <- maybeThrowM (AvsUserUnassociated uid) $ getBy $ UniqueUserAvsUser uid
setLicenceAvs userAvsPersonId lic
setLicenceAvs :: AvsPersonId -> AvsLicence -> DB ()
setLicenceAvs :: (MonadHandler m, MonadThrow m, HandlerSite m ~ UniWorX) =>
AvsPersonId -> AvsLicence -> m Bool
setLicenceAvs apid lic = do
let req = Set.singleton $ AvsPersonLicence { avsLicenceRampLicence = lic, avsLicencePersonID = apid }
setLicencesAvs req
-- setLicencesAvs :: Set AvsPersonLicence -> DB ()
--setLicencesAvs :: Set AvsPersonLicence -> Handler Bool
setLicencesAvs :: (MonadHandler m, MonadThrow m, HandlerSite m ~ UniWorX) =>
Set AvsPersonLicence -> m ()
setLicencesAvs pls = do
AvsQuery{..} <- maybeThrowM AvsInterfaceUnavailable $ getsYesod $ view _appAvsQuery
response <- throwLeftM . avsQuerySetLicences $ AvsQuerySetLicences pls
case response of
AvsResponseSetLicencesError{..} -> do
let msg = "Set licence failed completely: " <> avsResponseSetLicencesStatus <> ". Details: " <> avsResponseSetLicencesMessage
$logErrorS "AVS" msg
throwM $ AvsSetLicencesFailed avsResponseSetLicencesStatus
AvsResponseSetLicences responses ->
forM_ responses $ \AvsLicenceResponse{..} ->
unless (sloppyBool avsResponseSuccess) $ do
-- TODO: create an Admin Problems overview page
$logErrorS "AVS" $ "Set licence failed for " <> tshow avsResponsePersonID <> " due to " <> cropText avsResponseMessage
Set AvsPersonLicence -> m Bool
setLicencesAvs persLics = do
AvsQuery{avsQuerySetLicences=aqsl} <- maybeThrowM AvsInterfaceUnavailable $ getsYesod $ view _appAvsQuery
aux aqsl True persLics
where
aux aqsl batch0_ok pls
| Set.null pls = return batch0_ok
| otherwise = do
let (batch1, batch2) = Set.splitAt avsMaxSetLicenceAtOnce pls
response <- throwLeftM $ aqsl $ AvsQuerySetLicences batch1
case response of
AvsResponseSetLicencesError{..} -> do
let msg = "Set AVS licences failed utterly: " <> avsResponseSetLicencesStatus <> ". Details: " <> cropText avsResponseSetLicencesMessage
$logErrorS "AVS" msg
throwM $ AvsSetLicencesFailed avsResponseSetLicencesStatus
AvsResponseSetLicences msgs -> do
let (ok,bad') = Set.partition (sloppyBool . avsResponseSuccess) msgs
ok_ids = Set.map avsResponsePersonID ok
bad = Map.withoutKeys (setToMap avsResponsePersonID bad') ok_ids -- it is possible to receive an id multiple times, with only one success, but this is sufficient
batch1_ok = length ok == length batch1
forM_ bad $ \AvsLicenceResponse { avsResponsePersonID=api, avsResponseMessage=msg} ->
$logErrorS "AVS" $ "Set AVS Licence failed for " <> tshow api <> " due to " <> cropText msg
-- TODO: Admin Error page
aux aqsl (batch0_ok && batch1_ok) batch2 -- yay for tail recursion (TODO: maybe refactor?)
-- | Retrieve all currently valid driving licences and check against our database
-- Only react to changes as compared to last seen status in avs.model
-- TODO: turn into a job, once the interface is actually available
checkLicences :: Handler ()
checkLicences = do
{-
AvsQuery{..} <- maybeThrowM AvsInterfaceUnavailable $ getsYesod $ view _appAvsQuery
AvsResponseGetLicences licences <- throwLeftM avsQueryGetAllLicences
-- TODO: run in a background job, once the interface is actually available
synchAvsLicences :: Handler Bool
synchAvsLicences = do
AvsQuery{..} <- maybeThrowM AvsInterfaceUnavailable $ getsYesod $ view _appAvsQuery
allLicences <- throwLeftM avsQueryGetAllLicences
deltaLicences <- computeDifferingLicences allLicences
setResponse <- setLicencesAvs deltaLicences
if setResponse
then $logInfoS "AVS" "FRADrive Licences written to AVS successfully."
else $logWarnS "AVS" "Writing FRADrive Licences to AVS incomplete."
return setResponse
computeDifferingLicences :: AvsResponseGetLicences -> Handler (Set AvsPersonLicence)
computeDifferingLicences argl = do
(setTo0, setTo1, setTo2) <- getDifferingLicences argl
return $ Set.map (AvsPersonLicence AvsNoLicence) setTo0
<> Set.map (AvsPersonLicence AvsLicenceVorfeld) setTo1
<> Set.map (AvsPersonLicence AvsLicenceRollfeld) setTo2
retrieveDifferingLicences :: Handler (Set AvsPersonId, Set AvsPersonId, Set AvsPersonId)
retrieveDifferingLicences = do
AvsQuery{..} <- maybeThrowM AvsInterfaceUnavailable $ getsYesod $ view _appAvsQuery
allLicences <- throwLeftM avsQueryGetAllLicences
getDifferingLicences allLicences
getDifferingLicences :: AvsResponseGetLicences -> Handler (Set AvsPersonId, Set AvsPersonId, Set AvsPersonId)
getDifferingLicences (AvsResponseGetLicences licences) = do
now <- liftIO getCurrentTime
--let (vorfeld, nonvorfeld) = Set.partition (`avsPersonLicenceIs` AvsLicenceVorfeld) licences
-- rollfeld = Set.filter (`avsPersonLicenceIs` AvsLicenceRollfeld) nonvorfeld
let (noOrVorfeld, rollfeld) = Set.spanAntitone (`avsPersonLicenceIsLEQ` AvsLicenceVorfeld) licences
(_nolicence , vorfeld) = Set.spanAntitone (`avsPersonLicenceIsLEQ` AvsNoLicence) noOrVorfeld
idsRollfeld = avsLicencePersonId <$> Set.toList rollfeld
idsVorfeld = avsLicencePersonId <$> Set.toList vorfeld
-- Note: FRADrive users with 'R' also own 'F' qualification, but AvsGetResponseGetLicences yields only either
let nowaday = utctDay now
noOne = AvsPersonId 0
vorORrollfeld' = Set.dropWhileAntitone (`avsPersonLicenceIsLEQ` AvsNoLicence) licences
rollfeld' = Set.dropWhileAntitone (`avsPersonLicenceIsLEQ` AvsLicenceVorfeld) vorORrollfeld'
vorORrollfeld = Set.map avsLicencePersonID vorORrollfeld'
rollfeld = Set.map avsLicencePersonID rollfeld'
-- let licenceMap Map.map avsLicencePersonID $ avsMap.fromSet avsLicenceRampLicence licences
-- idsRollfeld = concat $ Map.lookup AvsLicenceRollfeld licenceMap
-- idsVorfeld = concat $ Map.lookup AvsLicenceVorfeld
antijoinAvsLicences :: AvsLicence -> Set AvsPersonId -> DB (Set AvsPersonId,Set AvsPersonId)
antijoinAvsLicences lic avsLics = fmap unwrapIds $
E.select $ do
((_qauli :& _qualUser :& usrAvs) :& excl) <-
E.from $ ( E.table @Qualification
`E.innerJoin` E.table @QualificationUser
`E.on` ( \(quali :& qualUser) ->
(quali E.^. QualificationId E.==. qualUser E.^. QualificationUserQualification)
-- NOTE: filters on the innerJoin must be part of ON-condition in order for anti-join to work!
E.&&. (quali E.^. QualificationAvsLicence E.==. E.justVal lic) -- correct type of licence
E.&&. (E.val nowaday `E.between` (qualUser E.^. QualificationUserFirstHeld
,qualUser E.^. QualificationUserValidUntil)) -- currently valid
E.&&. E.isNothing (qualUser E.^. QualificationUserBlockedDue) -- not blocked
)
`E.innerJoin` E.table @UserAvs
`E.on` (\(_ :& qualUser :& usrAvs) -> qualUser E.^. QualificationUserUser E.==. usrAvs E.^. UserAvsUser)
) `E.fullOuterJoin` E.toValues (set2NonEmpty noOne avsLics) -- left-hand side produces all currently valid matching qualifications
`E.on` (\((_ :& _ :& usrAvs) :& excl) -> usrAvs E.?. UserAvsPersonId E.==. excl)
E.where_ $ E.isNothing excl E.||. E.isNothing (usrAvs E.?. UserAvsPersonId) -- anti join
return (usrAvs E.?. UserAvsPersonId, excl)
now <- liftIO getCurrentTime
unwrapIds :: [(E.Value (Maybe AvsPersonId), E.Value (Maybe AvsPersonId))] -> (Set AvsPersonId, Set AvsPersonId)
unwrapIds = mapBoth (Set.delete noOne) . foldr aux mempty
where
aux (_, E.Value(Just api)) (l,r) = (l, Set.insert api r) -- we may assume here that each pair contains precisely one Just constructor
aux (E.Value(Just api), _) (l,r) = (Set.insert api l, r)
aux _ acc = acc -- should never occur
((vorfGrant, vorfRevoke), (rollGrant, rollRevoke)) <- runDB $ (,)
<$> antijoinAvsLicences AvsLicenceVorfeld vorORrollfeld
<*> antijoinAvsLicences AvsLicenceRollfeld rollfeld
let setTo0 = vorfRevoke -- ready to use with SET 0
setTo1 = (vorfGrant Set.\\ rollGrant ) `Set.union` (rollRevoke Set.\\ vorfRevoke)
setTo2 = (rollGrant Set.\\ vorfRevoke) `Set.intersection` (vorfGrant `Set.union` vorORrollfeld)
return (setTo0, setTo1, setTo2)
{- Cases to consider (AVS_Licence,has_valid_F, has_valid_R) -> (vorfeld@(toset,unset), rollfeld@(toset,unset)) :
A (0,0,0) -> ((_,_),(_,_)) : nop; avs_id not returned from queries, no problem
B (0,0,1) -> ((_,_),(x,_)) : nop; do nothing -- CHECK since id is returned by roll-query
C (0,1,0) -> ((x,_),(_,_)) : set F for id
D (0,1,1) -> ((x,_),(x,_)) : set R for id
E (1,0,0) -> ((_,x),(_,_)) : set 0 for id
F (1,0,1) -> ((_,x),(x,_)) : set 0 for id
G (1,1,0) -> ((_,_),(_,_)) : nop
H (1,1,1) -> ((_,_),(x,_)) : set R for id
I (2,0,0) -> ((_,x),(_,x)) : set 0 for id
J (2,0,1) -> ((_,x),(_,_)) : set 0 for id
K (2,1,0) -> ((_,_),(_,x)) : set F for id
L (2,1,1) -> ((_,_),(_,_)) : nop
PROBLEM: B & H in conflict! (Note that nop is automatic except for case B)
Results:
set to 0: determined by vorfeld-unset -- zuerst
set to 1: vorfeld-set && nicht in rollfeld-set || rollfeld-unset && nicht in vorfeld-unset
set to 2: rollfeld-set && nicht in vorfeld-unset && (in vorfeld-set || AVS_Licence>0 == vorORrollfeld)
-}
runDB $ do
E.select $ do
(qauli E.:& qualUser E.:& usrAvs) <-
E.from $ E.table @Qualification
`E.innerJoin` E.table @QualificationUser
`E.on` (\(quali E.:& qualUser) -> qual E.^. QualificationId E.==. qualUser E.^. QualificationUserQualification)
`E.innerJoin` E.table @UserAvs
`E.on` (\(_ E.:& qualUser E.:& usrAvs) -> qualUser E.^. QualificationUserUser E.==. usrAvs E.^. UserAvsUser)
E.where_ $ E.isJust (quali E.^. QualificationAvsLicence)
E.&&. (usrAvs E.^. QualificationAvsLicence `E.notIn` E.valList
-- WAS WILL ICH HIER WIRKLICH:
-- Liefere alle avsIds, welche die falsche Qualifikation zugewiesen bekommen haben?
-- Wie erhalte ich alle IDs, welche es KEINE Qualifikation haben? FROM valList scheint es nicht zu geben!
return
( userAvs E.^. UserAvsPersonId
, quali E.^. QualificationAvsLicence
)
--TODO this must be chunked into separate jobs/tasks
--forM licences $ \AvsPersonLicence{..} -> do
-}
error "CONTINUE HERE" -- TODO STUB
-- | Always update AVS Data
upsertAvsUser :: Text -> Handler (Maybe UserId) -- TODO: change to Entity
upsertAvsUser (discernAvsCardPersonalNo -> Just someid) = upsertAvsUserByCard someid -- Note: Right case is a number, it could be AvsCardNumber or AvsInternalPersonalNumber; we cannot know, but the latter is much more likely and useful to users!
upsertAvsUser otherId = -- attempt LDAP lookup to find by eMail
try (runDB $ ldapLookupAndUpsert otherId) >>= \case
Right Entity{entityVal=User{userCompanyPersonalNumber=Just persNo}} -> upsertAvsUserByCard (Right $ mkAvsInternalPersonalNo persNo)
Left (_err::SomeException) -> return Nothing -- TODO: ; merely for convenience, not necessary right now
_ -> return Nothing
upsertAvsUser :: Text -> Handler (Maybe UserId)
upsertAvsUser (discernAvsCardPersonalNo -> Just someid) = upsertAvsUserByCard someid -- Note: Right case is a number, it could be AvsPersonId or AvsInternalPersonalNumber; we cannot know, but the latter is much more likely and useful to users!
upsertAvsUser _other = return Nothing -- TODO: attempt LDAP lookup to find by eMail; merely for convenience, not necessary right now
{- maybe this code helps?
upsRes :: Either CampusUserConversionException (Entity User)
<- try $ upsertCampusUserByOther persNo
case upsRes of
Right Entity{entityKey=uid} -> insertUniqueEntity $ UserAvs api uid
_other -> return mbuid -- ==Nothing -- user could not be created somehow
-}
-- | Given CardNo or internal Number, retrieve UserId. Create non-existing users, if possible.
-- | Given CardNo or internal Number, retrieve UserId. Create non-existing users, if possible. Always update.
-- Throws errors if the avsInterface in unavailable or the user is non-unique within external AVS DB.
upsertAvsUserByCard :: Either AvsFullCardNo AvsInternalPersonalNo -> Handler (Maybe UserId) -- Idee: Eingabe ohne Punkt is AvsInternalPersonalNo mit Punkt is Ausweisnummer?!
upsertAvsUserByCard persNo = do
@ -187,11 +263,12 @@ upsertAvsUserByCard persNo = do
case Set.elems adps of
[] -> throwM AvsPersonSearchEmpty
(_:_:_) -> throwM AvsPersonSearchAmbiguous
[AvsDataPerson{avsPersonPersonID=appi}] -> do
mbuid <- runDB $ getBy $ UniqueUserAvsId appi
case mbuid of
(Just (Entity _ UserAvs{userAvsUser=uau})) -> return $ Just uau
Nothing -> upsertAvsUserById appi
[AvsDataPerson{avsPersonPersonID=api}] -> upsertAvsUserById api -- always trigger an update
-- do
-- mbuid <- runDB $ getBy $ UniqueUserAvsId api
-- case mbuid of
-- (Just (Entity _ UserAvs{userAvsUser=uau})) -> return $ Just uau
-- Nothing -> upsertAvsUserById api
@ -204,16 +281,18 @@ upsertAvsUserById api = do
mbuid <- getBy (UniqueUserAvsId api)
case (mbuid, mbapd) of
(Nothing, Just AvsDataPerson{..}) -- FRADriver User does not exist yet, but found in AVS and has Internal Personal Number
| Just persNo <- avsPersonInternalPersonalNo -> do
candidates <- selectKeysList [UserCompanyPersonalNumber ==. avsPersonInternalPersonalNo] []
| Just (avsInternalPersonalNo -> persNo) <- canonical avsPersonInternalPersonalNo -> do
$logInfoS "AVS" $ "Creating new user with avsInternalPersonalNo " <> tshow persNo
candidates <- selectKeysList [UserCompanyPersonalNumber ==. Just persNo] []
case candidates of
[uid] -> insertUniqueEntity $ UserAvs api uid
[uid] -> $logInfoS "AVS" "Matching user found, linking." >> insertUniqueEntity (UserAvs api uid avsPersonPersonNo)
(_:_) -> throwM AvsUserAmbiguous
[] -> do
upsRes :: Either CampusUserConversionException (Entity User)
<- try $ upsertCampusUserByCn persNo
<- try $ ldapLookupAndUpsert persNo
$logInfoS "AVS" $ "No matching existing user found. Attempted LDAP upsert returned: " <> tshow upsRes
case upsRes of
Right Entity{entityKey=uid} -> insertUniqueEntity $ UserAvs api uid -- pin/addr are updated in next step anyway
Right Entity{entityKey=uid} -> insertUniqueEntity $ UserAvs api uid avsPersonPersonNo -- pin/addr are updated in next step anyway
_other -> return mbuid -- ==Nothing -- user could not be created somehow
_other -> return mbuid
case (mbuid, mbapd) of
@ -222,10 +301,10 @@ upsertAvsUserById api = do
let firmAddress = guessLicenceAddress avsPersonPersonCards
mbCompany = firmAddress ^? _Just . _1 . _Just
userFirmAddr= plaintextToStoredMarkup . mergeCompanyAddress <$> firmAddress
addrCard = firmAddress ^? _Just . _3
pinCard = Set.lookupMax avsPersonPersonCards
userPin = tshowAvsFullCardNo . getFullCardNo <$> pinCard
userPin = tshowAvsFullCardNo . getFullCardNo <$> pinCard
fakeIdent = CI.mk $ "AVSID:" <> tshow api
fakeNo = CI.mk $ "AVSNO:" <> tshow avsPersonPersonNo
newUsr = AdminUserForm
{ aufTitle = Nothing
, aufFirstName = avsPersonFirstName
@ -236,65 +315,58 @@ upsertAvsUserById api = do
, aufSex = Nothing
, aufMobile = Nothing
, aufTelephone = Nothing
, aufFPersonalNumber = avsPersonInternalPersonalNo
, aufFPersonalNumber = avsInternalPersonalNo <$> canonical avsPersonInternalPersonalNo
, aufFDepartment = Nothing
, aufPostAddress = userFirmAddr
, aufPrefersPostal = isJust firmAddress
, aufPrefersPostal = True
, aufPinPassword = userPin
, aufEmail = fakeIdent -- Email is unknown in this version of the avs query, to be updated later (FUTURE TODO)
, aufEmail = fakeNo -- Email is unknown in this version of the avs query, to be updated later (FUTURE TODO)
, aufIdent = fakeIdent -- use AvsPersonId instead
, aufAuth = maybe AuthKindNoLogin (const AuthKindLDAP) avsPersonInternalPersonalNo -- FUTURE TODO: if email is known, use AuthKinfPWHash for email invite, if no internal personal number is known
}
mbUid <- addNewUser newUsr -- triggers JobSynchroniseLdapUser, JobSendPasswordReset and NotificationUserAutoModeUpdate -- TODO: check if these are failsafe
whenIsJust mbUid $ \uid -> runDB $ do
mbUid <- addNewUser newUsr -- triggers JobSynchroniseLdapUser, JobSendPasswordReset and NotificationUserAutoModeUpdate -- TODO: check if these are failsafe
whenIsJust mbUid $ \uid -> runDB $ do
now <- liftIO getCurrentTime
insert_ $ UserAvs avsPersonPersonID uid
-- forM_ avsPersonPersonCards $ -- save all cards for later
let cs :: Set AvsDataPersonCard = Set.fromList $ catMaybes [pinCard, addrCard]
forM_ cs $ -- only save used cards for the postal address update detection
\avsCard -> insert_ $ UserAvsCard avsPersonPersonID (avsDataCardNo avsCard) avsCard now
upsertUserCompany uid mbCompany
insert_ $ UserAvs avsPersonPersonID uid avsPersonPersonNo
forM_ avsPersonPersonCards $ -- save all cards for later
-- let cs :: Set AvsDataPersonCard = Set.fromList $ catMaybes [pinCard, addrCard]
-- forM_ cs $ -- only save used cards for the postal address update detection
\avsCard -> insert_ $ UserAvsCard avsPersonPersonID (avsDataCardNo avsCard) avsCard now
upsertUserCompany uid mbCompany
return mbUid
(Just (Entity _ UserAvs{userAvsUser=uid}), Just AvsDataPerson{avsPersonPersonCards}) -> do -- known user, update address and pinPassword
let firmAddress = guessLicenceAddress avsPersonPersonCards
mbCompany = firmAddress ^? _Just . _1 . _Just
userFirmAddr= plaintextToStoredMarkup . mergeCompanyAddress <$> firmAddress
addrCard = firmAddress ^? _Just . _3
mbCoFirmAddr= mergeCompanyAddress <$> firmAddress
userFirmAddr= plaintextToStoredMarkup <$> mbCoFirmAddr
pinCard = Set.lookupMax avsPersonPersonCards
userPin = tshowAvsFullCardNo . getFullCardNo <$> pinCard
runDB $ do
now <- liftIO getCurrentTime
upsertUserCompany uid mbCompany
whenIsJust addrCard $ \aCard ->
getBy (UniqueAvsCard $ avsDataCardNo aCard) >>= \case
(Just (Entity uac UserAvsCard{..})) | aCard == userAvsCardCard -> -- address seen before, no change
update uac [UserAvsCardLastSynch =. now]
_ -> do -- possibly new address data
void $ upsert UserAvsCard
{ userAvsCardPersonId = api
, userAvsCardCardNo = avsDataCardNo aCard
, userAvsCardCard = aCard
, userAvsCardLastSynch= now
}
[ UserAvsCardCard =. aCard
, UserAvsCardLastSynch =. now
]
when (isJust userFirmAddr) $ updateWhere [UserId ==. uid] [UserPostAddress =. userFirmAddr]
whenIsJust pinCard $ \pCard ->
unlessM (exists [UserAvsCardCardNo ==. avsDataCardNo pCard]) $ do
-- update pin, but only if it was unset or set to the value of an old card
oldCards <- selectList [UserAvsCardPersonId ==. api] []
userPin = tshowAvsFullCardNo . getFullCardNo <$> pinCard
runDB $ do
now <- liftIO getCurrentTime
oldCards <- selectList [UserAvsCardPersonId ==. api] []
let oldAddrs = Set.fromList $ mapMaybe (maybeCompanyAddress . userAvsCardCard . entityVal) oldCards
unless (maybe True (`Set.member` oldAddrs) mbCoFirmAddr) $ do -- update postal address, unless the exact address had been seen before
updateWhere [UserId ==. uid] [UserPostAddress =. userFirmAddr]
whenIsJust pinCard $ \pCard -> -- update pin, but only if it was unset or set to the value of an old card
unlessM (exists [UserAvsCardCardNo ==. avsDataCardNo pCard]) $ do
let oldPins = Just . tshowAvsFullCardNo . getFullCardNo . userAvsCardCard . entityVal <$> oldCards
updateWhere [UserId ==. uid, UserPinPassword !=. userPin, UserPinPassword <-. Nothing:oldPins]
[UserPinPassword =. userPin]
insert_ $ UserAvsCard api (avsDataCardNo pCard) pCard now
upsertUserCompany uid mbCompany
forM_ avsPersonPersonCards $ \aCard -> void $ upsert UserAvsCard
{ userAvsCardPersonId = api
, userAvsCardCardNo = avsDataCardNo aCard
, userAvsCardCard = aCard
, userAvsCardLastSynch = now
}
[ UserAvsCardCard =. aCard
, UserAvsCardLastSynch =. now
]
return $ Just uid
lookupAvsUser :: ( MonadThrow m, MonadHandler m, HandlerSite m ~ UniWorX ) =>
AvsPersonId -> m (Maybe AvsDataPerson)
lookupAvsUser api = Map.lookup api <$> lookupAvsUsers (Set.singleton api)
@ -317,3 +389,26 @@ lookupAvsUsers apis = do
AvsResponsePerson adps <- throwLeftM . avsQueryPerson $ def{avsPersonQueryCardNo = Just avsDataCardNo, avsPersonQueryVersionNo = Just avsDataVersionNo}
return $ mergeByPersonId adps acc2
-- | Like `Handler.Utils.getReceivers`, but calls upsertAvsUserById on each user to ensure that postal address is up-to-date
updateReceivers :: UserId -> Handler (Entity User, [Entity User], Bool)
updateReceivers uid = do
(underling :: Entity User, avsUnderling :: Maybe (Entity UserAvs), avsSupers :: [Entity UserAvs]) <- runDB $ (,,)
<$> getJustEntity uid
<*> getBy (UniqueUserAvsUser uid)
<*> (E.select $ do
(usrSuper :& usrAvs) <-
E.from $ E.table @UserSupervisor
`E.innerJoin` E.table @UserAvs
`E.on` (\(usrSuper :& userAvs) ->usrSuper E.^. UserSupervisorSupervisor E.==. userAvs E.^. UserAvsUser)
E.where_ $ (usrSuper E.^. UserSupervisorUser E.==. E.val uid)
E.&&. (usrSuper E.^. UserSupervisorRerouteNotifications)
pure usrAvs
)
let toUpdate = Set.fromList (userAvsPersonId . entityVal <$> mcons avsUnderling avsSupers)
forM_ toUpdate (void . upsertAvsUserById) -- update postaddress from AVS
let receiverIDs :: [UserId] = userAvsUser . entityVal <$> avsSupers
receivers <- runDB (catMaybes <$> mapM getEntity receiverIDs)
return $ if null receivers
then (underling, pure underling, True)
else (underling, receivers, underling `elem` receivers)

View File

@ -18,4 +18,3 @@ determineSystemFunctions ldapFuncs = \case
-- SJ: not sure this LDAP-specific key belongs here?
SystemStudent -> False -- "student" `Set.member` ldapFuncs -- no such key identified at FraPort
SystemPrinter -> False -- "department=IFM-IS2" zu viele Mitglieder
SystemSap -> False

View File

@ -11,6 +11,9 @@ import Import hiding (link)
import qualified Database.Esqueleto.Legacy as E
import qualified Database.Esqueleto.Utils as E hiding ((->.))
import Database.Esqueleto.Utils (mkExactFilter, mkExactFilterWith, mkContainsFilter, mkContainsFilterWith, anyFilter)
--import Database.Esqueleto.Experimental ((:&)(..))
--import qualified Database.Esqueleto.Experimental as Ex
import Handler.Utils.Table.Cells
import Handler.Utils.Table.Pagination
@ -705,6 +708,39 @@ fltrRelevantStudyFeaturesSemesterUI :: DBFilterUI
fltrRelevantStudyFeaturesSemesterUI = fltrFeaturesSemesterUI
---------------
-- Companies --
---------------
{-
-- colUserCompany :: (HandlerSite (DBCell m) ~ UniWorX, IsDBTable m c, HasEntity a User) => Colonnade Sortable a (DBCell m c)
colUserCompany = sortable (Just "user-company") (i18nCell MsgTableCompany) $ \heu -> do
let uid = heu ^. hasEntity . _entityKey
companies' <- liftHandler . runDB . E.select $ E.from $ \(usrComp `E.InnerJoin` comp) -> do
E.where_ $ usrComp E.^. UserCompanyUser E.==. E.val uid
E.on $ usrComp E.^. UserCompanyCompany E.==. comp E.^. CompanyId
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
cell $ toWgt $ mconcat companies
-}
colUserCompany' :: (IsDBTable (YesodDB UniWorX) c, HasEntity a User) => Colonnade Sortable a (DBCell (YesodDB UniWorX) c)
colUserCompany' = sortable (Just "user-company") (i18nCell MsgTableCompany) $ \heu ->
let uid = heu ^. hasEntity . _entityKey in
sqlCell $ do
companies' <- E.select $ E.from $ \(usrComp `E.InnerJoin` comp) -> do
E.where_ $ usrComp E.^. UserCompanyUser E.==. E.val uid
E.on $ usrComp E.^. UserCompanyCompany E.==. comp E.^. CompanyId
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
pure $ toWgt $ mconcat companies
----------------------------
-- Colonnade manipulation --
----------------------------

View File

@ -48,7 +48,7 @@ module Handler.Utils.Table.Pagination
, linkEitherCell, linkEitherCellM, linkEitherCellM'
, maybeAnchorCellM, maybeAnchorCellM', maybeLinkEitherCellM'
, anchorCellC, anchorCellCM, anchorCellCM', linkEitherCellCM', maybeLinkEitherCellCM'
, cellTooltip
, cellTooltip, cellTooltipIcon
, listCell, listCell', listCellOf, listCellOf'
, ilistCell, ilistCell', ilistCellOf, ilistCellOf'
, formCell, DBFormResult(..), getDBFormResult
@ -1689,10 +1689,13 @@ i18nCell msg = cell $ do
toWidget $ mr msg
cellTooltip :: (RenderMessage UniWorX msg, IsDBTable m a) => msg -> DBCell m a -> DBCell m a
cellTooltip msg = cellContents.mapped %~ (<> tipWdgt)
cellTooltip = cellTooltipIcon Nothing
cellTooltipIcon :: (RenderMessage UniWorX msg, IsDBTable m a) => Maybe Icon -> msg -> DBCell m a -> DBCell m a
cellTooltipIcon icn msg = cellContents.mapped %~ (<> tipWdgt)
where
tipWdgt = iconTooltip (msg2widget msg) Nothing True
tipWdgt = iconTooltip (msg2widget msg) icn True
-- | Always display widget; maybe a link if user is Authorized.
-- Also see variant `linkEmptyCell`
anchorCell :: (HasRoute UniWorX url, ToWidget UniWorX wgt, IsDBTable m a) => url -> wgt -> DBCell m a

View File

@ -47,14 +47,13 @@ import qualified Data.Text as Text
import Jobs.Types(Job, JobChildren)
abbrvName :: User -> Text
abbrvName User{userDisplayName, userFirstName, userSurname} =
if | (lastDisplayName : tsrif) <- reverse nameParts
abbrvName User{userDisplayName, userFirstName, userSurname} =
if | (lastDisplayName : tsrif) <- reverse nameParts
-> assemble $ reverse $ lastDisplayName : abbreviate tsrif
| otherwise
-> assemble $ abbreviate (Text.words userFirstName) <> [userSurname]
where
where
nameParts = Text.words userDisplayName
abbreviate = fmap (Text.take 1)
assemble = Text.intercalate "."
@ -72,11 +71,11 @@ userPrefersEmail = not . userPrefersLetter
getPostalPreferenceAndAddress :: User -> (Bool, Maybe [Text])
getPostalPreferenceAndAddress usr@User{..} =
(((userPrefersPostal || isNothing userPinPassword) && postPossible) || emailImpossible, pa)
where
where
orgEmail = CI.original userEmail
emailImpossible = not ('@' `textElem` orgEmail && '.' `textElem` orgEmail)
postPossible = isJust pa
pa = getPostalAddress usr
pa = getPostalAddress usr
getPostalAddress :: User -> Maybe [Text]
getPostalAddress User{..}
@ -85,22 +84,23 @@ getPostalAddress User{..}
| Just abt <- userCompanyDepartment
= Just $ if | "BVD" `isPrefixOf` abt -> [userDisplayName, abt, "Bodenverkehrsdienste"]
| otherwise -> [userDisplayName, abt, "Hausbriefkasten" ]
| otherwise
| otherwise
= Nothing
-- | Return Entity User and all Supervisors with rerouteNotifications as well as
-- | DEPRECATED, use Handler.Utis.Avs. updateReceivers instead
-- Return Entity User and all Supervisors with rerouteNotifications as well as
-- a boolean indicating if the user is own supervisor with rerouteNotifications
getReceivers :: UserId -> DB (Entity User, [Entity User], Bool)
getReceivers uid = do
underling <- getJustEntity uid
superVs <- selectList [UserSupervisorUser ==. uid, UserSupervisorRerouteNotifications ==. True] []
let superIds = userSupervisorSupervisor . entityVal <$> superVs
if null superIds
if null superIds
then return (underling, [underling], True)
else do
else do
supers <- selectList [UserId <-. superIds] []
if null supers then return (underling, [underling], True)
else
else
return (underling, supers, uid `elem` (entityKey <$> supers))
@ -152,7 +152,7 @@ matchesName (repack -> haystack) (repack -> needle)
guessUser :: PredDNF GuessUserInfo -- ^ guessing criteria
-> Maybe Int64 -- ^ Should the query be limited to a maximum number of results?
-> DB (Maybe (Either (NonEmpty (Entity User)) (Entity User))) -- ^ Just (Left _) in case of multiple results,
-> DB (Maybe (Either (NonEmpty (Entity User)) (Entity User))) -- ^ Just (Left _) in case of multiple results,
-- Just (Right _) in case of single result, and
-- Nothing in case of no result
guessUser (((Set.toList . toNullable) <$>) . Set.toList . dnfTerms -> criteria) mQueryLimit = $cachedHereBinary criteria $ go False
@ -161,7 +161,7 @@ guessUser (((Set.toList . toNullable) <$>) . Set.toList . dnfTerms -> criteria)
asWords = filter (not . Text.null) . Text.words . Text.strip
containsAsSet x y = E.and . map (\y' -> x `E.hasInfix` E.val y') $ asWords y
toSql user pl = bool id E.not_ (is _PLNegated pl) $ case pl ^. _plVar of
GuessUserMatrikelnummer userMatriculation' -> user E.^. UserMatrikelnummer E.==. E.val (Just userMatriculation')
GuessUserEduPersonPrincipalName userEPPN' -> user E.^. UserLdapPrimaryKey E.==. E.val (Just userEPPN')
@ -184,7 +184,7 @@ guessUser (((Set.toList . toNullable) <$>) . Set.toList . dnfTerms -> criteria)
$ criteria ^.. folded)
closeness :: Entity User -> Entity User -> Ordering
closeness ul ur = maximum $ impureNonNull $ criteria <&> \term ->
closeness ul ur = maximum $ impureNonNull $ criteria <&> \term ->
let
matches userField name = _entityVal . userField . to (`matchesName` name)
comp True userField guess = (term ^.. folded . _PLVariable . guess) <&> \name ->
@ -203,7 +203,7 @@ guessUser (((Set.toList . toNullable) <$>) . Set.toList . dnfTerms -> criteria)
]
, b <- [True,False]
]
-- Assuming the input list is sorted in descending order by closeness:
takeClosest [] = []
takeClosest [x] = [x]
@ -235,7 +235,7 @@ guessUser (((Set.toList . toNullable) <$>) . Set.toList . dnfTerms -> criteria)
convertLdapResults [] = Nothing
convertLdapResults [x] = Just $ Right x
convertLdapResults xs = Just $ Left $ NonEmpty.fromList xs
if
| [x] <- users'
, Just True == matchesMatriculation x || didLdap
@ -279,9 +279,9 @@ assimilateUser :: UserId -- ^ @newUserId@
-- ^ Move all relevant properties (submissions, corrections, grades, ...) from @oldUserId@ to @newUserId@
--
-- Fatal errors are thrown, non-fatal warnings are returned
assimilateUser newUserId oldUserId = mapReaderT execWriterT $ do
assimilateUser newUserId oldUserId = mapReaderT execWriterT $ do
E.insertSelectWithConflict
UniqueCourseFavourite
UniqueCourseFavourite
(E.from $ \courseFavourite -> do
E.where_ $ courseFavourite E.^. CourseFavouriteUser E.==. E.val oldUserId
return $ CourseFavourite
@ -398,7 +398,7 @@ assimilateUser newUserId oldUserId = mapReaderT execWriterT $ do
)
(\_current _excluded -> [])
deleteWhere [ SubmissionUserUser ==. oldUserId ]
do
collisions <- E.select . E.from $ \((submissionGroupUserA `E.InnerJoin` submissionGroupA) `E.InnerJoin` (submissionGroupUserB `E.InnerJoin` submissionGroupB)) -> do
E.on $ submissionGroupB E.^. SubmissionGroupId E.==. submissionGroupUserB E.^. SubmissionGroupUserSubmissionGroup
@ -587,7 +587,7 @@ assimilateUser newUserId oldUserId = mapReaderT execWriterT $ do
)
(\current excluded -> [ ExamPartResultLastChanged E.=. E.max (current E.^. ExamPartResultLastChanged) (excluded E.^. ExamPartResultLastChanged) ])
deleteWhere [ ExamPartResultUser ==. oldUserId ]
do
collision <- E.selectMaybe . E.from $ \(examBonusA `E.InnerJoin` examBonusB) -> do
E.on $ examBonusA E.^. ExamBonusExam E.==. examBonusB E.^. ExamBonusExam
@ -609,7 +609,7 @@ assimilateUser newUserId oldUserId = mapReaderT execWriterT $ do
)
(\current excluded -> [ ExamBonusLastChanged E.=. E.max (current E.^. ExamBonusLastChanged) (excluded E.^. ExamBonusLastChanged) ])
deleteWhere [ ExamBonusUser ==. oldUserId ]
let getExamResults = selectSource [ ExamResultUser ==. oldUserId ] []
upsertExamResult oldEREnt@(Entity oldERId oldER) = do
newER' <- getBy $ UniqueExamResult (examResultExam oldER) newUserId
@ -775,19 +775,19 @@ assimilateUser newUserId oldUserId = mapReaderT execWriterT $ do
delete oldSFId
in runConduit $ getStudyFeatures .| C.mapM_ upsertStudyFeatures
-- Qualifications and ongoing LMS
-- Qualifications and ongoing LMS
-- LmsUser: insertSelectWithConflict impossible due to 2 simultaneous uniqueness constraints; UniqueLmsIdent requires proper update, prohibits insert and then delete
-- updateWhere [ LmsUserUser ==. oldUserId ] [ LmsUserUser =. newUserId ] -- might fail due to UniqueLmsQualficationUuser
-- updateWhere [ LmsUserUser ==. oldUserId ] [ LmsUserUser =. newUserId ] -- might fail due to UniqueLmsQualficationUuser
oldLms <- selectList [ LmsUserUser ==. oldUserId ] [ Asc LmsUserQualification ]
newLms <- selectList [ LmsUserUser ==. newUserId ] [ Asc LmsUserQualification ]
let projQ = lmsUserQualification . entityVal
oldQs = Set.fromList (projQ <$> oldLms)
newQs = Set.fromList (projQ <$> newLms)
qConflicts = oldQs `Set.intersection` newQs
qResolvable = Set.fromList [ lmsUserQualification | Entity _ LmsUser{..} <- oldLms, isJust lmsUserEnded, lmsUserQualification `Set.member` qConflicts ]
qProblems = qConflicts `Set.difference` qResolvable
qResolvable = Set.fromList [ lmsUserQualification | Entity _ LmsUser{..} <- oldLms, isJust lmsUserEnded, lmsUserQualification `Set.member` qConflicts ]
qProblems = qConflicts `Set.difference` qResolvable
unless (Set.null qProblems) $ tellError $ UserAssimilateConflictingLmsQualifications qProblems
unless (Set.null qResolvable) $ deleteWhere [ LmsUserUser ==. oldUserId, LmsUserQualification <-. Set.toList qResolvable ] -- delete conflicting and finished LMS, which are still within auditDuration
unless (Set.null qResolvable) $ deleteWhere [ LmsUserUser ==. oldUserId, LmsUserQualification <-. Set.toList qResolvable ] -- delete conflicting and finished LMS, which are still within auditDuration
updateWhere [ LmsUserUser ==. oldUserId ] [ LmsUserUser =. newUserId ]
updateWhere [ QualificationEditUser ==. oldUserId ] [ QualificationEditUser =. newUserId ]
E.insertSelectWithConflict
@ -802,19 +802,19 @@ assimilateUser newUserId oldUserId = mapReaderT execWriterT $ do
E.<&> (qualificationUser E.^. QualificationUserFirstHeld)
E.<&> (qualificationUser E.^. QualificationUserBlockedDue)
)
(\current excluded ->
(\current excluded ->
[ QualificationUserValidUntil E.=. combineWith current excluded E.greatest QualificationUserValidUntil
, QualificationUserLastRefresh E.=. combineWith current excluded E.greatest QualificationUserLastRefresh
, QualificationUserFirstHeld E.=. combineWith current excluded E.least QualificationUserFirstHeld
, QualificationUserBlockedDue E.=. combineWith current excluded E.greatest QualificationUserBlockedDue -- Tested: PostgreSQL GREATEST/LEAST ignores NULL values
]
]
)
deleteWhere [ QualificationUserUser ==. oldUserId ]
-- Supervision is fully merged
E.insertSelectWithConflict
UniqueUserSupervisor
(E.from $ \userSupervisor -> do
(E.from $ \userSupervisor -> do
E.where_ $ userSupervisor E.^. UserSupervisorSupervisor E.==. E.val oldUserId
return $ UserSupervisor
E.<# E.val newUserId
@ -822,11 +822,11 @@ assimilateUser newUserId oldUserId = mapReaderT execWriterT $ do
E.<&> (userSupervisor E.^. UserSupervisorRerouteNotifications)
)
(\current excluded -> [ UserSupervisorRerouteNotifications E.=. (current E.^. UserSupervisorRerouteNotifications E.||. excluded E.^. UserSupervisorRerouteNotifications) ] )
deleteWhere [ UserSupervisorSupervisor ==. oldUserId]
deleteWhere [ UserSupervisorSupervisor ==. oldUserId]
E.insertSelectWithConflict
UniqueUserSupervisor
(E.from $ \userSupervisor -> do
(E.from $ \userSupervisor -> do
E.where_ $ userSupervisor E.^. UserSupervisorUser E.==. E.val oldUserId
return $ UserSupervisor
E.<# (userSupervisor E.^. UserSupervisorSupervisor)
@ -834,14 +834,14 @@ assimilateUser newUserId oldUserId = mapReaderT execWriterT $ do
E.<&> (userSupervisor E.^. UserSupervisorRerouteNotifications)
)
(\current excluded -> [ UserSupervisorRerouteNotifications E.=. (current E.^. UserSupervisorRerouteNotifications E.||. excluded E.^. UserSupervisorRerouteNotifications) ] )
deleteWhere [ UserSupervisorUser ==. oldUserId]
deleteWhere [ UserSupervisorUser ==. oldUserId]
-- Companies, in conflict, keep the newUser-Company as is
E.insertSelectWithConflict
UniqueUserCompany
(E.from $ \userCompany -> do
(E.from $ \userCompany -> do
E.where_ $ userCompany E.^. UserCompanyUser E.==. E.val oldUserId
return $ UserCompany
return $ UserCompany
E.<# E.val newUserId
E.<&> (userCompany E.^. UserCompanyCompany)
E.<&> (userCompany E.^. UserCompanySupervisor)
@ -877,4 +877,4 @@ combineWith :: (PersistEntity val, PersistField typ1) =>
-> (E.SqlExpr (E.Value typ1) -> E.SqlExpr (E.Value typ1) -> E.SqlExpr (E.Value typ2))
-> EntityField val typ1
-> E.SqlExpr (E.Value typ2)
combineWith x y f pj = f (x E.^. pj) (y E.^. pj)
combineWith x y f pj = f (x E.^. pj) (y E.^. pj)

View File

@ -59,6 +59,11 @@ nameWidget displayName surname = toWidget $ nameHtml displayName surname
userWidget :: HasUser c => c -> Widget
userWidget x = nameWidget (x ^. _userDisplayName) (x ^._userSurname)
linkUserWidget :: HasRoute UniWorX url => (CryptoUUIDUser -> url) -> Entity User -> Widget
linkUserWidget lnk (Entity uid usr) = do
uuid <- encrypt uid
simpleLink (userWidget usr) (lnk uuid)
-- | toWidget-Version of @nameEmailHtml@, for convenience
nameEmailWidget :: UserEmail -- ^ userEmail
-> Text -- ^ userDisplayName

View File

@ -17,6 +17,7 @@ import Import
import Jobs.Queue
-- import Jobs.Handler.Intervals.Utils
import Database.Esqueleto.Experimental ((:&)(..))
import qualified Database.Esqueleto.Experimental as E
--import qualified Database.Esqueleto.Legacy as E
-- import qualified Database.Esqueleto.PostgreSQL as E -- for insertSelect variant
@ -180,14 +181,14 @@ dispatchJobLmsResults qid = JobHandlerAtomic act
-- otherwise there is nothing to do: we cannot renew s qualification without a specified validDuration
-- result :: [(Entity QualificationUser, Entity LmsUser, Entity LmsResult)]
results <- E.select $ do
(quser E.:& luser E.:& lresult) <- E.from $
(quser :& luser :& lresult) <- E.from $
E.table @QualificationUser -- table not needed if renewal from lms completion day is used TODO: decide!
`E.innerJoin` E.table @LmsUser
`E.on` (\(quser E.:& luser) ->
`E.on` (\(quser :& luser) ->
luser E.^. LmsUserUser E.==. quser E.^. QualificationUserUser
E.&&. luser E.^. LmsUserQualification E.==. quser E.^. QualificationUserQualification)
`E.innerJoin` E.table @LmsResult
`E.on` (\(_ E.:& luser E.:& lresult) ->
`E.on` (\(_ :& luser :& lresult) ->
luser E.^. LmsUserIdent E.==. lresult E.^. LmsResultIdent
E.&&. luser E.^. LmsUserQualification E.==. lresult E.^. LmsResultQualification)
E.where_ $ quser E.^. QualificationUserQualification E.==. E.val qid
@ -239,9 +240,9 @@ dispatchJobLmsUserlist qid = JobHandlerAtomic act
now <- liftIO getCurrentTime
-- result :: [(Entity LmsUser, Entity LmsUserlist)]
results <- E.select $ do
(luser E.:& lulist) <- E.from $
(luser :& lulist) <- E.from $
E.table @LmsUser `E.leftJoin` E.table @LmsUserlist
`E.on` (\(luser E.:& lulist) -> luser E.^. LmsUserIdent E.=?. lulist E.?. LmsUserlistIdent
`E.on` (\(luser :& lulist) -> luser E.^. LmsUserIdent E.=?. lulist E.?. LmsUserlistIdent
E.&&. luser E.^. LmsUserQualification E.=?. lulist E.?. LmsUserlistQualification)
E.where_ $ luser E.^. LmsUserQualification E.==. E.val qid
E.&&. E.isNothing (luser E.^. LmsUserEnded) -- do not process closed learners

View File

@ -45,7 +45,7 @@ o .:?~ key = o .: key <|> maybe empty parseJSON go
-- Like (.:?) but maps Just null to Nothing, ie. Nothing instead of Just ""
(.:?!) :: (MonoFoldable a, FromJSON a) => Object -> Text -> Parser (Maybe a)
(.:?!) o k = null2nothing <$> (o .:? k)
(.:?!) o k = canonical <$> (o .:? k)
-- | `SloppyBool` successfully parses different variations of true/false
@ -81,7 +81,58 @@ instance FromJSON SloppyBool where
-- AVS Datatypes --
-------------------
type AvsInternalPersonalNo = Text -- ought to be all digits, type synonym for clarity/documentation within types
newtype AvsInternalPersonalNo = AvsInternalPersonalNo { avsInternalPersonalNo :: Text } -- ought to be all digits
deriving (Eq, Ord, Show, Generic, Typeable)
deriving newtype (NFData, PathPiece, PersistField, PersistFieldSql, Csv.ToField, Csv.FromField)
instance E.SqlString AvsInternalPersonalNo
-- AvsInternalPersonalNo is an untagged Text with respect to FromJSON/ToJSON, as needed by AVS API
normalizeAvsInternalPersonalNo :: Text -> Text
normalizeAvsInternalPersonalNo = Text.dropWhile (\c -> '0' == c || Char.isSpace c)
mkAvsInternalPersonalNo :: Text -> AvsInternalPersonalNo
mkAvsInternalPersonalNo = AvsInternalPersonalNo . normalizeAvsInternalPersonalNo
instance Canonical AvsInternalPersonalNo where
canonical (AvsInternalPersonalNo ipn) = AvsInternalPersonalNo $ Text.dropWhile (\c -> '0' == c || Char.isSpace c) ipn
instance FromJSON AvsInternalPersonalNo where
parseJSON x = AvsInternalPersonalNo . normalizeAvsInternalPersonalNo <$> parseJSON x
instance ToJSON AvsInternalPersonalNo where
toJSON (AvsInternalPersonalNo ipn) = toJSON $ normalizeAvsInternalPersonalNo ipn
type instance Element AvsInternalPersonalNo = Char
instance MonoFoldable AvsInternalPersonalNo where
ofoldMap f = ofoldr (mappend . f) mempty . avsInternalPersonalNo
ofoldr x y = Text.foldr x y . avsInternalPersonalNo
ofoldl' x y = Text.foldl' x y . avsInternalPersonalNo
otoList = Text.unpack . avsInternalPersonalNo
oall x = Text.all x . avsInternalPersonalNo
oany x = Text.any x . avsInternalPersonalNo
onull = Text.null . avsInternalPersonalNo
olength = Text.length . avsInternalPersonalNo
ofoldr1Ex x = Text.foldr1 x . avsInternalPersonalNo
ofoldl1Ex' x = Text.foldl1' x . avsInternalPersonalNo
headEx = Text.head . avsInternalPersonalNo
lastEx = Text.last . avsInternalPersonalNo
{-# INLINE ofoldMap #-}
{-# INLINE ofoldr #-}
{-# INLINE ofoldl' #-}
{-# INLINE otoList #-}
{-# INLINE oall #-}
{-# INLINE oany #-}
{-# INLINE onull #-}
{-# INLINE olength #-}
{-# INLINE ofoldr1Ex #-}
{-# INLINE ofoldl1Ex' #-}
{-# INLINE headEx #-}
{-# INLINE lastEx #-}
{-
instance {-# OVERLAPS #-} Canonical (Maybe AvsInternalPersonalNo) where
canonical (Just aipn) | ipn@(AvsInternalPersonalNo pn) <- canonical aipn, not (null pn) = Just ipn
canonical _ = Nothing
-}
-- CompleteCardNo = xxxxxxxx.y
-- where x is an 8 digit AvsCardNo prefixed by zeros, see normalizeAvsCardNo
@ -99,6 +150,11 @@ instance ToJSON AvsCardNo where
normalizeAvsCardNo :: Text -> Text
normalizeAvsCardNo = Text.justifyRight 8 '0'
instance Canonical AvsCardNo where
canonical AvsCardNo{..} = AvsCardNo $ normalizeAvsCardNo avsCardNo
-- canonical = AvsCardNo . normalizeAvsCardNo . avsCardNo
data AvsFullCardNo = AvsFullCardNo { avsFullCardNo :: AvsCardNo, avsFullCardVersion :: AvsVersionNo }
deriving (Eq, Ord, Generic, Typeable)
@ -117,7 +173,7 @@ readAvsFullCardNo _ = Nothing
discernAvsCardPersonalNo :: Text -> Maybe (Either AvsFullCardNo AvsInternalPersonalNo) -- Just implies it is a whole number or decimal with one digit after the point
discernAvsCardPersonalNo (Text.span Char.isDigit -> (c, pv))
| Text.null pv
= Just $ Right c
= Just $ Right $ mkAvsInternalPersonalNo c
| not $ Text.null c
, Just ('.', v) <- Text.uncons pv
, Just (Char.isDigit -> True, "") <- Text.uncons v
@ -256,18 +312,19 @@ instance Ord AvsDataPersonCard where
makeLenses_ ''AvsDataPersonCard
{-
instance Canonical AvsDataPersonCard where
canonical proto = proto { avsDataStreet = null2nothing $ avsDataStreet proto
, avsDataPostalCode = null2nothing $ avsDataPostalCode proto
, avsDataCity = null2nothing $ avsDataCity proto
, avsDataFirm = null2nothing $ avsDataFirm proto
canonical proto = proto { avsDataStreet = canonical $ avsDataStreet proto
, avsDataPostalCode = canonical $ avsDataPostalCode proto
, avsDataCity = canonical $ avsDataCity proto
, avsDataFirm = canonical $ avsDataFirm proto
}
-}
instance Canonical AvsDataPersonCard where
canonical proto =
proto & _avsDataStreet %~ null2nothing
& _avsDataPostalCode %~ null2nothing
& _avsDataCity %~ null2nothing
& _avsDataFirm %~ null2nothing
proto & _avsDataStreet %~ canonical
& _avsDataPostalCode %~ canonical
& _avsDataCity %~ canonical
& _avsDataFirm %~ canonical
& _avsDataCardNo %~ canonical
-- TODO: use canonical in FromJSON/ToJSON instances for consistency
instance FromJSON AvsDataPersonCard where
@ -281,7 +338,7 @@ instance FromJSON AvsDataPersonCard where
<*> v .:?! "PostalCode"
<*> v .:?! "City"
<*> v .:?! "Firm"
<*> v .: "CardNo"
<*> ((v .: "CardNo") <&> canonical)
<*> v .: "VersionNo"
instance ToJSON AvsDataPersonCard where
@ -289,16 +346,16 @@ instance ToJSON AvsDataPersonCard where
catMaybes
[ ("ValidTo" .=) <$> avsDataValidTo
, ("IssueDate" .=) <$> avsDataIssueDate
, ("Street" .=) <$> (avsDataStreet & null2nothing)
, ("PostalCode" .=) <$> (avsDataPostalCode & null2nothing)
, ("City" .=) <$> (avsDataCity & null2nothing)
, ("Firm" .=) <$> (avsDataFirm & null2nothing)
, ("Street" .=) <$> (avsDataStreet & canonical)
, ("PostalCode" .=) <$> (avsDataPostalCode & canonical)
, ("City" .=) <$> (avsDataCity & canonical)
, ("Firm" .=) <$> (avsDataFirm & canonical)
]
<>
[ "Valid" .= show avsDataValid
, "CardColor" .= avsDataCardColor
, "CardAreas" .= Set.foldl Text.snoc Text.empty avsDataCardAreas
, "CardNo" .= avsDataCardNo
, "CardNo" .= (avsDataCardNo & canonical)
, "VersionNo" .= avsDataVersionNo
]
derivePersistFieldJSON ''AvsDataPersonCard
@ -332,7 +389,7 @@ data AvsDataPerson = AvsDataPerson
makeLenses_ ''AvsDataPerson
instance Canonical AvsDataPerson where
canonical = over _avsPersonInternalPersonalNo null2nothing
canonical = over _avsPersonInternalPersonalNo canonical
. over _avsPersonPersonCards canonical
@ -347,7 +404,7 @@ instance FromJSON AvsDataPerson where
instance ToJSON AvsDataPerson where
toJSON AvsDataPerson{..} = object $
catMaybes [ ("InternalPersonalNo" .=) <$> (avsPersonInternalPersonalNo & null2nothing) ]
catMaybes [ ("InternalPersonalNo" .=) <$> (avsPersonInternalPersonalNo & canonical) ]
<>
[ "FirstName" .= avsPersonFirstName
, "LastName" .= avsPersonLastName
@ -470,7 +527,7 @@ newtype AvsQueryStatus = AvsQueryStatus (Set AvsPersonId)
deriving (Eq, Ord, Show, Generic, Typeable)
deriveJSON defaultOptions ''AvsQueryStatus
newtype AvsQueryGetLicences = AvsQueryGetLicences (Set AvsObjPersonId)
newtype AvsQueryGetLicences = AvsQueryGetLicences AvsObjPersonId -- this should have been a set, but the specification was implemented differently
deriving (Eq, Ord, Show, Generic, Typeable)
deriveJSON defaultOptions ''AvsQueryGetLicences

View File

@ -16,6 +16,18 @@ import Model.Types.TH.PathPiece
import qualified Data.Map as Map
{-
How to add a changelog item:
* pick a constructor name for the feature, e.g. MyGoodFeature
* add hamlet files in kebab-case for each langugage, e.g. /templates/i18n/changelog/my-good-feature.de-de-formal.hamlet
and /templates/i18n/changelog/my-good-feature.en-eu.hamlet
* if it is a bugfix, classify it in `classifyChangelogItem` below
* add list item (MyGoodFeature, date) to `changelogItemDays` below
Es können mehrere Changes an einem Tag stattfinden, aber jeder Change kann nur an einem Tag stattfinden.
Changes werden in die Datenbank eingetragen, d.h. sie müssen auch in der DB gelöscht werden, wenn diese nicht mehr angezeigt werden sollen!
-}
mkI18nWidgetEnum "Changelog" "changelog"
derivePersistFieldPathPiece ''ChangelogItem
@ -31,6 +43,19 @@ data ChangelogItemKind
makePrisms ''ChangelogItemKind
classifyChangelogItem :: ChangelogItem -> ChangelogItemKind
classifyChangelogItem = \case
--ChangelogBlaBla -> ChangelogItemBugfix
--ChangelogFradriveInitialRelease -> ChangelogItemFeature -- remove me once we have a bugfix
_other -> ChangelogItemFeature
changelogItemDays :: Map ChangelogItem Day
changelogItemDays = Map.fromListWithKey (\k d1 d2 -> bool (error $ "Duplicate changelog days for " <> show k) d1 $ d1 /= d2)
[ (ChangelogFradriveInitialRelease, [day|2022-12-12|])
]
{- FOR REFERENCE, PREVIOUS CHANGELOG (delete in the future, along with all translation files):
classifyChangelogItem :: ChangelogItem -> ChangelogItemKind
classifyChangelogItem = \case
ChangelogHaskellCampusLogin -> ChangelogItemBugfix
@ -150,3 +175,4 @@ changelogItemDays = Map.fromListWithKey (\k d1 d2 -> bool (error $ "Duplicate ch
, (ChangelogMaterialsVideoStreaming, [day|2020-11-10|])
, (ChangelogFixPersonalisedSheetFilesKeep, [day|2020-11-10|])
]
-}

View File

@ -81,7 +81,6 @@ data AuthTag -- sortiert nach gewünschter Reihenfolge auf /authpreds, d.h. Prä
| AuthExamOffice
| AuthSystemExamOffice
| AuthSystemPrinter
| AuthSystemSap
| AuthEvaluation
| AuthCourseRegistered
| AuthTutorialRegistered

View File

@ -15,8 +15,7 @@ data SystemFunction
= SystemExamOffice
| SystemFaculty
| SystemStudent
| SystemPrinter
| SystemSap
| SystemPrinter
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable)
deriving anyclass (Universe, Finite, Hashable, NFData)

View File

@ -278,6 +278,7 @@ data UserDefaultConf = UserDefaultConf
, userDefaultShowSex :: Bool
, userDefaultExamOfficeGetSynced :: Bool
, userDefaultExamOfficeGetLabels :: Bool
, userDefaultPrefersPostal :: Bool
} deriving (Show)
data PWHashConf = PWHashConf

View File

@ -363,6 +363,9 @@ toWgt :: ToMarkup a
toWgt = toWidget . toHtml
-- Convenience Functions to avoid type signatures:
text2markup :: Text -> Markup
text2markup t = [shamlet|#{t}|]
text2widget :: Text -> WidgetFor site ()
text2widget t = [whamlet|#{t}|]
@ -619,6 +622,10 @@ trd3 (_,_,z) = z
mTuple :: Applicative f => f a -> f b -> f (a, b)
mTuple = liftA2 (,)
-- From Data.Tuple.Extra
mapBoth :: (a -> b) -> (a,a) -> (b,b)
mapBoth f ~(a,b) = (f a, f b)
-----------
-- Lists --
-----------
@ -781,6 +788,9 @@ partitionKeysEither = over _2 (Map.mapKeysMonotonic . view $ singular _Right) .
mapFromSetM :: Applicative m => (k -> m v) -> Set k -> m (Map k v)
mapFromSetM = (sequenceA .) . Map.fromSet
setToMap :: (Ord k) => (v -> k) -> Set v -> Map k v
setToMap mkKey = Map.fromList . fmap (\x -> (mkKey x, x)) . Set.toList
mapFM :: (Applicative m, Ord k, Finite k) => (k -> m v) -> m (Map k v)
mapFM = sequenceA . mapF
@ -816,10 +826,10 @@ toNothing = const Nothing
toNothingS :: String -> Maybe b
toNothingS = const Nothing
-- a more general formulation probably possible
null2nothing :: MonoFoldable a => Maybe a -> Maybe a
null2nothing (Just x) | null x = Nothing
null2nothing other = other
-- replaced by a more general formulation, see canonical
-- null2nothing :: MonoFoldable a => Maybe a -> Maybe a
-- null2nothing (Just x) | null x = Nothing
-- null2nothing other = other
-- | Swap 'Nothing' for 'Just' and vice versa
-- This belongs into Module 'Utils' but we have a weird cyclic
@ -1047,6 +1057,16 @@ throwExceptT = exceptT throwM return
generalFinally :: MonadMask m => m a -> (ExitCase a -> m b) -> m a
generalFinally action finalizer = view _1 <$> generalBracket (return ()) (const finalizer) (const action)
-------------
-- Functor --
-------------
infixl 4 <<$>>
(<<$>>) :: (Functor f, Functor g) => (a -> b) -> f (g a) -> f (g b)
(<<$>>) f x = fmap f <$> x
------------
-- Monads --
------------
@ -1872,5 +1892,19 @@ makePrisms ''ExitCase
class Canonical a where
canonical :: a -> a
instance {-# OVERLAPPABLE #-} MonoFoldable mono => Canonical (Maybe mono) where
canonical (Just t) | null t = Nothing
canonical other = other
{-
instance {-# OVERLAPPABLE #-} (Canonical mono, MonoFoldable mono, Eq mono) => Canonical (Maybe mono) where
canonical r@(Just t) = let c = canonical t
in if null c then Nothing else
if t==c then r else Just c
canonical other = other
-}
-- this instance is more of a convenient abuse of the class (expand to Foldable)
instance (Ord a, Canonical a) => Canonical (Set a) where
canonical = Set.map canonical

View File

@ -27,6 +27,8 @@ type AVSPersonStatus = "PersonStatus" :> ReqBody '[JSON] AvsQueryStatus :> Po
type AVSGetRampLicences = "RampDrivingLicenceInfo" :> ReqBody '[JSON] AvsQueryGetLicences :> Post '[JSON] AvsResponseGetLicences
type AVSSetRampLicences = "RampDrivingLicence" :> ReqBody '[JSON] AvsQuerySetLicences :> Post '[JSON] AvsResponseSetLicences
avsMaxSetLicenceAtOnce :: Int
avsMaxSetLicenceAtOnce = 99 -- maximum input set size for avsQuerySetLicences as enforced by AVS
avsApi :: Proxy AVS
avsApi = Proxy
@ -45,15 +47,15 @@ data AvsQuery = AvsQuery
{ avsQueryPerson :: forall m. MonadIO m => AvsQueryPerson -> m (Either ClientError AvsResponsePerson)
, avsQueryStatus :: forall m. MonadIO m => AvsQueryStatus -> m (Either ClientError AvsResponseStatus)
, avsQuerySetLicences :: forall m. MonadIO m => AvsQuerySetLicences -> m (Either ClientError AvsResponseSetLicences)
, avsQueryGetLicences :: forall m. MonadIO m => AvsQueryGetLicences -> m (Either ClientError AvsResponseGetLicences)
-- , avsQueryGetLicences :: forall m. MonadIO m => AvsQueryGetLicences -> m (Either ClientError AvsResponseGetLicences) -- not supported by VSM
, avsQueryGetAllLicences :: forall m. MonadIO m => m (Either ClientError AvsResponseGetLicences)
}
makeLenses_ ''AvsQuery
-- | To query all active licences, a special argument must be prepared
-- | To query all active licences, a special constant argument must be prepared
avsQueryAllLicences :: AvsQueryGetLicences
avsQueryAllLicences = AvsQueryGetLicences $ Set.singleton $ AvsObjPersonId $ AvsPersonId 0
avsQueryAllLicences = AvsQueryGetLicences $ AvsObjPersonId $ AvsPersonId 0
mkAvsQuery :: BaseUrl -> BasicAuthData -> ClientEnv -> AvsQuery
@ -61,7 +63,7 @@ mkAvsQuery baseUrl basicAuth cliEnv = AvsQuery
{ avsQueryPerson = \q -> liftIO $ catch404toEmpty <$> runClientM (rawQueryPerson q) cliEnv
, avsQueryStatus = \q -> liftIO $ runClientM (rawQueryStatus q) cliEnv
, avsQuerySetLicences = \q -> liftIO $ runClientM (rawQuerySetLicences q) cliEnv
, avsQueryGetLicences = \q -> liftIO $ runClientM (rawQueryGetLicences q) cliEnv
-- , avsQueryGetLicences = \q -> liftIO $ runClientM (rawQueryGetLicences q) cliEnv
, avsQueryGetAllLicences = liftIO $ runClientM (rawQueryGetLicences avsQueryAllLicences) cliEnv
}
where
@ -87,21 +89,32 @@ getValidLicence cutoff licence' cards = Set.lookupMax validLicenceCards
cardMatch AvsDataPersonCard{..} =
avsDataValid && (avsDataValidTo >= cutoff) && (licence `Set.member` avsDataCardAreas)
guessLicenceAddress :: Set AvsDataPersonCard -> Maybe (Maybe Text, Text, AvsDataPersonCard)
guessLicenceAddress cards
| Just c <- Set.lookupMax cards
, card@AvsDataPersonCard{..} <- Set.foldr pickLicenceAddress c cards
, Just street <- avsDataStreet
getCompanyAddress :: AvsDataPersonCard -> Maybe (Maybe Text, Text, AvsDataPersonCard)
getCompanyAddress card@AvsDataPersonCard{..}
| Just street <- avsDataStreet
, Just pcode <- avsDataPostalCode
, Just city <- avsDataCity
= Just (avsDataFirm, Text.unlines [street, Text.unwords [pcode, city]], card)
| otherwise = Nothing
-- | Helper for guessLicenceAddress
-- | From a set of card, choose the one with the most complete postal address.
-- Returns company, postal address and the associated card where the address was taken from
guessLicenceAddress :: Set AvsDataPersonCard -> Maybe (Maybe Text, Text, AvsDataPersonCard)
guessLicenceAddress cards
| Just c <- Set.lookupMax cards
, card <- Set.foldr pickLicenceAddress c cards
= getCompanyAddress card
| otherwise = Nothing
-- | Helper for guessLicenceAddress or getCompanyAddress
mergeCompanyAddress :: (Maybe Text, Text, a) -> Text
mergeCompanyAddress (Nothing , addr, _) = addr
mergeCompanyAddress (Just firm, addr, _) = firm <> Text.cons '\n' addr
maybeCompanyAddress :: AvsDataPersonCard -> Maybe Text
maybeCompanyAddress = fmap mergeCompanyAddress . getCompanyAddress
hasAddress :: AvsDataPersonCard -> Bool
hasAddress AvsDataPersonCard{..} = isJust avsDataStreet && isJust avsDataCity && isJust avsDataPostalCode
@ -157,7 +170,7 @@ mergeAvsDataPerson = Map.unionWithKey merger
in AvsDataPerson
{ avsPersonFirstName = pickBy' Text.length avsPersonFirstName
, avsPersonLastName = pickBy' Text.length avsPersonLastName
, avsPersonInternalPersonalNo = pickBy' (Text.length . fromMaybe mempty) avsPersonInternalPersonalNo
, avsPersonInternalPersonalNo = pickBy' (maybe 0 length) avsPersonInternalPersonalNo
, avsPersonPersonNo = pickBy' id avsPersonPersonNo
, avsPersonPersonID = api -- keys must be identical due to call with insertWithKey
, avsPersonPersonCards = (Set.union `on` avsPersonPersonCards) pa pb

View File

@ -299,7 +299,11 @@ data FormIdentifier
| FIDExamAutoOccurrenceCalculate | FIDExamAutoOccurrenceConfirm | FIDExamAutoOccurrenceNudge UUID | FIDExamAutoOccurrenceIgnoreRoom UUID
| FIDTestDownload
| FIDAvsQueryPerson
| FIDAvsQueryStatus
| FIDAvsQueryStatus
| FIDAvsCreateUser
| FIDAvsQueryLicenceDiffs
| FIDAvsQueryLicence
| FIDAvsSetLicence
| FIDLmsLetter
deriving (Eq, Ord, Read, Show)

View File

@ -104,6 +104,7 @@ data Icon
| IconPrintCenter
| IconLetter
| IconAt
| IconSupervisor
deriving (Eq, Ord, Enum, Bounded, Show, Read, Generic, Typeable)
deriving anyclass (Universe, Finite, NFData)
@ -186,6 +187,7 @@ iconText = \case
IconPrintCenter -> "mail-bulk" -- From fontawesome v6 onwards: "envelope-bulk"
IconLetter -> "mail-bulk" -- Problem "envelope" already used for email as well
IconAt -> "at"
IconSupervisor -> "head-side" -- must be notably different to user
nullaryPathPiece ''Icon $ camelToPathPiece' 1
deriveLift ''Icon
@ -218,7 +220,7 @@ iconStacked ic0 ic1
<i .fas .fa-stack-2x .fa-#{iconText ic1}>
|]
-- Create an icon (defaults to "?") with a specified tooltip
-- Create an icon (defaults to "?") with a specified tooltip; inline-bool just affects the size of the icon
iconTooltip :: forall site. WidgetFor site () -> Maybe Icon -> Bool -> WidgetFor site ()
iconTooltip tooltip mIcon isInlineTooltip = let
ic = iconText $ fromMaybe IconTooltipDefault mIcon

View File

@ -125,6 +125,8 @@ makeClassyFor_ ''QualificationUser
makeClassyFor_ ''LmsUser
makeClassyFor_ ''LmsUserlist
makeClassyFor_ ''LmsResult
makeClassyFor_ ''UserAvs
makeClassyFor_ ''UserAvsCard
_entityKey :: Getter (Entity record) (Key record)
-- ^ Not a `Lens'` for safety

View File

@ -42,7 +42,7 @@ import System.Process.Typed -- for calling pdftk for pdf encryption
import Handler.Utils.Users
import Handler.Utils.DateTime
import Handler.Utils.Mail
import Handler.Utils.Widgets (nameHtml')
import Handler.Utils.Widgets (nameHtml')
import Jobs.Handler.SendNotification.Utils
-- import Model.Types.Markup -- TODO-QSV: should this module be moved accordingly?
@ -119,8 +119,8 @@ appMeta f (P.Pandoc m bs) = P.Pandoc (f m) bs
applyMetas :: (P.HasMeta p, Foldable t, P.ToMetaValue a) => t (Text, Maybe a) -> p -> p
applyMetas metas doc = Fold.foldr act doc metas
where
act (_, Nothing) acc = acc
act (k, Just v ) acc = P.setMeta k v acc
act (k, Just v) acc | notNull k = P.setMeta k v acc
act _ acc = acc
-- | Add meta to pandoc. Existing variables will be overwritten.
@ -377,7 +377,7 @@ data LetterRenewQualificationF = LetterRenewQualificationF
, lmsPin :: Text
, qualHolder :: UserDisplayName
, qualExpiry :: Day
, qualId :: QualificationId
, qualId :: QualificationId
, qualName :: Text
, qualShort :: Text
, qualDuration :: Maybe Int
@ -386,8 +386,8 @@ data LetterRenewQualificationF = LetterRenewQualificationF
instance MDLetter LetterRenewQualificationF where
getTemplate _ = templateRenewal
getMailSubject l = SomeMessage $ MsgMailSubjectQualificationRenewal $ qualShort l
getMailBody l = SomeMessage $ MsgMailBodyQualificationRenewal $ qualName l
getMailSubject l = SomeMessage $ MsgMailSubjectQualificationRenewal $ qualShort l
getMailBody l = SomeMessage $ MsgMailBodyQualificationRenewal $ qualName l
letterMeta LetterRenewQualificationF{..} _lang DateTimeFormatter{ format } = mkMeta
[ toMeta "login" lmsIdent
, toMeta "pin" lmsPin
@ -413,7 +413,7 @@ instance MDLetter LetterRenewQualificationF where
sendEmailOrLetter :: (MDLetter l) => UserId -> l -> Handler Bool
sendEmailOrLetter recipient letter = do
(underling, receivers, undercopy) <- liftHandler . runDB $ getReceivers recipient
(underling, receivers, undercopy) <- runDB $ getReceivers recipient
let tmpl = getTemplate $ pure letter
pjid = getPJId letter
-- Below are only needed if sent by email

View File

@ -11,8 +11,10 @@ module Utils.Set
, setPartitionEithers
, setFromFunc
, mapIntersectNotOne
, set2NonEmpty
) where
import qualified Data.List.NonEmpty as NonEmpty
import qualified Data.Set as Set
import qualified Data.Map.Strict()
import qualified Data.Map as Map
@ -65,4 +67,10 @@ setPartitionEithers :: (Ord a, Ord b) => Set (Either a b) -> (Set a, Set b)
setPartitionEithers = (,) <$> setMapMaybe (preview _Left) <*> setMapMaybe (preview _Right)
setFromFunc :: (Finite k, Ord k) => (k -> Bool) -> Set k
setFromFunc = Set.fromList . flip filter universeF
setFromFunc = Set.fromList . flip filter universeF
-- | convert a Set to NonEmpty, inserting a default value if necessary
set2NonEmpty :: a -> Set a -> NonEmpty.NonEmpty a
set2NonEmpty _ (Set.toList -> h:t) = h NonEmpty.:| t
set2NonEmpty d _ = d NonEmpty.:| []

View File

@ -0,0 +1,43 @@
$newline never
$# SPDX-FileCopyrightText: 2022 Steffen Jost <jost@tcs.ifi.lmu.de>
$#
$# SPDX-License-Identifier: AGPL-3.0-or-later
<section>
<h2>
_{MsgProblemsHeadingDrivers}
<dl .deflist>
<dt .deflist__dt>^{flagError driversHaveAvsIds}
<dd .deflist__dd>^{simpleLinkI MsgProblemsDriversHaveAvsIds ProblemWithoutAvsId}
$case diffLics
$of Left err
<dt .deflist__dt>^{flagError False}
<dd .deflist__dd>^{modal (i18n MsgProblemsAvsProblem) (Right err)}
$of Right (ok0,ok1,ok2)
<dt .deflist__dt>^{flagNonZero ok2}
<dd .deflist__dd>_{MsgProblemsDriverSynch2}
<dt .deflist__dt>^{flagNonZero ok1}
<dd .deflist__dd>_{MsgProblemsDriverSynch1}
<dt .deflist__dt>^{flagNonZero ok0}
<dd .deflist__dd>_{MsgProblemsDriverSynch0}
<dt .deflist__dt>^{flagWarning rDriversHaveFs}
<dd .deflist__dd>^{simpleLinkI MsgProblemsRDriversHaveFs ProblemFbutNoR}
<section>
<h2>
_{MsgProblemsHeadingUsers}
<dl .deflist>
<dt .deflist__dt>^{flagError usersAreReachable}
<dd .deflist__dd>^{simpleLinkI MsgProblemsUsersAreReachable ProblemUnreachableR}
<dt .deflist__dt>^{flagError noStalePrintJobs}
<dd .deflist__dd>^{simpleLinkI (MsgProblemsNoStalePrintJobs cutOffPrintDays) PrintCenterR}

View File

@ -6,7 +6,39 @@ $# SPDX-License-Identifier: AGPL-3.0-or-later
<section>
<p>
Person Search:
Upsert User by CardNo or Fraport Personnel Number:
^{crUsrForm}
$maybe answer <- mbCrUser
<p>
^{answer}
<section>
<p>
Get Licence by AVS Person ID:
^{getLicForm}
$maybe answer <- mbGetLic
<p>
^{answer}
<section>
<p>
Set Licence by AVS Person ID:
^{setLicForm}
$maybe answer <- mbSetLic
<p>
^{answer}
<section>
<p>
Synchronize licences with AVS.
^{qryLicForm}
$maybe answer <- mbQryLic
<p>
^{answer}
<section>
<p>
Person search:
^{personForm}
$maybe answer <- mbPerson
<p>
@ -15,7 +47,7 @@ $# SPDX-License-Identifier: AGPL-3.0-or-later
<section>
<p>
Person Status:
Person status:
^{statusForm}
$maybe answer <- mbStatus
<p>

View File

@ -15,6 +15,7 @@ $# SPDX-License-Identifier: AGPL-3.0-or-later
<p>
^{iconTooltip testTooltipMsg Nothing False}
$# ^{iconTooltip testTooltipMsg Nothing True} -- just a different size
^{messageTooltip msgInfoTooltip}
^{messageTooltip msgSuccessTooltip}
^{messageTooltip msgWarningTooltip}

View File

@ -1,9 +0,0 @@
$newline never
$# SPDX-FileCopyrightText: 2022 Gregor Kleen <gregor.kleen@ifi.lmu.de>
$#
$# SPDX-License-Identifier: AGPL-3.0-or-later
Kursassoziierte Studienfächer wurden abgeschafft.
<br>
Es werden nun an allen kursbezogenen Stellen jene Studiendaten angezeigt, die während des entsprechenden Semesters aktuell waren.

View File

@ -1,9 +0,0 @@
$newline never
$# SPDX-FileCopyrightText: 2022 Gregor Kleen <gregor.kleen@ifi.lmu.de>
$#
$# SPDX-License-Identifier: AGPL-3.0-or-later
Abolished course-associated features of study.
<br>
In course-related contexts now all study features which were up to date during the relevant term are displayed.

View File

@ -1,7 +0,0 @@
$newline never
$# SPDX-FileCopyrightText: 2022 Winnie Ros <winnie.ros@campus.lmu.de>
$#
$# SPDX-License-Identifier: AGPL-3.0-or-later
Benutzer:innen können sich in der Testphase komplett selbst löschen

View File

@ -1,7 +0,0 @@
$newline never
$# SPDX-FileCopyrightText: 2022 Sarah Vaupel <sarah.vaupel@ifi.lmu.de>
$#
$# SPDX-License-Identifier: AGPL-3.0-or-later
During testing users may completely delete their accounts.

View File

@ -1,7 +0,0 @@
$newline never
$# SPDX-FileCopyrightText: 2022 Gregor Kleen <gregor.kleen@ifi.lmu.de>
$#
$# SPDX-License-Identifier: AGPL-3.0-or-later
Zusätzliche Uhrzeit- und Datumsformate

View File

@ -1,7 +0,0 @@
$newline never
$# SPDX-FileCopyrightText: 2022 Gregor Kleen <gregor.kleen@ifi.lmu.de>
$#
$# SPDX-License-Identifier: AGPL-3.0-or-later
Additional date and time formats

View File

@ -1,7 +0,0 @@
$newline never
$# SPDX-FileCopyrightText: 2022 Gregor Kleen <gregor.kleen@ifi.lmu.de>
$#
$# SPDX-License-Identifier: AGPL-3.0-or-later
Zusätzliche Benachrichtigungen für Übungsblätter

View File

@ -1,7 +0,0 @@
$newline never
$# SPDX-FileCopyrightText: 2022 Sarah Vaupel <sarah.vaupel@ifi.lmu.de>
$#
$# SPDX-License-Identifier: AGPL-3.0-or-later
Additional notifications for exercise sheets.

View File

@ -1,7 +0,0 @@
$newline never
$# SPDX-FileCopyrightText: 2022 Gregor Kleen <gregor.kleen@ifi.lmu.de>
$#
$# SPDX-License-Identifier: AGPL-3.0-or-later
Bewerbungen für Zentralanmeldungen

View File

@ -1,7 +0,0 @@
$newline never
$# SPDX-FileCopyrightText: 2022 Gregor Kleen <gregor.kleen@ifi.lmu.de>
$#
$# SPDX-License-Identifier: AGPL-3.0-or-later
Applications for central allocations

View File

@ -1,7 +0,0 @@
$newline never
$# SPDX-FileCopyrightText: 2022 Gregor Kleen <gregor.kleen@ifi.lmu.de>
$#
$# SPDX-License-Identifier: AGPL-3.0-or-later
Das Eintragen von Fristen bis zu denen Nachrücker aus Zentralanmeldungen akzeptiert werden ist nun möglich

View File

@ -1,7 +0,0 @@
$newline never
$# SPDX-FileCopyrightText: 2022 Sarah Vaupel <sarah.vaupel@ifi.lmu.de>
$#
$# SPDX-License-Identifier: AGPL-3.0-or-later
It is now possible to specify deadlines up to which substitute registrations from central allocations are accepted.

View File

@ -1,7 +0,0 @@
$newline never
$# SPDX-FileCopyrightText: 2022 Winnie Ros <winnie.ros@campus.lmu.de>
$#
$# SPDX-License-Identifier: AGPL-3.0-or-later
Kurse, die an Zentralanmeldungen teilnehmen, können nun angeben bis zu welcher Frist sie Nachrücker:innen akzeptieren können

View File

@ -1,7 +0,0 @@
$newline never
$# SPDX-FileCopyrightText: 2022 Sarah Vaupel <sarah.vaupel@ifi.lmu.de>
$#
$# SPDX-License-Identifier: AGPL-3.0-or-later
Courses which participate in a central allocation may now specify a deadline up to which they are able to accept substitute registrations.

View File

@ -1,7 +0,0 @@
$newline never
$# SPDX-FileCopyrightText: 2022 Gregor Kleen <gregor.kleen@ifi.lmu.de>
$#
$# SPDX-License-Identifier: AGPL-3.0-or-later
Kurse zu Zentralanmeldungen eintragen

View File

@ -1,7 +0,0 @@
$newline never
$# SPDX-FileCopyrightText: 2022 Gregor Kleen <gregor.kleen@ifi.lmu.de>
$#
$# SPDX-License-Identifier: AGPL-3.0-or-later
Registration of courses for central allocation

View File

@ -1,7 +0,0 @@
$newline never
$# SPDX-FileCopyrightText: 2022 Gregor Kleen <gregor.kleen@ifi.lmu.de>
$#
$# SPDX-License-Identifier: AGPL-3.0-or-later
Benachrichtigungen, wenn neue Kurse zu Zentralanmeldungen hinzugefügt werden

View File

@ -1,7 +0,0 @@
$newline never
$# SPDX-FileCopyrightText: 2022 Sarah Vaupel <sarah.vaupel@ifi.lmu.de>
$#
$# SPDX-License-Identifier: AGPL-3.0-or-later
Notifications for new courses being added to central allocations.

View File

@ -1,7 +0,0 @@
$newline never
$# SPDX-FileCopyrightText: 2022 Gregor Kleen <gregor.kleen@ifi.lmu.de>
$#
$# SPDX-License-Identifier: AGPL-3.0-or-later
Benachrichtigungen für Zentralanmeldungen

View File

@ -1,7 +0,0 @@
$newline never
$# SPDX-FileCopyrightText: 2022 Gregor Kleen <gregor.kleen@ifi.lmu.de>
$#
$# SPDX-License-Identifier: AGPL-3.0-or-later
Notifications for central allocations

View File

@ -1,7 +0,0 @@
$newline never
$# SPDX-FileCopyrightText: 2022 Gregor Kleen <gregor.kleen@ifi.lmu.de>
$#
$# SPDX-License-Identifier: AGPL-3.0-or-later
Designänderungen

View File

@ -1,7 +0,0 @@
$newline never
$# SPDX-FileCopyrightText: 2022 Gregor Kleen <gregor.kleen@ifi.lmu.de>
$#
$# SPDX-License-Identifier: AGPL-3.0-or-later
Design changes

View File

@ -1,7 +0,0 @@
$newline never
$# SPDX-FileCopyrightText: 2022 Gregor Kleen <gregor.kleen@ifi.lmu.de>
$#
$# SPDX-License-Identifier: AGPL-3.0-or-later
Liste zugewiesener Abgaben lassen sich nun filtern

View File

@ -1,7 +0,0 @@
$newline never
$# SPDX-FileCopyrightText: 2022 Gregor Kleen <gregor.kleen@ifi.lmu.de>
$#
$# SPDX-License-Identifier: AGPL-3.0-or-later
Filters for list of assigned corrections

View File

@ -1,7 +0,0 @@
$newline never
$# SPDX-FileCopyrightText: 2022 Gregor Kleen <gregor.kleen@ifi.lmu.de>
$#
$# SPDX-License-Identifier: AGPL-3.0-or-later
Es kann die Abgabe einer Eigenständigkeitserklärung bei Anlegen einer Übungsblattabgabe gefordert werden.

View File

@ -1,7 +0,0 @@
$newline never
$# SPDX-FileCopyrightText: 2022 Gregor Kleen <gregor.kleen@ifi.lmu.de>
$#
$# SPDX-License-Identifier: AGPL-3.0-or-later
Submittors can be required to make a Statement of Authorship when creating their submission for an exercise sheet.

View File

@ -1,7 +0,0 @@
$newline never
$# SPDX-FileCopyrightText: 2022 Winnie Ros <winnie.ros@campus.lmu.de>
$#
$# SPDX-License-Identifier: AGPL-3.0-or-later
Automatische Anmeldung von Bewerber:innen in Kursen, die nicht an einer Zentralanmeldung teilnehmen (nach Bewertung der Bewerbung)

View File

@ -1,7 +0,0 @@
$newline never
$# SPDX-FileCopyrightText: 2022 Sarah Vaupel <sarah.vaupel@ifi.lmu.de>
$#
$# SPDX-License-Identifier: AGPL-3.0-or-later
Option to automatically accept applications for courses outside of central allocations.

View File

@ -1,7 +0,0 @@
$newline never
$# SPDX-FileCopyrightText: 2022 Winnie Ros <winnie.ros@campus.lmu.de>
$#
$# SPDX-License-Identifier: AGPL-3.0-or-later
Besseres Verschicken von Kursmitteilungen an Tutoriumsteilnehmer:innen

View File

@ -1,7 +0,0 @@
$newline never
$# SPDX-FileCopyrightText: 2022 Sarah Vaupel <sarah.vaupel@ifi.lmu.de>
$#
$# SPDX-License-Identifier: AGPL-3.0-or-later
Better sending of course communications to tutorial participants.

View File

@ -1,7 +0,0 @@
$newline never
$# SPDX-FileCopyrightText: 2022 Winnie Ros <winnie.ros@campus.lmu.de>
$#
$# SPDX-License-Identifier: AGPL-3.0-or-later
Anzeige von Abgaben, Tutorien und Klausuren auf der Seite für einzelne Kursteilnehmer:innen

View File

@ -1,7 +0,0 @@
$newline never
$# SPDX-FileCopyrightText: 2022 Sarah Vaupel <sarah.vaupel@ifi.lmu.de>
$#
$# SPDX-License-Identifier: AGPL-3.0-or-later
Submissions, tutorials, and exams are now shown on the detail page for course participants.

View File

@ -1,7 +0,0 @@
$newline never
$# SPDX-FileCopyrightText: 2022 Gregor Kleen <gregor.kleen@ifi.lmu.de>
$#
$# SPDX-License-Identifier: AGPL-3.0-or-later
Verbesserter Workflow & Fehlerbehandlung für CSV-Import

Some files were not shown because too many files have changed in this diff Show More