diff --git a/.gitignore b/.gitignore index ecf10c096..f744360b3 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,6 @@ src/Handler/Assist.bak src/Handler/Course.SnapCustom.hs *.orig /instance +.stack-work-* +.directory +tags diff --git a/ChangeLog.md b/ChangeLog.md new file mode 100644 index 000000000..401601e10 --- /dev/null +++ b/ChangeLog.md @@ -0,0 +1,42 @@ + * Version 18.09.2018 + + Tooltips funktionieren auch ohne JavaScript + + Kurskürzel müssen nur innerhalb eines Instituts eindeutig sein + + User Data zeigt nun alle momentan gespeicherten Datensätze an + + Unterstützung von Tabellenzusammenfassungen, z.B. Punktsummen + + Intelligente Verteilung von Abgaben auf Korrektoren (z.B. bei Krankheit) + + Übungsblätter können Abgabe von Dateien verbieten und angeben ob ZIP-Archive entpackt werden sollen + + * Version 06.08.2018 + + Einführung einer Option, ob Dateien automatisch heruntergeladen werden sollen + + * Version 01.08.2018 + + Verbesserter Campus-Login + (Ersatz einer C-Bibliothek mit undokumentierter Abhängigkeit durch selbst entwickelten Haskell-Code erlaubt nun auch Umlaute.) + + * Version 31.07.2018 + + Viele Verbesserung zur Anzeige von Korrekturen + + Kursliste über alle Semester hinweg (Top-Level-Navigation "Kurse"), wird in Zukunft Filter/Suchfunktion erhalten + + * Version 10.07.2018 + + Bugfixes, wählbares Format für Datum + + * Version 04.07.2018 + + Hinweis eingefügt, dass alle Daten des Systems spätestens im Dezember 2018 + gelöscht werden. + + * Version 03.07.2018 + + Willkommen bei Uni2work aka "You-need-to-work!" + diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 310b609cc..000000000 --- a/Dockerfile +++ /dev/null @@ -1,6 +0,0 @@ -FROM fpco/stack-build:lts-9.3 - -ENV DEBIAN_FRONTEND noninteractive - -RUN apt-get update -RUN apt-get install libldap2-dev libsasl2-dev \ No newline at end of file diff --git a/FragenSJ.txt b/FragenSJ.txt index c2219f2c1..6ddd8de2b 100644 --- a/FragenSJ.txt +++ b/FragenSJ.txt @@ -1,7 +1,7 @@ ** Sicherheitsabfragen? - Verschlüsselung des Zugriffs? - - SheetDelR tid csh sn : GET zeigt Sicherheitsabfrage + - SDelR tid csh sn : GET zeigt Sicherheitsabfrage POST löscht. Ist das so sinnvoll? Sicherheitsabfrage als PopUpMessage? @@ -9,7 +9,7 @@ - Utils.getKeyBy404 effiziente Variante, welche nur den Key liefert? Esq? (Sheet.hs -> fetchSheet) - - Handler.Sheet.postSheetDelR: deleteCascade für Files? Klappt das? + - Handler.Sheet.postSDelR: deleteCascade für Files? Klappt das? Kann man abfragen, was bei deleteCascade alles gelöscht wird? @@ -19,7 +19,7 @@ Links -> MenuItems verwenden wie bisher Page Titles -> setTitleI Buttons? -> Kann leicht geändert werden! - Was ist mit einfachen Text Feldern, z.B. die Beschriftung von Knöpfen wie in Handler.Course.getCourseListTermR, Zeile 66 "pageActions" für menuItemLabel? + Was ist mit einfachen Text Feldern, z.B. die Beschriftung von Knöpfen wie in Handler.Course.getTermCourseListR, Zeile 66 "pageActions" für menuItemLabel? ** Page pageActions - Berechtigungen prüfen? => Eigener Constructor statt NavbarLeft/Right?! diff --git a/README.md b/README.md index cf42dc5da..be734df7b 100644 --- a/README.md +++ b/README.md @@ -109,7 +109,7 @@ TABLE "user"; DROP TABLE "course" CASCADE; -- UserId 5 zum Lecturer in SchoolId 1 machen (27 ist laufende Nummer) -INSERT INTO "user_lecturer" (id,"user",school) VALUES (27,5,1); +INSERT INTO "user_lecturer" (id,"user","school") VALUES (27,5,1); -- Beenden: \q diff --git a/config/keter.yml b/config/keter.yml deleted file mode 100644 index 24177b0fc..000000000 --- a/config/keter.yml +++ /dev/null @@ -1,42 +0,0 @@ -root: .. - -stanzas: - - type: webapp - - # Name of your executable. You are unlikely to need to change this. - # Note that all file paths are relative to the keter.yml file. - # - # The path given is for Stack projects. If you're still using cabal, change - # to - # exec: ../dist/build/uniworx/uniworx - exec: ../dist/bin/uniworx - - # Command line options passed to your application. - args: [] - - hosts: - - testworx.tcs.ifi.lmu.de - - ssl: true - - forward-env: - - LDAPURI - - LDAPDN - - LDAPPW - - LDAPBN - - DUMMY_LOGIN - - DETAILED_LOGGING - - LOG_ALL - -# Use the following to automatically copy your bundle upon creation via `yesod -# keter`. Uses `scp` internally, so you can set it to a remote destination -# copy-to: user@host:/opt/keter/incoming/ -copy-to: keter@testworx.tcs.ifi.lmu.de:/opt/keter/incoming/ -copy-to-args: - - "-P 30363" - -# If you would like to have Keter automatically create a PostgreSQL database -# and set appropriate environment variables for it to be discovered, uncomment -# the following line. -plugins: - postgres: true diff --git a/config/keter.yml b/config/keter.yml new file mode 120000 index 000000000..3b8c9db84 --- /dev/null +++ b/config/keter.yml @@ -0,0 +1 @@ +keter_testworx.yml \ No newline at end of file diff --git a/config/keter_testworx.yml b/config/keter_testworx.yml new file mode 100644 index 000000000..102573866 --- /dev/null +++ b/config/keter_testworx.yml @@ -0,0 +1,49 @@ +root: .. + +stanzas: + - type: webapp + + # Name of your executable. You are unlikely to need to change this. + # Note that all file paths are relative to the keter.yml file. + # + # The path given is for Stack projects. If you're still using cabal, change + # to + # exec: ../dist/build/uniworx/uniworx + exec: ../dist/bin/uniworx + + # Command line options passed to your application. + args: [] + + hosts: + - testworx.tcs.ifi.lmu.de + + ssl: true + + forward-env: + - LDAPHOST + - LDAPTLS + - LDAPPORT + - LDAPUSER + - LDAPPASS + - LDAPBASE + - LDAPSCOPE + - LDAPTIMEOUT + - DUMMY_LOGIN + - DETAILED_LOGGING + - LOG_ALL + - PWFILE + - CRYPTOID_KEYFILE + - IP_FROM_HEADER + +# Use the following to automatically copy your bundle upon creation via `yesod +# keter`. Uses `scp` internally, so you can set it to a remote destination +# copy-to: user@host:/opt/keter/incoming/ +copy-to: keter@testworx.tcs.ifi.lmu.de:/opt/keter/incoming/ +copy-to-args: + - "-P 30363" + +# If you would like to have Keter automatically create a PostgreSQL database +# and set appropriate environment variables for it to be discovered, uncomment +# the following line. +plugins: + postgres: true diff --git a/config/keter_uni2work.yml b/config/keter_uni2work.yml new file mode 100644 index 000000000..aefd5a30a --- /dev/null +++ b/config/keter_uni2work.yml @@ -0,0 +1,50 @@ +root: .. + +stanzas: + - type: webapp + + # Name of your executable. You are unlikely to need to change this. + # Note that all file paths are relative to the keter.yml file. + # + # The path given is for Stack projects. If you're still using cabal, change + # to + # exec: ../dist/build/uniworx/uniworx + exec: ../dist/bin/uniworx + + # Command line options passed to your application. + args: [] + + hosts: + - uni2work.ifi.lmu.de + + ssl: true + + forward-env: + - LDAPHOST + - LDAPTLS + - LDAPPORT + - LDAPUSER + - LDAPPASS + - LDAPBASE + - LDAPSCOPE + - LDAPTIMEOUT + - DETAILED_LOGGING + - LOG_ALL + - PWFILE + - CRYPTOID_KEYFILE + - IP_FROM_HEADER + +# Use the following to automatically copy your bundle upon creation via `yesod +# keter`. Uses `scp` internally, so you can set it to a remote destination +# copy-to: user@host:/opt/keter/incoming/ +copy-to: root@uni2work.ifi.lmu.de:/opt/keter/incoming/ +copy-to-args: [] + + +# If you would like to have Keter automatically create a PostgreSQL database +# and set appropriate environment variables for it to be discovered, uncomment +# the following line. +plugins: + postgres: + - server: uniworxdb + port: 5432 diff --git a/config/settings.yml b/config/settings.yml index 3c2ef80c3..84708ced3 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -1,27 +1,29 @@ # Values formatted like "_env:ENV_VAR_NAME:default_value" can be overridden by the specified environment variable. # See https://github.com/yesodweb/yesod/wiki/Configuration#overriding-configuration-values-with-environment-variables +# NB: If you need a numeric value (e.g. 123) to parse as a String, wrap it in single quotes (e.g. "_env:PGPASS:'123'") +# See https://github.com/yesodweb/yesod/wiki/Configuration#parsing-numeric-values-as-strings static-dir: "_env:STATIC_DIR:static" host: "_env:HOST:*4" # any IPv4 host -port: "_env:PORT:3000" # NB: The port `yesod devel` uses is distinct from this value. Set the `yesod devel` port from the command line. +port: "_env:PORT:3000" ip-from-header: "_env:IP_FROM_HEADER:false" - -# Default behavior: determine the application root from the request headers. -# Uncomment to set an explicit approot approot: "_env:APPROOT:http://localhost:3000" +detailed-logging: "_env:DETAILED_LOGGING:false" +should-log-all: "_env:LOG_ALL:false" +minimum-log-level: "_env:LOGLEVEL:warn" +auth-dummy-login: "_env:DUMMY_LOGIN:false" +allow-deprecated: "_env:ALLOW_DEPRECATED:false" + +auth-pw-hash: + algorithm: "pbkdf2" + strength: 14 + # Optional values with the following production defaults. -# In development, they default to the inverse. -# -detailed-logging: "_env:DETAILED_LOGGING:false" -should-log-all: "_env:LOG_ALL:false" +# In development, they default to true. # reload-templates: false # mutable-static: false # skip-combining: false -auth-dummy-login: "_env:DUMMY_LOGIN:false" - -# NB: If you need a numeric value (e.g. 123) to parse as a String, wrap it in single quotes (e.g. "_env:PGPASS:'123'") -# See https://github.com/yesodweb/yesod/wiki/Configuration#parsing-numeric-values-as-strings database: user: "_env:PGUSER:uniworx" @@ -33,15 +35,22 @@ database: poolsize: "_env:PGPOOLSIZE:10" ldap: - uri: "_env:LDAPURI:ldap://localhost:389" - dn: "_env:LDAPDN:uniworx" - password: "_env:LDAPPW:" - basename: "_env:LDAPBN:" + host: "_env:LDAPHOST:" + tls: "_env:LDAPTLS:" + port: "_env:LDAPPORT:389" + user: "_env:LDAPUSER:" + pass: "_env:LDAPPASS:" + baseDN: "_env:LDAPBASE:" + scope: "_env:LDAPSCOPE:WholeSubtree" + timeout: "_env:LDAPTIMEOUT:5" -userDefaultFavourites: 12 +user-defaults: + max-favourites: 12 + theme: Default + date-time-format: "%a %d %b %Y %R" + date-format: "%d.%m.%Y" + time-format: "%R" + download-files: false cryptoid-keyfile: "_env:CRYPTOID_KEYFILE:cryptoid_key.bf" instance-idfile: "_env:INSTANCEID_FILE:instance" - -copyright: ©Institute for Informatics, LMU Munich -#analytics: UA-YOURCODE diff --git a/config/submission-blacklist b/config/submission-blacklist new file mode 100644 index 000000000..ad2a62ccf --- /dev/null +++ b/config/submission-blacklist @@ -0,0 +1,12 @@ +$# Syntax: +$# - Leere zeilen werden ignoriert +$# - Zeilen, die mit '$#' beginnen, werden ignoriert +$# - Verbleibende Zeilen werden jeweils als `Glob`-Pattern kompiliert + +$# Ignoriere rekursiv alle Ordner __MACOSX und ihren Inhalt +**/__MACOSX +**/__MACOSX/* +**/__MACOSX/**/* + +$# Ignoriere rekursiv alle Dateien .DS_Store (Mac OS) +**/.DS_Store diff --git a/db.hs b/db.hs new file mode 100755 index 000000000..ca4c312a6 --- /dev/null +++ b/db.hs @@ -0,0 +1,329 @@ +#!/usr/bin/env stack +-- stack runghc --package uniworx + +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE PackageImports #-} +{-# LANGUAGE NoImplicitPrelude #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE RecordWildCards #-} + +import "uniworx" Import hiding (Option(..)) +import "uniworx" Application (db, getAppDevSettings) + +import Database.Persist.Postgresql +import Database.Persist.Sql +import Control.Monad.Logger + +import System.Console.GetOpt +import System.Exit (exitWith, ExitCode(..)) +import System.IO (hPutStrLn, stderr) + +import qualified Data.ByteString as BS + +import Database.Persist.Sql (toSqlKey) + +import Data.Time + + +data DBAction = DBClear + | DBMigrate + | DBFill + +argsDescr :: [OptDescr DBAction] +argsDescr = + [ Option ['c'] ["clear"] (NoArg DBClear) "Delete everything accessable by the current database user" + , Option ['m'] ["migrate"] (NoArg DBMigrate) "Perform database migration" + , Option ['f'] ["fill"] (NoArg DBFill) "Fill database with example data" + ] + + +main :: IO () +main = do + args <- map unpack <$> getArgs + case getOpt Permute argsDescr args of + (acts@(_:_), [], []) -> forM_ acts $ \case + DBClear -> runStderrLoggingT $ do -- We don't use `db` here, since we do /not/ want any migrations to run, yet + settings <- liftIO getAppDevSettings + withPostgresqlConn (pgConnStr $ appDatabaseConf settings) . runSqlConn $ do + rawExecute "drop owned by current_user;" [] + DBMigrate -> db $ return () + DBFill -> db $ fillDb + (_, _, errs) -> do + forM_ errs $ hPutStrLn stderr + hPutStrLn stderr $ usageInfo "db.hs" argsDescr + exitWith $ ExitFailure 2 + +insertFile :: FilePath -> DB FileId +insertFile fileTitle = do + fileContent <- liftIO $ Just <$> BS.readFile ("testdata/" <> fileTitle) + fileModified <- liftIO getCurrentTime + insert File{..} + +fillDb :: DB () +fillDb = do + AppSettings{ appUserDefaults = UserDefaultConf{..}, .. } <- getsYesod appSettings + now <- liftIO getCurrentTime + let + insert' :: PersistRecordBackend r (YesodPersistBackend UniWorX) => r -> YesodDB UniWorX (Key r) + insert' = fmap (either entityKey id) . insertBy + summer2017 = TermIdentifier 2017 Summer + winter2017 = TermIdentifier 2017 Winter + summer2018 = TermIdentifier 2018 Summer + gkleen <- insert User + { userIdent = "G.Kleen@campus.lmu.de" + , userAuthentication = AuthLDAP + , userMatrikelnummer = Nothing + , userEmail = "G.Kleen@campus.lmu.de" + , userDisplayName = "Gregor Kleen" + , userSurname = "Kleen" + , userMaxFavourites = 6 + , userTheme = ThemeDefault + , userDateTimeFormat = userDefaultDateTimeFormat + , userDateFormat = userDefaultDateFormat + , userTimeFormat = userDefaultTimeFormat + , userDownloadFiles = userDefaultDownloadFiles + } + fhamann <- insert User + { userIdent = "felix.hamann@campus.lmu.de" + , userAuthentication = AuthLDAP + , userMatrikelnummer = Nothing + , userEmail = "felix.hamann@campus.lmu.de" + , userDisplayName = "Felix Hamann" + , userSurname = "Hamann" + , userMaxFavourites = userDefaultMaxFavourites + , userTheme = ThemeDefault + , userDateTimeFormat = userDefaultDateTimeFormat + , userDateFormat = userDefaultDateFormat + , userTimeFormat = userDefaultTimeFormat + , userDownloadFiles = userDefaultDownloadFiles + } + jost <- insert User + { userIdent = "jost@tcs.ifi.lmu.de" + , userAuthentication = AuthLDAP + , userMatrikelnummer = Nothing + , userEmail = "jost@tcs.ifi.lmu.de" + , userDisplayName = "Steffen Jost" + , userSurname = "Jost" + , userMaxFavourites = 14 + , userTheme = ThemeMossGreen + , userDateTimeFormat = userDefaultDateTimeFormat + , userDateFormat = userDefaultDateFormat + , userTimeFormat = userDefaultTimeFormat + , userDownloadFiles = userDefaultDownloadFiles + } + void . insert $ User + { userIdent = "max@campus.lmu.de" + , userAuthentication = AuthLDAP + , userMatrikelnummer = Nothing + , userEmail = "max@campus.lmu.de" + , userDisplayName = "Max Musterstudent" + , userSurname = "Musterstudent" + , userMaxFavourites = 7 + , userTheme = ThemeAberdeenReds + , userDateTimeFormat = userDefaultDateTimeFormat + , userDateFormat = userDefaultDateFormat + , userTimeFormat = userDefaultTimeFormat + , userDownloadFiles = userDefaultDownloadFiles + } + void . insert $ User + { userIdent = "tester@campus.lmu.de" + , userAuthentication = AuthLDAP + , userMatrikelnummer = Just "999" + , userEmail = "tester@campus.lmu.de" + , userDisplayName = "Tina Tester" + , userSurname = "von Terror" + , userMaxFavourites = 5 + , userTheme = ThemeAberdeenReds + , userDateTimeFormat = userDefaultDateTimeFormat + , userDateFormat = userDefaultDateFormat + , userTimeFormat = userDefaultTimeFormat + , userDownloadFiles = userDefaultDownloadFiles + } + void . repsert (TermKey summer2017) $ Term + { termName = summer2017 + , termStart = fromGregorian 2017 04 09 + , termEnd = fromGregorian 2017 07 14 + , termHolidays = [] + , termLectureStart = fromGregorian 2017 04 09 + , termLectureEnd = fromGregorian 2018 07 14 + , termActive = False + } + void . repsert (TermKey winter2017) $ Term + { termName = winter2017 + , termStart = fromGregorian 2017 10 16 + , termEnd = fromGregorian 2018 02 10 + , termHolidays = [fromGregorian 2017 12 24..fromGregorian 2018 01 06] + , termLectureStart = fromGregorian 2017 10 16 + , termLectureEnd = fromGregorian 2018 02 10 + , termActive = True + } + void . repsert (TermKey summer2018) $ Term + { termName = summer2018 + , termStart = fromGregorian 2018 04 09 + , termEnd = fromGregorian 2018 07 14 + , termHolidays = [] + , termLectureStart = fromGregorian 2018 04 09 + , termLectureEnd = fromGregorian 2018 07 14 + , termActive = True + } + ifi <- insert' $ School "Institut für Informatik" "IfI" + mi <- insert' $ School "Institut für Mathematik" "MI" + void . insert' $ UserAdmin gkleen ifi + void . insert' $ UserAdmin gkleen mi + void . insert' $ UserAdmin fhamann ifi + void . insert' $ UserAdmin jost ifi + void . insert' $ UserAdmin jost mi + void . insert' $ UserLecturer gkleen ifi + void . insert' $ UserLecturer fhamann ifi + void . insert' $ UserLecturer jost ifi + let + sdBsc = StudyDegreeKey' 82 + sdMst = StudyDegreeKey' 88 + repsert sdBsc $ StudyDegree 82 (Just "BSc") (Just "Bachelor" ) + repsert sdMst $ StudyDegree 88 (Just "MSc") (Just "Master" ) + let + sdInf = StudyTermsKey' 79 + sdMath = StudyTermsKey' 105 + repsert sdInf $ StudyTerms 79 (Just "Inf") (Just "Informatik") + repsert sdMath $ StudyTerms 105 (Just "M" ) (Just "Mathematik") + -- FFP + ffp <- insert' Course + { courseName = "Fortgeschrittene Funktionale Programmierung" + , courseDescription = Nothing + , courseLinkExternal = Nothing + , courseShorthand = "FFP" + , courseTerm = TermKey summer2018 + , courseSchool = ifi + , courseCapacity = Just 20 + , courseRegisterFrom = Just now + , courseRegisterTo = Just ((3600 * 24 * 60) `addUTCTime` now ) + , courseDeregisterUntil = Nothing + , courseRegisterSecret = Nothing + , courseMaterialFree = True + } + insert_ $ CourseEdit jost now ffp + void . insert $ DegreeCourse ffp sdBsc sdInf + void . insert $ DegreeCourse ffp sdMst sdInf + void . insert $ Lecturer jost ffp + void . insert $ Lecturer gkleen ffp + sheetkey <- insert $ Sheet ffp "AdHoc-Gruppen" Nothing NotGraded (Arbitrary 3) Nothing Nothing now now Nothing Nothing (Upload True) + insert_ $ SheetEdit gkleen now sheetkey + sheetkey <- insert $ Sheet ffp "Feste Gruppen" Nothing NotGraded RegisteredGroups Nothing Nothing now now Nothing Nothing (Upload True) + insert_ $ SheetEdit gkleen now sheetkey + sheetkey <- insert $ Sheet ffp "Keine Gruppen" Nothing NotGraded NoGroups Nothing Nothing now now Nothing Nothing (Upload True) + insert_ $ SheetEdit gkleen now sheetkey + -- EIP + eip <- insert' Course + { courseName = "Einführung in die Programmierung" + , courseDescription = Nothing + , courseLinkExternal = Nothing + , courseShorthand = "EIP" + , courseTerm = TermKey summer2017 + , courseSchool = ifi + , courseCapacity = Just 20 + , courseRegisterFrom = Nothing + , courseRegisterTo = Nothing + , courseDeregisterUntil = Nothing + , courseRegisterSecret = Nothing + , courseMaterialFree = True + } + insert_ $ CourseEdit fhamann now eip + void . insert' $ DegreeCourse eip sdBsc sdInf + void . insert' $ Lecturer fhamann eip + -- interaction design + ixd <- insert' Course + { courseName = "Interaction Design (User Experience Design I & II)" + , courseDescription = Nothing + , courseLinkExternal = Nothing + , courseShorthand = "IXD" + , courseTerm = TermKey summer2018 + , courseSchool = ifi + , courseCapacity = Just 20 + , courseRegisterFrom = Just now + , courseRegisterTo = Just ((3600 * 24 * 60) `addUTCTime` now ) + , courseDeregisterUntil = Nothing + , courseRegisterSecret = Nothing + , courseMaterialFree = True + } + insert_ $ CourseEdit fhamann now ixd + void . insert' $ DegreeCourse ixd sdBsc sdInf + void . insert' $ Lecturer fhamann ixd + -- concept development + ux3 <- insert' Course + { courseName = "Concept Development (User Experience Design III)" + , courseDescription = Nothing + , courseLinkExternal = Nothing + , courseShorthand = "UX3" + , courseTerm = TermKey winter2017 + , courseSchool = ifi + , courseCapacity = Just 30 + , courseRegisterFrom = Nothing + , courseRegisterTo = Nothing + , courseDeregisterUntil = Nothing + , courseRegisterSecret = Nothing + , courseMaterialFree = True + } + insert_ $ CourseEdit fhamann now ux3 + void . insert' $ DegreeCourse ux3 sdBsc sdInf + void . insert' $ Lecturer fhamann ux3 + -- promo + pmo <- insert' Course + { courseName = "Programmierung und Modellierung" + , courseDescription = Nothing + , courseLinkExternal = Nothing + , courseShorthand = "ProMo" + , courseTerm = TermKey summer2018 + , courseSchool = ifi + , courseCapacity = Just 50 + , courseRegisterFrom = Just now + , courseRegisterTo = Nothing + , courseDeregisterUntil = Nothing + , courseRegisterSecret = Nothing + , courseMaterialFree = True + } + insert_ $ CourseEdit jost now pmo + void . insert $ DegreeCourse pmo sdBsc sdInf + void . insert $ Lecturer jost pmo + sh1 <- insert Sheet + { sheetCourse = pmo + , sheetName = "Blatt 1" + , sheetDescription = Nothing + , sheetType = Normal 6 + , sheetGrouping = Arbitrary 3 + , sheetMarkingText = Nothing + , sheetVisibleFrom = Just now + , sheetActiveFrom = now + , sheetActiveTo = (14 * nominalDay) `addUTCTime` now + , sheetUploadMode = Upload True + , sheetHintFrom = Nothing + , sheetSolutionFrom = Nothing + } + void . insert $ SheetEdit jost now sh1 + void . insert $ SheetCorrector jost sh1 (Load (Just True) 0) CorrectorNormal + void . insert $ SheetCorrector gkleen sh1 (Load (Just True) 1) CorrectorNormal + h102 <- insertFile "H10-2.hs" + h103 <- insertFile "H10-3.hs" + pdf10 <- insertFile "ProMo_Uebung10.pdf" + void . insert $ SheetFile sh1 h102 SheetHint + void . insert $ SheetFile sh1 h103 SheetSolution + void . insert $ SheetFile sh1 pdf10 SheetExercise + -- datenbanksysteme + dbs <- insert' Course + { courseName = "Datenbanksysteme" + , courseDescription = Nothing + , courseLinkExternal = Nothing + , courseShorthand = "DBS" + , courseTerm = TermKey summer2018 + , courseSchool = ifi + , courseCapacity = Just 50 + , courseRegisterFrom = Nothing + , courseRegisterTo = Nothing + , courseDeregisterUntil = Nothing + , courseRegisterSecret = Nothing + , courseMaterialFree = True + } + insert_ $ CourseEdit gkleen now dbs + void . insert' $ DegreeCourse dbs sdBsc sdInf + void . insert' $ DegreeCourse dbs sdBsc sdMath + void . insert' $ Lecturer gkleen dbs + void . insert' $ Lecturer jost dbs diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 000000000..56f0d5164 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env sh + +configFile="" + +case "$1" in + test) + ln -svf "keter_testworx.yml" config/keter.yml + + yesod keter + ;; + production) + ln -svf "keter_uni2work.yml" config/keter.yml + + yesod keter && git tag -f live && git push origin live + ;; + *) + echo "Usage: $0 (test|production)" >&2 + exit 2 + ;; +esac + diff --git a/fill-db.hs b/fill-db.hs deleted file mode 100755 index 66118aba6..000000000 --- a/fill-db.hs +++ /dev/null @@ -1,217 +0,0 @@ -#!/usr/bin/env stack --- stack runghc --package uniworx - -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE PackageImports #-} -{-# LANGUAGE NoImplicitPrelude #-} -{-# LANGUAGE TypeFamilies #-} - -import "uniworx" Import -import "uniworx" Application (db) - -import Database.Persist.Sql (toSqlKey) - -import Data.Time - -main :: IO () -main = db $ do - defaultFavourites <- getsYesod $ appDefaultFavourites . appSettings - now <- liftIO getCurrentTime - let - insert' :: PersistRecordBackend r (YesodPersistBackend UniWorX) => r -> YesodDB UniWorX (Key r) - insert' = fmap (either entityKey id) . insertBy - summer2017 = TermIdentifier 2017 Summer - winter2017 = TermIdentifier 2017 Winter - summer2018 = TermIdentifier 2018 Summer - gkleen <- insert' User - { userPlugin = "LDAP" - , userIdent = "G.Kleen@campus.lmu.de" - , userMatrikelnummer = Nothing - , userEmail = "G.Kleen@campus.lmu.de" - , userDisplayName = "Gregor Kleen" - , userMaxFavourites = 6 - } - fhamann <- insert' User - { userPlugin = "LDAP" - , userIdent = "felix.hamann@campus.lmu.de" - , userMatrikelnummer = Nothing - , userEmail = "felix.hamann@campus.lmu.de" - , userDisplayName = "Felix Hamann" - , userMaxFavourites = defaultFavourites - } - jost <- insert' User - { userPlugin = "LDAP" - , userIdent = "jost@tcs.ifi.lmu.de" - , userMatrikelnummer = Nothing - , userEmail = "jost@tcs.ifi.lmu.de" - , userDisplayName = "Steffen Jost" - , userMaxFavourites = 14 - } - void . repsert (TermKey summer2017) $ Term - { termName = summer2017 - , termStart = fromGregorian 2017 04 09 - , termEnd = fromGregorian 2017 07 14 - , termHolidays = [] - , termLectureStart = fromGregorian 2017 04 09 - , termLectureEnd = fromGregorian 2018 07 14 - , termActive = False - } - void . repsert (TermKey winter2017) $ Term - { termName = winter2017 - , termStart = fromGregorian 2017 10 16 - , termEnd = fromGregorian 2018 02 10 - , termHolidays = [fromGregorian 2017 12 24..fromGregorian 2018 01 06] - , termLectureStart = fromGregorian 2017 10 16 - , termLectureEnd = fromGregorian 2018 02 10 - , termActive = True - } - void . repsert (TermKey summer2018) $ Term - { termName = summer2018 - , termStart = fromGregorian 2018 04 09 - , termEnd = fromGregorian 2018 07 14 - , termHolidays = [] - , termLectureStart = fromGregorian 2018 04 09 - , termLectureEnd = fromGregorian 2018 07 14 - , termActive = True - } - ifi <- insert' $ School "Institut für Informatik" "IfI" - mi <- insert' $ School "Institut für Mathematik" "MI" - void . insert' $ UserAdmin gkleen ifi - void . insert' $ UserAdmin gkleen mi - void . insert' $ UserAdmin fhamann ifi - void . insert' $ UserAdmin jost ifi - void . insert' $ UserAdmin jost mi - void . insert' $ UserLecturer gkleen ifi - void . insert' $ UserLecturer fhamann ifi - void . insert' $ UserLecturer jost ifi - let - sdBsc = StudyDegreeKey' 82 - sdMst = StudyDegreeKey' 88 - repsert sdBsc $ StudyDegree 82 (Just "BSc") (Just "Bachelor" ) - repsert sdMst $ StudyDegree 88 (Just "MSc") (Just "Master" ) - let - sdInf = StudyTermsKey' 79 - sdMath = StudyTermsKey' 105 - repsert sdInf $ StudyTerms 79 (Just "Inf") (Just "Informatik") - repsert sdMath $ StudyTerms 105 (Just "M" ) (Just "Mathematik") - -- FFP - ffp <- insert' Course - { courseName = "Fortgeschrittene Funktionale Programmierung" - , courseDescription = Nothing - , courseLinkExternal = Nothing - , courseShorthand = "ffp" - , courseTermId = TermKey summer2018 - , courseSchoolId = ifi - , courseCapacity = Just 20 - , courseHasRegistration = True - , courseRegisterFrom = Just now - , courseRegisterTo = Just ((3600 * 24 * 60) `addUTCTime` now ) - , courseDeregisterUntil = Nothing - , courseRegisterSecret = Nothing - , courseMaterialFree = True - } - insert_ $ CourseEdit jost now ffp - void . insert' $ DegreeCourse ffp sdBsc sdInf - void . insert' $ DegreeCourse ffp sdMst sdInf - void . insert' $ Lecturer jost ffp - void . insert' $ Lecturer gkleen ffp - void . insert' $ Corrector gkleen ffp (ByProportion 1) - sheetkey <- insert' $ Sheet ffp "Blatt 1" Nothing NotGraded NoGroups Nothing Nothing now now Nothing Nothing - insert_ $ SheetEdit gkleen now sheetkey - -- EIP - eip <- insert' Course - { courseName = "Einführung in die Programmierung" - , courseDescription = Nothing - , courseLinkExternal = Nothing - , courseShorthand = "eip" - , courseTermId = TermKey summer2017 - , courseSchoolId = ifi - , courseCapacity = Just 20 - , courseHasRegistration = False - , courseRegisterFrom = Nothing - , courseRegisterTo = Nothing - , courseDeregisterUntil = Nothing - , courseRegisterSecret = Nothing - , courseMaterialFree = True - } - insert_ $ CourseEdit fhamann now eip - void . insert' $ DegreeCourse eip sdBsc sdInf - void . insert' $ Lecturer fhamann eip - -- interaction design - ixd <- insert' Course - { courseName = "Interaction Design (User Experience Design I & II)" - , courseDescription = Nothing - , courseLinkExternal = Nothing - , courseShorthand = "ixd" - , courseTermId = TermKey summer2018 - , courseSchoolId = ifi - , courseCapacity = Just 20 - , courseHasRegistration = True - , courseRegisterFrom = Just now - , courseRegisterTo = Just ((3600 * 24 * 60) `addUTCTime` now ) - , courseDeregisterUntil = Nothing - , courseRegisterSecret = Nothing - , courseMaterialFree = True - } - insert_ $ CourseEdit fhamann now ixd - void . insert' $ DegreeCourse ixd sdBsc sdInf - void . insert' $ Lecturer fhamann ixd - -- concept development - ux3 <- insert' Course - { courseName = "Concept Development (User Experience Design III)" - , courseDescription = Nothing - , courseLinkExternal = Nothing - , courseShorthand = "ux3" - , courseTermId = TermKey winter2017 - , courseSchoolId = ifi - , courseCapacity = Just 30 - , courseHasRegistration = False - , courseRegisterFrom = Nothing - , courseRegisterTo = Nothing - , courseDeregisterUntil = Nothing - , courseRegisterSecret = Nothing - , courseMaterialFree = True - } - insert_ $ CourseEdit fhamann now ux3 - void . insert' $ DegreeCourse ux3 sdBsc sdInf - void . insert' $ Lecturer fhamann ux3 - -- promo - pmo <- insert' Course - { courseName = "Programmierung und Modellierung" - , courseDescription = Nothing - , courseLinkExternal = Nothing - , courseShorthand = "pmo" - , courseTermId = TermKey summer2017 - , courseSchoolId = ifi - , courseCapacity = Just 50 - , courseHasRegistration = False - , courseRegisterFrom = Nothing - , courseRegisterTo = Nothing - , courseDeregisterUntil = Nothing - , courseRegisterSecret = Nothing - , courseMaterialFree = True - } - insert_ $ CourseEdit jost now pmo - void . insert' $ DegreeCourse pmo sdBsc sdInf - void . insert' $ Lecturer jost pmo - -- datenbanksysteme - dbs <- insert' Course - { courseName = "Datenbanksysteme" - , courseDescription = Nothing - , courseLinkExternal = Nothing - , courseShorthand = "dbs" - , courseTermId = TermKey summer2018 - , courseSchoolId = ifi - , courseCapacity = Just 50 - , courseHasRegistration = False - , courseRegisterFrom = Nothing - , courseRegisterTo = Nothing - , courseDeregisterUntil = Nothing - , courseRegisterSecret = Nothing - , courseMaterialFree = True - } - insert_ $ CourseEdit gkleen now dbs - void . insert' $ DegreeCourse dbs sdBsc sdInf - void . insert' $ DegreeCourse dbs sdBsc sdMath - void . insert' $ Lecturer gkleen dbs - void . insert' $ Lecturer jost dbs diff --git a/ghci.sh b/ghci.sh index 1c0ac289f..5139c7c72 100755 --- a/ghci.sh +++ b/ghci.sh @@ -5,4 +5,15 @@ export DETAILED_LOGGING=true export LOG_ALL=true export DUMMY_LOGIN=true -exec -- stack ghci --flag uniworx:dev --flag uniworx:library-only uniworx +move-back() { + mv -v .stack-work .stack-work-ghci + [[ -d .stack-work-run ]] && mv -v .stack-work-run .stack-work +} + +if [[ -d .stack-work-ghci ]]; then + [[ -d .stack-work ]] && mv -v .stack-work .stack-work-run + mv -v .stack-work-ghci .stack-work + trap move-back EXIT +fi + +stack ghci --flag uniworx:dev --flag uniworx:library-only diff --git a/messages/campus/de.msg b/messages/campus/de.msg new file mode 100644 index 000000000..5fdf477b7 --- /dev/null +++ b/messages/campus/de.msg @@ -0,0 +1,5 @@ +CampusIdentNote: Campus-Kennung bitte inkl. Domain-Teil (@campus.lmu.de) angeben. +CampusIdent: Campus-Kennung +CampusPassword: Passwort +CampusSubmit: Abschicken +CampusInvalidCredentials: Ungültige Logindaten \ No newline at end of file diff --git a/messages/de.msg b/messages/de.msg deleted file mode 100644 index 04a970e74..000000000 --- a/messages/de.msg +++ /dev/null @@ -1,26 +0,0 @@ -SummerTerm year@Integer: Sommersemester #{tshow year} -WinterTerm year@Integer: Wintersemester #{tshow year}/#{tshow $ succ year} -PSLimitNonPositive: “pagesize” muss größer als null sein -Page n@Int64: #{tshow n} -TermEdited tid@TermIdentifier: Semester #{termToText tid} erfolgreich editiert. -TermNewTitle: Semester editiere/anlegen. -InvalidInput: Eingaben bitte korrigieren. -CourseNewOk tid@TermIdentifier courseShortHand@Text: Kurs #{termToText tid}-#{courseShortHand} wurde erfolgreich erstellt. -CourseEditOk tid@TermIdentifier courseShortHand@Text: Kurs #{termToText tid}-#{courseShortHand} wurde erfolgreich geändert. -CourseNewDupShort tid@TermIdentifier courseShortHand@Text: Kurs #{termToText tid}-#{courseShortHand} konnte nicht erstellt werden: Es gibt bereits einen anderen Kurs mit dem Kürzel #{courseShortHand} in diesem Semester. -CourseEditDupShort tid@TermIdentifier courseShortHand@Text: Kurs #{termToText tid}-#{courseShortHand} konnte nicht geändert werden: Es gibt bereits einen anderen Kurs mit dem Kürzel #{courseShortHand} in diesem Semester. -FFSheetName: Name -SheetNewOk tid@TermIdentifier courseShortHand@Text sheetName@Text: Neues Übungsblatt #{sheetName} wurde im Kurs #{termToText tid}-#{courseShortHand} erfolgreich erstellt. -SheetTitle tid@TermIdentifier courseShortHand@Text sheetName@Text: #{termToText tid}-#{courseShortHand} #{sheetName} -SheetTitleNew tid@TermIdentifier courseShortHand@Text : #{termToText tid}-#{courseShortHand}: Neues Übungsblatt -SheetEditOk tid@TermIdentifier courseShortHand@Text sheetName@Text: Übungsblatt #{sheetName} aus Kurs #{termToText tid}-#{courseShortHand} wurde gespeichert. -SheetNameDup tid@TermIdentifier courseShortHand@Text sheetName@Text: Es gibt bereits ein Übungsblatt #{sheetName} in diesem Kurs #{termToText tid}-#{courseShortHand}. -SheetDelTitle tid@TermIdentifier courseShortHand@Text sheetName@Text: Übungsblatt #{sheetName} wirklich aus Kurs #{termToText tid}-#{courseShortHand} herauslöschen? -SheetDelText submissionNo@Int: Dies kann nicht mehr rückgängig gemacht werden! Alle Einreichungen gehen ebenfalls verloren! Es gibt #{show submissionNo} Abgaben. -SheetDelOk tid@TermIdentifier courseShortHand@Text sheetName@Text: #{termToText tid}-#{courseShortHand}: Übungsblatt #{sheetName} gelöscht. -UnauthorizedSchoolAdmin: Sie sind nicht als Administrator für dieses Institut eingetragen. -UnauthorizedSchoolLecturer: Sie sind nicht als Veranstalter für dieses Institut eingetragen. -UnauthorizedLecturer: Sie sind nicht als Veranstalter für diese Veranstaltung eingetragen. -UnauthorizedCorrector: Sie sind nicht als Korrektor für diese Veranstaltung eingetragen. -UnauthorizedParticipant: Sie sind nicht als Teilnehmer für diese Veranstaltung registriert. -OnlyUploadOneFile: Bitte nur eine Datei hochladen. \ No newline at end of file diff --git a/messages/dummy/de.msg b/messages/dummy/de.msg new file mode 100644 index 000000000..f3ca7cae1 --- /dev/null +++ b/messages/dummy/de.msg @@ -0,0 +1 @@ +DummyIdent: Nutzer-Kennung \ No newline at end of file diff --git a/messages/pw-hash/de.msg b/messages/pw-hash/de.msg new file mode 100644 index 000000000..9fb1eb5e4 --- /dev/null +++ b/messages/pw-hash/de.msg @@ -0,0 +1,2 @@ +PWHashIdent: Identifikation +PWHashPassword: Passwort \ No newline at end of file diff --git a/messages/uniworx/de.msg b/messages/uniworx/de.msg new file mode 100644 index 000000000..c1b8fcca7 --- /dev/null +++ b/messages/uniworx/de.msg @@ -0,0 +1,310 @@ +BtnSubmit: Senden +BtnAbort: Abbrechen +BtnDelete: Löschen +BtnRegister: Anmelden +BtnDeregister: Abmelden +BtnHijack: Sitzung übernehmen + +Registered: Angemeldet +RegisterFrom: Anmeldungen von +RegisterTo: Anmeldungen bis +DeRegUntil: Abmeldungen bis + +SummerTerm year@Integer: Sommersemester #{display year} +WinterTerm year@Integer: Wintersemester #{display year}/#{display $ succ year} +SummerTermShort year@Integer: SoSe #{display year} +WinterTermShort year@Integer: WiSe #{display year}/#{display $ mod (succ year) 100} +PSLimitNonPositive: “pagesize” muss größer als null sein +Page n@Int64: #{display n} + +TermsHeading: Semesterübersicht +TermCurrent: Aktuelles Semester +TermEditHeading: Semester editieren/anlegen +TermEditTid tid@TermId: Semester #{display tid} editieren +TermEdited tid@TermId: Semester #{display tid} erfolgreich editiert. +TermNewTitle: Semester editieren/anlegen. +InvalidInput: Eingaben bitte korrigieren. +Term: Semester +TermPlaceholder: W/S + vierstellige Jahreszahl + +SchoolListHeading: Übersicht über verwaltete Institute +SchoolHeading school@SchoolName: Übersicht #{display school} + +LectureStart: Beginn Vorlesungen + +Course: Kurs +CourseShort: Kürzel +CourseCapacity: Kapazität +CourseCapacityTip: Falls angegeben wird die Anzahl an Kursanmeldungen, die zugelassen werden, beschränkt +CourseNoCapacity: In diesem Kurs sind keine Plätze mehr frei. +CourseRegisterOk: Sie wurden angemeldet +CourseDeregisterOk: Sie wurden abgemeldet +CourseSecretWrong: Falsches Kennwort +CourseSecret: Zugangspasswort +CourseNewOk tid@TermId ssh@SchoolId csh@CourseShorthand: Kurs #{display tid}-#{display ssh}-#{csh} wurde erfolgreich erstellt. +CourseEditOk tid@TermId ssh@SchoolId csh@CourseShorthand: Kurs #{display tid}-#{display ssh}-#{csh} wurde erfolgreich geändert. +CourseNewDupShort tid@TermId ssh@SchoolId csh@CourseShorthand: Kurs #{display tid}-#{display ssh}-#{csh} konnte nicht erstellt werden: Es gibt bereits einen anderen Kurs mit dem Kürzel #{csh} in diesem Semester. +CourseEditDupShort tid@TermId ssh@SchoolId csh@CourseShorthand: Kurs #{display tid}-#{display ssh}-#{csh} konnte nicht geändert werden: Es gibt bereits einen anderen Kurs mit dem Kürzel #{csh} in diesem Semester. +FFSheetName: Name +TermCourseListHeading tid@TermId: Kursübersicht #{display tid} +TermSchoolCourseListHeading tid@TermId school@SchoolName: Kursübersicht #{display tid} für #{display school} +CourseListTitle: Alle Kurse +TermCourseListTitle tid@TermId: Kurse #{display tid} +TermSchoolCourseListTitle tid@TermId school@SchoolName: Kurse #{display tid} für #{display school} +CourseNewHeading: Neuen Kurs anlegen +CourseEditHeading tid@TermId ssh@SchoolId csh@CourseShorthand: Kurs #{display tid}-#{display ssh}-#{csh} editieren +CourseEditTitle: Kurs editieren/anlegen +CourseMembers: Teilnehmer +CourseMembersCount num@Int64: #{display num} +CourseMembersCountLimited num@Int64 max@Int64: #{display num}/#{display max} +CourseName: Name +CourseDescription: Beschreibung +CourseDescriptionTip: Beliebiges HTML-Markup ist gestattet +CourseHomepage: Homepage +CourseShorthand: Kürzel +CourseShorthandUnique: Muss innerhalb des Semesters eindeutig sein +CourseSemester: Semester +CourseSchool: Institut +CourseSchoolShort: Fach +CourseSecretTip: Anmeldung zum Kurs erfordert Eingabe des Passworts, sofern gesetzt +CourseRegisterFromTip: Ohne Datum ist keine Anmeldung möglich +CourseRegisterToTip: Anmeldung darf auch ohne Begrenzung möglich sein +CourseDeregisterUntilTip: Abmeldung darf auch ohne Begrenzung möglich sein + +NoSuchTerm tid@TermId: Semester #{display tid} gibt es nicht. +NoSuchSchool ssh@SchoolId: Institut #{display ssh} gibt es nicht. +NoSuchCourseShorthand csh@CourseShorthand: Kein Kurs mit Kürzel #{display csh} bekannt. +NoSuchCourse: Keinen passenden Kurs gefunden. + +Sheet: Blatt +SheetList tid@TermId ssh@SchoolId csh@CourseShorthand: #{display tid}-#{display ssh}-#{csh} Übersicht Übungsblätter +SheetNewHeading tid@TermId ssh@SchoolId csh@CourseShorthand: #{display tid}-#{display ssh}-#{csh} Neues Übungsblatt anlegen +SheetNewOk tid@TermId ssh@SchoolId csh@CourseShorthand sheetName@SheetName: #{sheetName} wurde als neues Übungsblatt im Kurs #{display tid}-#{display ssh}-#{csh} erfolgreich erstellt. +SheetTitle tid@TermId ssh@SchoolId csh@CourseShorthand sheetName@SheetName: #{display tid}-#{display ssh}-#{csh} #{sheetName} +SheetTitleNew tid@TermId ssh@SchoolId csh@CourseShorthand : #{display tid}-#{display ssh}-#{csh}: Neues Übungsblatt +SheetEditHead tid@TermId ssh@SchoolId csh@CourseShorthand sheetName@SheetName: #{display tid}-#{display ssh}-#{csh} #{sheetName} editieren +SheetEditOk tid@TermId ssh@SchoolId csh@CourseShorthand sheetName@SheetName: Übungsblatt #{sheetName} aus Kurs #{display tid}-#{display ssh}-#{csh} wurde gespeichert. +SheetNameDup tid@TermId ssh@SchoolId csh@CourseShorthand sheetName@SheetName: Es gibt bereits ein Übungsblatt #{sheetName} in diesem Kurs #{display tid}-#{display ssh}-#{csh}. +SheetDelHead tid@TermId ssh@SchoolId csh@CourseShorthand sheetName@SheetName: #{sheetName} wirklich aus Kurs #{display tid}-#{display ssh}-#{csh} herauslöschen? +SheetDelText submissionNo@Int: Dies kann nicht mehr rückgängig gemacht werden! Alle Einreichungen gehen ebenfalls verloren! Es gibt #{display submissionNo} Abgaben. +SheetDelOk tid@TermId ssh@SchoolId csh@CourseShorthand sheetName@SheetName: #{display tid}-#{display ssh}-#{csh}: #{sheetName} gelöscht. + +SheetUploadMode: Abgabe von Dateien +SheetExercise: Aufgabenstellung +SheetHint: Hinweis +SheetHintFrom: Hinweis ab +SheetSolution: Lösung +SheetSolutionFrom: Lösung ab +SheetMarking: Hinweise für Korrektoren +SheetType: Wertung +SheetInvisible: Dieses Übungsblatt ist für Teilnehmer momentan unsichtbar! +SheetInvisibleUntil mFrom@Text: Dieses Übungsblatt ist für Teilnehmer momentan unsichtbar bis #{mFrom}! +SheetName: Name +SheetDescription: Hinweise für Teilnehmer +SheetGroup: Gruppenabgabe +SheetVisibleFrom: Sichtbar ab +SheetVisibleFromTip: Ohne Datum wird das Blatt nie sichtbar, z.B. weil es noch nicht fertig ist +SheetActiveFrom: Aktiv ab +SheetActiveFromTip: Abgabe und Download der Aufgabenstellung ist erst ab diesem Datum möglich +SheetActiveTo: Abgabefrist +SheetHintFromTip: Ohne Datum nie für Teilnehmer sichtbar, Korrektoren können diese Dateien immer herunterladen +SheetSolutionFromTip: Ohne Datum nie für Teilnehmer sichtbar, Korrektoren können diese Dateien immer herunterladen +SheetMarkingTip: Hinweise zur Korrektur, sichtbar nur für Korrektoren + +SheetErrVisibility: Sichtbarkeit muss vor Beginn der Abgabefrist liegen +SheetErrDeadlineEarly: Ende der Abgabefrist muss nach deren Beginn liegen +SheetErrHintEarly: Hinweise dürfen erst nach Beginn der Abgabefrist herausgegeben werden +SheetErrSolutionEarly: Die Lösung sollte erst nach Ende der Abgabefrist herausgegeben werden + + +Deadline: Abgabe +Done: Eingereicht + +Submission: Abgabenummer +SubmissionsCourse tid@TermId ssh@SchoolId csh@CourseShorthand: Alle Abgaben Kurs #{display tid}-#{display ssh}-#{csh} +SubmissionsSheet sheetName@SheetName: Abgaben für #{sheetName} +SubmissionWrongSheet: Abgabenummer gehört nicht zum angegebenen Übungsblatt. +SubmissionAlreadyExists: Sie haben bereits eine Abgabe zu diesem Übungsblatt. +SubmissionEditHead tid@TermId ssh@SchoolId csh@CourseShorthand sheetName@SheetName: #{display tid}-#{display ssh}-#{csh} #{sheetName}: Abgabe editieren/anlegen +CorrectionHead tid@TermId ssh@SchoolId csh@CourseShorthand sheetName@SheetName cid@CryptoFileNameSubmission: #{display tid}-#{display ssh}-#{csh} #{sheetName}: Korrektur +SubmissionMember g@Int: Mitabgebende(r) ##{display g} +SubmissionArchive: Zip-Archiv der Abgabedatei(en) +SubmissionFile: Datei zur Abgabe +SubmissionFiles: Abgegebene Dateien +SubmissionAlreadyExistsFor email@UserEmail: #{email} hat bereits eine Abgabe zu diesem bÜbungsblatt. + +SubmissionGroupName: Gruppenname + +CorrectionsTitle: Zugewiesene Korrekturen +CourseCorrectionsTitle: Korrekturen für diesen Kurs +CorrectorsHead sheetName@SheetName: Korrektoren für #{sheetName} + +Unauthorized: Sie haben hierfür keine explizite Berechtigung. +UnauthorizedAnd l@Text r@Text: (#{l} UND #{r}) +UnauthorizedOr l@Text r@Text: (#{l} ODER #{r}) +UnauthorizedSchoolAdmin: Sie sind nicht als Administrator für dieses Institut eingetragen. +UnauthorizedSchoolLecturer: Sie sind nicht als Veranstalter für dieses Institut eingetragen. +UnauthorizedLecturer: Sie sind nicht als Veranstalter für diese Veranstaltung eingetragen. +UnauthorizedCorrector: Sie sind nicht als Korrektor für diese Veranstaltung eingetragen. +UnauthorizedSheetCorrector: Sie sind nicht als Korrektor für dieses Übungsblatt eingetragen. +UnauthorizedCorrectorAny: Sie sind nicht als Korrektor für eine Veranstaltung eingetragen. +UnauthorizedParticipant: Sie sind nicht als Teilnehmer für diese Veranstaltung registriert. +UnauthorizedCourseTime: Dieses Kurs erlaubt momentan keine Anmeldungen. +UnauthorizedSheetTime: Dieses Übungsblatt ist momentan nicht freigegeben. +UnauthorizedSubmissionOwner: Sie sind an dieser Abgabe nicht beteiligt. +UnauthorizedSubmissionRated: Diese Abgabe ist noch nicht korrigiert. +UnauthorizedSubmissionCorrector: Sie sind nicht der Korrektor für diese Abgabe. +OnlyUploadOneFile: Bitte nur eine Datei hochladen. +DeprecatedRoute: Diese Ansicht ist obsolet und könnte in Zukunft entfallen. +UnfreeMaterials: Die Materialien für diese Veranstaltung sind nicht allgemein freigegeben. +MaterialFree: Kursmaterialien ohne Anmeldung zugänglich +UnauthorizedWrite: Sie haben hierfür keine Schreibberechtigung +UnsupportedAuthPredicate tag@String shownRoute@String: "!#{tag}" wurde auf eine Route angewandt, die dies nicht unterstützt: #{shownRoute} + +EMail: E-Mail +EMailUnknown email@UserEmail: E-Mail #{email} gehört zu keinem bekannten Benutzer. +NotAParticipant email@UserEmail tid@TermId csh@CourseShorthand: #{email} ist nicht im Kurs #{display tid}-#{csh} angemeldet. +TooManyParticipants: Es wurden zu viele Mitabgebende angegeben + +AddCorrector: Zusätzlicher Korrektor +CorrectorExists email@UserEmail: #{email} ist bereits als Korrektor eingetragen +SheetCorrectorsTitle tid@TermId ssh@SchoolId csh@CourseShorthand sheetName@SheetName: Korrektoren für #{display tid}-#{display ssh}-#{csh} #{sheetName} +CountTutProp: Tutorien zählen gegen Proportion +Corrector: Korrektor +Correctors: Korrektoren +CorState: Status +CorByTut: Nach Tutorium +CorProportion: Anteil +CorByProportionOnly proportion@Rational: #{display proportion} Anteile +CorByProportionIncludingTutorial proportion@Rational: #{display proportion} Anteile - Tutorium +CorByProportionExcludingTutorial proportion@Rational: #{display proportion} Anteile + Tutorium + +DeleteRow: Zeile entfernen +ProportionNegative: Anteile dürfen nicht negativ sein +CorrectorsUpdated: Korrektoren erfolgreich aktualisiert +CorrectorsPlaceholder: Korrektoren... +CorrectorsDefaulted: Korrektoren-Liste wurde aus bisherigen Übungsblättern diesen Kurses generiert. Es sind keine Daten gespeichert. + +Users: Benutzer +HomeHeading: Aktuelle Termine +LoginHeading: Authentifizierung +LoginTitle: Authentifizierung +ProfileHeading: Benutzereinstellungen +ProfileDataHeading: Gespeicherte Benutzerdaten +ImpressumHeading: Impressum + +NumCourses n@Int64: #{display n} Kurse +CloseAlert: Schliessen + +Name: Name +MatrikelNr: Matrikelnummer +Theme: Oberflächen Design +Favoriten: Anzahl gespeicherter Favoriten +Plugin: Plugin +Ident: Identifikation +Settings: Individuelle Benutzereinstellungen +SettingsUpdate: Einstellungen wurden gespeichert. + +MultiFileUploadInfo: (Mehrere Dateien mit Shift oder Strg auswählen) + +NrColumn: Nr +SelectColumn: Auswahl + +CorrDownload: Herunterladen +CorrUploadField: Korrekturen +CorrUpload: Korrekturen hochladen +CorrSetCorrector: Korrektor zuweisen +CorrAutoSetCorrector: Korrekturen verteilen +NatField xyz@Text: #{xyz} muss eine natürliche Zahl sein! + +SubmissionsAlreadyAssigned num@Int64: #{display num} Abgaben waren bereits einem Korrektor zugeteilt und wurden nicht verändert: +UpdatedAssignedCorrectorSingle num@Int64: #{display num} Abgaben wurden dem neuen Korrektor zugeteilt. +NoCorrector: Kein Korrektor +RemovedCorrections num@Int64: Korrektur-Daten wurden von #{display num} Abgaben entfernt. +UpdatedAssignedCorrectorsAuto num@Int64: #{display num} Abgaben wurden unter den Korrektoren aufgeteilt. +CouldNotAssignCorrectorsAuto num@Int64: #{display num} Abgaben konnten nicht automatisch zugewiesen werden: +SelfCorrectors num@Int64: #{display num} Abgaben wurden Abgebenden als eigenem Korrektor zugeteilt! + + +CorrectionsUploaded num@Int64: #{display num} Korrekturen wurden gespeichert: +NoCorrectionsUploaded: In der hochgeladenen Datei wurden keine Korrekturen gefunden. + +RatingBy: Korrigiert von +AssignedTime: Zuteilung +AchievedBonusPoints: Erreichte Bonuspunkte +AchievedNormalPoints: Erreichte Punkte +AchievedPassPoints: Erreichte Punkte +AchievedOf achieved@Points possible@Points: #{display achieved} von #{display possible} +PassAchievedOf points@Points passingPoints@Points maxPoints@Points: #{display points} von #{display maxPoints} (Bestanden ab #{display passingPoints}) +PassedResult: Ergebnis +Passed: Bestanden +NotPassed: Nicht bestanden +RatingTime: Korrigiert +RatingComment: Kommentar +SubmissionUsers: Studenten +Rating: Korrektur +RatingPoints: Punkte +RatingPercent: Erreicht +RatingFiles: Korrigierte Dateien +PointsNotPositive: Punktzahl darf nicht negativ sein +RatingPointsDone: Abgabe zählt als korrigiert, gdw. Punktezahl gesetzt ist + +FileTitle: Dateiname +FileModified: Letzte Änderung + + +Corrected: Korrigiert +FileCorrected: Korrigiert (Dateien) +FileCorrectedDeleted: Korrigiert (gelöscht) +RatingUpdated: Korrektur gespeichert +RatingDeleted: Korrektur zurückgesetzt +RatingFilesUpdated: Korrigierte Dateien überschrieben + +NoTableContent: Kein Tabelleninhalt +NoUpcomingSheetDeadlines: Keine anstehenden Übungsblätter + +AdminFor: Administrator +LecturerFor: Dozent +LecturersFor: Dozenten +UserListTitle: Komprehensive Benutzerliste + +DateTimeFormat: Datums- und Uhrzeitformat +DateFormat: Datumsformat +TimeFormat: Uhrzeitformat +DownloadFiles: Dateien automatisch herunterladen +DownloadFilesTip: Wenn gesetzt werden Dateien von Abgaben und Übungsblättern automatisch als Download behandelt, ansonsten ist das Verhalten browserabhängig (es können z.B. PDFs im Browser geöffnet werden). + +InvalidDateTimeFormat: Ungültiges Datums- und Zeitformat, JJJJ-MM-TTTHH:MM[:SS] Format erwartet +AmbiguousUTCTime: Der angegebene Zeitpunkt lässt sich nicht eindeutig zu UTC konvertieren +IllDefinedUTCTime: Der angegebene Zeitpunkt lässt sich nicht zu UTC konvertieren + +LastEdits: Letzte Änderungen +EditedBy name@Text time@Text: #{time} durch #{name} +LastEdit: Letzte Änderung +LastEditByUser: Ihre letzte Bearbeitung +NoEditByUser: Nicht von Ihnen bearbeitet + +SubmissionFilesIgnored: Es wurden Dateien in der hochgeladenen Abgabe ignoriert: +SubmissionDoesNotExist smid@CryptoFileNameSubmission: Es existiert keine Abgabe mit Nummer #{toPathPiece smid}. + +LDAPLoginTitle: Campus-Login +PWHashLoginTitle: Uni2Work-Login +PWHashLoginNote: Dieses Formular ist zu verwenden, wenn Sie vom Uni2Work-Team spezielle Logindaten erhalten haben. Normale Nutzer melden sich bitte via Campus-Login an! +DummyLoginTitle: Development-Login + +CorrectorNormal: Normal +CorrectorMissing: Abwesend +CorrectorExcused: Entschuldigt + +DayIsAHoliday tid@TermId date@Text: #{date} ist ein Feiertag +DayIsOutOfLecture tid@TermId date@Text: #{date} ist außerhalb der Vorlesungszeit des #{display tid} +DayIsOutOfTerm tid@TermId date@Text: #{date} liegt nicht im #{display tid} + +UploadModeNone: Kein Upload +UploadModeUnpack: Upload, einzelne Datei +UploadModeNoUnpack: Upload, ZIP-Archive entpacken + +SubmissionNoUploadExpected: Es ist keine Abgabe von Dateien vorgesehen. + +FieldPrimary: Hauptfach +FieldSecondary: Nebenfach diff --git a/models b/models index 708d933f8..b36d6f93e 100644 --- a/models +++ b/models @@ -1,15 +1,23 @@ -User - plugin Text - ident Text +User json + ident (CI Text) + authentication AuthenticationMode matrikelnummer Text Maybe - email Text - displayName Text - maxFavourites Int default=12 - UniqueAuthentication plugin ident + email (CI Text) + displayName Text + surname Text -- always use: nameWidget displayName surname + maxFavourites Int default=12 + theme Theme default='Default' + dateTimeFormat DateTimeFormat "default='%a %d %b %Y %R'" + dateFormat DateTimeFormat "default='%d.%m.%Y'" + timeFormat DateTimeFormat "default='%R'" + downloadFiles Bool default=false + UniqueAuthentication ident + UniqueEmail email + deriving Show UserAdmin user UserId school SchoolId - UniqueUserAdmin school user + UniqueUserAdmin user school UserLecturer user UserId school SchoolId @@ -20,7 +28,7 @@ StudyFeatures field StudyTermsId type StudyFieldType semester Int - UniqueUserSubject user degree field + -- UniqueUserSubject user degree field -- There exists a counterexample StudyDegree key Int shorthand Text Maybe @@ -33,7 +41,7 @@ StudyTerms Primary key Term json name TermIdentifier -- unTermKey :: TermId -> TermIdentifier - start Day -- TermKey :: TermIdentifier -< TermId + start Day -- TermKey :: TermIdentifier -> TermId end Day holidays [Day] lectureStart Day @@ -42,30 +50,33 @@ Term json Primary name -- newtype Key Term = TermKey { unTermKey :: TermIdentifier } deriving Show -- type TermId = Key Term School json - name Text - shorthand Text + name (CI Text) + shorthand (CI Text) UniqueSchool name + UniqueSchoolShorthand shorthand -- required for Normalisation of CI Text + Primary shorthand -- newtype Key School = SchoolKey { unSchoolKey :: SchoolShorthand } deriving Eq DegreeCourse json course CourseId degree StudyDegreeId terms StudyTermsId UniqueDegreeCourse course degree terms -Course - name Text - description Html Maybe - linkExternal Text Maybe - shorthand Text - termId TermId - schoolId SchoolId - capacity Int Maybe - hasRegistration Bool -- canRegisterNow = hasRegistration && maybe False (<= currentTime) registerFrom && maybe True (>= currentTime) registerTo +Course + name (CI Text) + description Html Maybe + linkExternal Text Maybe + shorthand (CI Text) + term TermId + school SchoolId + capacity Int64 Maybe + -- canRegisterNow = maybe False (<= currentTime) registerFrom && maybe True (>= currentTime) registerTo registerFrom UTCTime Maybe registerTo UTCTime Maybe deregisterUntil UTCTime Maybe registerSecret Text Maybe -- Falls ein Passwort erforderlich ist - materialFree Bool default=true - CourseTermShort termId shorthand + materialFree Bool + TermSchoolCourseShort term school shorthand + TermSchoolCourseName term school name CourseEdit user UserId time UTCTime @@ -75,31 +86,19 @@ CourseFavourite time UTCTime course CourseId UniqueCourseFavourite user course + deriving Show Lecturer - userId UserId - courseId CourseId - UniqueLecturer userId courseId -Corrector - userId UserId - courseId CourseId - load Load - -- SELECT submissionID FROM Tutorial, TutorialUser, Submission, Sheet - -- WHERE ( tutorialTutor = correctorUserId - -- && tutorialCourse = correctorCourseId - -- && tutorialUserTutorial = tutorialId - -- && submissionUser = tutorialUserUser - -- && sheetId = submissionSheetId - -- && sheetCourse = correctorCourseId - -- ) - UniqueCorrector userId courseId + user UserId + course CourseId + UniqueLecturer user course CourseParticipant - courseId CourseId - userId UserId + course CourseId + user UserId registration UTCTime - UniqueParticipant userId courseId + UniqueParticipant user course Sheet - courseId CourseId - name Text + course CourseId + name (CI Text) description Html Maybe type SheetType grouping SheetGroup @@ -109,64 +108,73 @@ Sheet activeTo UTCTime hintFrom UTCTime Maybe solutionFrom UTCTime Maybe - CourseSheet courseId name + uploadMode UploadMode + CourseSheet course name SheetEdit user UserId time UTCTime sheet SheetId +SheetCorrector + user UserId + sheet SheetId + load Load + state CorrectorState default='CorrectorNormal' + UniqueSheetCorrector user sheet + deriving Show Eq Ord SheetFile - sheetId SheetId - fileId FileId + sheet SheetId + file FileId type SheetFileType - UniqueSheetFile fileId sheetId type + UniqueSheetFile file sheet type File title FilePath content ByteString Maybe -- Nothing iff this is a directory modified UTCTime deriving Show Eq Submission - sheetId SheetId - ratingPoints Points Maybe - ratingComment Text Maybe - ratingBy UserId Maybe - ratingTime UTCTime Maybe + sheet SheetId + ratingPoints Points Maybe -- "Just" does not mean done + ratingComment Text Maybe -- "Just" does not mean done + ratingBy UserId Maybe -- assigned corrector + ratingAssigned UTCTime Maybe -- time assigned corrector + ratingTime UTCTime Maybe -- "Just" here indicates done! deriving Show SubmissionEdit user UserId time UTCTime submission SubmissionId SubmissionFile - submissionId SubmissionId - fileId FileId - isUpdate Bool - isDeletion Bool - UniqueSubmissionFile fileId submissionId isUpdate + submission SubmissionId + file FileId + isUpdate Bool -- is this the file updated by a corrector (original will always be retained) + isDeletion Bool -- only set if isUpdate is also set, but file was deleted by corrector + UniqueSubmissionFile file submission isUpdate deriving Show SubmissionUser - userId UserId - submissionId SubmissionId - UniqueSubmissionUser userId submissionId + user UserId + submission SubmissionId + UniqueSubmissionUser user submission SubmissionGroup - courseId CourseId - name Text + course CourseId + name Text Maybe SubmissionGroupEdit user UserId time UTCTime submissionGroup SubmissionGroupId SubmissionGroupUser - submissionGroupId SubmissionGroupId - userId UserId - UniqueSubmissionGroupUser submissionGroupId userId + submissionGroup SubmissionGroupId + user UserId + UniqueSubmissionGroupUser submissionGroup user Tutorial json name Text - tutor UserId + tutor UserId course CourseId TutorialUser - userId UserId - tutorialId TutorialId - UniqueTutorialUser userId tutorialId + user UserId + tutorial TutorialId + UniqueTutorialUser user tutorial Booking - termId TermId + term TermId begin UTCTime end UTCTime weekly Bool @@ -183,17 +191,17 @@ Room building Text Maybe -- BookingRoom -- subject RoomForId --- roomId RoomId --- bookingId BookingId --- UniqueRoomCourse subject roomId bookingId +-- room RoomId +-- booking BookingId +-- UniqueRoomCourse subject room booking +RoomFor - courseId CourseId - tutorialId TutorialId - examId ExamId --- data RoomFor = RoomForCourseIdSum CourseId | RoomForTutorialIdSum TutorialId ... + course CourseId + tutorial TutorialId + exam ExamId +-- data RoomFor = RoomForCourseSum CourseId | RoomForTutorialSum TutorialId ... -- EXAMS ARE TODO: Exam - courseId CourseId + course CourseId name Text description Text begin UTCTime @@ -208,13 +216,12 @@ Exam -- time UTCTime -- exam ExamId --ExamUser --- userId UserId +-- user UserId -- examId ExamId -- -- CONTINUE HERE: Include rating in this table or separately? --- UniqueExamUser userId examId +-- UniqueExamUser user examId -- By default this file is used in Model.hs (which is imported by Foundation.hs) -QueuedNotification - recipient UserId +QueuedJob content Value created UTCTime lockInstance UUID Maybe diff --git a/package.yaml b/package.yaml index dc63b7a45..3b5db7a49 100644 --- a/package.yaml +++ b/package.yaml @@ -20,7 +20,7 @@ dependencies: - classy-prelude-conduit >=0.10.2 - bytestring >=0.9 && <0.11 - text >=0.11 && <2.0 -- persistent >=2.0 && <2.8 +- persistent >=2.7.2 && <2.8 - persistent-postgresql >=2.1.1 && <2.8 - persistent-template >=2.0 && <2.8 - template-haskell @@ -74,12 +74,25 @@ dependencies: - generic-deriving - blaze-html - conduit-resumablesink >=0.2 -- yesod-auth-ldap -- LDAP - parsec - uuid - exceptions - stm-conduit +- lens +- MonadRandom +- email-validate +- scientific +- tz +- system-locale +- th-lift-instances +- gitrev +- Glob +- ldap-client +- connection +- universe +- universe-base +- random-shuffle +- th-abstraction # The library contains all of our application code. The executable # defined below is just a thin wrapper. @@ -92,6 +105,7 @@ library: - -Wall - -fwarn-tabs - -O0 + - -ddump-splices cpp-options: -DDEVELOPMENT else: ghc-options: diff --git a/routes b/routes index 075a60fd4..6116665dc 100644 --- a/routes +++ b/routes @@ -1,41 +1,91 @@ -/static StaticR Static appStatic -/auth AuthR Auth getAuth +-- +-- Accesss granted via tags; default is no accesss. +-- Permission must be explicitly granted. +-- +-- Access permission is the disjunction of permit tags +-- Tags are split on "AND" to encode conjunction. +-- +-- Note that nested routes automatically inherit all tags from the parent. +-- +-- Admins always have access to entities within their assigned schools. +-- +-- Access Tags: +-- !free -- free for all +-- !lecturer -- lecturer for this course (or the school, if route is not connected to a course) +-- !corrector -- corrector for this sheet (or the submission, if route is connected to a submission, or the course, if route is not connected to a sheet, or any course, if route is not connected to a course) +-- !registered -- participant for this course (no effect outside of courses) +-- !owner -- part of the group of owners of this submission +-- !capacity -- course this route is associated with has at least one unit of participant capacity +-- +-- !materials -- only if course allows all materials to be free (no meaning outside of courses) +-- !time -- access depends on time somehow +-- !isRead -- only if it is read-only access (i.e. GET but not POST) +-- !isWrite -- only if it is write access (i.e. POST only) why needed??? +-- +-- !deprecated -- like free, but logs and gives a warning; entirely disabled in production +-- -/favicon.ico FaviconR GET -/robots.txt RobotsR GET +/static StaticR Static appStatic !free +/auth AuthR Auth getAuth !free -/ HomeR GET POST -/profile ProfileR GET -/users UsersR GET !adminAny +/favicon.ico FaviconR GET !free +/robots.txt RobotsR GET !free -/term TermShowR GET -/term/edit TermEditR GET POST !adminAny -/term/#TermId/edit TermEditExistR GET !adminAny +/ HomeR GET !free +/users UsersR GET -- no tags, i.e. admins only +/admin/test AdminTestR GET POST +/admin/user/#CryptoUUIDUser AdminUserR GET +/admin/user/#CryptoUUIDUser/hijack AdminHijackUserR POST +/info VersionR GET !free -/course/ CourseListR GET -!/course/new CourseNewR GET POST !lecturerAny -!/course/#TermId CourseListTermR GET -/course/#TermId/#Text CourseR !updateFavourite: - /show CourseShowR GET POST - /edit CourseEditR GET POST !lecturer +/profile ProfileR GET POST !free !free +/profile/data ProfileDataR GET POST !free !free - /ex SheetR !registered: - / SheetListR GET - /#Text/show SheetShowR GET !time - /#Text/#SheetFileType/#FilePath SheetFileR GET !time - /new SheetNewR GET POST !lecturer - /#Text/edit SheetEditR GET POST !lecturer - /#Text/delete SheetDelR GET POST !lecturer +/term TermShowR GET !free +/term/current TermCurrentR GET !free +/term/edit TermEditR GET POST +/term/#TermId/edit TermEditExistR GET +!/term/#TermId TermCourseListR GET !free +!/term/#TermId/#SchoolId TermSchoolCourseListR GET !free + +/school SchoolListR GET +/school/#SchoolId SchoolShowR GET --- TODO below -/submission SubmissionListR GET POST -/submission/#CryptoUUIDSubmission SubmissionR GET POST -/submissions.zip SubmissionDownloadMultiArchiveR POST -!/submission/archive/#FilePath SubmissionDownloadArchiveR GET -!/submission/#CryptoUUIDSubmission/#FilePath SubmissionDownloadSingleR GET +-- For Pattern Synonyms see Foundation +/course/ CourseListR GET !free +!/course/new CourseNewR GET POST !lecturer +!/course/new/#{Maybe TermId}/#{Maybe SchoolId}/#{Maybe CourseShorthand} CourseNewTemplateR GET !lecturer +/course/#TermId/#SchoolId/#CourseShorthand CourseR !lecturer: + / CShowR GET !free + /register CRegisterR POST !timeANDcapacity + /edit CEditR GET POST + /delete CDeleteR GET POST !lecturerANDempty + /users CUsersR GET + /user/#CryptoUUIDUser CUserR GET + /correctors CHiWisR GET + /subs CCorrectionsR GET POST + /ex SheetListR GET !registered !materials + !/ex/new SheetNewR GET POST + /ex/#SheetName SheetR: + / SShowR GET !timeANDregistered !timeANDmaterials !corrector + /edit SEditR GET POST + /delete SDelR GET POST + /subs SSubsR GET POST -- for lecturer only + /subs/new SubmissionNewR GET POST !timeANDregistered + /subs/own SubmissionOwnR GET !free -- just redirect + /sub/#CryptoFileNameSubmission SubmissionR !correctorANDisRead: + / SubShowR GET POST !ownerANDtime !ownerANDisRead + /archive/#{ZIPArchiveName SubmissionFileType} SubArchiveR GET !owner + /correction CorrectionR GET POST !corrector !ownerANDisReadANDrated + !/#SubmissionFileType/*FilePath SubDownloadR GET !owner + /correctors SCorrR GET POST + !/#SheetFileType/*FilePath SFileR GET !timeANDregistered !timeANDmaterials !corrector -!/#UUID CryptoUUIDDispatchR GET --- For demonstration -/course/#CryptoUUIDCourse/edit CourseEditIDR GET +/corrections CorrectionsR GET POST !corrector !lecturer +/corrections/upload CorrectionsUploadR GET POST !corrector !lecturer + + +!/#UUID CryptoUUIDDispatchR GET !free -- just redirect +-- !/*{CI FilePath} CryptoFileNameDispatchR GET !free -- Disabled until preliminary check for valid cID exists diff --git a/shell.nix b/shell.nix index 3c37a979e..d305354a1 100644 --- a/shell.nix +++ b/shell.nix @@ -10,7 +10,7 @@ let drv = haskellPackages.callPackage ./uniworx.nix {}; postgresSchema = pkgs.writeText "schema.sql" '' - CREATE USER uniworx; + CREATE USER uniworx WITH SUPERUSER; CREATE DATABASE uniworx_test; GRANT ALL ON DATABASE uniworx_test TO uniworx; CREATE DATABASE uniworx; @@ -24,13 +24,12 @@ let override = oldAttrs: { nativeBuildInputs = oldAttrs.nativeBuildInputs ++ (with pkgs; [ postgresql cabal-install ]) ++ (with haskellPackages; [ stack yesod-bin ]); shellHook = '' - ${oldAttrs.shellHook} export PROMPT_INFO="${oldAttrs.name}" pgDir=$(mktemp -d) pgSockDir=$(mktemp -d) pgLogFile=$(mktemp) - pg_ctl init -D ''${pgDir} + initdb --no-locale -D ''${pgDir} pg_ctl start -D ''${pgDir} -l ''${pgLogFile} -w -o "-k ''${pgSockDir} -c listen_addresses=''' -c hba_file='${postgresHba}' -c unix_socket_permissions=0700" export PGHOST=''${pgSockDir} PGLOG=''${pgLogFile} psql -f ${postgresSchema} postgres @@ -42,6 +41,8 @@ let } trap cleanup EXIT + + ${oldAttrs.shellHook} ''; }; diff --git a/src/Application.hs b/src/Application.hs index 9a0e7883c..931f53e3c 100644 --- a/src/Application.hs +++ b/src/Application.hs @@ -7,7 +7,7 @@ {-# LANGUAGE RecordWildCards #-} {-# OPTIONS_GHC -fno-warn-orphans #-} module Application - ( getApplicationDev + ( getApplicationDev, getAppDevSettings , appMain , develMain , makeFoundation @@ -19,6 +19,7 @@ module Application -- * for GHCI , handler , db + , addPWEntry ) where import Control.Monad.Logger (liftLoc, runLoggingT) @@ -46,7 +47,12 @@ import qualified Data.UUID.V4 as UUID import System.Directory import System.FilePath -import Notifications +import Jobs + +import qualified Data.Text.Encoding as Text +import Yesod.Auth.Util.PasswordStore + +import qualified Data.ByteString.Lazy as LBS -- Import all relevant handler modules here. -- (HPack takes care to add new modules to our cabal file nowadays.) @@ -54,10 +60,13 @@ import Handler.Common import Handler.Home import Handler.Profile import Handler.Users +import Handler.Admin import Handler.Term +import Handler.School import Handler.Course import Handler.Sheet import Handler.Submission +import Handler.Corrections import Handler.CryptoIDDispatch @@ -83,7 +92,7 @@ makeFoundation appSettings@(AppSettings{..}) = do appCryptoIDKey <- readKeyFile appCryptoIDKeyFile appInstanceID <- maybe UUID.nextRandom readInstanceIDFile appInstanceIDFile - (appNotificationCtl, recvChan) <- atomically $ do + (appJobCtl, recvChan) <- atomically $ do chan <- newBroadcastTMChan recvChan <- dupTMChan chan return (chan, recvChan) @@ -108,24 +117,24 @@ makeFoundation appSettings@(AppSettings{..}) = do (pgPoolSize appDatabaseConf) -- Perform database migration using our application's logging settings. - flip runLoggingT logFunc $ runSqlPool (runMigration migrateAll) pool + flip runLoggingT logFunc $ runSqlPool migrateAll pool - void . fork . handleNotifications $ (mkFoundation pool) { appNotificationCtl = recvChan } + void . fork . handleJobs $ (mkFoundation pool) { appJobCtl = recvChan } -- Return the foundation return $ mkFoundation pool readInstanceIDFile :: FilePath -> IO UUID -readInstanceIDFile idFile = handle generateInstead $ readFileUtf8 idFile >>= parseText +readInstanceIDFile idFile = handle generateInstead $ LBS.readFile idFile >>= parseBS where - parseText :: Text -> IO UUID - parseText = maybe (throwString "appInstanceIDFile does not contain an UUID") return . UUID.fromText + parseBS :: LBS.ByteString -> IO UUID + parseBS = maybe (throwString "appInstanceIDFile does not contain an UUID encoded in network byte order") return . UUID.fromByteString generateInstead :: IOException -> IO UUID generateInstead e | isDoesNotExistError e = do createDirectoryIfMissing True $ takeDirectory idFile instanceId <- UUID.nextRandom - writeFileUtf8 idFile $ UUID.toText instanceId + LBS.writeFile idFile $ UUID.toByteString instanceId return instanceId | otherwise = throw e @@ -225,7 +234,7 @@ getApplicationRepl = do shutdownApp :: UniWorX -> IO () shutdownApp UniWorX{..} = do - atomically $ closeTMChan appNotificationCtl + atomically $ closeTMChan appJobCtl --------------------------------------------- @@ -239,3 +248,11 @@ handler h = getAppDevSettings >>= makeFoundation >>= flip unsafeHandler h -- | Run DB queries db :: ReaderT SqlBackend (HandlerT UniWorX IO) a -> IO a db = handler . runDB + +addPWEntry :: User + -> Text {-^ Password -} + -> IO () +addPWEntry User{..} (Text.encodeUtf8 -> pw) = db $ do + PWHashConf{..} <- getsYesod $ appAuthPWHash . appSettings + (AuthPWHash . Text.decodeUtf8 -> userAuthentication) <- liftIO $ makePasswordWith pwHashAlgorithm pw pwHashStrength + void $ insert User{..} diff --git a/src/Auth/Dummy.hs b/src/Auth/Dummy.hs new file mode 100644 index 000000000..809db8647 --- /dev/null +++ b/src/Auth/Dummy.hs @@ -0,0 +1,63 @@ +{-# LANGUAGE NoImplicitPrelude + , RecordWildCards + , TemplateHaskell + , FlexibleContexts + , TypeFamilies + , OverloadedStrings + #-} + +module Auth.Dummy + ( dummyLogin + , DummyMessage(..) + ) where + +import Import.NoFoundation +import Database.Persist.Sql (SqlBackendCanRead) + +import Utils.Form + +import Data.CaseInsensitive (CI) +import qualified Data.CaseInsensitive as CI + + +data DummyMessage = MsgDummyIdent + + +dummyForm :: ( RenderMessage site FormMessage + , RenderMessage site DummyMessage + , YesodPersist site + , SqlBackendCanRead (YesodPersistBackend site) + , Button site SubmitButton + , Show (ButtonCssClass site) + ) => AForm (HandlerT site IO) (CI Text) +dummyForm = areq (selectField userList) (fslI MsgDummyIdent) Nothing + <* submitButton + where + userList = fmap mkOptionList . runDB $ map toOption <$> selectList [] [Asc UserIdent] + toOption (Entity _ User{..}) = Option (CI.original userIdent) userIdent (CI.original userIdent) + +dummyLogin :: ( YesodAuth site + , YesodPersist site + , SqlBackendCanRead (YesodPersistBackend site) + , RenderMessage site FormMessage + , RenderMessage site DummyMessage + , Button site SubmitButton + , Show (ButtonCssClass site) + ) => AuthPlugin site +dummyLogin = AuthPlugin{..} + where + apName = "dummy" + -- apDispatch :: Text -> [Text] -> HandlerT Auth (HandlerT site IO) TypedContent + apDispatch "POST" [] = do + ((loginRes, _), _) <- lift . runFormPost $ renderAForm FormStandard dummyForm + case loginRes of + FormFailure errs -> do + lift . forM_ errs $ addMessage Error . toHtml + redirect LoginR + FormMissing -> redirect LoginR + FormSuccess ident -> + lift . setCredsRedirect $ Creds "dummy" (CI.original ident) [] + apDispatch _ _ = notFound + apLogin toMaster = do + (login, loginEnctype) <- handlerToWidget . generateFormPost $ renderAForm FormStandard dummyForm + $(widgetFile "widgets/dummy-login-form") diff --git a/src/Auth/LDAP.hs b/src/Auth/LDAP.hs new file mode 100644 index 000000000..32c185519 --- /dev/null +++ b/src/Auth/LDAP.hs @@ -0,0 +1,171 @@ +{-# LANGUAGE RecordWildCards + , OverloadedStrings + , TemplateHaskell + , ViewPatterns + , TypeFamilies + , FlexibleContexts + , FlexibleInstances + , NoImplicitPrelude + , ScopedTypeVariables + #-} + +module Auth.LDAP + ( campusLogin + , CampusUserException(..) + , campusUser + , CampusMessage(..) + , Ldap.AttrList, Ldap.Attr(..), Ldap.AttrValue + ) where + +import Import.NoFoundation +import Control.Lens +import Network.Connection + +import Data.CaseInsensitive (CI) +import qualified Data.CaseInsensitive as CI + +import qualified Control.Monad.Catch as Exc + +import Utils.Form + +import qualified Ldap.Client as Ldap + +import qualified Data.Text.Encoding as Text + +import qualified Yesod.Auth.Message as Msg + + +data CampusLogin = CampusLogin + { campusIdent :: CI Text + , campusPassword :: Text + } + +data CampusMessage = MsgCampusIdentNote + | MsgCampusIdent + | MsgCampusPassword + | MsgCampusSubmit + | MsgCampusInvalidCredentials + + +findUser :: LdapConf -> Ldap.Ldap -> Text -> [Ldap.Attr] -> IO [Ldap.SearchEntry] +findUser LdapConf{..} ldap campusIdent = Ldap.search ldap ldapBase userSearchSettings userFilter + where + userFilter = userPrincipalName Ldap.:= Text.encodeUtf8 campusIdent + userSearchSettings = mconcat + [ Ldap.scope ldapScope + , Ldap.size 2 + , Ldap.time ldapTimeout + , Ldap.derefAliases Ldap.DerefAlways + ] + +userPrincipalName :: Ldap.Attr +userPrincipalName = Ldap.Attr "userPrincipalName" + +campusForm :: ( RenderMessage site FormMessage + , RenderMessage site CampusMessage + , Button site SubmitButton + , Show (ButtonCssClass site) + ) => AForm (HandlerT site IO) CampusLogin +campusForm = CampusLogin + <$> areq ciField (fslpI MsgCampusIdent "user.name@campus.lmu.de" & setTooltip MsgCampusIdentNote) Nothing + <*> areq passwordField (fslI MsgCampusPassword) Nothing + <* submitButton + +campusLogin :: forall site. + ( YesodAuth site + , RenderMessage site FormMessage + , RenderMessage site CampusMessage + , Button site SubmitButton + , Show (ButtonCssClass site) + ) => LdapConf -> AuthPlugin site +campusLogin conf@LdapConf{..} = AuthPlugin{..} + where + apName = "LDAP" + apDispatch :: Text -> [Text] -> HandlerT Auth (HandlerT site IO) TypedContent + apDispatch "POST" [] = do + ((loginRes, _), _) <- lift . runFormPost $ renderAForm FormStandard campusForm + case loginRes of + FormFailure errs -> do + forM_ errs $ addMessage Error . toHtml + redirect LoginR + FormMissing -> redirect LoginR + FormSuccess CampusLogin{ campusIdent = CI.original -> campusIdent, ..} -> do + ldapResult <- liftIO . Ldap.with ldapHost ldapPort $ \ldap -> do + Ldap.bind ldap (Ldap.Dn campusIdent) (Ldap.Password $ Text.encodeUtf8 campusPassword) + Ldap.bind ldap ldapDn ldapPassword + findUser conf ldap campusIdent [userPrincipalName] + case ldapResult of + Left err + | Ldap.ResponseError (Ldap.ResponseErrorCode _ Ldap.InvalidCredentials _ _) <- err + -> do + $logDebugS "LDAP" "Invalid credentials" + loginErrorMessageI LoginR Msg.InvalidLogin + | otherwise -> do + $logErrorS "LDAP" $ "Error during login: " <> tshow err + loginErrorMessageI LoginR Msg.AuthError + Right searchResults + | [Ldap.SearchEntry (Ldap.Dn userDN) userAttrs] <- searchResults + , Just [principalName] <- lookup userPrincipalName userAttrs + , Right credsIdent <- Text.decodeUtf8' principalName + -> do + $logDebugS "LDAP" $ tshow searchResults + lift . setCredsRedirect $ Creds apName credsIdent [("DN", userDN)] + | otherwise -> do + $logWarnS "LDAP" $ "Could not extract principal name: " <> tshow searchResults + loginErrorMessageI LoginR Msg.AuthError + apDispatch _ _ = notFound + apLogin toMaster = do + (login, loginEnctype) <- handlerToWidget . generateFormPost $ renderAForm FormStandard campusForm + $(widgetFile "widgets/campus-login-form") + +data CampusUserException = CampusUserLdapError Ldap.LdapError + | CampusUserHostNotResolved String + | CampusUserLineTooLong + | CampusUserHostCannotConnect String [IOException] + | CampusUserNoResult + | CampusUserAmbiguous + deriving (Show, Eq, Typeable) + +instance Exception CampusUserException + +campusUser :: (MonadIO m, MonadThrow m) => LdapConf -> Creds site -> m (Ldap.AttrList []) +campusUser conf@LdapConf{..} Creds{..} = liftIO . (`catches` errHandlers) $ either (throwM . CampusUserLdapError) return <=< Ldap.with ldapHost ldapPort $ \ldap -> do + Ldap.bind ldap ldapDn ldapPassword + results <- case lookup "DN" credsExtra of + Just userDN -> do + let userFilter = Ldap.Present userPrincipalName + userSearchSettings = mconcat + [ Ldap.scope Ldap.BaseObject + , Ldap.size 2 + , Ldap.time ldapTimeout + , Ldap.derefAliases Ldap.DerefAlways + ] + Ldap.search ldap (Ldap.Dn userDN) userSearchSettings userFilter [] + Nothing -> do + findUser conf ldap credsIdent [] + case results of + [] -> throwM CampusUserNoResult + [Ldap.SearchEntry _ attrs] -> return attrs + _otherwise -> throwM CampusUserAmbiguous + where + errHandlers = [ Exc.Handler $ \LineTooLong -> throwM CampusUserLineTooLong + , Exc.Handler $ \(HostNotResolved host) -> throwM $ CampusUserHostNotResolved host + , Exc.Handler $ \(HostCannotConnect host excs) -> throwM $ CampusUserHostCannotConnect host excs + ] + +-- ldapConfig :: UniWorX -> LDAPConfig +-- ldapConfig _app@(appSettings -> settings) = LDAPConfig +-- { usernameFilter = \u -> principalName <> "=" <> u +-- , identifierModifier +-- , ldapUri = appLDAPURI settings +-- , initDN = appLDAPDN settings +-- , initPass = appLDAPPw settings +-- , baseDN = appLDAPBaseName settings +-- , ldapScope = LdapScopeSubtree +-- } +-- where +-- principalName :: IsString a => a +-- principalName = "userPrincipalName" +-- identifierModifier _ entry = case lookup principalName $ leattrs entry of +-- Just [n] -> Text.pack n +-- _ -> error "Could not determine user principal name" diff --git a/src/Auth/PWHash.hs b/src/Auth/PWHash.hs new file mode 100644 index 000000000..ba7198710 --- /dev/null +++ b/src/Auth/PWHash.hs @@ -0,0 +1,105 @@ +{-# LANGUAGE NoImplicitPrelude + , QuasiQuotes + , TemplateHaskell + , ViewPatterns + , RecordWildCards + , OverloadedStrings + , FlexibleContexts + , TypeFamilies + #-} + +module Auth.PWHash + ( hashLogin + , PWHashMessage(..) + ) where + +import Import.NoFoundation +import Database.Persist.Sql (SqlBackendCanRead) + +import Utils.Form + +import Data.CaseInsensitive (CI) +import qualified Data.CaseInsensitive as CI + +import Yesod.Auth.Util.PasswordStore (verifyPasswordWith) + +import qualified Yesod.Auth.Message as Msg + + +data HashLogin = HashLogin + { hashIdent :: CI Text + , hashPassword :: Text + } + +data PWHashMessage = MsgPWHashIdent + | MsgPWHashPassword + + +hashForm :: ( RenderMessage site FormMessage + , RenderMessage site PWHashMessage + , Button site SubmitButton + , Show (ButtonCssClass site) + ) => AForm (HandlerT site IO) HashLogin +hashForm = HashLogin + <$> areq ciField (fslpI MsgPWHashIdent "Identifikation") Nothing + <*> areq passwordField (fslpI MsgPWHashPassword "Passwort") Nothing + <* submitButton + + +hashLogin :: ( YesodAuth site + , YesodPersist site + , SqlBackendCanRead (YesodPersistBackend site) + , RenderMessage site FormMessage + , RenderMessage site PWHashMessage + , Button site SubmitButton + , Show (ButtonCssClass site) + ) => PWHashAlgorithm -> AuthPlugin site +hashLogin pwHashAlgo = AuthPlugin{..} + where + apName = "PWHash" + apDispatch "POST" [] = do + ((loginRes, _), _) <- lift . runFormPost $ renderAForm FormStandard hashForm + case loginRes of + FormFailure errs -> do + forM_ errs $ addMessage Error . toHtml + redirect LoginR + FormMissing -> redirect LoginR + FormSuccess HashLogin{..} -> do + user <- lift . runDB . getBy $ UniqueAuthentication hashIdent + case user of + Just (Entity _ User{ userAuthentication = AuthPWHash{..}, userIdent = CI.original -> userIdent }) + | verifyPasswordWith pwHashAlgo (2^) (encodeUtf8 hashPassword) (encodeUtf8 authPWHash) -> -- (2^) is magic. + lift . setCredsRedirect $ Creds apName userIdent [] + other -> do + $logDebugS "PWHash" $ tshow other + loginErrorMessageI LoginR Msg.InvalidLogin + -- apDispatch "GET" [] = do + -- authData <- lookupBasicAuth + -- pwdata <- liftIO $ Yaml.decodeFileEither fp + + -- addHeader "WWW-Authenticate" [st|Basic realm="uni2work maintenance auth" charset="UTF-8"|] + + -- case pwdata of + -- Left err -> $logDebugS "Auth" $ tshow err + -- Right pws -> $logDebugS "Auth" $ tshow (length pws) <> " pw entries" + + -- case (authData, pwdata) of + -- (Nothing, _) -> do + -- notAuthenticated + -- (Just (usr, (Text.encodeUtf8 -> pw)), Right pwdata') + -- | [ PWEntry{ pwUser = pwUser@(User{..}), pwHash = (Text.encodeUtf8 -> pwHash) } ] + -- <- [ pwe | pwe@PWEntry{..} <- pwdata' + -- , let User{..} = pwUser + -- , userIdent == usr + -- , userPlugin == apName + -- ] + -- , verifyPassword pw pwHash + -- -> lift $ do + -- runDB . void $ insertUnique pwUser + -- setCredsRedirect $ Creds apName userIdent [] + -- _ -> permissionDenied "Invalid auth" + apDispatch _ _ = notFound + apLogin toMaster = do + (login, loginEnctype) <- handlerToWidget . generateFormPost $ renderAForm FormStandard hashForm + $(widgetFile "widgets/hash-login-form") + diff --git a/src/CryptoID.hs b/src/CryptoID.hs index ed2864eab..e2f6361cb 100644 --- a/src/CryptoID.hs +++ b/src/CryptoID.hs @@ -1,8 +1,11 @@ {-# LANGUAGE DataKinds #-} +{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE NoImplicitPrelude #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE FlexibleInstances, TypeSynonymInstances #-} +{-# LANGUAGE RecordWildCards, ViewPatterns, PatternGuards #-} +{-# LANGUAGE PatternSynonyms #-} {-# OPTIONS_GHC -fno-warn-orphans #-} module CryptoID @@ -17,23 +20,46 @@ import CryptoID.TH import ClassyPrelude hiding (fromString) import Model +import qualified Data.CryptoID as E import Data.CryptoID.Poly.ImplicitNamespace import Data.UUID.Cryptographic.ImplicitNamespace import System.FilePath.Cryptographic.ImplicitNamespace -import Data.UUID.Types +import qualified Data.Text as Text + +-- import Data.UUID.Types import Web.PathPieces - -instance PathPiece UUID where - fromPathPiece = fromString . unpack - toPathPiece = pack . toString +import Data.CaseInsensitive (CI) +import qualified Data.CaseInsensitive as CI --- Generates CryptoUUID... Datatypes +-- Generates CryptoUUID... and CryptoFileName... Datatypes decCryptoIDs [ ''SubmissionId - , ''CourseId - , ''SheetId , ''FileId + , ''UserId ] -{- TODO: Do we need/want CryptoUUIDs for Sheet numbers? -} + +instance {-# OVERLAPS #-} namespace ~ CryptoIDNamespace (CI FilePath) SubmissionId => PathPiece (E.CryptoID namespace (CI FilePath)) where + fromPathPiece (Text.unpack -> piece) = do + piece' <- (stripPrefix `on` map CI.mk) "uwa" piece + return . CryptoID . CI.mk $ map CI.original piece' + toPathPiece = Text.pack . ("uwa" <>) . CI.foldedCase . ciphertext + + +newtype SubmissionMode = SubmissionMode (Maybe CryptoFileNameSubmission) + deriving (Show, Read, Eq) + +pattern NewSubmission :: SubmissionMode +pattern NewSubmission = SubmissionMode Nothing +pattern ExistingSubmission :: CryptoFileNameSubmission -> SubmissionMode +pattern ExistingSubmission cID = SubmissionMode (Just cID) + +instance PathPiece SubmissionMode where + fromPathPiece "new" = Just $ SubmissionMode Nothing + fromPathPiece s = SubmissionMode . Just <$> fromPathPiece s + + toPathPiece (SubmissionMode Nothing) = "new" + toPathPiece (SubmissionMode (Just x)) = toPathPiece x + + diff --git a/src/Data/CaseInsensitive/Instances.hs b/src/Data/CaseInsensitive/Instances.hs new file mode 100644 index 000000000..ea5253f44 --- /dev/null +++ b/src/Data/CaseInsensitive/Instances.hs @@ -0,0 +1,56 @@ +{-# LANGUAGE NoImplicitPrelude #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE OverloadedStrings #-} +{-# OPTIONS_GHC -fno-warn-orphans #-} + +module Data.CaseInsensitive.Instances + () where + +import ClassyPrelude.Yesod + +import Data.CaseInsensitive (CI) +import qualified Data.CaseInsensitive as CI + +import Database.Persist.Sql + +import Text.Blaze (ToMarkup(..)) + +import Data.Text (Text) +import qualified Data.Text.Encoding as Text + + +instance PersistField (CI Text) where + toPersistValue ciText = PersistDbSpecific . Text.encodeUtf8 $ CI.original ciText + fromPersistValue (PersistDbSpecific bs) = Right . CI.mk $ Text.decodeUtf8 bs + fromPersistValue x = Left . pack $ "Expected PersistDbSpecific, received: " ++ show x + +instance PersistField (CI String) where + toPersistValue ciText = PersistDbSpecific . Text.encodeUtf8 . pack $ CI.original ciText + fromPersistValue (PersistDbSpecific bs) = Right . CI.mk . unpack $ Text.decodeUtf8 bs + fromPersistValue x = Left . pack $ "Expected PersistDbSpecific, received: " ++ show x + +instance PersistFieldSql (CI Text) where + sqlType _ = SqlOther "citext" + +instance PersistFieldSql (CI String) where + sqlType _ = SqlOther "citext" + +instance ToJSON a => ToJSON (CI a) where + toJSON = toJSON . CI.original + +instance (FromJSON a, CI.FoldCase a) => FromJSON (CI a) where + parseJSON = fmap CI.mk . parseJSON + +instance ToMessage a => ToMessage (CI a) where + toMessage = toMessage . CI.original + +instance ToMarkup a => ToMarkup (CI a) where + toMarkup = toMarkup . CI.original + preEscapedToMarkup = preEscapedToMarkup . CI.original + +instance ToWidget site a => ToWidget site (CI a) where + toWidget = toWidget . CI.original + +instance RenderMessage site a => RenderMessage site (CI a) where + renderMessage f ls msg = renderMessage f ls $ CI.original msg diff --git a/src/Foundation.hs b/src/Foundation.hs index 38de1681c..86e90471b 100644 --- a/src/Foundation.hs +++ b/src/Foundation.hs @@ -1,16 +1,17 @@ {-# LANGUAGE NoImplicitPrelude #-} {-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TemplateHaskell, QuasiQuotes #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE ViewPatterns #-} +{-# LANGUAGE LambdaCase #-} {-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE NamedFieldPuns #-} -{-# LANGUAGE PatternGuards #-} +{-# LANGUAGE PatternGuards, MultiWayIf #-} {-# LANGUAGE TupleSections #-} -{-# LANGUAGE FlexibleInstances, UndecidableInstances #-} +{-# LANGUAGE FlexibleInstances, UndecidableInstances, FlexibleContexts #-} module Foundation where @@ -22,17 +23,21 @@ import Text.Jasmine (minifym) -- Used only when in "auth-dummy-login" setting is enabled. import Yesod.Auth.Message import Yesod.Auth.Dummy -import Yesod.Auth.LDAP +import Auth.LDAP +import Auth.PWHash +import Auth.Dummy -import LDAP.Data (LDAPScope(..)) -import LDAP.Search (LDAPEntry(..)) +import qualified Network.Wai as W (requestMethod, pathInfo) import Yesod.Default.Util (addStaticContentExternal) import Yesod.Core.Types (Logger) import qualified Yesod.Core.Unsafe as Unsafe +import Data.CaseInsensitive (CI) import qualified Data.CaseInsensitive as CI import qualified Data.Text.Encoding as TE +import qualified Data.CryptoID as E + import Data.ByteArray (convert) import Crypto.Hash (Digest, SHAKE256) import Crypto.Hash.Conduit (sinkHash) @@ -45,6 +50,16 @@ import qualified Data.ByteString.Lazy as Lazy.ByteString import qualified Data.Text as Text import qualified Data.Text.Encoding as Text +import Data.List (foldr1) +import qualified Data.List as List +import Data.Set (Set) +import qualified Data.Set as Set +import Data.Map (Map, (!?)) +import qualified Data.Map as Map + +import Data.Monoid (Any(..)) + + import Data.Conduit (($$)) import Data.Conduit.List (sourceList) @@ -52,12 +67,39 @@ import qualified Database.Esqueleto as E import Control.Monad.Except (MonadError(..), runExceptT) import Control.Monad.Trans.Maybe (MaybeT(..)) +import Control.Monad.Trans.Reader (runReader) +import Control.Monad.Trans.Writer (WriterT(..)) +import Control.Monad.Writer.Class (MonadWriter(..)) +import Control.Monad.Catch (handleAll) +import qualified Control.Monad.Catch as C import System.FilePath import Handler.Utils.Templates import Handler.Utils.StudyFeatures +import Control.Lens +import Utils +import Utils.Form +import Utils.Lens +import Data.Aeson hiding (Error) +import Data.Aeson.TH +import qualified Data.Yaml as Yaml + +import Text.Shakespeare.Text (st) + + +instance DisplayAble b => DisplayAble (E.CryptoID a b) where + display = display . ciphertext + +instance {-# OVERLAPS #-} namespace ~ CryptoIDNamespace (CI FilePath) SubmissionId => DisplayAble (E.CryptoID namespace (CI FilePath)) where + display = toPathPiece + +instance DisplayAble TermId where + display = termToText . unTermKey + +instance DisplayAble SchoolId where + display = CI.original . unSchoolKey -- infixl 9 :$: -- pattern a :$: b = a b @@ -74,11 +116,12 @@ data UniWorX = UniWorX , appLogger :: Logger , appCryptoIDKey :: CryptoIDKey , appInstanceID :: UUID - , appNotificationCtl :: TMChan NotificationCtl + , appJobCtl :: TMChan JobCtl } -data NotificationCtl = NCtlFlush - | NCtlSend QueuedNotificationId +data JobCtl = NCtlFlush + | NCtlPerform QueuedJobId + deriving (Eq, Ord, Read, Show) -- This is where we define all of the routes in our application. For a full -- explanation of the syntax, please see: @@ -90,19 +133,28 @@ data NotificationCtl = NCtlFlush -- http://www.yesodweb.com/book/scaffolding-and-the-site-template#scaffolding-and-the-site-template_foundation_and_application_modules -- -- This function also generates the following type synonyms: --- type Handler = HandlerT UniWorX IO --- type Widget = WidgetT UniWorX IO () +-- type Handler x = HandlerT UniWorX IO x +-- type Widget = WidgetT UniWorX IO () mkYesodData "UniWorX" $(parseRoutesFile "routes") +-- | Convenient Type Synonyms: +type DB a = YesodDB UniWorX a +type Form x = Html -> MForm (HandlerT UniWorX IO) (FormResult x, Widget) +type MsgRenderer = MsgRendererS UniWorX -- see Utils + -- Pattern Synonyms for convenience -pattern CSheetR tid csh ptn = CourseR tid csh (SheetR ptn) +pattern CSheetR tid ssh csh shn ptn + = CourseR tid ssh csh (SheetR shn ptn) +pattern CSubmissionR tid ssh csh shn cid ptn + = CSheetR tid ssh csh shn (SubmissionR cid ptn) +-- Menus and Favourites data MenuItem = MenuItem { menuItemLabel :: Text - , menuItemIcon :: Maybe Text + , menuItemIcon :: Maybe Text , menuItemRoute :: Route UniWorX - , menuItemAccessCallback' :: Handler Bool + , menuItemAccessCallback' :: Handler Bool -- Check whether action is shown in ADDITION to authorization (which is always checked) } menuItemAccessCallback :: MenuItem -> Handler Bool @@ -116,13 +168,13 @@ data MenuTypes -- Semantische Rolle: | NavbarRight { menuItem :: MenuItem } -- Generell, nahezu immer sichtbar | NavbarSecondary { menuItem :: MenuItem } -- Generell, nahezu immer sichtbar | PageActionPrime { menuItem :: MenuItem } -- Seitenspezifische Aktion, häufig - | PageActionSecondary { menuItem :: MenuItem } -- Seitenspezifische Aktion, selten + | PageActionSecondary { menuItem :: MenuItem } -- Seitenspezifische Aktion, selten (nicht im MouseOver enthalten, immer hinten gelistet) --- | Convenient Type Synonyms: -type DB a = YesodDB UniWorX a -type Form x = Html -> MForm (HandlerT UniWorX IO) (FormResult x, Widget) - -mkMessage "UniWorX" "messages" "de" +-- Messages +mkMessage "UniWorX" "messages/uniworx" "de" +mkMessageVariant "UniWorX" "Campus" "messages/campus" "de" +mkMessageVariant "UniWorX" "Dummy" "messages/dummy" "de" +mkMessageVariant "UniWorX" "PWHash" "messages/pw-hash" "de" -- This instance is required to use forms. You can modify renderMessage to -- achieve customized and internationalized form validation messages. @@ -135,6 +187,292 @@ instance RenderMessage UniWorX TermIdentifier where Winter -> renderMessage' $ MsgWinterTerm year where renderMessage' = renderMessage foundation ls +instance RenderMessage UniWorX StudyFieldType where + renderMessage foundation ls = \case + FieldPrimary -> renderMessage' MsgFieldPrimary + FieldSecondary -> renderMessage' MsgFieldSecondary + where renderMessage' = renderMessage foundation ls + +newtype ShortTermIdentifier = ShortTermIdentifier TermIdentifier + deriving (Eq, Ord, Read, Show) + +instance RenderMessage UniWorX ShortTermIdentifier where + renderMessage foundation ls (ShortTermIdentifier TermIdentifier{..}) = case season of + Summer -> renderMessage' $ MsgSummerTermShort year + Winter -> renderMessage' $ MsgWinterTermShort year + where renderMessage' = renderMessage foundation ls + +instance RenderMessage UniWorX String where + renderMessage f ls str = renderMessage f ls $ Text.pack str + +instance RenderMessage UniWorX SheetFileType where + renderMessage foundation ls = \case + SheetExercise -> renderMessage' MsgSheetExercise + SheetHint -> renderMessage' MsgSheetHint + SheetSolution -> renderMessage' MsgSheetSolution + SheetMarking -> renderMessage' MsgSheetMarking + where renderMessage' = renderMessage foundation ls + +instance RenderMessage UniWorX CorrectorState where + renderMessage foundation ls = \case + CorrectorNormal -> renderMessage' MsgCorrectorNormal + CorrectorMissing -> renderMessage' MsgCorrectorMissing + CorrectorExcused -> renderMessage' MsgCorrectorExcused + where renderMessage' = renderMessage foundation ls + + +instance RenderMessage UniWorX Load where + renderMessage foundation ls = \case + (Load {byTutorial=Nothing , byProportion=p}) -> renderMessage' $ MsgCorByProportionOnly p + (Load {byTutorial=Just True , byProportion=p}) -> renderMessage' $ MsgCorByProportionIncludingTutorial p + (Load {byTutorial=Just False, byProportion=p}) -> renderMessage' $ MsgCorByProportionExcludingTutorial p + where renderMessage' = renderMessage foundation ls + + +instance RenderMessage UniWorX (UnsupportedAuthPredicate (Route UniWorX)) where + renderMessage f ls (UnsupportedAuthPredicate tag route) = renderMessage f ls $ MsgUnsupportedAuthPredicate tag (show route) + + +data instance ButtonCssClass UniWorX = BCDefault | BCPrimary | BCSuccess | BCInfo | BCWarning | BCDanger | BCLink + deriving (Enum, Eq, Ord, Bounded, Read, Show) + +instance Button UniWorX SubmitButton where + label BtnSubmit = [whamlet|_{MsgBtnSubmit}|] + + cssClass BtnSubmit = BCPrimary + + +getTimeLocale' :: [Lang] -> TimeLocale +getTimeLocale' = $(timeLocaleMap [("de", "de_DE.utf8")]) + +appTZ :: TZ +appTZ = $(includeSystemTZ "Europe/Berlin") + + +-- Access Control +data AccessPredicate + = APPure (Route UniWorX -> Bool -> Reader MsgRenderer AuthResult) + | APHandler (Route UniWorX -> Bool -> Handler AuthResult) + | APDB (Route UniWorX -> Bool -> DB AuthResult) + +orAR, andAR :: MsgRenderer -> AuthResult -> AuthResult -> AuthResult +orAR _ Authorized _ = Authorized +orAR _ _ Authorized = Authorized +orAR _ AuthenticationRequired _ = AuthenticationRequired +orAR _ _ AuthenticationRequired = AuthenticationRequired +orAR mr (Unauthorized x) (Unauthorized y) = Unauthorized . render mr $ MsgUnauthorizedOr x y +-- and +andAR mr (Unauthorized x) (Unauthorized y) = Unauthorized . render mr $ MsgUnauthorizedAnd x y +andAR _ reason@(Unauthorized x) _ = reason +andAR _ _ reason@(Unauthorized x) = reason +andAR _ Authorized other = other +andAR _ AuthenticationRequired _ = AuthenticationRequired + +orAP,andAP :: AccessPredicate -> AccessPredicate -> AccessPredicate +orAP = liftAR orAR (== Authorized) +andAP = liftAR andAR (const False) + +liftAR :: (MsgRenderer -> AuthResult -> AuthResult -> AuthResult) + -> (AuthResult -> Bool) -- ^ Predicate to Short-Circuit on first argument + -> AccessPredicate -> AccessPredicate -> AccessPredicate +-- Ensure to first evaluate Pure conditions, then Handler before DB +liftAR ops sc (APPure f) (APPure g) = APPure $ \r w -> shortCircuitM sc (f r w) (g r w) . ops =<< ask +liftAR ops sc (APHandler f) (APHandler g) = APHandler $ \r w -> shortCircuitM sc (f r w) (g r w) . ops =<< getMsgRenderer +liftAR ops sc (APDB f) (APDB g) = APDB $ \r w -> shortCircuitM sc (f r w) (g r w) . ops =<< getMsgRenderer +liftAR ops sc (APPure f) apg = liftAR ops sc (APHandler $ \r w -> runReader (f r w) <$> getMsgRenderer) apg +liftAR ops sc apf apg@(APPure _) = liftAR ops sc apg apf +liftAR ops sc (APHandler f) apdb = liftAR ops sc (APDB $ \r w -> lift $ f r w) apdb +liftAR ops sc apdb apg@(APHandler _) = liftAR ops sc apg apdb + + +trueAP,falseAP :: AccessPredicate +trueAP = APPure . const . const $ return Authorized +falseAP = APPure . const . const $ Unauthorized . ($ MsgUnauthorized) . render <$> ask -- always use adminAP instead + + +adminAP :: AccessPredicate -- access for admins (of appropriate school in case of course-routes) +adminAP = APDB $ \route _ -> case route of + -- Courses: access only to school admins + CourseR tid ssh csh _ -> exceptT return return $ do + authId <- maybeExceptT AuthenticationRequired $ lift maybeAuthId + [E.Value c] <- lift . E.select . E.from $ \(course `E.InnerJoin` userAdmin) -> do + E.on $ course E.^. CourseSchool E.==. userAdmin E.^. UserAdminSchool + E.where_ $ userAdmin E.^. UserAdminUser E.==. E.val authId + E.&&. course E.^. CourseTerm E.==. E.val tid + E.&&. course E.^. CourseSchool E.==. E.val ssh + E.&&. course E.^. CourseShorthand E.==. E.val csh + return (E.countRows :: E.SqlExpr (E.Value Int64)) + guardMExceptT (c > 0) (unauthorizedI MsgUnauthorizedSchoolAdmin) + return Authorized + -- other routes: access to any admin is granted here + _other -> exceptT return return $ do + authId <- maybeExceptT AuthenticationRequired $ lift maybeAuthId + adrights <- lift $ selectFirst [UserAdminUser ==. authId] [] + guardMExceptT (isJust adrights) (unauthorizedI $ MsgUnauthorized) + return Authorized + + +knownTags :: Map (CI Text) AccessPredicate +knownTags = Map.fromList -- should not throw exceptions, i.e. no getBy404 or requireAuthId + [("free", trueAP) + ,("deprecated", APHandler $ \r _ -> do + $logWarnS "AccessControl" ("deprecated route: " <> tshow r) + addMessageI Error MsgDeprecatedRoute + allow <- appAllowDeprecated . appSettings <$> getYesod + return $ bool (Unauthorized "Deprecated Route") Authorized allow + ) + ,("lecturer", APDB $ \route _ -> case route of + CourseR tid ssh csh _ -> exceptT return return $ do + authId <- maybeExceptT AuthenticationRequired $ lift maybeAuthId + [E.Value c] <- lift . E.select . E.from $ \(course `E.InnerJoin` lecturer) -> do + E.on $ course E.^. CourseId E.==. lecturer E.^. LecturerCourse + E.where_ $ lecturer E.^. LecturerUser E.==. E.val authId + E.&&. course E.^. CourseTerm E.==. E.val tid + E.&&. course E.^. CourseSchool E.==. E.val ssh + E.&&. course E.^. CourseShorthand E.==. E.val csh + return (E.countRows :: E.SqlExpr (E.Value Int64)) + guardMExceptT (c>0) (unauthorizedI MsgUnauthorizedLecturer) + return Authorized + _ -> exceptT return return $ do + authId <- maybeExceptT AuthenticationRequired $ lift maybeAuthId + void . maybeMExceptT (unauthorizedI MsgUnauthorizedSchoolLecturer) $ selectFirst [UserLecturerUser ==. authId] [] + return Authorized + ) + ,("corrector", APDB $ \route _ -> exceptT return return $ do + authId <- maybeExceptT AuthenticationRequired $ lift maybeAuthId + resList <- lift . E.select . E.from $ \(course `E.InnerJoin` sheet `E.InnerJoin` sheetCorrector) -> do + E.on $ sheetCorrector E.^. SheetCorrectorSheet E.==. sheet E.^. SheetId + E.on $ sheet E.^. SheetCourse E.==. course E.^. CourseId + E.where_ $ sheetCorrector E.^. SheetCorrectorUser E.==. E.val authId + return (course E.^. CourseId, sheet E.^. SheetId) + let + resMap :: Map CourseId (Set SheetId) + resMap = Map.fromListWith Set.union [ (cid, Set.singleton sid) | (E.Value cid, E.Value sid) <- resList ] + case route of + CSubmissionR _ _ _ _ cID _ -> maybeT (unauthorizedI MsgUnauthorizedSubmissionCorrector) $ do + sid <- catchIfMaybeT (const True :: CryptoIDError -> Bool) $ decrypt cID + Submission{..} <- MaybeT . lift $ get sid + guard $ maybe False (== authId) submissionRatingBy + return Authorized + CSheetR tid ssh csh shn _ -> maybeT (unauthorizedI MsgUnauthorizedSheetCorrector) $ do + Entity cid _ <- MaybeT . lift . getBy $ TermSchoolCourseShort tid ssh csh + Entity sid _ <- MaybeT . lift . getBy $ CourseSheet cid shn + guard $ sid `Set.member` fromMaybe Set.empty (resMap !? cid) + return Authorized + CourseR tid ssh csh _ -> maybeT (unauthorizedI MsgUnauthorizedCorrector) $ do + Entity cid _ <- MaybeT . lift . getBy $ TermSchoolCourseShort tid ssh csh + guard $ cid `Set.member` Map.keysSet resMap + return Authorized + _ -> do + guardMExceptT (not $ Map.null resMap) (unauthorizedI MsgUnauthorizedCorrectorAny) + return Authorized + ) + ,("time", APDB $ \route _ -> case route of + CSheetR tid ssh csh shn subRoute -> maybeT (unauthorizedI MsgUnauthorizedSheetTime) $ do + Entity cid _ <- MaybeT . getBy $ TermSchoolCourseShort tid ssh csh + Entity _sid Sheet{..} <- MaybeT . getBy $ CourseSheet cid shn + cTime <- liftIO getCurrentTime + let + visible = NTop sheetVisibleFrom <= NTop (Just cTime) + active = sheetActiveFrom <= cTime && cTime <= sheetActiveTo + + guard visible + + case subRoute of + SFileR SheetExercise _ -> guard $ sheetActiveFrom <= cTime + SFileR SheetHint _ -> guard $ maybe False (<= cTime) sheetHintFrom + SFileR SheetSolution _ -> guard $ maybe False (<= cTime) sheetSolutionFrom + SubmissionNewR -> guard active + SubmissionR _ _ -> guard active + _ -> return () + + return Authorized + + CourseR tid ssh csh CRegisterR -> maybeT (unauthorizedI MsgUnauthorizedCourseTime) $ do + Entity cid Course{..} <- MaybeT . getBy $ TermSchoolCourseShort tid ssh csh + cTime <- (NTop . Just) <$> liftIO getCurrentTime + guard $ NTop courseRegisterFrom <= cTime + && NTop courseRegisterTo >= cTime + return Authorized + + r -> $unsupportedAuthPredicate "time" r + ) + ,("registered", APDB $ \route _ -> case route of + CourseR tid ssh csh _ -> exceptT return return $ do + authId <- maybeExceptT AuthenticationRequired $ lift maybeAuthId + [E.Value c] <- lift . E.select . E.from $ \(course `E.InnerJoin` courseParticipant) -> do + E.on $ course E.^. CourseId E.==. courseParticipant E.^. CourseParticipantCourse + E.where_ $ courseParticipant E.^. CourseParticipantUser E.==. E.val authId + E.&&. course E.^. CourseTerm E.==. E.val tid + E.&&. course E.^. CourseSchool E.==. E.val ssh + E.&&. course E.^. CourseShorthand E.==. E.val csh + return (E.countRows :: E.SqlExpr (E.Value Int64)) + guardMExceptT (c > 0) (unauthorizedI MsgUnauthorizedParticipant) + return Authorized + r -> $unsupportedAuthPredicate "registered" r + ) + ,("capacity", APDB $ \route _ -> case route of + CourseR tid ssh csh _ -> maybeT (unauthorizedI MsgCourseNoCapacity) $ do + Entity cid Course{..} <- MaybeT . getBy $ TermSchoolCourseShort tid ssh csh + registered <- lift $ fromIntegral <$> count [ CourseParticipantCourse ==. cid ] + guard $ NTop courseCapacity > NTop (Just registered) + return Authorized + r -> $unsupportedAuthPredicate "capacity" r + ) + ,("materials", APDB $ \route _ -> case route of + CourseR tid ssh csh _ -> maybeT (unauthorizedI MsgUnfreeMaterials) $ do + Entity _ Course{..} <- MaybeT . getBy $ TermSchoolCourseShort tid ssh csh + guard courseMaterialFree + return Authorized + r -> $unsupportedAuthPredicate "materials" r + ) + ,("owner", APDB $ \route _ -> case route of + CSubmissionR _ _ _ _ cID _ -> exceptT return return $ do + sid <- catchIfMExceptT (const $ unauthorizedI MsgUnauthorizedSubmissionOwner) (const True :: CryptoIDError -> Bool) $ decrypt cID + authId <- maybeExceptT AuthenticationRequired $ lift maybeAuthId + void . maybeMExceptT (unauthorizedI MsgUnauthorizedSubmissionOwner) . getBy $ UniqueSubmissionUser authId sid + return Authorized + r -> $unsupportedAuthPredicate "owner" r + ) + ,("rated", APDB $ \route _ -> case route of + CSubmissionR _ _ _ _ cID _ -> maybeT (unauthorizedI MsgUnauthorizedSubmissionRated) $ do + sid <- catchIfMaybeT (const True :: CryptoIDError -> Bool) $ decrypt cID + sub <- MaybeT $ get sid + guard $ submissionRatingDone sub + return Authorized + r -> $unsupportedAuthPredicate "rated" r + ) + ,("isRead", APHandler . const $ bool (return Authorized) (unauthorizedI MsgUnauthorizedWrite)) + ,("isWrite", APHandler . const $ bool (unauthorizedI MsgUnauthorized) (return Authorized)) + ] + + +tag2ap :: Text -> AccessPredicate +tag2ap t = case Map.lookup (CI.mk t) knownTags of + (Just acp) -> acp + Nothing -> APHandler $ \_route _isWrite -> do -- Can this be pure like falseAP? GK: not if we want to log a message (which we definitely should) + $logWarnS "AccessControl" $ "'" <> t <> "' not known to access control" + unauthorizedI MsgUnauthorized + +route2ap :: Route UniWorX -> AccessPredicate +route2ap r = foldr orAP adminAP attrsAND -- adminAP causes all to be in DB!!! GK: Due to shortCircuitM this (while still true) is no longer costly (we do a `runDB` but then only actually send off queries, if needed) + where + attrsAND = map splitAND $ Set.toList $ routeAttrs r + splitAND = foldr1 andAP . map tag2ap . Text.splitOn "AND" + +evalAccessDB :: Route UniWorX -> Bool -> DB AuthResult -- all requests, regardless of POST/GET, use isWriteRequest otherwise +evalAccessDB r w = case route2ap r of + (APPure p) -> lift $ runReader (p r w) <$> getMsgRenderer + (APHandler p) -> lift $ p r w + (APDB p) -> p r w + +evalAccess :: Route UniWorX -> Bool -> Handler AuthResult +evalAccess r w = case route2ap r of + (APPure p) -> runReader (p r w) <$> getMsgRenderer + (APHandler p) -> p r w + (APDB p) -> runDB $ p r w + + -- Please see the documentation for the Yesod typeclass. There are a number -- of settings which can be configured by overriding methods here. @@ -152,6 +490,8 @@ instance Yesod UniWorX where 120 -- timeout in minutes "client_session_key.aes" + maximumContentLength _ _ = Just $ 50 * 2^20 + -- Yesod Middleware allows you to run code before and after each handler function. -- The defaultYesodMiddleware adds the response header "Vary: Accept, Accept-Language" and performs authorization checks. -- Some users may also want to add the defaultCsrfMiddleware, which: @@ -159,36 +499,50 @@ instance Yesod UniWorX where -- b) Validates that incoming write requests include that token in either a header or POST parameter. -- To add it, chain it together with the defaultMiddleware: yesodMiddleware = defaultYesodMiddleware . defaultCsrfMiddleware -- For details, see the CSRF documentation in the Yesod.Core.Handler module of the yesod-core package. - yesodMiddleware handler = do - res <- defaultYesodMiddleware handler - void . runMaybeT $ do - route@(routeAttrs -> attrs) <- MaybeT getCurrentRoute - case route of - CourseR tid csh _ | "updateFavourite" `elem` attrs -> do - uid <- MaybeT maybeAuthId - $(logDebug) "Favourites save" - now <- liftIO $ getCurrentTime - void . lift . runDB . runMaybeT $ do - cid <- MaybeT . getKeyBy $ CourseTermShort tid csh - user <- MaybeT $ get uid - -- update Favourites - lift $ upsertBy - (UniqueCourseFavourite uid cid) - (CourseFavourite uid now cid) - [CourseFavouriteTime =. now] - -- prune Favourites to user-defined size - oldFavs <- lift $ selectKeysList - [ CourseFavouriteUser ==. uid] - [ Desc CourseFavouriteTime - , OffsetBy $ userMaxFavourites user - ] - lift $ mapM delete oldFavs + yesodMiddleware = defaultYesodMiddleware . normalizeRouteMiddleware . defaultCsrfMiddleware . updateFavouritesMiddleware + where + updateFavouritesMiddleware :: Handler a -> Handler a + updateFavouritesMiddleware handler = (*> handler) . runMaybeT $ do + route <- MaybeT getCurrentRoute + case route of -- update Course Favourites here + CourseR tid ssh csh _ -> do + void . lift . runDB . runMaybeT $ do + guardM . lift $ (== Authorized) <$> evalAccessDB (CourseR tid ssh csh CShowR) False + $logDebugS "updateFavourites" "Updating favourites" - _other -> return () - return res + now <- liftIO $ getCurrentTime + uid <- MaybeT $ liftHandlerT maybeAuthId + cid <- MaybeT . getKeyBy $ TermSchoolCourseShort tid ssh csh + user <- MaybeT $ get uid + let courseFavourite = CourseFavourite uid now cid + + $logDebugS "updateFavourites" [st|Updating/Inserting: #{tshow courseFavourite}|] + -- update Favourites + void . lift $ upsertBy + (UniqueCourseFavourite uid cid) + courseFavourite + [CourseFavouriteTime =. now] + -- prune Favourites to user-defined size + oldFavs <- lift $ selectKeysList + [ CourseFavouriteUser ==. uid] + [ Desc CourseFavouriteTime + , OffsetBy $ userMaxFavourites user + ] + lift . forM_ oldFavs $ \fav -> do + $logDebugS "updateFavourites" "Deleting old favourite." + delete fav + _other -> return () + normalizeRouteMiddleware :: Handler a -> Handler a + normalizeRouteMiddleware handler = (*> handler) . runMaybeT $ do + route <- MaybeT getCurrentRoute + (route', getAny -> changed) <- lift . runDB . runWriterT $ foldM (&) route routeNormalizers + when changed $ do + $logDebugS "normalizeRouteMiddleware" [st|Redirecting to #{tshow route'}|] + redirectWith movedPermanently301 route' defaultLayout widget = do master <- getYesod + let AppSettings { appUserDefaults = UserDefaultConf{..}, .. } = appSettings master mmsgs <- getMessages mcurrentRoute <- getCurrentRoute @@ -196,78 +550,94 @@ instance Yesod UniWorX where -- Get the breadcrumbs, as defined in the YesodBreadcrumbs instance. (title, parents) <- breadcrumbs - let - menu = defaultLinks ++ maybe [] pageActions mcurrentRoute +-- let isParent :: Route UniWorX -> Bool +-- isParent r = r == (fst parents) + + let menu = defaultLinks ++ maybe [] pageActions mcurrentRoute menuTypes <- filterM (menuItemAccessCallback . menuItem) menu - -- Lookup Favourites if possible - favourites' <- do - muid <- maybeAuthId - case muid of - Nothing -> return [] - (Just uid) -> runDB . E.select . E.from $ \(course `E.InnerJoin` courseFavourite) -> do - E.on (course E.^. CourseId E.==. courseFavourite E.^. CourseFavouriteCourse) - E.where_ (courseFavourite E.^. CourseFavouriteUser E.==. E.val uid) - E.orderBy [ E.asc $ course E.^. CourseShorthand ] - return course + isAuth <- isJust <$> maybeAuthId + -- Lookup Favourites & Theme if possible -- TODO: cache this info in a cookie?! + (favourites', currentTheme) <- do + muid <- maybeAuthPair + case muid of + Nothing -> return ([],userDefaultTheme) + (Just (uid,user)) -> do + favs <- runDB $ E.select . E.from $ \(course `E.InnerJoin` courseFavourite) -> do + E.on (course E.^. CourseId E.==. courseFavourite E.^. CourseFavouriteCourse) + E.where_ (courseFavourite E.^. CourseFavouriteUser E.==. E.val uid) + E.orderBy [ E.asc $ course E.^. CourseShorthand ] + return course + return (favs, userTheme user) favourites <- forM favourites' $ \(Entity _ c@Course{..}) -> let - courseRoute = CourseR courseTermId courseShorthand CourseShowR + courseRoute = CourseR courseTerm courseSchool courseShorthand CShowR in (c, courseRoute, ) <$> filterM (menuItemAccessCallback . menuItem) (pageActions courseRoute) + let highlight :: Route UniWorX -> Bool -- highlight last route in breadcrumbs, favorites taking priority + highlight = let crumbs = mcons mcurrentRoute $ fst <$> reverse parents + navItems = map snd3 favourites ++ map (menuItemRoute . menuItem) menuTypes + highR = find (`elem` navItems) . uncurry (++) $ partition (`elem` map snd3 favourites) crumbs + in \r -> Just r == highR + favouriteTerms :: [TermIdentifier] + favouriteTerms = Set.toDescList $ foldMap (\(Course{..}, _, _) -> Set.singleton $ unTermKey courseTerm) favourites + favouriteTerm :: TermIdentifier -> [(Course, Route UniWorX, [MenuTypes])] + favouriteTerm tid = filter (\(Course{..}, _, _) -> unTermKey courseTerm == tid) favourites + -- We break up the default layout into two components: -- default-layout is the contents of the body tag, and -- default-layout-wrapper is the entire page. Since the final -- value passed to hamletToRepHtml cannot be a widget, this allows -- you to use normal widget features in default-layout. + let navbar :: Widget navbar = $(widgetFile "widgets/navbar") asidenav :: Widget asidenav = $(widgetFile "widgets/asidenav") + contentHeadline :: Maybe Widget + contentHeadline = pageHeading =<< mcurrentRoute breadcrumbs :: Widget breadcrumbs = $(widgetFile "widgets/breadcrumbs") pageactionprime :: Widget - pageactionprime = $(widgetFile "widgets/pageactionprime") - -- functions to determine if there are page-actions + pageactionprime = $(widgetFile "widgets/pageactionprime") -- TODO: rename, since it also shows secondary pageActions now + -- functions to determine if there are page-actions (primary or secondary) isPageActionPrime :: MenuTypes -> Bool - isPageActionPrime (PageActionPrime _) = True + isPageActionPrime (PageActionPrime _) = True + isPageActionPrime (PageActionSecondary _) = True isPageActionPrime _ = False hasPageActions :: Bool hasPageActions = any isPageActionPrime menuTypes pc <- widgetToPageContent $ do - addStylesheetRemote "https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,600,800,900" + addStylesheetRemote "https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,600,800,900|Roboto:300,400,600" + addScript $ StaticR js_zepto_js + addScript $ StaticR js_fetchPolyfill_js + addScript $ StaticR js_urlPolyfill_js addScript $ StaticR js_featureChecker_js + addScript $ StaticR js_flatpickr_js + addScript $ StaticR js_tabber_js + addStylesheet $ StaticR css_flatpickr_css + addStylesheet $ StaticR css_tabber_css addStylesheet $ StaticR css_fonts_css - addStylesheet $ StaticR css_icons_css + addStylesheet $ StaticR css_fontawesome_css $(widgetFile "default-layout") $(widgetFile "standalone/modal") $(widgetFile "standalone/showHide") $(widgetFile "standalone/inputs") + $(widgetFile "standalone/tooltip") + $(widgetFile "standalone/tabber") + $(widgetFile "standalone/alerts") + $(widgetFile "standalone/datepicker") withUrlRenderer $(hamletFile "templates/default-layout-wrapper.hamlet") - + -- The page to be redirected to when authentication is required. authRoute _ = Just $ AuthR LoginR - isAuthorized (AuthR _) _ = return Authorized - isAuthorized HomeR _ = return Authorized - isAuthorized FaviconR _ = return Authorized - isAuthorized RobotsR _ = return Authorized - isAuthorized (StaticR _) _ = return Authorized - isAuthorized ProfileR _ = isAuthenticated - isAuthorized TermShowR _ = return Authorized - isAuthorized CourseListR _ = return Authorized - isAuthorized (CourseListTermR _) _ = return Authorized - isAuthorized (CourseR _ _ CourseShowR) _ = return Authorized - isAuthorized (CryptoUUIDDispatchR _) _ = return Authorized - isAuthorized SubmissionListR _ = isAuthenticated - isAuthorized SubmissionDownloadMultiArchiveR _ = isAuthenticated --- isAuthorized TestR _ = return Authorized - isAuthorized route isWrite = runDB $ isAuthorizedDB route isWrite + isAuthorized = evalAccess -- This function creates static content files in the static folder -- and names them based on a hash of their content. This allows @@ -301,212 +671,461 @@ instance Yesod UniWorX where -- What messages should be logged. The following includes all messages when -- in development, and warnings and errors in production. - shouldLog app _source level = - appShouldLogAll (appSettings app) - || level == LevelWarn - || level == LevelError + shouldLog app _source level = appShouldLogAll (appSettings app) || level >= appMinimumLogLevel (appSettings app) makeLogger = return . appLogger -isAuthorizedDB :: Route UniWorX -> Bool -> YesodDB UniWorX AuthResult -isAuthorizedDB route@(routeAttrs -> attrs) writeable - | "adminAny" `member` attrs = adminAccess Nothing - | "lecturerAny" `member` attrs = lecturerAccess Nothing - - - -isAuthorizedDB UsersR _ = adminAccess Nothing -isAuthorizedDB (SubmissionR cID) _ = submissionAccess $ Right cID -isAuthorizedDB (SubmissionDownloadSingleR cID _) _ = submissionAccess $ Right cID -isAuthorizedDB (SubmissionDownloadArchiveR (splitExtension -> (baseName, _))) _ = submissionAccess . Left . CryptoID $ CI.mk baseName -isAuthorizedDB TermEditR _ = adminAccess Nothing -isAuthorizedDB (TermEditExistR _) _ = adminAccess Nothing -isAuthorizedDB CourseNewR _ = lecturerAccess Nothing -isAuthorizedDB (CourseR t c CourseEditR) _ = courseLecturerAccess . entityKey =<< getBy404 (CourseTermShort t c) -isAuthorizedDB (CourseR t c (SheetR SheetListR)) False = return Authorized -- -isAuthorizedDB (CourseR t c (SheetR SheetListR)) _ = courseLecturerAccess . entityKey =<< getBy404 (CourseTermShort t c) -isAuthorizedDB (CourseR t c (SheetR (SheetShowR s))) _ = return Authorized -- TODO: nur für angemeldete Kursteilnehmer falls sichtbar, sonst nur Lectrurer oder Korrektor -isAuthorizedDB (CourseR t c (SheetR (SheetFileR s _ _))) _ = return Authorized -- TODO: nur für angemeldete Kursteilnehmer falls sichtbar, sonst nur Lectrurer oder Korrektor -isAuthorizedDB (CourseR t c (SheetR SheetNewR)) _ = courseLecturerAccess . entityKey =<< getBy404 (CourseTermShort t c) -isAuthorizedDB (CourseR t c (SheetR (SheetEditR s))) _ = courseLecturerAccess . entityKey =<< getBy404 (CourseTermShort t c) -isAuthorizedDB (CourseR t c (SheetR (SheetDelR s))) _ = courseLecturerAccess . entityKey =<< getBy404 (CourseTermShort t c) -isAuthorizedDB (CourseEditIDR cID) _ = do - courseId <- decrypt cID - courseLecturerAccess courseId -isAuthorizedDB _route _isWrite = return $ Unauthorized "No access to this route." -- Calling isAuthorized here creates infinite loop! - -submissionAccess :: Either CryptoFileNameSubmission CryptoUUIDSubmission -> YesodDB UniWorX AuthResult -submissionAccess cID = do - authId <- lift requireAuthId - submissionId <- either decrypt decrypt cID - Submission{..} <- get404 submissionId - submissionUsers <- map (submissionUserUserId . entityVal) <$> selectList [SubmissionUserSubmissionId ==. submissionId] [] - let auth = authId `elem` submissionUsers || Just authId == submissionRatingBy - return $ case auth of - True -> Authorized - False -> Unauthorized "No access to this submission" - -adminAccess :: Maybe SchoolId -- ^ If @Just@, matched exactly against 'userAdminSchool' - -> YesodDB UniWorX AuthResult -adminAccess school = do - authId <- lift requireAuthId - adrights <- selectList ((UserAdminUser ==. authId) : maybe [] (\s -> [UserAdminSchool ==. s]) school) [] - return $ if (not $ null adrights) - then Authorized - else Unauthorized "No admin access" -- TODO internationalize - -lecturerAccess :: Maybe SchoolId - -> YesodDB UniWorX AuthResult -lecturerAccess school = do - authId <- lift requireAuthId - lecrights <- selectList ((UserLecturerUser ==. authId) : maybe [] (\s -> [UserLecturerSchool ==. s]) school) [] - return $ if (not $ null lecrights) - then Authorized - else Unauthorized "No lecturer access" -- TODO internationalize - -lecturerAccess' :: SchoolId -> YesodDB UniWorX AuthResult -lecturerAccess' = authorizedFor UniqueSchoolLecturer MsgUnauthorizedSchoolLecturer - -courseLecturerAccess :: CourseId -> YesodDB UniWorX AuthResult -courseLecturerAccess = authorizedFor UniqueLecturer MsgUnauthorizedLecturer - -courseCorrectorAccess :: CourseId -> YesodDB UniWorX AuthResult -courseCorrectorAccess = authorizedFor UniqueCorrector MsgUnauthorizedCorrector - -courseParticipantAccess :: CourseId -> YesodDB UniWorX AuthResult -courseParticipantAccess = authorizedFor UniqueParticipant MsgUnauthorizedParticipant - -authorizedFor :: ( PersistEntityBackend record ~ BaseBackend backend - , PersistEntity record, PersistUniqueRead backend - , YesodAuth master, RenderMessage master msg - ) - => (AuthId master -> t -> Unique record) -> msg -> t -> ReaderT backend (HandlerT master IO) AuthResult -authorizedFor authType msg courseId = do - authId <- lift requireAuthId - access <- getBy $ authType authId courseId - case access of - (Just _) -> return Authorized - Nothing -> unauthorizedI msg - -isAuthorizedDB' :: Route UniWorX -> Bool -> YesodDB UniWorX Bool -isAuthorizedDB' route isWrite = (== Authorized) <$> isAuthorizedDB route isWrite - -isAuthorized' :: Route UniWorX -> Bool -> Handler Bool -isAuthorized' route isWrite = runDB $ isAuthorizedDB' route isWrite -- Define breadcrumbs. instance YesodBreadcrumbs UniWorX where - breadcrumb TermShowR = return ("Semester", Just HomeR) - breadcrumb TermEditR = return ("Neu", Just TermShowR) - breadcrumb (TermEditExistR _) = return ("Editieren", Just TermShowR) + breadcrumb (AuthR _) = return ("Login" , Just HomeR) + breadcrumb HomeR = return ("Uni2work", Nothing) + breadcrumb UsersR = return ("Benutzer", Just HomeR) + breadcrumb AdminTestR = return ("Test" , Just HomeR) + breadcrumb (AdminUserR _) = return ("Users" , Just UsersR) + breadcrumb VersionR = return ("Impressum" , Just HomeR) - breadcrumb CourseListR = return ("Kurs", Just HomeR) - breadcrumb (CourseListTermR term) = return (toPathPiece term, Just TermShowR) - breadcrumb (CourseR term course CourseShowR) = return (course, Just $ CourseListTermR term) - breadcrumb CourseNewR = return ("Neu", Just CourseListR) - breadcrumb (CourseR _ _ CourseEditR) = return ("Editieren", Just CourseListR) + breadcrumb ProfileR = return ("Profile" , Just HomeR) + breadcrumb ProfileDataR = return ("Data" , Just ProfileR) - breadcrumb (CourseR tid csh (SheetR SheetListR)) = return ("Übungen",Just $ CourseR tid csh CourseShowR) - breadcrumb (CourseR tid csh (SheetR SheetNewR )) = return ("Neu", Just $ CourseR tid csh $ SheetR SheetListR) - breadcrumb (CourseR tid csh (SheetR (SheetShowR shn))) = return (shn, Just $ CourseR tid csh $ SheetR SheetListR) - breadcrumb (CourseR tid csh (SheetR (SheetEditR shn))) = return ("Edit", Just $ CourseR tid csh $ SheetR $ SheetShowR shn) - breadcrumb (CourseR tid csh (SheetR (SheetDelR shn))) = return ("DELETE", Just $ CourseR tid csh $ SheetR $ SheetShowR shn) + breadcrumb TermShowR = return ("Semester" , Just HomeR) + breadcrumb TermCurrentR = return ("Aktuell" , Just TermShowR) + breadcrumb TermEditR = return ("Neu" , Just TermCurrentR) + breadcrumb (TermEditExistR tid) = return ("Editieren" , Just $ TermCourseListR tid) + breadcrumb (TermCourseListR (unTermKey -> tid)) = getMessageRender <&> \mr -> (mr $ ShortTermIdentifier tid, Nothing) - breadcrumb SubmissionListR = return ("Abgaben", Just HomeR) - breadcrumb (SubmissionR _) = return ("Abgabe", Just SubmissionListR) + breadcrumb (TermSchoolCourseListR tid ssh) = return (CI.original $ unSchoolKey ssh, Just $ TermCourseListR tid) + + breadcrumb CourseListR = return ("Kurse" , Just HomeR) + breadcrumb CourseNewR = return ("Neu" , Just CourseListR) + breadcrumb (CourseR tid ssh csh CShowR) = return (CI.original csh, Just $ TermSchoolCourseListR tid ssh) + -- (CourseR tid ssh csh CRegisterR) -- is POST only + breadcrumb (CourseR tid ssh csh CEditR) = return ("Editieren", Just $ CourseR tid ssh csh CShowR) + breadcrumb (CourseR tid ssh csh CCorrectionsR) = return ("Abgaben",Just $ CourseR tid ssh csh CShowR) + breadcrumb (CourseR tid ssh csh SheetListR) = return ("Übungen" , Just $ CourseR tid ssh csh CShowR) + breadcrumb (CourseR tid ssh csh SheetNewR ) = return ("Neu", Just $ CourseR tid ssh csh SheetListR) + + breadcrumb (CSheetR tid ssh csh shn SShowR) = return (CI.original shn, Just $ CourseR tid ssh csh SheetListR) + breadcrumb (CSheetR tid ssh csh shn SEditR) = return ("Edit", Just $ CSheetR tid ssh csh shn SShowR) + breadcrumb (CSheetR tid ssh csh shn SDelR ) = return ("DELETE", Just $ CSheetR tid ssh csh shn SShowR) + breadcrumb (CSheetR tid ssh csh shn SSubsR) = return ("Abgaben", Just $ CSheetR tid ssh csh shn SShowR) + breadcrumb (CSheetR tid ssh csh shn SubmissionNewR) = return ("Abgabe", Just $ CSheetR tid ssh csh shn SShowR) + breadcrumb (CSheetR tid ssh csh shn SubmissionOwnR) = return ("Abgabe", Just $ CSheetR tid ssh csh shn SShowR) + breadcrumb (CSubmissionR tid ssh csh shn _ SubShowR) = return ("Abgabe", Just $ CSheetR tid ssh csh shn SShowR) +-- (CSubmissionR tid ssh csh shn _ SubArchiveR) -- just for Download + breadcrumb (CSubmissionR tid ssh csh shn cid CorrectionR) = return ("Korrektur", Just $ CSubmissionR tid ssh csh shn cid SubShowR) +-- (CSubmissionR tid ssh csh shn _ SubDownloadR) -- just for Download + breadcrumb (CSheetR tid ssh csh shn SCorrR) = return ("Korrektoren", Just $ CSheetR tid ssh csh shn SShowR) + -- (CSheetR tid ssh csh shn SFileR) -- just for Downloads + -- Others + breadcrumb (CorrectionsR) = return ("Korrekturen", Just HomeR) + breadcrumb (CorrectionsUploadR) = return ("Hochladen", Just CorrectionsR) + breadcrumb _ = return ("Uni2work", Nothing) -- Default is no breadcrumb at all + +submissionList :: TermId -> CourseShorthand -> SheetName -> UserId -> DB [E.Value SubmissionId] +submissionList tid csh shn uid = E.select . E.from $ \(course `E.InnerJoin` sheet `E.InnerJoin` submission `E.InnerJoin` submissionUser) -> do + E.on $ submissionUser E.^. SubmissionUserSubmission E.==. submission E.^. SubmissionId + E.on $ submission E.^. SubmissionSheet E.==. sheet E.^. SheetId + E.on $ sheet E.^. SheetCourse E.==. course E.^. CourseId + + E.where_ $ submissionUser E.^. SubmissionUserUser E.==. E.val uid + E.&&. sheet E.^. SheetName E.==. E.val shn + E.&&. course E.^. CourseShorthand E.==. E.val csh + E.&&. course E.^. CourseTerm E.==. E.val tid + + return $ submission E.^. SubmissionId - breadcrumb HomeR = return ("UniworkY", Nothing) - breadcrumb (AuthR _) = return ("Login", Just HomeR) - breadcrumb ProfileR = return ("Profile", Just HomeR) - breadcrumb _ = return ("home", Nothing) -pageActions :: Route UniWorX -> [MenuTypes] -pageActions (CourseR tid csh CourseShowR) = - [ PageActionPrime $ MenuItem - { menuItemLabel = "Übungsblätter" - , menuItemIcon = Nothing - , menuItemRoute = CSheetR tid csh SheetListR - , menuItemAccessCallback' = return True - } - ] -pageActions (CourseR tid csh (SheetR SheetListR)) = - [ PageActionPrime $ MenuItem - { menuItemLabel = "Neues Übungsblatt" - , menuItemIcon = Nothing - , menuItemRoute = CSheetR tid csh SheetNewR - , menuItemAccessCallback' = return True - } - ] -pageActions TermShowR = - [ PageActionPrime $ MenuItem - { menuItemLabel = "Neues Semester" - , menuItemIcon = Nothing - , menuItemRoute = TermEditR - , menuItemAccessCallback' = return True - } - ] -pageActions (CourseListTermR _) = - [ PageActionPrime $ MenuItem - { menuItemLabel = "Neuer Kurs" - , menuItemIcon = Just "book" - , menuItemRoute = CourseNewR - , menuItemAccessCallback' = return True - } - ] -pageActions _ = [] defaultLinks :: [MenuTypes] defaultLinks = -- Define the menu items of the header. - [ NavbarRight $ MenuItem + [ NavbarAside $ MenuItem { menuItemLabel = "Home" , menuItemIcon = Just "home" , menuItemRoute = HomeR , menuItemAccessCallback' = return True } , NavbarRight $ MenuItem - { menuItemLabel = "Profile" - , menuItemIcon = Just "profile" + { menuItemLabel = "Impressum" + , menuItemIcon = Just "book" + , menuItemRoute = VersionR + , menuItemAccessCallback' = return True + } + , NavbarRight $ MenuItem + { menuItemLabel = "Profil" + , menuItemIcon = Just "cogs" , menuItemRoute = ProfileR , menuItemAccessCallback' = isJust <$> maybeAuthPair } , NavbarSecondary $ MenuItem { menuItemLabel = "Login" - , menuItemIcon = Just "login" + , menuItemIcon = Just "sign-in-alt" , menuItemRoute = AuthR LoginR , menuItemAccessCallback' = isNothing <$> maybeAuthPair } , NavbarSecondary $ MenuItem { menuItemLabel = "Logout" - , menuItemIcon = Just "logout" + , menuItemIcon = Just "sign-out-alt" , menuItemRoute = AuthR LogoutR , menuItemAccessCallback' = isJust <$> maybeAuthPair } , NavbarAside $ MenuItem - { menuItemLabel = "Aktuelle Veranstaltungen" - , menuItemIcon = Just "book" - , menuItemRoute = CourseListR -- should be CourseListActiveR or similar in the future - , menuItemAccessCallback' = return True - } - , NavbarAside $ MenuItem - { menuItemLabel = "Alte Veranstaltungen" - , menuItemIcon = Just "book" - , menuItemRoute = CourseListR -- should be CourseListInactiveR or similar in the future - , menuItemAccessCallback' = return True - } - , NavbarAside $ MenuItem - { menuItemLabel = "Veranstaltungen" - , menuItemIcon = Just "book" + { menuItemLabel = "Kurse" + , menuItemIcon = Just "calendar-alt" , menuItemRoute = CourseListR , menuItemAccessCallback' = return True } + , NavbarAside $ MenuItem + { menuItemLabel = "Semester" + , menuItemIcon = Just "graduation-cap" + , menuItemRoute = TermShowR + , menuItemAccessCallback' = return True + } + , NavbarAside $ MenuItem + { menuItemLabel = "Korrekturen" + , menuItemIcon = Just "check" + , menuItemRoute = CorrectionsR + , menuItemAccessCallback' = return True + } , NavbarAside $ MenuItem { menuItemLabel = "Benutzer" - , menuItemIcon = Just "user" + , menuItemIcon = Just "users" , menuItemRoute = UsersR , menuItemAccessCallback' = return True -- Creates a LOOP: (Authorized ==) <$> isAuthorized UsersR False } ] + +pageActions :: Route UniWorX -> [MenuTypes] +{- + Icons: https://fontawesome.com/icons?d=gallery + Guideline: use icons without boxes/frames, only non-pro + + Please keep sorted according to routes +-} +pageActions (HomeR) = + [ +-- NavbarAside $ MenuItem +-- { menuItemLabel = "Benutzer" +-- , menuItemIcon = Just "users" +-- , menuItemRoute = UsersR +-- , menuItemAccessCallback' = return True +-- } +-- , + NavbarAside $ MenuItem + { menuItemLabel = "AdminDemo" + , menuItemIcon = Just "screwdriver" + , menuItemRoute = AdminTestR + , menuItemAccessCallback' = return True + } + ] +pageActions (ProfileR) = + [ PageActionPrime $ MenuItem + { menuItemLabel = "Gespeicherte Daten anzeigen" + , menuItemIcon = Just "book" + , menuItemRoute = ProfileDataR + , menuItemAccessCallback' = return True + } + ] +pageActions TermShowR = + [ PageActionPrime $ MenuItem + { menuItemLabel = "Neues Semester anlegen" + , menuItemIcon = Nothing + , menuItemRoute = TermEditR + , menuItemAccessCallback' = return True + } + ] +pageActions (TermCourseListR tid) = + [ PageActionPrime $ MenuItem + { menuItemLabel = "Neuen Kurs anlegen" + , menuItemIcon = Just "book" + , menuItemRoute = CourseNewR + , menuItemAccessCallback' = return True + } + , PageActionPrime $ MenuItem + { menuItemLabel = "Semster editieren" + , menuItemIcon = Nothing + , menuItemRoute = TermEditExistR tid + , menuItemAccessCallback' = return True + } + ] +pageActions (CourseListR) = + [ PageActionPrime $ MenuItem + { menuItemLabel = "Neuen Kurs anlegen" + , menuItemIcon = Just "book" + , menuItemRoute = CourseNewR + , menuItemAccessCallback' = return True + } + ] +pageActions (CourseR tid ssh csh CShowR) = + [ PageActionPrime $ MenuItem + { menuItemLabel = "Übungsblätter" + , menuItemIcon = Nothing + , menuItemRoute = CourseR tid ssh csh SheetListR + , menuItemAccessCallback' = do --TODO always show for lecturer + let sheetRouteAccess shn = (== Authorized) <$> (isAuthorized (CSheetR tid ssh csh shn SShowR) False) + muid <- maybeAuthId + (sheets,lecturer) <- runDB $ do + cid <- getKeyBy404 $ TermSchoolCourseShort tid ssh csh + sheets <- map (sheetName.entityVal) <$> selectList [SheetCourse ==. cid] [Desc SheetActiveFrom] + lecturer <- case muid of + Nothing -> return False + (Just uid) -> existsBy $ UniqueLecturer uid cid + return (sheets,lecturer) + or2M (return lecturer) $ anyM sheets sheetRouteAccess + } + , PageActionPrime $ MenuItem + { menuItemLabel = "Abgaben" + , menuItemIcon = Nothing + , menuItemRoute = CourseR tid ssh csh CCorrectionsR + , menuItemAccessCallback' = return True + } + , PageActionPrime $ MenuItem + { menuItemLabel = "Neues Übungsblatt anlegen" + , menuItemIcon = Nothing + , menuItemRoute = CourseR tid ssh csh SheetNewR + , menuItemAccessCallback' = return True + } + , PageActionSecondary $ MenuItem + { menuItemLabel = "Kurs editieren" + , menuItemIcon = Nothing + , menuItemRoute = CourseR tid ssh csh CEditR + , menuItemAccessCallback' = return True + } + , PageActionSecondary $ MenuItem + { menuItemLabel = "Neuen Kurs klonen" + , menuItemIcon = Nothing + , menuItemRoute = CourseNewTemplateR (Just tid) (Just ssh) (Just csh) + , menuItemAccessCallback' = return True + } + ] +pageActions (CourseR tid ssh csh SheetListR) = + [ PageActionPrime $ MenuItem + { menuItemLabel = "Neues Übungsblatt anlegen" + , menuItemIcon = Nothing + , menuItemRoute = CourseR tid ssh csh SheetNewR + , menuItemAccessCallback' = return True + } + ] +pageActions (CSheetR tid ssh csh shn SShowR) = + [ PageActionPrime $ MenuItem + { menuItemLabel = "Abgabe anlegen" + , menuItemIcon = Nothing + , menuItemRoute = CSheetR tid ssh csh shn SubmissionNewR + , menuItemAccessCallback' = runDB . maybeT (return False) $ do + uid <- MaybeT $ liftHandlerT maybeAuthId + submissions <- lift $ submissionList tid csh shn uid + guard $ null submissions + return True + } + , PageActionPrime $ MenuItem + { menuItemLabel = "Abgabe ansehen" + , menuItemIcon = Nothing + , menuItemRoute = CSheetR tid ssh csh shn SubmissionOwnR + , menuItemAccessCallback' = runDB . maybeT (return False) $ do + uid <- MaybeT $ liftHandlerT maybeAuthId + submissions <- lift $ submissionList tid csh shn uid + guard . not $ null submissions + return True + } + , PageActionPrime $ MenuItem + { menuItemLabel = "Korrektoren" + , menuItemIcon = Nothing + , menuItemRoute = CSheetR tid ssh csh shn SCorrR + , menuItemAccessCallback' = return True + } + , PageActionPrime $ MenuItem + { menuItemLabel = "Abgaben" + , menuItemIcon = Nothing + , menuItemRoute = CSheetR tid ssh csh shn SSubsR + , menuItemAccessCallback' = return True + } + , PageActionPrime $ MenuItem + { menuItemLabel = "Blatt Editieren" + , menuItemIcon = Nothing + , menuItemRoute = CSheetR tid ssh csh shn SEditR + , menuItemAccessCallback' = return True + } + ] +pageActions (CSheetR tid ssh csh shn SSubsR) = + [ PageActionPrime $ MenuItem + { menuItemLabel = "Korrektoren" + , menuItemIcon = Nothing + , menuItemRoute = CSheetR tid ssh csh shn SCorrR + , menuItemAccessCallback' = return True + } + ] +pageActions (CSubmissionR tid ssh csh shn cid SubShowR) = + [ PageActionPrime $ MenuItem + { menuItemLabel = "Korrektur" + , menuItemIcon = Nothing + , menuItemRoute = CSubmissionR tid ssh csh shn cid CorrectionR + , menuItemAccessCallback' = return True + } + ] +pageActions (CSheetR tid ssh csh shn SCorrR) = + [ PageActionPrime $ MenuItem + { menuItemLabel = "Abgaben" + , menuItemIcon = Nothing + , menuItemRoute = CSheetR tid ssh csh shn SSubsR + , menuItemAccessCallback' = return True + } + , PageActionSecondary $ MenuItem + { menuItemLabel = "Edit " <> (CI.original shn) + , menuItemIcon = Nothing + , menuItemRoute = CSheetR tid ssh csh shn SEditR + , menuItemAccessCallback' = return True + } + ] +pageActions (CorrectionsR) = + [ PageActionPrime $ MenuItem + { menuItemLabel = "Korrekturen hochladen" + , menuItemIcon = Nothing + , menuItemRoute = CorrectionsUploadR + , menuItemAccessCallback' = return True + } + ] +pageActions _ = [] + + +i18nHeading :: (MonadWidget m, RenderMessage site msg, HandlerSite m ~ site) => msg -> m () +i18nHeading msg = liftWidgetT $ toWidget =<< getMessageRender <*> pure msg + +pageHeading :: Route UniWorX -> Maybe Widget +pageHeading (AuthR _) + = Just $ i18nHeading MsgLoginHeading +pageHeading HomeR + = Just $ i18nHeading MsgHomeHeading +pageHeading UsersR + = Just $ i18nHeading MsgUsers +pageHeading (AdminTestR) + = Just $ [whamlet|Internal Code Demonstration Page|] +pageHeading (AdminUserR _) + = Just $ [whamlet|User Display for Admin|] +pageHeading (VersionR) + = Just $ i18nHeading MsgImpressumHeading + +pageHeading ProfileR + = Just $ i18nHeading MsgProfileHeading +pageHeading ProfileDataR + = Just $ i18nHeading MsgProfileDataHeading + +pageHeading TermShowR + = Just $ i18nHeading MsgTermsHeading +pageHeading TermCurrentR + = Just $ i18nHeading MsgTermCurrent +pageHeading TermEditR + = Just $ i18nHeading MsgTermEditHeading +pageHeading (TermEditExistR tid) + = Just $ i18nHeading $ MsgTermEditTid tid +pageHeading (TermCourseListR tid) + = Just . i18nHeading . MsgTermCourseListHeading $ tid +pageHeading (TermSchoolCourseListR tid ssh) + = Just $ do + School{schoolName=school} <- handlerToWidget $ runDB $ get404 ssh + i18nHeading $ MsgTermSchoolCourseListHeading tid school + +pageHeading (SchoolListR) + = Just $ i18nHeading MsgSchoolListHeading +pageHeading (SchoolShowR ssh) + = Just $ do + School{schoolName=school} <- handlerToWidget $ runDB $ get404 ssh + i18nHeading $ MsgSchoolHeading school + +pageHeading (CourseListR) + = Just $ i18nHeading $ MsgCourseListTitle +pageHeading CourseNewR + = Just $ i18nHeading MsgCourseNewHeading +pageHeading (CourseR tid ssh csh CShowR) + = Just $ do + Entity _ Course{..} <- handlerToWidget . runDB . getBy404 $ TermSchoolCourseShort tid ssh csh + toWidget courseName +-- (CourseR tid csh CRegisterR) -- just for POST +pageHeading (CourseR tid ssh csh CEditR) + = Just $ i18nHeading $ MsgCourseEditHeading tid ssh csh +pageHeading (CourseR tid ssh csh CCorrectionsR) + = Just $ i18nHeading $ MsgSubmissionsCourse tid ssh csh +pageHeading (CourseR tid ssh csh SheetListR) + = Just $ i18nHeading $ MsgSheetList tid ssh csh +pageHeading (CourseR tid ssh csh SheetNewR) + = Just $ i18nHeading $ MsgSheetNewHeading tid ssh csh +pageHeading (CSheetR tid ssh csh shn SShowR) + = Just $ i18nHeading $ MsgSheetTitle tid ssh csh shn +pageHeading (CSheetR tid ssh csh shn SEditR) + = Just $ i18nHeading $ MsgSheetEditHead tid ssh csh shn +pageHeading (CSheetR tid ssh csh shn SDelR) + = Just $ i18nHeading $ MsgSheetDelHead tid ssh csh shn +pageHeading (CSheetR tid ssh csh shn SSubsR) + = Just $ i18nHeading $ MsgSubmissionsSheet shn +pageHeading (CSheetR tid ssh csh shn SubmissionNewR) + = Just $ i18nHeading $ MsgSubmissionEditHead tid ssh csh shn +pageHeading (CSheetR tid ssh csh shn SubmissionOwnR) + = Just $ i18nHeading $ MsgSubmissionEditHead tid ssh csh shn +pageHeading (CSubmissionR tid ssh csh shn _ SubShowR) -- TODO: Rethink this one! + = Just $ i18nHeading $ MsgSubmissionEditHead tid ssh csh shn +-- (CSubmissionR tid csh shn cid SubArchiveR) -- just a download +pageHeading (CSubmissionR tid ssh csh shn cid CorrectionR) + = Just $ i18nHeading $ MsgCorrectionHead tid ssh csh shn cid +-- (CSubmissionR tid csh shn cid SubDownloadR) -- just a download +pageHeading (CSheetR tid ssh csh shn SCorrR) + = Just $ i18nHeading $ MsgCorrectorsHead shn +-- (CSheetR tid ssh csh shn SFileR) -- just for Downloads + +pageHeading CorrectionsR + = Just $ i18nHeading MsgCorrectionsTitle +pageHeading CorrectionsUploadR + = Just $ i18nHeading MsgCorrUpload + +-- TODO: add headings for more single course- and single term-pages +pageHeading _ + = Nothing + + +routeNormalizers :: [Route UniWorX -> WriterT Any (ReaderT (YesodPersistBackend UniWorX) (HandlerT UniWorX IO)) (Route UniWorX)] +routeNormalizers = + [ normalizeRender + , ncSchool + , ncCourse + , ncSheet + ] + where + normalizeRender route = route <$ do + YesodRequest{..} <- liftHandlerT getRequest + let original = (W.pathInfo reqWaiRequest, reqGetParams) + rendered = renderRoute route + if + | (isSuffixOf `on` fst) original rendered -> do -- FIXME: this breaks when subsite prefixes are dynamic + $logDebugS "normalizeRender" [st|#{tshow rendered} matches #{tshow original}|] + | otherwise -> do + $logDebugS "normalizeRender" [st|Redirecting because #{tshow rendered} does not match #{tshow original}|] + tell $ Any True + maybeOrig f route = maybeT (return route) $ f route + hasChanged a b + | ((/=) `on` CI.original) a b = do + $logDebugS "routeNormalizers" [st|#{tshow a} /= #{tshow b}|] + tell $ Any True + | otherwise = return () + ncSchool = maybeOrig $ \route -> do + TermSchoolCourseListR tid ssh <- return route + let schoolShort :: SchoolShorthand + schoolShort = unSchoolKey ssh + Entity ssh' _ <- MaybeT . lift . getBy $ UniqueSchoolShorthand schoolShort + (hasChanged `on` unSchoolKey)ssh ssh' + return $ TermSchoolCourseListR tid ssh' + ncCourse = maybeOrig $ \route -> do + CourseR tid ssh csh subRoute <- return route + Entity _ Course{..} <- MaybeT . lift . getBy $ TermSchoolCourseShort tid ssh csh + hasChanged csh courseShorthand + (hasChanged `on` unSchoolKey) ssh courseSchool + return $ CourseR tid courseSchool courseShorthand subRoute + ncSheet = maybeOrig $ \route -> do + CSheetR tid ssh csh shn subRoute <- return route + Entity cid _ <- MaybeT . lift . getBy $ TermSchoolCourseShort tid ssh csh + Entity _ Sheet{..} <- MaybeT . lift . getBy $ CourseSheet cid shn + hasChanged shn sheetName + return $ CSheetR tid ssh csh sheetName subRoute + + -- How to run database actions. instance YesodPersist UniWorX where type YesodPersistBackend UniWorX = SqlBackend @@ -524,89 +1143,140 @@ instance YesodAuth UniWorX where -- Override the above two destinations when a Referer: header is present redirectToReferer _ = True - authenticate creds@(Creds{..}) = runDB . fmap (either id id) . runExceptT $ do - let (userPlugin, userIdent) - | isDummy - , [dummyPlugin, dummyIdent] <- Text.splitOn ":" credsIdent - = (dummyPlugin, dummyIdent) + loginHandler = do + toParent <- getRouteToParent + lift . defaultLayout $ do + plugins <- getsYesod authPlugins + $logDebugS "Auth" $ "Enabled plugins: " <> Text.intercalate ", " (map apName plugins) + + setTitleI MsgLoginTitle + $(widgetFile "login") + + authenticate Creds{..} = runDB $ do + let + userIdent = CI.mk credsIdent + uAuth = UniqueAuthentication userIdent + + isDummy = credsPlugin == "dummy" + isPWHash = credsPlugin == "PWHash" + + excHandlers + | isDummy || isPWHash + = [ C.Handler $ \err -> do + addMessage Error (toHtml $ tshow (err :: CampusUserException)) + $logErrorS "LDAP" $ tshow err + acceptExisting + ] + | otherwise + = [ C.Handler $ \case + CampusUserNoResult -> do + $logWarnS "LDAP" $ "User lookup failed after successful login for " <> credsIdent + return . UserError $ IdentifierNotFound credsIdent + CampusUserAmbiguous -> do + $logWarnS "LDAP" $ "Multiple LDAP results for " <> credsIdent + return . UserError $ IdentifierNotFound credsIdent + err -> do + $logErrorS "LDAP" $ tshow err + return $ ServerError "LDAP lookup failed" + ] + + acceptExisting = maybe (UserError $ IdentifierNotFound credsIdent) (Authenticated . entityKey) <$> getBy uAuth + + $logDebugS "auth" $ tshow Creds{..} + AppSettings{ appUserDefaults = UserDefaultConf{..}, ..} <- getsYesod appSettings + + flip catches excHandlers $ case appLdapConf of + Just ldapConf -> fmap (either id id) . runExceptT $ do + ldapData <- campusUser ldapConf $ Creds credsPlugin (CI.original userIdent) credsExtra + $logDebugS "LDAP" $ "Successful LDAP lookup: " <> tshow ldapData + + let + userMatrikelnummer' = lookup (Attr "LMU-Stud-Matrikelnummer") ldapData + userEmail' = lookup (Attr "mail") ldapData + userDisplayName' = lookup (Attr "displayName") ldapData + userSurname' = lookup (Attr "sn") ldapData + + userAuthentication + | isPWHash = error "PWHash should only work for users that are already known" + | otherwise = AuthLDAP + + userEmail <- if + | Just [bs] <- userEmail' + , Right userEmail <- Text.decodeUtf8' bs + -> return $ CI.mk userEmail | otherwise - = (credsPlugin, credsIdent) - isDummy = credsPlugin == "dummy" - uAuth = UniqueAuthentication userPlugin userIdent + -> throwError $ ServerError "Could not retrieve user email" + userDisplayName <- if + | Just [bs] <- userDisplayName' + , Right userDisplayName <- Text.decodeUtf8' bs + -> return userDisplayName + | otherwise + -> throwError $ ServerError "Could not retrieve user name" + userSurname <- if + | Just [bs] <- userSurname' + , Right userSurname <- Text.decodeUtf8' bs + -> return userSurname + | otherwise + -> throwError $ ServerError "Could not retrieve user surname" + userMatrikelnummer <- if + | Just [bs] <- userMatrikelnummer' + , Right userMatrikelnummer <- Text.decodeUtf8' bs + -> return $ Just userMatrikelnummer + | Nothing <- userMatrikelnummer' + -> return Nothing + | otherwise + -> throwError $ ServerError "Could not decode user matriculation" - $logDebugS "auth" $ tshow ((userPlugin, userIdent), creds) + let + newUser = User + { userMaxFavourites = userDefaultMaxFavourites + , userTheme = userDefaultTheme + , userDateTimeFormat = userDefaultDateTimeFormat + , userDateFormat = userDefaultDateFormat + , userTimeFormat = userDefaultTimeFormat + , userDownloadFiles = userDefaultDownloadFiles + , .. + } + userUpdate = [ UserMatrikelnummer =. userMatrikelnummer + , UserDisplayName =. userDisplayName + , UserSurname =. userSurname + , UserEmail =. userEmail + ] - when isDummy . (throwError =<<) . lift $ - maybe (UserError $ IdentifierNotFound credsIdent) (Authenticated . entityKey) <$> getBy uAuth + userId <- lift $ entityKey <$> upsertBy uAuth newUser userUpdate - let - userMatrikelnummer = lookup "LMU-Stud-Matrikelnummer" credsExtra - userEmail' = lookup "mail" credsExtra - userDisplayName' = lookup "displayName" credsExtra + let + userStudyFeatures = concat <$> mapM (parseStudyFeatures userId) userStudyFeatures' + userStudyFeatures' = do + (k, v) <- ldapData + guard $ k == Attr "dfnEduPersonFeaturesOfStudy" + v' <- v + Right str <- return $ Text.decodeUtf8' v' + return str - userEmail <- maybe (throwError $ ServerError "Could not retrieve user email") return userEmail' - userDisplayName <- maybe (throwError $ ServerError "Could not retrieve user name") return userDisplayName' + fs <- either (\err -> throwError . ServerError $ "Could not parse features of study: " <> err) return userStudyFeatures - let - userMaxFavourites = 12 -- TODO: appDefaultFavourites appSettings - newUser = User{..} - userUpdate = [ UserMatrikelnummer =. userMatrikelnummer - , UserDisplayName =. userDisplayName - , UserEmail =. userEmail - ] + lift $ deleteWhere [StudyFeaturesUser ==. userId] - userId <- lift $ entityKey <$> upsertBy uAuth newUser userUpdate + forM_ fs $ \StudyFeatures{..} -> do + lift . insertMaybe studyFeaturesDegree $ StudyDegree (unStudyDegreeKey studyFeaturesDegree) Nothing Nothing + lift . insertMaybe studyFeaturesField $ StudyTerms (unStudyTermsKey studyFeaturesField) Nothing Nothing - let - userStudyFeatures = concat <$> mapM (parseStudyFeatures userId) userStudyFeatures' - userStudyFeatures' = [ v | (k, v) <- credsExtra, k == "dfnEduPersonFeaturesOfStudy" ] + lift $ insertMany_ fs + return $ Authenticated userId + Nothing -> acceptExisting - fs <- either (\err -> throwError . ServerError $ "Could not parse features of study: " <> err) return userStudyFeatures - - lift $ deleteWhere [StudyFeaturesUser ==. userId] - - forM_ fs $ \StudyFeatures{..} -> do - lift . insertMaybe studyFeaturesDegree $ StudyDegree (unStudyDegreeKey studyFeaturesDegree) Nothing Nothing - lift . insertMaybe studyFeaturesField $ StudyTerms (unStudyTermsKey studyFeaturesField) Nothing Nothing - - lift $ insertMany_ fs - return $ Authenticated userId where insertMaybe key val = get key >>= maybe (insert_ val) (\_ -> return ()) - -- You can add other plugins like Google Email, email or OAuth here - authPlugins app = [genericAuthLDAP $ ldapConfig app] ++ extraAuthPlugins - -- Enable authDummy login if enabled. - where extraAuthPlugins = [authDummy | appAuthDummyLogin $ appSettings app] + authPlugins (appSettings -> AppSettings{..}) = catMaybes + [ campusLogin <$> appLdapConf + , Just . hashLogin $ pwHashAlgorithm appAuthPWHash + , dummyLogin <$ guard appAuthDummyLogin + ] authHttpManager = getHttpManager -ldapConfig :: UniWorX -> LDAPConfig -ldapConfig _app@(appSettings -> settings) = LDAPConfig - { usernameFilter = \u -> principalName <> "=" <> u - , identifierModifier - , ldapUri = appLDAPURI settings - , initDN = appLDAPDN settings - , initPass = appLDAPPw settings - , baseDN = appLDAPBaseName settings - , ldapScope = LdapScopeSubtree - } - where - principalName :: IsString a => a - principalName = "userPrincipalName" - identifierModifier _ entry = case lookup principalName $ leattrs entry of - Just [n] -> Text.pack n - _ -> error "Could not determine user principal name" - --- | Access function to determine if a user is logged in. -isAuthenticated :: Handler AuthResult -isAuthenticated = do - muid <- maybeAuthId - return $ case muid of - Nothing -> Unauthorized "You must login to access this page" - Just _ -> Authorized - - instance YesodAuthPersist UniWorX -- Useful when writing code that is re-usable outside of the Handler context. diff --git a/src/Handler/Admin.hs b/src/Handler/Admin.hs new file mode 100644 index 000000000..156961629 --- /dev/null +++ b/src/Handler/Admin.hs @@ -0,0 +1,71 @@ +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE NoImplicitPrelude #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ViewPatterns #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE QuasiQuotes #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE FlexibleContexts #-} + +module Handler.Admin where + +import Import +import Handler.Utils + +-- import Data.Time +-- import qualified Data.Text as T +-- import Data.Function ((&)) +-- import Yesod.Form.Bootstrap3 + +import Web.PathPieces (showToPathPiece, readFromPathPiece) + +-- import Colonnade hiding (fromMaybe) +-- import Yesod.Colonnade + +-- import qualified Data.UUID.Cryptographic as UUID + +-- BEGIN - Buttons needed only here +data CreateButton = CreateMath | CreateInf -- Dummy for Example + deriving (Enum, Eq, Ord, Bounded, Read, Show) + +instance PathPiece CreateButton where -- for displaying the button only, not really for paths + toPathPiece = showToPathPiece + fromPathPiece = readFromPathPiece + +instance Button UniWorX CreateButton where + label CreateMath = [whamlet|Mathematik|] + label CreateInf = "Informatik" + + cssClass CreateMath = BCInfo + cssClass CreateInf = BCPrimary +-- END Button needed here + + +getAdminTestR :: Handler Html -- Demo Page. Referenzimplementierungen sollte hier gezeigt werden! +getAdminTestR = do + (btnWdgt, btnEnctype) <- generateFormPost (buttonForm :: Form CreateButton) + defaultLayout $ do + -- setTitle "Uni2work Admin Testpage" + $(widgetFile "adminTest") + +postAdminTestR :: Handler Html +postAdminTestR = do + ((btnResult,_), _) <- runFormPost $ buttonForm + case btnResult of + (FormSuccess CreateInf) -> addMessage Info "Informatik-Knopf gedrückt" + (FormSuccess CreateMath) -> addMessage Warning "Knopf Mathematik erkannt" + _other -> addMessage Warning "KEIN Knopf erkannt" + getAdminTestR + + +getAdminUserR :: CryptoUUIDUser -> Handler Html +getAdminUserR uuid = do + uid <- decrypt uuid + User{..} <- runDB $ get404 uid + defaultLayout $ + [whamlet| +