diff --git a/messages/uniworx/de-de-formal.msg b/messages/uniworx/de-de-formal.msg index 4fb47bab6..97b46520e 100644 --- a/messages/uniworx/de-de-formal.msg +++ b/messages/uniworx/de-de-formal.msg @@ -17,9 +17,13 @@ BtnHijack: Sitzung übernehmen BtnSave: Speichern PressSaveToSave: Änderungen werden erst durch Drücken des Knopfes "Speichern" gespeichert. BtnHandIn: Abgeben -BtnCandidatesInfer: Studienfachzuordnung automatisch lernen -BtnCandidatesDeleteConflicts: Konflikte löschen -BtnCandidatesDeleteAll: Alle Beobachtungen löschen +BtnNameCandidatesInfer: Studienfach-Namens-Zuordnung automatisch lernen +BtnNameCandidatesDeleteConflicts: Namenskonflikte löschen +BtnNameCandidatesDeleteAll: Alle Namens-Beobachtungen löschen +BtnParentCandidatesInfer: Unterstudiengangs-Zuordnung automatisch lernen +BtnParentCandidatesDeleteAll: Alle Unterstudiengangs-Beobachtungen löschen +BtnStandaloneCandidatesDeleteAll: Alle Einzelstudiengangs-Beobachtungen löschen +BtnStandaloneCandidatesDeleteRedundant: Redundante Einzelstudiengangs-Beobachtungen löschen BtnResetTokens: Authorisierungs-Tokens invalidieren BtnLecInvAccept: Annehmen BtnLecInvDecline: Ablehnen @@ -761,9 +765,15 @@ AdminFeaturesHeading: Studiengänge StudyTerms: Studiengänge StudyTerm: Studiengang NoStudyTermsKnown: Keine Studiengänge bekannt -StudyFeatureInference: Studiengangschlüssel-Inferenz -StudyFeatureInferenceNoConflicts: Keine Konflikte beobachtet -StudyFeatureInferenceConflictsHeading: Studiengangseinträge mit beobachteten Konflikten +StudyFeaturesDegrees: Abschlüsse +StudyFeaturesTerms: Studiengänge +StudyFeaturesNameCandidates: Namens-Kandidaten +StudyFeaturesParentCandidates: Kandidaten für Unterstudiengänge +StudyFeaturesStandaloneCandidates: Kandidaten für Einzelstudiengänge +StudyFeatureNameInference: Studiengangschlüssel-Inferenz +StudyFeatureParentInference: Unterstudiengang-Inferenz +StudyFeatureInferenceNoNameConflicts: Keine Konflikte beobachtet +StudyFeatureInferenceNameConflictsHeading: Studiengangseinträge mit beobachteten Konflikten StudyFeatureAge: Fachsemester StudyFeatureDegree: Abschluss FieldPrimary: Hauptfach @@ -784,11 +794,17 @@ StudyTermsShort: Studiengangkürzel StudyTermsChangeSuccess: Zuordnung Studiengänge aktualisiert StudyDegreeChangeSuccess: Zuordnung Abschlüsse aktualisiert StudyCandidateIncidence: Synchronisation -AmbiguousCandidatesRemoved n@Int: #{show n} #{pluralDE n "uneindeutiger Kandidat" "uneindeutige Kandiaten"} entfernt -RedundantCandidatesRemoved n@Int: #{show n} bereits #{pluralDE n "bekannter Kandidat" "bekannte Kandiaten"} entfernt -CandidatesInferred n@Int: #{show n} neue #{pluralDE n "Studiengangszuordnung" "Studiengangszuordnungen"} inferiert -NoCandidatesInferred: Keine neuen Studienganszuordnungen inferiert -AllIncidencesDeleted: Alle Beobachtungen wurden gelöscht. +AmbiguousNameCandidatesRemoved n@Int: #{show n} #{pluralDE n "uneindeutiger Kandidat" "uneindeutige Kandiaten"} entfernt +RedundantNameCandidatesRemoved n@Int: #{show n} bereits #{pluralDE n "bekannter Namenskandidat" "bekannte Namenskandiaten"} entfernt +RedundantParentCandidatesRemoved n@Int: #{show n} bereits #{pluralDE n "bekannter Elternkandidat" "bekannte Elternkandiaten"} entfernt +RedundantStandaloneCandidatesRemoved n@Int: #{show n} bereits #{pluralDE n "bekannter Einzelstudiengangskandidat" "bekannte Einzelstudiengangskandiaten"} entfernt +NameCandidatesInferred n@Int: #{show n} neue #{pluralDE n "Studiengangszuordnung" "Studiengangszuordnungen"} inferiert +NoNameCandidatesInferred: Keine neuen Studienganszuordnungen inferiert +ParentCandidatesInferred n@Int: #{show n} #{pluralDE n "neuer Unterstudiengang" "neue Unterstudiengänge"} inferiert +NoParentCandidatesInferred: Keine neuen Unterstudiengänge inferiert +AllNameIncidencesDeleted: Alle Namens-Beobachtungen wurden gelöscht. +AllParentIncidencesDeleted: Alle Unterstudiengang-Beobachtungen wurden gelöscht. +AllStandaloneIncidencesDeleted: Alle Einzelstudiengang-Beobachtungen wurden gelöscht. IncidencesDeleted n@Int: #{show n} #{pluralDE n "Beobachtung" "Beobachtungen"} gelöscht StudyTermIsNew: Neu StudyFeatureConflict: Es wurden Konflikte in der Studiengang-Zuordnung gefunden @@ -2077,6 +2093,13 @@ ShortSexNotApplicable: k.A. ShowSex: Geschlechter anderer Nutzer anzeigen ShowSexTip: Sollen in Kursteilnehmer-Tabellen u.Ä. die Geschlechter der Nutzer angezeigt werden? +StudySubTermsChildKey: Kind +StudySubTermsChildName: Kindname +StudySubTermsParentKey: Elter +StudySubTermsParentName: Eltername +StudyTermsDefaultDegree: Default Abschluss +StudyTermsDefaultFieldType: Default Typ + MenuLanguage: Sprache LanguageChanged: Sprache erfolgreich geändert @@ -2137,4 +2160,4 @@ Deficit: Defizit MetricNoSamples: Keine Messwerte MetricName: Name -MetricValue: Wert \ No newline at end of file +MetricValue: Wert diff --git a/messages/uniworx/en-eu.msg b/messages/uniworx/en-eu.msg index e2f1d630a..b7708491a 100644 --- a/messages/uniworx/en-eu.msg +++ b/messages/uniworx/en-eu.msg @@ -17,9 +17,13 @@ BtnHijack: Hijack session BtnSave: Save PressSaveToSave: Changes will only be saved after clicking "Save". BtnHandIn: Hand in submission -BtnCandidatesInfer: Infer mapping -BtnCandidatesDeleteConflicts: Delete conflicts -BtnCandidatesDeleteAll: Delete all observations +BtnNameCandidatesInfer: Infer name-mapping +BtnNameCandidatesDeleteConflicts: Delete name-conflicts +BtnNameCandidatesDeleteAll: Delete all name-observations +BtnParentCandidatesInfer: Infer parent-relation +BtnParentCandidatesDeleteAll: Delete all parent-observations +BtnStandaloneCandidatesDeleteAll: Delete all standalone-observations +BtnStandaloneCandidatesDeleteRedundant: Delete redundant standalone-observations BtnResetTokens: Invalidate tokens BtnLecInvAccept: Accept BtnLecInvDecline: Decline @@ -384,6 +388,8 @@ UnauthorizedTokenNotStarted: Your authorisation-token is not yet valid. UnauthorizedTokenInvalid: Your authorisation-token could not be processed. UnauthorizedTokenInvalidRoute: Your authorisation-token is not valid for this page. UnauthorizedTokenInvalidAuthority: Your authorisation-token is based in an user's rights who does not exist anymore. +UnauthorizedTokenInvalidAuthorityGroup: Your authorisation-token is based in an user groups rights which does not exist anymore. +UnauthorizedTokenInvalidAuthorityValue: The specification of the rights in which your authorisation-token is based, could not be interpreted. UnauthorizedToken404: Authorisation-tokens cannot be processed on error pages. UnauthorizedSiteAdmin: You are no system-wide administrator. UnauthorizedSchoolAdmin: You are no administrator for this department. @@ -756,9 +762,15 @@ AdminFeaturesHeading: Features of study StudyTerms: Fields of study StudyTerm: Field of study NoStudyTermsKnown: No known features of study -StudyFeatureInference: Infer field of study mapping -StudyFeatureInferenceNoConflicts: No observed conflicts -StudyFeatureInferenceConflictsHeading: Fields of study with observed conflicts +StudyFeaturesDegrees: Degrees +StudyFeaturesTerms: Terms of Study +StudyFeaturesNameCandidates: Name candidates +StudyFeaturesParentCandidates: Parent candidates +StudyFeaturesStandaloneCandidates: Standalone candidates +StudyFeatureNameInference: Infer field of study mapping +StudyFeatureParentInference: Infer field of study parent relation +StudyFeatureInferenceNoNameConflicts: No observed conflicts +StudyFeatureInferenceNameConflictsHeading: Fields of study with observed conflicts StudyFeatureAge: Semester StudyFeatureDegree: Degree FieldPrimary: Major @@ -779,11 +791,17 @@ StudyTermsShort: Field shorthand StudyTermsChangeSuccess: Successfully updated fields of study StudyDegreeChangeSuccess: Successfully updated degrees StudyCandidateIncidence: Synchronisation -AmbiguousCandidatesRemoved n: Successfully removed #{n} ambiguous #{pluralEN n "candidate" "candidates"} -RedundantCandidatesRemoved n: Successfully removed #{n} rendundant #{pluralEN n "candidate" "candidates"} -CandidatesInferred n: Successfully inferred #{n} field #{pluralEN n "mapping" "mappings"} -NoCandidatesInferred: No new mappings inferred -AllIncidencesDeleted: Successfully deleted all observations +AmbiguousNameCandidatesRemoved n: Successfully removed #{n} ambiguous #{pluralEN n "candidate" "candidates"} +RedundantNameCandidatesRemoved n: Successfully removed #{n} rendundant #{pluralEN n "name-candidate" "name-candidates"} +RedundantParentCandidatesRemoved n: Successfully removed #{n} rendundant #{pluralEN n "parent-candidate" "parent-candidates"} +RedundantStandaloneCandidatesRemoved n: Successfully removed #{n} rendundant #{pluralEN n "standalone-candidate" "standalone-candidates"} +NameCandidatesInferred n: Successfully inferred #{n} field #{pluralEN n "mapping" "mappings"} +NoNameCandidatesInferred: No new name-mappings inferred +AllNameIncidencesDeleted: Successfully deleted all name observations +AllParentIncidencesDeleted: Successfully deleted all parent-relation observations +AllStandaloneIncidencesDeleted: Successfully deleted all standalone observations +ParentCandidatesInferred n: Successfully inferred #{n} field #{pluralEN n "parent-relation" "parent-reliations"} +NoParentCandidatesInferred: No new parent-relations inferred IncidencesDeleted n: Successfully deleted #{show n} #{pluralEN n "observation" "observations"} StudyTermIsNew: New StudyFeatureConflict: Observed conflicts in field mapping @@ -2070,6 +2088,12 @@ ShortSexNotApplicable: N/A ShowSex: Show sex of other users ShowSexTip: Should users' sex be displayed in (among others) lists of course participants? +StudySubTermsChildKey: Child +StudySubTermsChildName: Child-Name +StudySubTermsParentKey: Parent +StudySubTermsParentName: Parent-Name +StudyTermsDefaultDegree: Default degree +StudyTermsDefaultFieldType: Default type MenuLanguage: Language LanguageChanged: Language changed successfully diff --git a/models/exams.model b/models/exams.model index 7eff47789..c23917bc7 100644 --- a/models/exams.model +++ b/models/exams.model @@ -64,5 +64,5 @@ ExamCorrector UniqueExamCorrector exam user ExamPartCorrector part ExamPartId - corrector ExamCorrector + corrector ExamCorrectorId UniqueExamPartCorrector part corrector \ No newline at end of file diff --git a/models/users.model b/models/users.model index 55ee044db..87c9c6a87 100644 --- a/models/users.model +++ b/models/users.model @@ -55,11 +55,13 @@ StudyFeatures -- multiple entries possible for students pursuing several degree user UserId degree StudyDegreeId -- Abschluss, i.e. Master, Bachelor, etc. field StudyTermsId -- Fach, i.e. Informatics, Philosophy, etc. + subField StudyTermsId Maybe type StudyFieldType -- Major or minor, i.e. Haupt-/Nebenfach semester Int updated UTCTime default=now() -- last update from LDAP valid Bool default=true -- marked as active in LDAP (students may switch, but LDAP never forgets) UniqueStudyFeatures user degree field type semester + deriving Eq Show -- UniqueUserSubject ubuser degree field -- There exists a counterexample StudyDegree -- Studienabschluss key Int -- LMU-internal key @@ -67,22 +69,37 @@ StudyDegree -- Studienabschluss name Text Maybe -- description given by LDAP Primary key -- column key is used as actual DB row key -- newtype Key StudyDegree = StudyDegreeKey' { unStudyDegreeKey :: Int } - deriving Show + deriving Eq Show StudyTerms -- Studiengang - key Int -- LMU-internal key + key Int -- standardised key shorthand Text Maybe -- admin determined shorthand name Text Maybe -- description given by LDAP + defaultDegree StudyDegreeId Maybe + defaultType StudyFieldType Maybe Primary key -- column key is used as actual DB row key -- newtype Key StudyTerms = StudyTermsKey' { unStudyTermsKey :: Int } - deriving Show -StudyTermCandidate -- No one at LMU is willing and able to tell us the meaning of the keys for StudyDegrees and StudyTerms. + deriving Eq Ord Show +StudySubTerms + child StudyTermsId + parent StudyTermsId + UniqueStudySubTerms child parent +StudyTermNameCandidate -- No one at LMU is willing and able to tell us the meaning of the keys for StudyDegrees and StudyTerms. -- Each LDAP login provides an unordered set of keys and an unordered set of plain text description with an unknown 1-1 correspondence. -- This table helps us to infer which key belongs to which plain text by recording possible combinations at login. -- If a login provides n keys and n plan texts, then n^2 rows with the same incidence are created, storing all combinations incidence TermCandidateIncidence -- random id, generated once per login to associate matching pairs - key Int -- a possible key for the studyTermName + key Int -- a possible key for the studyTermName or studySubTermName name Text -- studyTermName as plain text from LDAP deriving Show Eq Ord +StudySubTermParentCandidate + incidence TermCandidateIncidence + key Int + parent Int + deriving Show Eq Ord +StudyTermStandaloneCandidate + incidence TermCandidateIncidence + key Int + deriving Show Eq Ord UserGroupMember group UserGroupName @@ -91,4 +108,4 @@ UserGroupMember UniquePrimaryUserGroupMember group primary !force UniqueUserGroupMember group user - \ No newline at end of file + diff --git a/package.yaml b/package.yaml index c5339dc30..ea41ab47e 100644 --- a/package.yaml +++ b/package.yaml @@ -68,7 +68,7 @@ dependencies: - cereal - mtl - sandi - - esqueleto + - esqueleto >=3.1.0 - mime-types - generic-deriving - blaze-html diff --git a/src/Auth/LDAP.hs b/src/Auth/LDAP.hs index e35bfce1b..5acb12e95 100644 --- a/src/Auth/LDAP.hs +++ b/src/Auth/LDAP.hs @@ -7,7 +7,7 @@ module Auth.LDAP , ldapUserPrincipalName, ldapUserEmail, ldapUserDisplayName , ldapUserMatriculation, ldapUserFirstName, ldapUserSurname , ldapUserTitle, ldapUserStudyFeatures, ldapUserFieldName - , ldapUserSchoolAssociation, ldapSex + , ldapUserSchoolAssociation, ldapUserSubTermsSemester, ldapSex ) where import Import.NoFoundation @@ -62,7 +62,7 @@ findUser LdapConf{..} ldap ident retAttrs = fromMaybe [] <$> findM (assertM (not , Ldap.derefAliases Ldap.DerefAlways ] -ldapUserPrincipalName, ldapUserDisplayName, ldapUserMatriculation, ldapUserFirstName, ldapUserSurname, ldapUserTitle, ldapUserStudyFeatures, ldapUserFieldName, ldapUserSchoolAssociation, ldapSex :: Ldap.Attr +ldapUserPrincipalName, ldapUserDisplayName, ldapUserMatriculation, ldapUserFirstName, ldapUserSurname, ldapUserTitle, ldapUserStudyFeatures, ldapUserFieldName, ldapUserSchoolAssociation, ldapSex, ldapUserSubTermsSemester :: Ldap.Attr ldapUserPrincipalName = Ldap.Attr "userPrincipalName" ldapUserDisplayName = Ldap.Attr "displayName" ldapUserMatriculation = Ldap.Attr "LMU-Stud-Matrikelnummer" @@ -70,9 +70,10 @@ ldapUserFirstName = Ldap.Attr "givenName" ldapUserSurname = Ldap.Attr "sn" ldapUserTitle = Ldap.Attr "title" ldapUserStudyFeatures = Ldap.Attr "dfnEduPersonFeaturesOfStudy" -ldapUserFieldName = Ldap.Attr "dfnEduPersonFieldOfStudyString" +ldapUserFieldName = Ldap.Attr "LMU-Stg-Fach" ldapUserSchoolAssociation = Ldap.Attr "LMU-IFI-eduPersonOrgUnitDNString" ldapSex = Ldap.Attr "schacGender" +ldapUserSubTermsSemester = Ldap.Attr "LMU-Stg-FachundFS" ldapUserEmail :: NonEmpty Ldap.Attr ldapUserEmail = Ldap.Attr "mail" :| diff --git a/src/Database/Esqueleto/Utils.hs b/src/Database/Esqueleto/Utils.hs index b9cc734bb..5eae4b916 100644 --- a/src/Database/Esqueleto/Utils.hs +++ b/src/Database/Esqueleto/Utils.hs @@ -20,6 +20,7 @@ module Database.Esqueleto.Utils , maybe , SqlProject(..) , (->.) + , fromSqlKey , module Database.Esqueleto.Utils.TH ) where @@ -250,3 +251,6 @@ instance (PersistEntity val, PersistField typ) => SqlProject val typ (Maybe (E.E (->.) :: E.SqlExpr (E.Value a) -> Text -> E.SqlExpr (E.Value b) (->.) expr t = E.unsafeSqlBinOp "->" expr $ E.val t + +fromSqlKey :: (ToBackendKey SqlBackend entity, PersistField (Key entity)) => E.SqlExpr (E.Value (Key entity)) -> E.SqlExpr (E.Value Int64) +fromSqlKey = E.veryUnsafeCoerceSqlExprValue diff --git a/src/Database/Esqueleto/Utils/TH.hs b/src/Database/Esqueleto/Utils/TH.hs index 52cd68cdc..b0c6a3699 100644 --- a/src/Database/Esqueleto/Utils/TH.hs +++ b/src/Database/Esqueleto/Utils/TH.hs @@ -1,8 +1,10 @@ +{-# LANGUAGE UndecidableInstances #-} + module Database.Esqueleto.Utils.TH ( SqlIn(..) , sqlInTuple, sqlInTuples , unValueN, unValueNIs - , sqlIJproj, sqlLOJproj + , sqlIJproj, sqlLOJproj, sqlFOJproj ) where import ClassyPrelude @@ -21,8 +23,17 @@ import Utils.TH class E.SqlSelect a r => SqlIn a r | a -> r, r -> a where sqlIn :: a -> [r] -> E.SqlExpr (E.Value Bool) -instance PersistField a => SqlIn (E.SqlExpr (E.Value a)) (E.Value a) where - x `sqlIn` xs = x `E.in_` E.valList (map E.unValue xs) +instance SqlEq a => SqlIn (E.SqlExpr (E.Value a)) (E.Value a) where + sqlIn x = foldr (\x' e -> e E.||. sqlEq (E.val $ E.unValue x') x) (E.val False) + +class PersistField a => SqlEq a where + sqlEq :: E.SqlExpr (E.Value a) -> E.SqlExpr (E.Value a) -> E.SqlExpr (E.Value Bool) + +instance {-# OVERLAPPABLE #-} PersistField a => SqlEq a where + sqlEq = (E.==.) + +instance PersistField a => SqlEq (Maybe a) where + sqlEq a b = (E.isNothing a E.&&. E.isNothing b) E.||. a E.==. b sqlInTuples :: [Int] -> DecsQ sqlInTuples = mapM sqlInTuple @@ -35,10 +46,10 @@ sqlInTuple arity = do xsV <- newName "xs" let - matchE = lam1E (tupP $ map (\vV -> conP 'E.Value [varP vV]) vVs) (foldr1 (\e1 e2 -> [e|$(e1) E.&&. $(e2)|]) . map (\(varE -> vE, varE -> xE) -> [e|E.val $(vE) E.==. $(xE)|]) $ zip vVs xVs) + matchE = lam1E (tupP $ map (\vV -> conP 'E.Value [varP vV]) vVs) (foldr1 (\e1 e2 -> [e|$(e1) E.&&. $(e2)|]) . map (\(varE -> vE, varE -> xE) -> [e|E.val $(vE) `sqlEq` $(xE)|]) $ zip vVs xVs) tupTy f = foldl (\typ v -> typ `appT` f (varT v)) (tupleT arity) tyVars - instanceD (cxt $ map (\v -> [t|PersistField $(varT v)|]) tyVars) [t|SqlIn $(tupTy $ \v -> [t|E.SqlExpr (E.Value $(v))|]) $(tupTy $ \v -> [t|E.Value $(v)|])|] + instanceD (cxt $ map (\v -> [t|SqlEq $(varT v)|]) tyVars) [t|SqlIn $(tupTy $ \v -> [t|E.SqlExpr (E.Value $(v))|]) $(tupTy $ \v -> [t|E.Value $(v)|])|] [ funD 'sqlIn [ clause [tupP $ map varP xVs, varP xsV] ( guardedB @@ -84,3 +95,6 @@ sqlIJproj = leftAssociativePairProjection 'E.InnerJoin sqlLOJproj :: Int -> Int -> ExpQ sqlLOJproj = leftAssociativePairProjection 'E.LeftOuterJoin + +sqlFOJproj :: Int -> Int -> ExpQ +sqlFOJproj = leftAssociativePairProjection 'E.FullOuterJoin diff --git a/src/Database/Persist/Class/Instances.hs b/src/Database/Persist/Class/Instances.hs index 8666a2c87..8fc9eb20b 100644 --- a/src/Database/Persist/Class/Instances.hs +++ b/src/Database/Persist/Class/Instances.hs @@ -34,6 +34,3 @@ uniqueToMap = fmap Map.fromList $ zip <$> persistUniqueToFieldNames <*> persistU instance PersistEntity record => Eq (Unique record) where (==) = (==) `on` uniqueToMap - -instance PersistEntity record => Show (Unique record) where - showsPrec p = showsPrec p . uniqueToMap diff --git a/src/Foundation.hs b/src/Foundation.hs index 688793330..483064a9f 100644 --- a/src/Foundation.hs +++ b/src/Foundation.hs @@ -47,6 +47,7 @@ import qualified Data.Map as Map import qualified Data.HashSet as HashSet import Data.List (nubBy, (!!), findIndex, inits) +import qualified Data.List as List import Web.Cookie @@ -3390,36 +3391,113 @@ upsertCampusUser ldapData Creds{..} = do Right str <- return $ Text.decodeUtf8' v' return str - fs <- either (throwM . CampusUserInvalidFeaturesOfStudy . tshow) return userStudyFeatures + userSubTermsSemesters = forM userSubTermsSemesters' parseSubTermsSemester + userSubTermsSemesters' = do + (k, v) <- ldapData + guard $ k == ldapUserSubTermsSemester + v' <- v + Right str <- return $ Text.decodeUtf8' v' + return str + + fs' <- either (throwM . CampusUserInvalidFeaturesOfStudy . tshow) return userStudyFeatures + sts <- either (throwM . CampusUserInvalidFeaturesOfStudy . tshow) return userSubTermsSemesters let studyTermCandidates = Set.fromList $ do - name <- termNames - StudyFeatures{ studyFeaturesField = StudyTermsKey' key } <- fs - return (key, name) + let sfKeys = unStudyTermsKey . studyFeaturesField <$> fs' + subTermsKeys = unStudyTermsKey . fst <$> sts + + (,) <$> sfKeys ++ subTermsKeys <*> termNames + + let + assimilateSubTerms :: [(StudyTermsId, Int)] -> [StudyFeatures] -> WriterT (Set (StudyTermsId, Maybe StudyTermsId)) DB [StudyFeatures] + assimilateSubTerms [] xs = return xs + assimilateSubTerms ((subterm, subSemester) : subterms) unusedFeats = do + standalone <- lift $ get subterm + case standalone of + _other + | (match : matches, unusedFeats') <- partition + (\StudyFeatures{..} -> subterm == studyFeaturesField + && subSemester == studyFeaturesSemester + ) unusedFeats + -> do + $logDebugS "Campus" [st|Ignoring subterm “#{tshow subterm}” and matching feature “#{tshow match}”|] + (:) match <$> assimilateSubTerms subterms (matches ++ unusedFeats') + | any ((== subterm) . studyFeaturesField) unusedFeats + -> do + $logDebugS "Campus" [st|Ignoring subterm “#{tshow subterm}” due to feature of matching field|] + assimilateSubTerms subterms unusedFeats + Just StudyTerms{..} + | Just defDegree <- studyTermsDefaultDegree + , Just defType <- studyTermsDefaultType + -> do + $logDebugS "Campus" [st|Applying default for standalone study term “#{tshow subterm}”|] + (:) (StudyFeatures userId defDegree subterm Nothing defType subSemester now True) <$> assimilateSubTerms subterms unusedFeats + Nothing + | [] <- unusedFeats -> do + $logDebugS "Campus" [st|Saw subterm “#{tshow subterm}” when no fos-terms remain|] + tell $ Set.singleton (subterm, Nothing) + assimilateSubTerms subterms [] + _other -> do + knownParents <- lift $ map (studySubTermsParent . entityVal) <$> selectList [ StudySubTermsChild ==. subterm ] [] + let matchingFeatures = case knownParents of + [] -> filter ((== subSemester) . studyFeaturesSemester) unusedFeats + ps -> filter (\StudyFeatures{studyFeaturesField, studyFeaturesSemester} -> any (== studyFeaturesField) ps && studyFeaturesSemester == subSemester) unusedFeats + when (null knownParents) . forM_ matchingFeatures $ \StudyFeatures{..} -> + tell $ Set.singleton (subterm, Just studyFeaturesField) + if + | not $ null knownParents -> do + $logDebugS "Campus" [st|Applying subterm “#{tshow subterm}” to #{tshow matchingFeatures}|] + (++) (matchingFeatures & traverse . _studyFeaturesSubField %~ (<|> Just subterm)) <$> assimilateSubTerms subterms (unusedFeats List.\\ matchingFeatures) + | otherwise -> do + $logDebugS "Campus" [st|Ignoring subterm “#{tshow subterm}”|] + assimilateSubTerms subterms unusedFeats + $logDebugS "Campus" [st|Terms for “#{credsIdent}”: #{tshow (sts, fs')}|] + (fs, studyFieldParentCandidates) <- runWriterT $ assimilateSubTerms sts fs' + + let studyTermCandidateIncidence = fromMaybe (error "Could not convert studyTermCandidateIncidence-Hash to UUID") -- Should never happen . UUID.fromByteString . fromStrict . (convert :: Digest (SHAKE128 128) -> ByteString) . runConduitPure - $ sourceList (toStrict . Binary.encode <$> Set.toList studyTermCandidates) .| sinkHash + $ sourceList ((toStrict . Binary.encode <$> Set.toList studyTermCandidates) ++ (toStrict . Binary.encode <$> Set.toList studyFieldParentCandidates)) .| sinkHash - candidatesRecorded <- E.selectExists . E.from $ \candidate -> - E.where_ $ candidate E.^. StudyTermCandidateIncidence E.==. E.val studyTermCandidateIncidence + candidatesRecorded <- E.selectExists . E.from $ \(candidate `E.FullOuterJoin` parentCandidate `E.FullOuterJoin` standaloneCandidate) -> do + E.on $ candidate E.?. StudyTermNameCandidateIncidence E.==. standaloneCandidate E.?. StudyTermStandaloneCandidateIncidence + E.on $ candidate E.?. StudyTermNameCandidateIncidence E.==. parentCandidate E.?. StudySubTermParentCandidateIncidence + E.where_ $ candidate E.?. StudyTermNameCandidateIncidence E.==. E.just (E.val studyTermCandidateIncidence) + E.||. parentCandidate E.?. StudySubTermParentCandidateIncidence E.==. E.just (E.val studyTermCandidateIncidence) + E.||. standaloneCandidate E.?. StudyTermStandaloneCandidateIncidence E.==. E.just (E.val studyTermCandidateIncidence) unless candidatesRecorded $ do let studyTermCandidates' = do - (studyTermCandidateKey, studyTermCandidateName) <- Set.toList studyTermCandidates - return StudyTermCandidate{..} + (studyTermNameCandidateKey, studyTermNameCandidateName) <- Set.toList studyTermCandidates + let studyTermNameCandidateIncidence = studyTermCandidateIncidence + return StudyTermNameCandidate{..} insertMany_ studyTermCandidates' + let + studySubTermParentCandidates' = do + (StudyTermsKey' studySubTermParentCandidateKey, Just (StudyTermsKey' studySubTermParentCandidateParent)) <- Set.toList studyFieldParentCandidates + let studySubTermParentCandidateIncidence = studyTermCandidateIncidence + return StudySubTermParentCandidate{..} + insertMany_ studySubTermParentCandidates' + + let + studyTermStandaloneCandidates' = do + (StudyTermsKey' studyTermStandaloneCandidateKey, Nothing) <- Set.toList studyFieldParentCandidates + let studyTermStandaloneCandidateIncidence = studyTermCandidateIncidence + return StudyTermStandaloneCandidate{..} + insertMany_ studyTermStandaloneCandidates' + E.updateWhere [StudyFeaturesUser ==. userId] [StudyFeaturesValid =. False] forM_ fs $ \f@StudyFeatures{..} -> do insertMaybe studyFeaturesDegree $ StudyDegree (unStudyDegreeKey studyFeaturesDegree) Nothing Nothing - insertMaybe studyFeaturesField $ StudyTerms (unStudyTermsKey studyFeaturesField) Nothing Nothing - void $ upsert f [StudyFeaturesUpdated =. now, StudyFeaturesValid =. True] + insertMaybe studyFeaturesField $ StudyTerms (unStudyTermsKey studyFeaturesField) Nothing Nothing Nothing Nothing + void $ upsert f [StudyFeaturesUpdated =. now, StudyFeaturesValid =. True, StudyFeaturesSubField =. studyFeaturesSubField] associateUserSchoolsByTerms userId let diff --git a/src/Handler/Admin.hs b/src/Handler/Admin.hs index 740609be0..0002200e8 100644 --- a/src/Handler/Admin.hs +++ b/src/Handler/Admin.hs @@ -1,507 +1,18 @@ -module Handler.Admin where +module Handler.Admin + ( module Handler.Admin + ) where import Import + import Handler.Utils -import Jobs -import Data.Aeson.Encode.Pretty (encodePrettyToTextBuilder) -import Control.Monad.Trans.Except -import Control.Monad.Trans.Writer (mapWriterT) - --- import Data.Time -import Data.Char (isDigit) -import qualified Data.Text as Text --- import Data.Function ((&)) --- import Yesod.Form.Bootstrap3 - -import qualified Data.Set as Set -import qualified Data.Map as Map - -import Database.Persist.Sql (fromSqlKey) -import qualified Database.Esqueleto as E -import Database.Esqueleto.Utils (mkExactFilter, mkContainsFilter) - -import qualified Handler.Utils.TermCandidates as Candidates - --- import Colonnade hiding (fromMaybe) --- import Yesod.Colonnade - --- import qualified Data.UUID.Cryptographic as UUID +import Handler.Admin.Test as Handler.Admin +import Handler.Admin.ErrorMessage as Handler.Admin +import Handler.Admin.StudyFeatures as Handler.Admin getAdminR :: Handler Html -getAdminR = -- do +getAdminR = siteLayoutMsg MsgAdminHeading $ do setTitleI MsgAdminHeading i18n MsgAdminPageEmpty - --- BEGIN - Buttons needed only here -data ButtonCreate = CreateMath | CreateInf -- Dummy for Example - deriving (Enum, Eq, Ord, Bounded, Read, Show, Generic, Typeable) -instance Universe ButtonCreate -instance Finite ButtonCreate - -nullaryPathPiece ''ButtonCreate camelToPathPiece - -instance Button UniWorX ButtonCreate where - btnLabel CreateMath = [whamlet|Mathematik|] - btnLabel CreateInf = "Informatik" - - btnClasses CreateMath = [BCIsButton, BCInfo] - btnClasses CreateInf = [BCIsButton, BCPrimary] --- END Button needed only here - -emailTestForm :: AForm (HandlerFor UniWorX) (Email, MailContext) -emailTestForm = (,) - <$> areq emailField (fslI MsgMailTestFormEmail) Nothing - <*> ( MailContext - <$> (Languages <$> areq (reorderField appLanguagesOpts) (fslI MsgMailTestFormLanguages) Nothing) - <*> (toMailDateTimeFormat - <$> areq (selectField $ dateTimeFormatOptions SelFormatDateTime) (fslI MsgDateTimeFormat) Nothing - <*> areq (selectField $ dateTimeFormatOptions SelFormatDate) (fslI MsgDateFormat) Nothing - <*> areq (selectField $ dateTimeFormatOptions SelFormatTime) (fslI MsgTimeFormat) Nothing - ) - ) - where - toMailDateTimeFormat dt d t = \case - SelFormatDateTime -> dt - SelFormatDate -> d - SelFormatTime -> t - -makeDemoForm :: Int -> Form (Int,Bool,Double) -makeDemoForm n = identifyForm ("adminTestForm" :: Text) $ \html -> do - (result, widget) <- flip (renderAForm FormStandard) html $ (,,) - <$> areq (minIntFieldI n ("Zahl" :: Text)) (fromString $ "Ganzzahl > " ++ show n) Nothing - <* aformSection MsgFormBehaviour - <*> areq checkBoxField "Muss nächste Zahl größer sein?" (Just True) - <*> areq doubleField "Fliesskommazahl" Nothing - -- NO LONGER DESIRED IN AFORMS: - -- <* submitButton - return $ case result of - FormSuccess fsres - | errorMsgs <- validateResult fsres - , not $ null errorMsgs -> (FormFailure errorMsgs, widget) - _otherwise -> (result, widget) - where - validateResult :: (Int,Bool,Double) -> [Text] - validateResult (i,True,d) | fromIntegral i >= d = [tshow d <> " ist nicht größer als " <> tshow i, "Zweite Fehlermeldung", "Dritte Fehlermeldung"] - validateResult _other = [] - - -getAdminTestR, postAdminTestR :: Handler Html -- Demo Page. Referenzimplementierungen sollte hier gezeigt werden! -getAdminTestR = postAdminTestR -postAdminTestR = do - ((btnResult, btnWdgt), btnEnctype) <- runFormPost $ identifyForm ("buttons" :: Text) (buttonForm :: Form ButtonCreate) - let btnForm = wrapForm btnWdgt def - { formAction = Just $ SomeRoute AdminTestR - , formEncoding = btnEnctype - , formSubmit = FormNoSubmit - } - case btnResult of - (FormSuccess CreateInf) -> addMessage Info "Informatik-Knopf gedrückt" - (FormSuccess CreateMath) -> addMessage Warning "Knopf Mathematik erkannt" - FormMissing -> return () - _other -> addMessage Warning "KEIN Knopf erkannt" - - ((emailResult, emailWidget), emailEnctype) <- runFormPost . identifyForm ("email" :: Text) $ renderAForm FormStandard emailTestForm - formResultModal emailResult AdminTestR $ \(email, ls) -> do - jId <- mapWriterT runDB $ do - jId <- queueJob $ JobSendTestEmail email ls - tell . pure $ Message Success [shamlet|Email-test gestartet (Job ##{tshow (fromSqlKey jId)})|] (Just IconEmail) - return jId - runReaderT (writeJobCtl $ JobCtlPerform jId) =<< getYesod - addMessage Warning [shamlet|Inkorrekt ausgegebener Alert|] -- For testing alert handling when short circuiting; for proper (not fallback-solution) handling always use `tell` within `formResultModal` - - let emailWidget' = wrapForm emailWidget def - { formAction = Just . SomeRoute $ AdminTestR - , formEncoding = emailEnctype - , formAttrs = [("uw-async-form", "")] - } - - - let demoFormAction (_i,_b,_d) = addMessage Info "All ok." - ((demoResult, formWidget),formEnctype) <- runFormPost $ makeDemoForm 7 - formResult demoResult demoFormAction - let showDemoResult = [whamlet| - $maybe (i,b,d) <- formResult' demoResult - Received values: -