diff --git a/config/settings.yml b/config/settings.yml index 735afe776..51966ee5d 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -19,6 +19,7 @@ should-log-all: "_env:LOG_ALL:false" # mutable-static: false # skip-combining: false auth-dummy-login: "_env:DUMMY_LOGIN:false" +allow-deprecated: "_env:ALLOW_DEPRECATED: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 diff --git a/messages/de.msg b/messages/de.msg index 6dd0a5196..d7eb1b787 100644 --- a/messages/de.msg +++ b/messages/de.msg @@ -1,3 +1,12 @@ +BtnSubmit: Senden +BtnAbort: Abbrechen +BtnDelete: Löschen +BtnRegister: Anmelden +BtnDeregister: Abmelden + +RegisterFrom: Anmeldungen von +RegisterTo: Anmeldungen bis + SummerTerm year@Integer: Sommersemester #{tshow year} WinterTerm year@Integer: Wintersemester #{tshow year}/#{tshow $ succ year} PSLimitNonPositive: “pagesize” muss größer als null sein @@ -12,6 +21,7 @@ TermEditHeading: Semester editieren/anlegen LectureStart: Beginn Vorlesungen Course: Kurs +CourseSecret: Zugangspasswort 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. @@ -44,6 +54,7 @@ UnauthorizedCorrector: Sie sind nicht als Korrektor für diese Veranstaltung ein 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. UnauthorizedSubmissionCorrector: Sie sind nicht der Korrektor für diese Abgabe. @@ -53,6 +64,7 @@ UnfreeMaterials: Die Materialien für diese Veranstaltung sind nicht allgemein f UnauthorizedWrite: Sie haben hierfür keine Schreibberechtigung Submission: Abgabenummer + SubmissionWrongSheet: Abgabenummer gehört nicht zum angegebenen Übungsblatt. SubmissionAlreadyExists: Sie haben bereits eine Abgabe zu diesem Übungsblatt. SubmissionTitle tid@TermIdentifier courseShortHand@Text sheetName@Text: #{termToText tid}-#{courseShortHand} #{sheetName}: Abgabe editieren/anlegen @@ -63,12 +75,25 @@ SubmissionAlreadyExistsFor user@Text: #{user} hat bereits eine Abgabe zu diesem CorrectionsTitle: Zugewiesene Korrekturen CourseCorrectionsTitle: Korrekturen für diesen Kurs -Corrector: Korrektor EMail: E-Mail EMailUnknown email@Text: E-Mail #{email} gehört zu keinem bekannten Benutzer. NotAParticipant user@Text tid@TermIdentifier csh@Text: #{user} ist nicht im Kurs #{termToText tid}-#{csh} angemeldet. +AddCorrector: Zusätzlicher Korrektor +CorrectorExists user@Text: #{user} ist bereits als Korrektor eingetragen +SheetCorrectorsTitle tid@TermIdentifier courseShortHand@Text sheetName@Text: Korrektoren für #{termToText tid}-#{courseShortHand} #{sheetName} +CountTutProp: Tutorien zählen gegen Proportion +Corrector: Korrektor +Correctors: Korrektoren +CorByTut: Nach Tutorium +CorProportion: Anteil +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. + HomeHeading: Aktuelle Termine ProfileHeading: Benutzerprofil und Einstellungen ProfileDataHeading: Gespeicherte Benutzerdaten @@ -97,4 +122,5 @@ NrColumn: Nr SelectColumn: Auswahl CorrDownload: Herunterladen -CorrSetCorrector: Korrektor zuweisen \ No newline at end of file +CorrSetCorrector: Korrektor zuweisen +NatField xyz@Text: #{xyz} muss eine natürliche Zahl sein! diff --git a/models b/models index ad99eabd2..070bc1a17 100644 --- a/models +++ b/models @@ -60,7 +60,7 @@ Course term TermId school SchoolId capacity Int Maybe - hasRegistration Bool -- canRegisterNow = hasRegistration && maybe False (<= currentTime) registerFrom && maybe True (>= currentTime) registerTo + -- canRegisterNow = maybe False (<= currentTime) registerFrom && maybe True (>= currentTime) registerTo registerFrom UTCTime Maybe registerTo UTCTime Maybe deregisterUntil UTCTime Maybe @@ -119,6 +119,7 @@ SheetCorrector sheet SheetId load Load UniqueSheetCorrector user sheet + deriving Show Eq Ord SheetFile sheet SheetId file FileId diff --git a/package.yaml b/package.yaml index bb217ec2b..ccfb37678 100644 --- a/package.yaml +++ b/package.yaml @@ -81,6 +81,7 @@ dependencies: - exceptions - lens - MonadRandom +- email-validate # The library contains all of our application code. The executable # defined below is just a thin wrapper. diff --git a/routes b/routes index 016e15340..bc7a61877 100644 --- a/routes +++ b/routes @@ -21,7 +21,7 @@ -- !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 +-- !deprecated -- like free, but logs and gives a warning; entirely disabled in production -- /static StaticR Static appStatic !free @@ -30,9 +30,10 @@ /favicon.ico FaviconR GET !free /robots.txt RobotsR GET !free -/ HomeR GET !free -/users UsersR GET -- no tags, i.e. admins only +/ HomeR GET !free +/users UsersR GET -- no tags, i.e. admins only /admin/test AdminTestR GET POST +/admin/user/#CryptoUUIDUser AdminUserR GET /profile ProfileR GET POST !free !free /profile/data ProfileDataR GET !free !free @@ -47,7 +48,8 @@ /course/ CourseListR GET !free !/course/new CourseNewR GET POST !lecturer /course/#TermId/#Text CourseR !lecturer: - /show CShowR GET POST !free + /show CShowR GET !free + /register CRegisterR POST !time /edit CEditR GET POST /corrections CourseCorrectionsR GET POST /ex SheetListR GET !registered !materials @@ -60,9 +62,11 @@ !/sub/new SubmissionNewR GET POST !timeANDregistered !/sub/own SubmissionOwnR GET !free !/sub/#CryptoFileNameSubmission SubmissionR GET POST !owner !corrector + /correctors SCorrR GET POST /corrections CorrectionsR GET POST !free + -- TODO below !/#{ZIPArchiveName SubmissionId} SubmissionDownloadArchiveR GET !deprecated !/#CryptoFileNameSubmission/*FilePath SubmissionDownloadSingleR GET !deprecated diff --git a/src/CryptoID.hs b/src/CryptoID.hs index 7019689ea..28fb616d1 100644 --- a/src/CryptoID.hs +++ b/src/CryptoID.hs @@ -39,7 +39,7 @@ instance PathPiece UUID where instance (CI.FoldCase s, PathPiece s) => PathPiece (CI s) where fromPathPiece = fmap CI.mk . fromPathPiece - toPathPiece = toPathPiece . CI.original + toPathPiece = toPathPiece . CI.foldedCase instance {-# OVERLAPS #-} PathMultiPiece FilePath where fromPathMultiPiece = Just . unpack . intercalate "/" @@ -47,12 +47,13 @@ instance {-# OVERLAPS #-} PathMultiPiece FilePath where instance (CI.FoldCase s, PathMultiPiece s) => PathMultiPiece (CI s) where fromPathMultiPiece = fmap CI.mk . fromPathMultiPiece - toPathMultiPiece = toPathMultiPiece . CI.original + toPathMultiPiece = toPathMultiPiece . CI.foldedCase -- Generates CryptoUUID... and CryptoFileName... Datatypes decCryptoIDs [ ''SubmissionId , ''FileId + , ''UserId ] {- TODO: Do we need/want CryptoUUIDs for Sheet numbers? -} diff --git a/src/Foundation.hs b/src/Foundation.hs index 28ae4faa4..c4e22c72e 100644 --- a/src/Foundation.hs +++ b/src/Foundation.hs @@ -1,6 +1,5 @@ {-# LANGUAGE NoImplicitPrelude #-} {-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE OverloadedLists #-} {-# LANGUAGE TemplateHaskell, QuasiQuotes #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE TypeFamilies #-} @@ -236,12 +235,13 @@ adminAP = APDB $ \case knownTags :: Map (CI Text) AccessPredicate -knownTags = -- should not throw exceptions, i.e. no getBy404 or requireAuthId +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 - return Authorized + allow <- appAllowDeprecated . appSettings <$> getYesod + return $ bool (Unauthorized "Deprecated Route") Authorized allow ) ,("lecturer", APDB $ \case CourseR tid csh _ -> exceptT return return $ do @@ -289,20 +289,28 @@ knownTags = -- should not throw exceptions, i.e. no getBy404 or requireAuthId return Authorized ) ,("time", APDB $ \case - CSheetR tid csh shn subRoute -> maybeT (unauthorizedI MsgUnauthorizedSheetTime) $ do - Entity cid _ <- MaybeT . getBy $ CourseTermShort tid csh - Entity sid Sheet{..} <- MaybeT . getBy $ CourseSheet cid shn - cTime <- liftIO getCurrentTime - let started = sheetActiveFrom <= cTime || NTop sheetVisibleFrom <= (NTop $ Just cTime) - case subRoute of - SFileR SheetExercise _ -> guard started - SFileR SheetHint _ -> guard $ maybe False (<= cTime) sheetHintFrom - SFileR SheetSolution _ -> guard $ maybe False (<= cTime) sheetSolutionFrom - SFileR SheetMarking _ -> mzero -- only for correctors and lecturers - SubmissionNewR -> guard $ sheetActiveFrom <= cTime && cTime <= sheetActiveTo - _ -> guard started - return Authorized - r -> do + CSheetR tid csh shn subRoute -> maybeT (unauthorizedI MsgUnauthorizedSheetTime) $ do + Entity cid _ <- MaybeT . getBy $ CourseTermShort tid csh + Entity sid Sheet{..} <- MaybeT . getBy $ CourseSheet cid shn + cTime <- liftIO getCurrentTime + let started = sheetActiveFrom <= cTime || NTop sheetVisibleFrom <= (NTop $ Just cTime) + case subRoute of + SFileR SheetExercise _ -> guard started + SFileR SheetHint _ -> guard $ maybe False (<= cTime) sheetHintFrom + SFileR SheetSolution _ -> guard $ maybe False (<= cTime) sheetSolutionFrom + SFileR SheetMarking _ -> mzero -- only for correctors and lecturers + SubmissionNewR -> guard $ sheetActiveFrom <= cTime && cTime <= sheetActiveTo + _ -> guard started + return Authorized + + CourseR tid csh CRegisterR -> maybeT (unauthorizedI MsgUnauthorizedCourseTime) $ do + Entity cid Course{..} <- MaybeT . getBy $ CourseTermShort tid csh + cTime <- (NTop . Just) <$> liftIO getCurrentTime + guard $ NTop courseRegisterFrom <= cTime + && NTop courseRegisterTo >= cTime + return Authorized + + r -> do $logErrorS "AccessControl" $ "'!time' used on route that doesn't support it: " <> tshow r unauthorizedI MsgUnauthorized ) @@ -406,9 +414,9 @@ instance Yesod UniWorX where -- 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 <- MaybeT getCurrentRoute + guardM . lift $ (== Authorized) <$> isAuthorized route False case route of -- update Course Favourites here CourseR tid csh _ -> do uid <- MaybeT maybeAuthId @@ -431,7 +439,7 @@ instance Yesod UniWorX where lift $ mapM_ delete oldFavs _other -> return () - return res + defaultYesodMiddleware handler -- handler is executed afterwards, so Favourites are updated immediately defaultLayout widget = do master <- getYesod @@ -568,7 +576,6 @@ instance Yesod UniWorX where makeLogger = return . appLogger - -- Define breadcrumbs. instance YesodBreadcrumbs UniWorX where breadcrumb TermShowR = return ("Semester", Just HomeR) @@ -579,38 +586,55 @@ instance YesodBreadcrumbs UniWorX where breadcrumb (TermCourseListR term) = return (toPathPiece term, Just TermShowR) breadcrumb (CourseR term course CShowR) = return (course, Just $ TermCourseListR term) breadcrumb CourseNewR = return ("Neu", Just CourseListR) - breadcrumb (CourseR _ _ CEditR) = return ("Editieren", Just CourseListR) + breadcrumb (CourseR tid csh CEditR) = return ("Editieren", Just $ CourseR tid csh CShowR) breadcrumb (CourseR tid csh SheetListR) = return ("Übungen",Just $ CourseR tid csh CShowR) breadcrumb (CourseR tid csh SheetNewR ) = return ("Neu", Just $ CourseR tid csh SheetListR) breadcrumb (CSheetR tid csh shn SShowR) = return (shn, Just $ CourseR tid csh SheetListR) breadcrumb (CSheetR tid csh shn SEditR) = return ("Edit", Just $ CSheetR tid csh shn SShowR) breadcrumb (CSheetR tid csh shn SDelR ) = return ("DELETE", Just $ CSheetR tid csh shn SShowR) + breadcrumb (CSheetR tid csh shn SCorrR) = return ("Korrektoren", Just $ CSheetR tid csh shn SShowR) breadcrumb (CSheetR tid csh shn (SubmissionR _)) = return ("Abgabe", Just $ CSheetR tid csh shn SShowR) breadcrumb SubmissionListR = return ("Abgaben", Just HomeR) - breadcrumb HomeR = return ("UniWorkY", Nothing) - breadcrumb (AuthR _) = return ("Login", Just HomeR) - breadcrumb ProfileR = return ("Profile", Just HomeR) + breadcrumb HomeR = return ("UniWorkY", Nothing) + breadcrumb (AuthR _) = return ("Login", Just HomeR) + breadcrumb ProfileR = return ("Profile", Just HomeR) breadcrumb ProfileDataR = return ("Data", Just ProfileR) breadcrumb _ = return ("home", Nothing) pageActions :: Route UniWorX -> [MenuTypes] pageActions (CourseR tid csh CShowR) = [ PageActionPrime $ MenuItem - { menuItemLabel = "Übungsblätter" - , menuItemIcon = Nothing - , menuItemRoute = CourseR tid csh SheetListR - , menuItemAccessCallback' = return True - } - , PageActionPrime $ MenuItem { menuItemLabel = "Kurs Editieren" , menuItemIcon = Nothing , menuItemRoute = CourseR tid csh CEditR , menuItemAccessCallback' = return True } + , PageActionPrime $ MenuItem + { menuItemLabel = "Übungsblätter" + , menuItemIcon = Nothing + , menuItemRoute = CourseR tid csh SheetListR + , menuItemAccessCallback' = do --TODO always show for lecturer + let sheetRouteAccess shn = (== Authorized) <$> (isAuthorized (CSheetR tid csh shn SShowR) False) + muid <- maybeAuthId + (sheets,lecturer) <- runDB $ do + cid <- getKeyBy404 $ CourseTermShort tid 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 + } + , PageActionSecondary $ MenuItem + { menuItemLabel = "Neues Übungsblatt anlegen" + , menuItemIcon = Nothing + , menuItemRoute = CourseR tid csh SheetNewR + , menuItemAccessCallback' = return True + } ] pageActions (CourseR tid csh SheetListR) = [ PageActionPrime $ MenuItem @@ -628,11 +652,17 @@ pageActions (CSheetR tid csh shn SShowR) = , menuItemAccessCallback' = return True -- TODO: check that no submission already exists } , PageActionPrime $ MenuItem - { menuItemLabel = "Abgabe" + { menuItemLabel = "Abgabe ansehen" , menuItemIcon = Nothing , menuItemRoute = CSheetR tid csh shn SubmissionOwnR , menuItemAccessCallback' = return True -- TODO: check that a submission already exists } + , PageActionPrime $ MenuItem + { menuItemLabel = "Korrektoren" + , menuItemIcon = Nothing + , menuItemRoute = CSheetR tid csh shn SCorrR + , menuItemAccessCallback' = return True + } ] pageActions TermShowR = [ PageActionPrime $ MenuItem diff --git a/src/Handler/Admin.hs b/src/Handler/Admin.hs index cafd75dbe..1fc340912 100644 --- a/src/Handler/Admin.hs +++ b/src/Handler/Admin.hs @@ -58,3 +58,14 @@ postAdminTestR = do _other -> return () getAdminTestR + +getAdminUserR :: CryptoUUIDUser -> Handler Html +getAdminUserR uuid = do + uid <- decrypt uuid + User{..} <- runDB $ get404 uid + defaultLayout $ + [whamlet| +