Merge branch 'master' into initial_thoughts_on_frontend
This commit is contained in:
commit
50cebd92bf
5
.gitignore
vendored
5
.gitignore
vendored
@ -18,7 +18,8 @@ cabal.sandbox.config
|
|||||||
*.swp
|
*.swp
|
||||||
*.keter
|
*.keter
|
||||||
*~
|
*~
|
||||||
\#*
|
**/\#*
|
||||||
|
**/.\#*
|
||||||
uniworx.cabal
|
uniworx.cabal
|
||||||
uniworx.nix
|
uniworx.nix
|
||||||
.gup/
|
.gup/
|
||||||
@ -26,4 +27,4 @@ uniworx.nix
|
|||||||
*.kate-swp
|
*.kate-swp
|
||||||
src/Handler/Assist.bak
|
src/Handler/Assist.bak
|
||||||
src/Handler/Course.SnapCustom.hs
|
src/Handler/Course.SnapCustom.hs
|
||||||
|
*.orig
|
||||||
|
|||||||
27
Datenschutznotizen.txt
Normal file
27
Datenschutznotizen.txt
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
* Datensparsamkeit: nur Speichern was notwendig ist; Dokumentieren, warum was gespeichert wird!
|
||||||
|
* Verfügbarkeit: Backup / aktuelles System; nicht nur eine Person, Anfragen werden organisiert beantwortet
|
||||||
|
* Integrität: Konsistenzcheck bei Datenübertragen (z.B. LDAP), Sicherheit vor bösen Absichten, Änderungen protokolliert
|
||||||
|
* Vertraulichkeit: Jeder Benutzer sollte nur auf das zugreifen was unbedingt nötig ist; Backup Verschlüsselung
|
||||||
|
* Nichtverkettbarkeit: (eher irrelevant für unseren Anwendungsfall)
|
||||||
|
* Transparenz: User weiß was über ihn gespeichert wird; Dokumentation; Vorfälle schnell melden?
|
||||||
|
* Intervenierbarkeit: Korrektur/Löschpflichten - auch im Backup; z.B. Korrekturen bei Einspielen des Backups einpflegen; Backup Verschlüsselung; Bei Löschanforderungen muss teilweise gelöscht werden (nur was Notenrelevant muss aufgehoben werden, Hausaufgaben werden gelöscht; Anzeige gelöschter Teilnehmer)
|
||||||
|
* Wer ist Datenschutzverantwortlicher? Steffen!?!
|
||||||
|
=> Sofort anzeigen, wenn etwas schiefläuft.
|
||||||
|
|
||||||
|
|
||||||
|
Fragen:
|
||||||
|
- Was ist mit Abschreiber-Flags: Keine Flags, sondern protokollieren: bei Überschreiten einer Schwelle sollte jemand mit entsprechender Befugnis benachrichtigt werden, Student sollte die Eintragungen sehen, Assistenten nicht
|
||||||
|
|
||||||
|
|
||||||
|
Aktionen:
|
||||||
|
- Felder für Aufbewahrungsfristen zu jedem Datensatz
|
||||||
|
- List gelöschter Kennungen
|
||||||
|
- Zugangsberechtigungen für Vorlesungen/Übungen
|
||||||
|
- Regularien für Prozess; Aufbewahrungsfristen, Verwaltungsrechtliche Fragen, Bayrisches E-Goverment Gesetz, Daten signierbar/verifizieren;
|
||||||
|
|
||||||
|
-> Aktuelle Archivierung von prüfungsrelevanten Daten (Klausur-Lagerung) ist nicht Gesetz-Konform; da Papier-Lagerung nicht konform ist.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
71
fill-db.hs
Executable file
71
fill-db.hs
Executable file
@ -0,0 +1,71 @@
|
|||||||
|
#!/usr/bin/env stack
|
||||||
|
-- stack runghc
|
||||||
|
|
||||||
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
{-# LANGUAGE PackageImports #-}
|
||||||
|
{-# LANGUAGE NoImplicitPrelude #-}
|
||||||
|
|
||||||
|
import "uniworx" Import
|
||||||
|
import "uniworx" Application (db)
|
||||||
|
|
||||||
|
import Data.Time
|
||||||
|
|
||||||
|
main :: IO ()
|
||||||
|
main = db $ do
|
||||||
|
now <- liftIO getCurrentTime
|
||||||
|
let
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
void . insert $ 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 . insert $ 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 $ UserLecturer gkleen ifi
|
||||||
|
ifiBsc <- insert $ Degree "Bachelor Informatik" ifi
|
||||||
|
ifiMsc <- insert $ Degree "Master Informatik" ifi
|
||||||
|
ffp <- insert Course
|
||||||
|
{ courseName = "Fortgeschrittene Funktionale Programmierung"
|
||||||
|
, courseDescription = Nothing
|
||||||
|
, courseLinkExternal = Nothing
|
||||||
|
, courseShorthand = "ffp"
|
||||||
|
, courseTermId = TermKey summer2018
|
||||||
|
, courseSchoolId = ifi
|
||||||
|
, courseCapacity = Just 20
|
||||||
|
, courseCreated = now
|
||||||
|
, courseChanged = now
|
||||||
|
, courseCreatedBy = gkleen
|
||||||
|
, courseChangedBy = gkleen
|
||||||
|
, courseHasRegistration = True
|
||||||
|
, courseRegisterFrom = Just now
|
||||||
|
, courseRegisterTo = Just ((3600 * 24 * 60) `addUTCTime` now )
|
||||||
|
}
|
||||||
|
void . insert $ DegreeCourse ifiBsc ffp
|
||||||
|
void . insert $ DegreeCourse ifiMsc ffp
|
||||||
|
void . insert $ Lecturer gkleen ffp
|
||||||
|
void . insert $ Corrector gkleen ffp (ByProportion 1)
|
||||||
|
void . insert $ Sheet ffp "Blatt 1" Nothing NotGraded Nothing now now Nothing Nothing now now gkleen gkleen
|
||||||
8
ghci.sh
Executable file
8
ghci.sh
Executable file
@ -0,0 +1,8 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
unset HOST
|
||||||
|
export DETAILED_LOGGING=true
|
||||||
|
export LOG_ALL=true
|
||||||
|
export DUMMY_LOGIN=true
|
||||||
|
|
||||||
|
exec -- stack ghci --flag uniworx:dev --flag uniworx:library-only
|
||||||
10
models
10
models
@ -37,8 +37,8 @@ Term json
|
|||||||
lectureStart Day
|
lectureStart Day
|
||||||
lectureEnd Day
|
lectureEnd Day
|
||||||
active Bool
|
active Bool
|
||||||
Primary name
|
Primary name -- newtype Key Term = TermKey { unTermKey :: TermIdentifier }
|
||||||
deriving Show
|
deriving Show -- type TermId = Key Term
|
||||||
School json
|
School json
|
||||||
name Text
|
name Text
|
||||||
shorthand Text
|
shorthand Text
|
||||||
@ -94,8 +94,9 @@ CourseParticipant
|
|||||||
Sheet
|
Sheet
|
||||||
courseId CourseId
|
courseId CourseId
|
||||||
name Text
|
name Text
|
||||||
|
description Html Maybe
|
||||||
type SheetType
|
type SheetType
|
||||||
markingText Text Maybe
|
markingText Html Maybe
|
||||||
activeFrom UTCTime
|
activeFrom UTCTime
|
||||||
activeTo UTCTime
|
activeTo UTCTime
|
||||||
hintFrom UTCTime Maybe
|
hintFrom UTCTime Maybe
|
||||||
@ -104,6 +105,7 @@ Sheet
|
|||||||
changed UTCTime
|
changed UTCTime
|
||||||
createdBy UserId
|
createdBy UserId
|
||||||
changedBy UserId
|
changedBy UserId
|
||||||
|
CourseSheet courseId name
|
||||||
SheetFile
|
SheetFile
|
||||||
sheetId SheetId
|
sheetId SheetId
|
||||||
fileId FileId
|
fileId FileId
|
||||||
@ -200,6 +202,6 @@ Exam
|
|||||||
ExamUser
|
ExamUser
|
||||||
userId UserId
|
userId UserId
|
||||||
examId ExamId
|
examId ExamId
|
||||||
-- CONTINUE HERE: Inlcude rating in this table or seperatly?
|
-- CONTINUE HERE: Include rating in this table or separately?
|
||||||
UniqueExamUser userId examId
|
UniqueExamUser userId examId
|
||||||
-- By default this file is used in Model.hs (which is imported by Foundation.hs)
|
-- By default this file is used in Model.hs (which is imported by Foundation.hs)
|
||||||
|
|||||||
@ -10,6 +10,7 @@ dependencies:
|
|||||||
# version 1.0 had a bug in reexporting Handler, causing trouble
|
# version 1.0 had a bug in reexporting Handler, causing trouble
|
||||||
- classy-prelude-yesod >=0.10.2 && <1.0 || >=1.1
|
- classy-prelude-yesod >=0.10.2 && <1.0 || >=1.1
|
||||||
|
|
||||||
|
- foreign-store
|
||||||
- yesod >=1.4.3 && <1.5
|
- yesod >=1.4.3 && <1.5
|
||||||
- yesod-core >=1.4.30 && <1.5
|
- yesod-core >=1.4.30 && <1.5
|
||||||
- yesod-auth >=1.4.0 && <1.5
|
- yesod-auth >=1.4.0 && <1.5
|
||||||
@ -64,6 +65,7 @@ dependencies:
|
|||||||
- filepath-crypto
|
- filepath-crypto
|
||||||
- cryptoids-types
|
- cryptoids-types
|
||||||
- cryptoids
|
- cryptoids
|
||||||
|
- cryptoids-class
|
||||||
- binary
|
- binary
|
||||||
- mtl
|
- mtl
|
||||||
- sandi
|
- sandi
|
||||||
@ -75,6 +77,8 @@ dependencies:
|
|||||||
- yesod-auth-ldap
|
- yesod-auth-ldap
|
||||||
- LDAP
|
- LDAP
|
||||||
- parsec
|
- parsec
|
||||||
|
- uuid
|
||||||
|
- exceptions
|
||||||
|
|
||||||
# The library contains all of our application code. The executable
|
# The library contains all of our application code. The executable
|
||||||
# defined below is just a thin wrapper.
|
# defined below is just a thin wrapper.
|
||||||
@ -87,7 +91,6 @@ library:
|
|||||||
- -Wall
|
- -Wall
|
||||||
- -fwarn-tabs
|
- -fwarn-tabs
|
||||||
- -O0
|
- -O0
|
||||||
- -ddump-splices
|
|
||||||
cpp-options: -DDEVELOPMENT
|
cpp-options: -DDEVELOPMENT
|
||||||
else:
|
else:
|
||||||
ghc-options:
|
ghc-options:
|
||||||
|
|||||||
22
routes
22
routes
@ -6,22 +6,24 @@
|
|||||||
|
|
||||||
/ HomeR GET POST
|
/ HomeR GET POST
|
||||||
/profile ProfileR GET
|
/profile ProfileR GET
|
||||||
|
/users UsersR GET
|
||||||
|
|
||||||
/term TermShowR GET
|
/term TermShowR GET
|
||||||
/term/edit TermEditR GET POST
|
/term/edit TermEditR GET POST
|
||||||
/term/#TermIdentifier/edit TermEditExistR GET
|
/term/#TermId/edit TermEditExistR GET
|
||||||
|
|
||||||
/course/ CourseListR GET
|
/course/ CourseListR GET
|
||||||
!/course/new CourseEditR GET POST
|
!/course/new CourseEditR GET POST
|
||||||
!/course/#TermIdentifier CourseListTermR GET
|
!/course/#TermId CourseListTermR GET
|
||||||
/course/#TermIdentifier/#Text/edit CourseEditExistR GET
|
/course/#TermId/#Text/edit CourseEditExistR GET
|
||||||
/course/#TermIdentifier/#Text/show CourseShowR GET POST
|
/course/#TermId/#Text/show CourseShowR GET POST
|
||||||
|
|
||||||
/course/#TermIdentifier/#Text/sheet/ SheetListR GET
|
|
||||||
/course/#TermIdentifier/#Text/sheet/new SheetNewR GET
|
|
||||||
/course/#TermIdentifier/#Text/sheet/#SheetId/show SheetShowR GET
|
|
||||||
/course/#TermIdentifier/#Text/sheet/#SheetId/edit SheetEditR GET
|
|
||||||
|
|
||||||
|
/course/#TermId/#Text/sheet/ SheetListR GET
|
||||||
|
/course/#TermId/#Text/sheet/#Text/show SheetShowR GET
|
||||||
|
/course/#TermId/#Text/sheet/#Text/#SheetFileType/#FilePath SheetFileR GET
|
||||||
|
/course/#TermId/#Text/sheet/new SheetNewR GET POST
|
||||||
|
/course/#TermId/#Text/sheet/#SheetId/edit SheetEditR GET POST
|
||||||
|
/course/#TermId/#Text/sheet/#SheetId/delete SheetDelR GET POST
|
||||||
|
|
||||||
/submission SubmissionListR GET POST
|
/submission SubmissionListR GET POST
|
||||||
/submission/#CryptoUUIDSubmission SubmissionR GET POST
|
/submission/#CryptoUUIDSubmission SubmissionR GET POST
|
||||||
@ -29,5 +31,7 @@
|
|||||||
!/submission/archive/#FilePath SubmissionDownloadArchiveR GET
|
!/submission/archive/#FilePath SubmissionDownloadArchiveR GET
|
||||||
!/submission/#CryptoUUIDSubmission/#FilePath SubmissionDownloadSingleR GET
|
!/submission/#CryptoUUIDSubmission/#FilePath SubmissionDownloadSingleR GET
|
||||||
|
|
||||||
|
!/#UUID CryptoUUIDDispatchR GET
|
||||||
|
|
||||||
-- For demonstration
|
-- For demonstration
|
||||||
/course/#CryptoUUIDCourse/edit CourseEditExistIDR GET
|
/course/#CryptoUUIDCourse/edit CourseEditExistIDR GET
|
||||||
|
|||||||
23
shell.nix
23
shell.nix
@ -1,4 +1,4 @@
|
|||||||
{ nixpkgs ? import <nixpkgs> {}, compiler ? null }:
|
{ nixpkgs ? import <nixpkgs> {}, compiler ? "ghc822" }:
|
||||||
|
|
||||||
let
|
let
|
||||||
inherit (nixpkgs) pkgs;
|
inherit (nixpkgs) pkgs;
|
||||||
@ -22,7 +22,7 @@ let
|
|||||||
'';
|
'';
|
||||||
|
|
||||||
override = oldAttrs: {
|
override = oldAttrs: {
|
||||||
nativeBuildInputs = oldAttrs.nativeBuildInputs ++ (with pkgs; [ postgresql ]) ++ (with haskellPackages; [ stack stack-run yesod-bin ]);
|
nativeBuildInputs = oldAttrs.nativeBuildInputs ++ (with pkgs; [ postgresql cabal-install ]) ++ (with haskellPackages; [ stack yesod-bin ]);
|
||||||
shellHook = ''
|
shellHook = ''
|
||||||
${oldAttrs.shellHook}
|
${oldAttrs.shellHook}
|
||||||
export PROMPT_INFO="${oldAttrs.name}"
|
export PROMPT_INFO="${oldAttrs.name}"
|
||||||
@ -36,13 +36,18 @@ let
|
|||||||
psql -f ${postgresSchema} postgres
|
psql -f ${postgresSchema} postgres
|
||||||
printf "Postgres logfile is %s\nPostgres socket directory is %s\n" ''${pgLogFile} ''${pgSockDir}
|
printf "Postgres logfile is %s\nPostgres socket directory is %s\n" ''${pgLogFile} ''${pgSockDir}
|
||||||
|
|
||||||
env --unset=shellHook zsh
|
cleanup() {
|
||||||
ret=$?
|
pg_ctl stop -D ''${pgDir}
|
||||||
|
rm -rvf ''${pgDir} ''${pgSockDir} ''${pgLogFile}
|
||||||
|
}
|
||||||
|
|
||||||
pg_ctl stop -D ''${pgDir}
|
trap cleanup EXIT
|
||||||
rm -rvf ''${pgDir} ''${pgSockDir} ''${pgLogFile}
|
|
||||||
exit ''${ret}
|
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
in
|
|
||||||
pkgs.stdenv.lib.overrideDerivation drv.env override
|
dummy = pkgs.stdenv.mkDerivation {
|
||||||
|
name = "interactive-uniworx-environment";
|
||||||
|
shellHook = "";
|
||||||
|
};
|
||||||
|
in pkgs.stdenv.lib.overrideDerivation dummy override
|
||||||
|
#pkgs.stdenv.lib.overrideDerivation drv.env override
|
||||||
|
|||||||
@ -42,10 +42,12 @@ import System.Log.FastLogger (defaultBufSize, newStdoutLoggerSet,
|
|||||||
import Handler.Common
|
import Handler.Common
|
||||||
import Handler.Home
|
import Handler.Home
|
||||||
import Handler.Profile
|
import Handler.Profile
|
||||||
|
import Handler.Users
|
||||||
import Handler.Term
|
import Handler.Term
|
||||||
import Handler.Course
|
import Handler.Course
|
||||||
import Handler.Sheet
|
import Handler.Sheet
|
||||||
import Handler.Submission
|
import Handler.Submission
|
||||||
|
import Handler.CryptoIDDispatch
|
||||||
|
|
||||||
|
|
||||||
-- This line actually creates our YesodDispatch instance. It is the second half
|
-- This line actually creates our YesodDispatch instance. It is the second half
|
||||||
|
|||||||
@ -7,8 +7,9 @@
|
|||||||
|
|
||||||
module CryptoID
|
module CryptoID
|
||||||
( module CryptoID
|
( module CryptoID
|
||||||
, module Data.UUID.Cryptographic
|
, module Data.CryptoID.Poly.ImplicitNamespace
|
||||||
, module Data.CryptoID.Poly
|
, module Data.UUID.Cryptographic.ImplicitNamespace
|
||||||
|
, module System.FilePath.Cryptographic.ImplicitNamespace
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import CryptoID.TH
|
import CryptoID.TH
|
||||||
@ -16,10 +17,10 @@ import CryptoID.TH
|
|||||||
import ClassyPrelude hiding (fromString)
|
import ClassyPrelude hiding (fromString)
|
||||||
import Model
|
import Model
|
||||||
|
|
||||||
import Data.CryptoID
|
import Data.CryptoID.Poly.ImplicitNamespace
|
||||||
import Data.CryptoID.Poly hiding (encrypt, decrypt)
|
import Data.UUID.Cryptographic.ImplicitNamespace
|
||||||
|
import System.FilePath.Cryptographic.ImplicitNamespace
|
||||||
|
|
||||||
import Data.UUID.Cryptographic
|
|
||||||
import Data.UUID.Types
|
import Data.UUID.Types
|
||||||
import Web.PathPieces
|
import Web.PathPieces
|
||||||
|
|
||||||
@ -28,10 +29,10 @@ instance PathPiece UUID where
|
|||||||
fromPathPiece = fromString . unpack
|
fromPathPiece = fromString . unpack
|
||||||
toPathPiece = pack . toString
|
toPathPiece = pack . toString
|
||||||
|
|
||||||
decKeysBinary [ ''SubmissionId
|
|
||||||
, ''CourseId
|
|
||||||
]
|
|
||||||
|
|
||||||
decTypeAliases [ "Submission"
|
-- Generates CryptoUUID... Datatypes
|
||||||
, "Course"
|
decCryptoIDs [ ''SubmissionId
|
||||||
]
|
, ''CourseId
|
||||||
|
, ''SheetId
|
||||||
|
]
|
||||||
|
{- TODO: Do we need/want CryptoUUIDs for Sheet numbers? -}
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import ClassyPrelude
|
|||||||
|
|
||||||
import Language.Haskell.TH
|
import Language.Haskell.TH
|
||||||
|
|
||||||
import Data.CryptoID (CryptoID)
|
import Data.CryptoID.Class.ImplicitNamespace
|
||||||
import Data.UUID.Types (UUID)
|
import Data.UUID.Types (UUID)
|
||||||
import Data.Binary (Binary(..))
|
import Data.Binary (Binary(..))
|
||||||
import Data.Binary.SerializationLength
|
import Data.Binary.SerializationLength
|
||||||
@ -19,28 +19,30 @@ import System.FilePath (FilePath)
|
|||||||
import Database.Persist.Sql (toSqlKey, fromSqlKey)
|
import Database.Persist.Sql (toSqlKey, fromSqlKey)
|
||||||
|
|
||||||
|
|
||||||
decTypeAliases :: [String] -> Q [Dec]
|
decCryptoIDs :: [Name] -> DecsQ
|
||||||
decTypeAliases = return . concatMap decTypeAliases'
|
decCryptoIDs = fmap concat . mapM decCryptoID
|
||||||
where
|
where
|
||||||
decTypeAliases' :: String -> [Dec]
|
decCryptoID :: Name -> DecsQ
|
||||||
decTypeAliases' n
|
decCryptoID n@(conT -> t) = do
|
||||||
= [ TySynD cryptoIDn [] $ ConT ''CryptoID `AppT` LitT (StrTyLit n)
|
instances <- [d|
|
||||||
, TySynD cryptoUUIDn [] $ ConT cryptoIDn `AppT` ConT ''UUID
|
instance Binary $(t) where
|
||||||
, TySynD cryptoBase32n [] $ ConT cryptoIDn `AppT` (ConT ''CI `AppT` ConT ''FilePath)
|
get = $(varE 'toSqlKey) <$> get
|
||||||
|
put = put . $(varE 'fromSqlKey)
|
||||||
|
instance HasFixedSerializationLength $(t) where
|
||||||
|
type SerializationLength $(t) = SerializationLength Int64
|
||||||
|
|
||||||
|
type instance CryptoIDNamespace a $(t) = $(litT $ strTyLit ns)
|
||||||
|
|]
|
||||||
|
|
||||||
|
synonyms <- mapM cryptoIDSyn
|
||||||
|
[ (ConT ''UUID, "UUID")
|
||||||
|
, (ConT ''CI `AppT` ConT ''FilePath, "FileName")
|
||||||
|
]
|
||||||
|
|
||||||
|
return $ concat
|
||||||
|
[ instances
|
||||||
|
, synonyms
|
||||||
]
|
]
|
||||||
where
|
where
|
||||||
cryptoIDn = mkName $ "CryptoID" ++ n
|
ns = (\nb -> fromMaybe nb $ stripSuffix "Id" nb) $ nameBase n
|
||||||
cryptoUUIDn = mkName $ "CryptoUUID" ++ n
|
cryptoIDSyn (ct, str) = tySynD (mkName $ "Crypto" ++ str ++ ns) [] $ conT ''CryptoID `appT` return ct `appT` t
|
||||||
cryptoBase32n = mkName $ "CryptoFileName" ++ n
|
|
||||||
|
|
||||||
decKeysBinary :: [Name] -> DecsQ
|
|
||||||
decKeysBinary = fmap concat . mapM decKeyBinary
|
|
||||||
where
|
|
||||||
decKeyBinary :: Name -> DecsQ
|
|
||||||
decKeyBinary (conT -> t)
|
|
||||||
= [d| instance Binary $(t) where
|
|
||||||
get = $(varE 'toSqlKey) <$> get
|
|
||||||
put = put . $(varE 'fromSqlKey)
|
|
||||||
instance HasFixedSerializationLength $(t) where
|
|
||||||
type SerializationLength $(t) = SerializationLength Int64
|
|
||||||
|]
|
|
||||||
|
|||||||
@ -8,6 +8,7 @@
|
|||||||
{-# LANGUAGE RecordWildCards #-}
|
{-# LANGUAGE RecordWildCards #-}
|
||||||
{-# LANGUAGE NamedFieldPuns #-}
|
{-# LANGUAGE NamedFieldPuns #-}
|
||||||
{-# LANGUAGE PatternGuards #-}
|
{-# LANGUAGE PatternGuards #-}
|
||||||
|
{-# LANGUAGE FlexibleInstances, UndecidableInstances #-}
|
||||||
|
|
||||||
module Foundation where
|
module Foundation where
|
||||||
|
|
||||||
@ -48,8 +49,6 @@ import Data.Conduit.List (sourceList)
|
|||||||
import Control.Monad.Except (MonadError(..), runExceptT)
|
import Control.Monad.Except (MonadError(..), runExceptT)
|
||||||
import Handler.Utils.StudyFeatures
|
import Handler.Utils.StudyFeatures
|
||||||
|
|
||||||
import qualified Data.UUID.Cryptographic as UUID
|
|
||||||
import qualified System.FilePath.Cryptographic as FilePath
|
|
||||||
import System.FilePath
|
import System.FilePath
|
||||||
|
|
||||||
import Handler.Utils.Templates
|
import Handler.Utils.Templates
|
||||||
@ -78,7 +77,7 @@ data UniWorX = UniWorX
|
|||||||
--
|
--
|
||||||
-- This function also generates the following type synonyms:
|
-- This function also generates the following type synonyms:
|
||||||
-- type Handler = HandlerT UniWorX IO
|
-- type Handler = HandlerT UniWorX IO
|
||||||
-- type Widget = WidgetT UniWorX IO ()
|
-- type Widget = WidgetT UniWorX IO ()
|
||||||
mkYesodData "UniWorX" $(parseRoutesFile "routes")
|
mkYesodData "UniWorX" $(parseRoutesFile "routes")
|
||||||
|
|
||||||
data MenuItem = MenuItem
|
data MenuItem = MenuItem
|
||||||
@ -135,6 +134,7 @@ instance Yesod UniWorX where
|
|||||||
isAuthorized CourseListR _ = return Authorized
|
isAuthorized CourseListR _ = return Authorized
|
||||||
isAuthorized (CourseListTermR _) _ = return Authorized
|
isAuthorized (CourseListTermR _) _ = return Authorized
|
||||||
isAuthorized (CourseShowR _ _) _ = return Authorized
|
isAuthorized (CourseShowR _ _) _ = return Authorized
|
||||||
|
isAuthorized (CryptoUUIDDispatchR _) _ = return Authorized
|
||||||
isAuthorized SubmissionListR _ = isAuthenticated
|
isAuthorized SubmissionListR _ = isAuthenticated
|
||||||
isAuthorized SubmissionDownloadMultiArchiveR _ = isAuthenticated
|
isAuthorized SubmissionDownloadMultiArchiveR _ = isAuthenticated
|
||||||
-- isAuthorized TestR _ = return Authorized
|
-- isAuthorized TestR _ = return Authorized
|
||||||
@ -180,24 +180,24 @@ instance Yesod UniWorX where
|
|||||||
makeLogger = return . appLogger
|
makeLogger = return . appLogger
|
||||||
|
|
||||||
isAuthorizedDB :: Route UniWorX -> Bool -> YesodDB UniWorX AuthResult
|
isAuthorizedDB :: Route UniWorX -> Bool -> YesodDB UniWorX AuthResult
|
||||||
|
isAuthorizedDB UsersR _ = adminAccess Nothing
|
||||||
isAuthorizedDB (SubmissionR cID) _ = submissionAccess $ Right cID
|
isAuthorizedDB (SubmissionR cID) _ = submissionAccess $ Right cID
|
||||||
isAuthorizedDB (SubmissionDownloadSingleR cID _) _ = submissionAccess $ Right cID
|
isAuthorizedDB (SubmissionDownloadSingleR cID _) _ = submissionAccess $ Right cID
|
||||||
isAuthorizedDB (SubmissionDownloadArchiveR (splitExtension -> (baseName, _))) _ = submissionAccess . Left . CryptoID $ CI.mk baseName
|
isAuthorizedDB (SubmissionDownloadArchiveR (splitExtension -> (baseName, _))) _ = submissionAccess . Left . CryptoID $ CI.mk baseName
|
||||||
isAuthorizedDB TermEditR _ = adminAccess Nothing
|
isAuthorizedDB TermEditR _ = adminAccess Nothing
|
||||||
isAuthorizedDB (TermEditExistR _) _ = adminAccess Nothing
|
isAuthorizedDB (TermEditExistR _) _ = adminAccess Nothing
|
||||||
isAuthorizedDB CourseEditR _ = lecturerAccess Nothing
|
isAuthorizedDB CourseEditR _ = lecturerAccess Nothing
|
||||||
isAuthorizedDB (CourseEditExistR t c) _ = courseLecturerAccess . entityKey =<< getBy404 (CourseTermShort (TermKey t) c)
|
isAuthorizedDB (CourseEditExistR t c) _ = courseLecturerAccess . entityKey =<< getBy404 (CourseTermShort t c)
|
||||||
|
isAuthorizedDB (SheetNewR t c) _ = courseLecturerAccess . entityKey =<< getBy404 (CourseTermShort t c)
|
||||||
isAuthorizedDB (CourseEditExistIDR cID) _ = do
|
isAuthorizedDB (CourseEditExistIDR cID) _ = do
|
||||||
cIDKey <- getsYesod appCryptoIDKey
|
courseId <- decrypt cID
|
||||||
courseId <- UUID.decrypt cIDKey cID
|
|
||||||
courseLecturerAccess courseId
|
courseLecturerAccess courseId
|
||||||
isAuthorizedDB route isWrite = return $ Unauthorized "No access to this route." -- Calling isAuthorized here creates infinite loop!
|
isAuthorizedDB _route _isWrite = return $ Unauthorized "No access to this route." -- Calling isAuthorized here creates infinite loop!
|
||||||
|
|
||||||
submissionAccess :: Either CryptoFileNameSubmission CryptoUUIDSubmission -> YesodDB UniWorX AuthResult
|
submissionAccess :: Either CryptoFileNameSubmission CryptoUUIDSubmission -> YesodDB UniWorX AuthResult
|
||||||
submissionAccess cID = do
|
submissionAccess cID = do
|
||||||
authId <- lift requireAuthId
|
authId <- lift requireAuthId
|
||||||
cIDKey <- getsYesod appCryptoIDKey
|
submissionId <- either decrypt decrypt cID
|
||||||
submissionId <- either (FilePath.decrypt cIDKey) (UUID.decrypt cIDKey) cID
|
|
||||||
Submission{..} <- get404 submissionId
|
Submission{..} <- get404 submissionId
|
||||||
submissionUsers <- map (submissionUserUserId . entityVal) <$> selectList [SubmissionUserSubmissionId ==. submissionId] []
|
submissionUsers <- map (submissionUserUserId . entityVal) <$> selectList [SubmissionUserSubmissionId ==. submissionId] []
|
||||||
let auth = authId `elem` submissionUsers || Just authId == submissionRatingBy
|
let auth = authId `elem` submissionUsers || Just authId == submissionRatingBy
|
||||||
@ -244,11 +244,14 @@ instance YesodBreadcrumbs UniWorX where
|
|||||||
breadcrumb (TermEditExistR _) = return ("Editieren", Just TermShowR)
|
breadcrumb (TermEditExistR _) = return ("Editieren", Just TermShowR)
|
||||||
|
|
||||||
breadcrumb CourseListR = return ("Kurs", Just HomeR)
|
breadcrumb CourseListR = return ("Kurs", Just HomeR)
|
||||||
breadcrumb (CourseListTermR term) = return (termToText term, Just TermShowR)
|
breadcrumb (CourseListTermR term) = return (toPathPiece term, Just TermShowR)
|
||||||
breadcrumb (CourseShowR term course) = return (course, Just $ CourseListTermR term)
|
breadcrumb (CourseShowR term course) = return (course, Just $ CourseListTermR term)
|
||||||
breadcrumb CourseEditR = return ("Neu", Just CourseListR)
|
breadcrumb CourseEditR = return ("Neu", Just CourseListR)
|
||||||
breadcrumb (CourseEditExistR _ _) = return ("Editieren", Just CourseListR)
|
breadcrumb (CourseEditExistR _ _) = return ("Editieren", Just CourseListR)
|
||||||
|
|
||||||
|
breadcrumb (SheetListR tid csh) = return ("Kurs", Just $ CourseShowR tid csh)
|
||||||
|
breadcrumb (SheetShowR tid csh _shn) = return ("Übungen", Just $ SheetListR tid csh)
|
||||||
|
|
||||||
breadcrumb SubmissionListR = return ("Abgaben", Just HomeR)
|
breadcrumb SubmissionListR = return ("Abgaben", Just HomeR)
|
||||||
breadcrumb (SubmissionR _) = return ("Abgabe", Just SubmissionListR)
|
breadcrumb (SubmissionR _) = return ("Abgabe", Just SubmissionListR)
|
||||||
|
|
||||||
@ -270,6 +273,11 @@ defaultLinks = -- Define the menu items of the header.
|
|||||||
, menuItemRoute = CourseListR
|
, menuItemRoute = CourseListR
|
||||||
, menuItemAccessCallback = return True
|
, menuItemAccessCallback = return True
|
||||||
}
|
}
|
||||||
|
, NavbarRight $ MenuItem
|
||||||
|
{ menuItemLabel = "Users"
|
||||||
|
, menuItemRoute = UsersR
|
||||||
|
, menuItemAccessCallback = return True -- Creates a LOOP: (Authorized ==) <$> isAuthorized UsersR False
|
||||||
|
}
|
||||||
, NavbarRight $ MenuItem
|
, NavbarRight $ MenuItem
|
||||||
{ menuItemLabel = "Profile"
|
{ menuItemLabel = "Profile"
|
||||||
, menuItemRoute = ProfileR
|
, menuItemRoute = ProfileR
|
||||||
@ -393,7 +401,7 @@ instance YesodAuth UniWorX where
|
|||||||
authHttpManager = getHttpManager
|
authHttpManager = getHttpManager
|
||||||
|
|
||||||
ldapConfig :: UniWorX -> LDAPConfig
|
ldapConfig :: UniWorX -> LDAPConfig
|
||||||
ldapConfig app@(appSettings -> settings) = LDAPConfig
|
ldapConfig _app@(appSettings -> settings) = LDAPConfig
|
||||||
{ usernameFilter = \u -> principalName <> "=" <> u
|
{ usernameFilter = \u -> principalName <> "=" <> u
|
||||||
, identifierModifier
|
, identifierModifier
|
||||||
, ldapUri = appLDAPURI settings
|
, ldapUri = appLDAPURI settings
|
||||||
@ -434,6 +442,11 @@ instance HasHttpManager UniWorX where
|
|||||||
unsafeHandler :: UniWorX -> Handler a -> IO a
|
unsafeHandler :: UniWorX -> Handler a -> IO a
|
||||||
unsafeHandler = Unsafe.fakeHandlerGetLogger appLogger
|
unsafeHandler = Unsafe.fakeHandlerGetLogger appLogger
|
||||||
|
|
||||||
|
|
||||||
|
instance (MonadThrow m, MonadHandler m, HandlerSite m ~ UniWorX) => MonadCrypto m where
|
||||||
|
type MonadCryptoKey m = CryptoIDKey
|
||||||
|
cryptoIDKey f = getsYesod appCryptoIDKey >>= f
|
||||||
|
|
||||||
-- Note: Some functionality previously present in the scaffolding has been
|
-- Note: Some functionality previously present in the scaffolding has been
|
||||||
-- moved to documentation in the Wiki. Following are some hopefully helpful
|
-- moved to documentation in the Wiki. Following are some hopefully helpful
|
||||||
-- links:
|
-- links:
|
||||||
|
|||||||
@ -26,20 +26,20 @@ import qualified Data.UUID.Cryptographic as UUID
|
|||||||
getCourseListR :: Handler TypedContent
|
getCourseListR :: Handler TypedContent
|
||||||
getCourseListR = redirect TermShowR
|
getCourseListR = redirect TermShowR
|
||||||
|
|
||||||
getCourseListTermR :: TermIdentifier -> Handler Html
|
getCourseListTermR :: TermId -> Handler Html
|
||||||
getCourseListTermR tidini = do
|
getCourseListTermR tidini = do
|
||||||
(term,courses) <- runDB $ (,)
|
(term,courses) <- runDB $ (,)
|
||||||
<$> get (TermKey tidini)
|
<$> get tidini
|
||||||
<*> selectList [CourseTermId ==. TermKey tidini] [Asc CourseShorthand]
|
<*> selectList [CourseTermId ==. tidini] [Asc CourseShorthand]
|
||||||
when (isNothing term) $ do
|
when (isNothing term) $ do
|
||||||
addMessage "warning" [shamlet| Semester #{termToText tidini} nicht gefunden. |]
|
addMessage "warning" [shamlet| Semester #{toPathPiece tidini} nicht gefunden. |]
|
||||||
redirect TermShowR
|
redirect TermShowR
|
||||||
-- TODO: several runDBs per TableRow are probably too inefficient!
|
-- TODO: several runDBs per TableRow are probably too inefficient!
|
||||||
let colonnadeTerms = mconcat
|
let colonnadeTerms = mconcat
|
||||||
[ headed "Kürzel" $ (\ckv ->
|
[ headed "Kürzel" $ (\ckv ->
|
||||||
let c = entityVal ckv
|
let c = entityVal ckv
|
||||||
shd = courseShorthand c
|
shd = courseShorthand c
|
||||||
tid = unTermKey $ courseTermId c
|
tid = courseTermId c
|
||||||
in [whamlet| <a href=@{CourseShowR tid shd}>#{shd} |] )
|
in [whamlet| <a href=@{CourseShowR tid shd}>#{shd} |] )
|
||||||
-- , headed "Institut" $ [shamlet| #{course} |]
|
-- , headed "Institut" $ [shamlet| #{course} |]
|
||||||
, headed "Beginn Anmeldung" $ fromString.(maybe "" formatTimeGerWD).courseRegisterFrom.entityVal
|
, headed "Beginn Anmeldung" $ fromString.(maybe "" formatTimeGerWD).courseRegisterFrom.entityVal
|
||||||
@ -52,7 +52,7 @@ getCourseListTermR tidini = do
|
|||||||
, headed " " $ (\ckv ->
|
, headed " " $ (\ckv ->
|
||||||
let c = entityVal ckv
|
let c = entityVal ckv
|
||||||
shd = courseShorthand c
|
shd = courseShorthand c
|
||||||
tid = unTermKey $ courseTermId c
|
tid = courseTermId c
|
||||||
in do
|
in do
|
||||||
adminLink <- handlerToWidget $ isAuthorized (CourseEditExistR tid shd ) False
|
adminLink <- handlerToWidget $ isAuthorized (CourseEditExistR tid shd ) False
|
||||||
-- if (adminLink==Authorized) then linkButton "Ändern" BCWarning (CourseEditExistR tid shd) else ""
|
-- if (adminLink==Authorized) then linkButton "Ändern" BCWarning (CourseEditExistR tid shd) else ""
|
||||||
@ -74,13 +74,13 @@ getCourseListTermR tidini = do
|
|||||||
-- defaultLayout $ do
|
-- defaultLayout $ do
|
||||||
setTitle "Semesterkurse"
|
setTitle "Semesterkurse"
|
||||||
linkButton "Neuen Kurs anlegen" BCPrimary CourseEditR
|
linkButton "Neuen Kurs anlegen" BCPrimary CourseEditR
|
||||||
encodeHeadedWidgetTable tableDefault colonnadeTerms courses -- (map entityVal courses)
|
encodeWidgetTable tableDefault colonnadeTerms courses -- (map entityVal courses)
|
||||||
|
|
||||||
getCourseShowR :: TermIdentifier -> Text -> Handler Html
|
getCourseShowR :: TermId -> Text -> Handler Html
|
||||||
getCourseShowR tid csh = do
|
getCourseShowR tid csh = do
|
||||||
mbAid <- maybeAuthId
|
mbAid <- maybeAuthId
|
||||||
(courseEnt,(schoolMB,participants,mbRegistered)) <- runDB $ do
|
(courseEnt,(schoolMB,participants,mbRegistered)) <- runDB $ do
|
||||||
courseEnt@(Entity cid course) <- getBy404 $ CourseTermShort (TermKey tid) csh
|
courseEnt@(Entity cid course) <- getBy404 $ CourseTermShort tid csh
|
||||||
dependent <- (,,)
|
dependent <- (,,)
|
||||||
<$> get (courseSchoolId course) -- join
|
<$> get (courseSchoolId course) -- join
|
||||||
<*> count [CourseParticipantCourseId ==. cid] -- join
|
<*> count [CourseParticipantCourseId ==. cid] -- join
|
||||||
@ -93,7 +93,7 @@ getCourseShowR tid csh = do
|
|||||||
let course = entityVal courseEnt
|
let course = entityVal courseEnt
|
||||||
(regWidget, regEnctype) <- generateFormPost $ identifyForm "registerBtn" $ registerButton $ mbRegistered
|
(regWidget, regEnctype) <- generateFormPost $ identifyForm "registerBtn" $ registerButton $ mbRegistered
|
||||||
defaultLayout $ do
|
defaultLayout $ do
|
||||||
setTitle $ [shamlet| #{termToText tid} - #{csh}|]
|
setTitle $ [shamlet| #{toPathPiece tid} - #{csh}|]
|
||||||
$(widgetFile "course")
|
$(widgetFile "course")
|
||||||
|
|
||||||
registerButton :: Bool -> Form ()
|
registerButton :: Bool -> Form ()
|
||||||
@ -104,11 +104,11 @@ registerButton registered =
|
|||||||
msg = if registered then "Abmelden" else "Anmelden"
|
msg = if registered then "Abmelden" else "Anmelden"
|
||||||
regMsg = msg :: BootstrapSubmit Text
|
regMsg = msg :: BootstrapSubmit Text
|
||||||
|
|
||||||
postCourseShowR :: TermIdentifier -> Text -> Handler Html
|
postCourseShowR :: TermId -> Text -> Handler Html
|
||||||
postCourseShowR tid csh = do
|
postCourseShowR tid csh = do
|
||||||
aid <- requireAuthId
|
aid <- requireAuthId
|
||||||
(cid, registered) <- runDB $ do
|
(cid, registered) <- runDB $ do
|
||||||
(Entity cid _) <- getBy404 $ CourseTermShort (TermKey tid) csh
|
(Entity cid _) <- getBy404 $ CourseTermShort tid csh
|
||||||
registered <- isJust <$> (getBy $ UniqueCourseParticipant cid aid)
|
registered <- isJust <$> (getBy $ UniqueCourseParticipant cid aid)
|
||||||
return (cid, registered)
|
return (cid, registered)
|
||||||
((regResult,_), _) <- runFormPost $ identifyForm "registerBtn" $ registerButton registered
|
((regResult,_), _) <- runFormPost $ identifyForm "registerBtn" $ registerButton registered
|
||||||
@ -133,9 +133,9 @@ getCourseEditR = do
|
|||||||
postCourseEditR :: Handler Html
|
postCourseEditR :: Handler Html
|
||||||
postCourseEditR = courseEditHandler Nothing
|
postCourseEditR = courseEditHandler Nothing
|
||||||
|
|
||||||
getCourseEditExistR :: TermIdentifier -> Text -> Handler Html
|
getCourseEditExistR :: TermId -> Text -> Handler Html
|
||||||
getCourseEditExistR tid csh = do
|
getCourseEditExistR tid csh = do
|
||||||
course <- runDB $ getBy $ CourseTermShort (TermKey tid) csh
|
course <- runDB $ getBy $ CourseTermShort tid csh
|
||||||
courseEditHandler course
|
courseEditHandler course
|
||||||
|
|
||||||
getCourseEditExistIDR :: CryptoUUIDCourse -> Handler Html
|
getCourseEditExistIDR :: CryptoUUIDCourse -> Handler Html
|
||||||
@ -155,12 +155,12 @@ courseEditHandler course = do
|
|||||||
| fAct == formActionDelete
|
| fAct == formActionDelete
|
||||||
, Just cid <- cfCourseId res -> do
|
, Just cid <- cfCourseId res -> do
|
||||||
runDB $ deleteCascade cid -- TODO Sicherheitsabfrage einbauen!
|
runDB $ deleteCascade cid -- TODO Sicherheitsabfrage einbauen!
|
||||||
let cti = termToText $ cfTerm res
|
let cti = toPathPiece $ cfTerm res
|
||||||
addMessage "info" [shamlet| Kurs #{cti}/#{cfShort res} wurde gelöscht!|]
|
addMessage "info" [shamlet| Kurs #{cti}/#{cfShort res} wurde gelöscht!|]
|
||||||
redirect $ CourseListTermR $ cfTerm res
|
redirect $ CourseListTermR $ cfTerm res
|
||||||
| fAct == formActionSave
|
| fAct == formActionSave
|
||||||
, Just cid <- cfCourseId res -> do
|
, Just cid <- cfCourseId res -> do
|
||||||
let tid = TermKey $ cfTerm res
|
let tid = cfTerm res
|
||||||
actTime <- liftIO getCurrentTime
|
actTime <- liftIO getCurrentTime
|
||||||
updateokay <- runDB $ do
|
updateokay <- runDB $ do
|
||||||
exists <- getBy $ CourseTermShort tid $ cfShort res
|
exists <- getBy $ CourseTermShort tid $ cfShort res
|
||||||
@ -179,7 +179,7 @@ courseEditHandler course = do
|
|||||||
, CourseChanged =. actTime
|
, CourseChanged =. actTime
|
||||||
]
|
]
|
||||||
return upokay
|
return upokay
|
||||||
let cti = termToText $ cfTerm res
|
let cti = toPathPiece $ cfTerm res
|
||||||
if updateokay
|
if updateokay
|
||||||
then do
|
then do
|
||||||
addMessage "info" [shamlet| Kurs #{cti}/#{cfShort res} wurde geändert. |]
|
addMessage "info" [shamlet| Kurs #{cti}/#{cfShort res} wurde geändert. |]
|
||||||
@ -195,7 +195,7 @@ courseEditHandler course = do
|
|||||||
, courseDescription = cfDesc res
|
, courseDescription = cfDesc res
|
||||||
, courseLinkExternal = cfLink res
|
, courseLinkExternal = cfLink res
|
||||||
, courseShorthand = cfShort res
|
, courseShorthand = cfShort res
|
||||||
, courseTermId = TermKey $ cfTerm res
|
, courseTermId = cfTerm res
|
||||||
, courseSchoolId = cfSchool res
|
, courseSchoolId = cfSchool res
|
||||||
, courseCapacity = cfCapacity res
|
, courseCapacity = cfCapacity res
|
||||||
, courseHasRegistration = cfHasReg res
|
, courseHasRegistration = cfHasReg res
|
||||||
@ -209,11 +209,11 @@ courseEditHandler course = do
|
|||||||
case insertOkay of
|
case insertOkay of
|
||||||
(Just cid) -> do
|
(Just cid) -> do
|
||||||
runDB $ insert_ $ Lecturer aid cid
|
runDB $ insert_ $ Lecturer aid cid
|
||||||
let cti = termToText $ cfTerm res
|
let cti = toPathPiece $ cfTerm res
|
||||||
addMessage "info" [shamlet|Kurs #{cti}/#{cfShort res} wurde angelegt.|]
|
addMessage "info" [shamlet|Kurs #{cti}/#{cfShort res} wurde angelegt.|]
|
||||||
redirect $ CourseListTermR $ cfTerm res
|
redirect $ CourseListTermR $ cfTerm res
|
||||||
Nothing -> do
|
Nothing -> do
|
||||||
let cti = termToText $ cfTerm res
|
let cti = toPathPiece $ cfTerm res
|
||||||
addMessage "danger" [shamlet|Es gibt bereits einen Kurs #{cfShort res} in Semester #{cti}.|]
|
addMessage "danger" [shamlet|Es gibt bereits einen Kurs #{cfShort res} in Semester #{cti}.|]
|
||||||
(FormFailure _,_) -> addMessage "warning" "Bitte Eingabe korrigieren."
|
(FormFailure _,_) -> addMessage "warning" "Bitte Eingabe korrigieren."
|
||||||
_other -> return ()
|
_other -> return ()
|
||||||
@ -231,7 +231,7 @@ data CourseForm = CourseForm
|
|||||||
, cfDesc :: Maybe Html
|
, cfDesc :: Maybe Html
|
||||||
, cfLink :: Maybe Text
|
, cfLink :: Maybe Text
|
||||||
, cfShort :: Text
|
, cfShort :: Text
|
||||||
, cfTerm :: TermIdentifier
|
, cfTerm :: TermId
|
||||||
, cfSchool :: SchoolId
|
, cfSchool :: SchoolId
|
||||||
, cfCapacity :: Maybe Int
|
, cfCapacity :: Maybe Int
|
||||||
, cfHasReg :: Bool
|
, cfHasReg :: Bool
|
||||||
@ -250,7 +250,7 @@ courseToForm cEntity = CourseForm
|
|||||||
, cfDesc = courseDescription course
|
, cfDesc = courseDescription course
|
||||||
, cfLink = courseLinkExternal course
|
, cfLink = courseLinkExternal course
|
||||||
, cfShort = courseShorthand course
|
, cfShort = courseShorthand course
|
||||||
, cfTerm = unTermKey $ courseTermId course
|
, cfTerm = courseTermId course
|
||||||
, cfSchool = courseSchoolId course
|
, cfSchool = courseSchoolId course
|
||||||
, cfCapacity = courseCapacity course
|
, cfCapacity = courseCapacity course
|
||||||
, cfHasReg = courseHasRegistration course
|
, cfHasReg = courseHasRegistration course
|
||||||
@ -277,8 +277,8 @@ newCourseForm template = identForm FIDcourse $ \html -> do
|
|||||||
-- & addAttr "disabled" "disabled"
|
-- & addAttr "disabled" "disabled"
|
||||||
& setTooltip "Muss innerhalb des Semesters eindeutig sein")
|
& setTooltip "Muss innerhalb des Semesters eindeutig sein")
|
||||||
(cfShort <$> template)
|
(cfShort <$> template)
|
||||||
<*> areq termExistsField (fsb "Semester") (cfTerm <$> template)
|
<*> areq termActiveField (fsb "Semester") (cfTerm <$> template)
|
||||||
<*> areq (selectField schools) (fsb "Institut") (cfSchool <$> template)
|
<*> areq schoolField (fsb "Institut") (cfSchool <$> template)
|
||||||
<*> aopt (natField "Kapazität") (fsb "Kapazität") (cfCapacity <$> template)
|
<*> aopt (natField "Kapazität") (fsb "Kapazität") (cfCapacity <$> template)
|
||||||
<*> areq checkBoxField (fsb "Anmeldung") (cfHasReg <$> template)
|
<*> areq checkBoxField (fsb "Anmeldung") (cfHasReg <$> template)
|
||||||
<*> aopt utcTimeField (fsb "Anmeldung von:") (cfRegFrom <$> template)
|
<*> aopt utcTimeField (fsb "Anmeldung von:") (cfRegFrom <$> template)
|
||||||
@ -299,15 +299,11 @@ newCourseForm template = identForm FIDcourse $ \html -> do
|
|||||||
|]
|
|]
|
||||||
)
|
)
|
||||||
_ -> (result, widget)
|
_ -> (result, widget)
|
||||||
where
|
-- where
|
||||||
-- cid :: Maybe CourseId
|
-- cid :: Maybe CourseId
|
||||||
-- cid = join $ cfCourseId <$> template
|
-- cid = join $ cfCourseId <$> template
|
||||||
--
|
|
||||||
-- schools :: GHandler UniWorX UniWorX (OptionList SchoolId)
|
|
||||||
schools = do
|
|
||||||
entities <- runDB $ selectList [] [Asc SchoolShorthand]
|
|
||||||
optionsPairs $ map (\school -> (schoolShorthand $ entityVal school, entityKey school)) entities
|
|
||||||
|
|
||||||
validateCourse :: CourseForm -> [Text]
|
validateCourse :: CourseForm -> [Text]
|
||||||
validateCourse (CourseForm{..}) =
|
validateCourse (CourseForm{..}) =
|
||||||
[ msg | (False, msg) <-
|
[ msg | (False, msg) <-
|
||||||
|
|||||||
62
src/Handler/CryptoIDDispatch.hs
Normal file
62
src/Handler/CryptoIDDispatch.hs
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
{-# LANGUAGE NoImplicitPrelude
|
||||||
|
, DataKinds
|
||||||
|
, KindSignatures
|
||||||
|
, TypeFamilies
|
||||||
|
, FlexibleInstances
|
||||||
|
, TypeOperators
|
||||||
|
, RankNTypes
|
||||||
|
, PolyKinds
|
||||||
|
, RecordWildCards
|
||||||
|
, MultiParamTypeClasses
|
||||||
|
, ScopedTypeVariables
|
||||||
|
, ViewPatterns
|
||||||
|
#-}
|
||||||
|
|
||||||
|
module Handler.CryptoIDDispatch
|
||||||
|
( getCryptoUUIDDispatchR
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Import hiding (Proxy)
|
||||||
|
|
||||||
|
import Data.Proxy
|
||||||
|
|
||||||
|
import Yesod.Core.Types (HandlerContents(..), ErrorResponse(..))
|
||||||
|
|
||||||
|
import qualified Control.Monad.Catch as E (Handler(..))
|
||||||
|
|
||||||
|
|
||||||
|
class CryptoRoute ciphertext plaintext where
|
||||||
|
cryptoIDRoute :: p plaintext -> ciphertext -> Handler (Route UniWorX)
|
||||||
|
|
||||||
|
instance CryptoRoute UUID SubmissionId where
|
||||||
|
cryptoIDRoute _ (CryptoID -> cID) = do
|
||||||
|
(_ :: SubmissionId) <- decrypt cID
|
||||||
|
|
||||||
|
return $ SubmissionR cID
|
||||||
|
|
||||||
|
|
||||||
|
class Dispatch ciphertext (x :: [*]) where
|
||||||
|
dispatchID :: p x -> ciphertext -> Handler (Maybe (Route UniWorX))
|
||||||
|
|
||||||
|
instance Dispatch ciphertext '[] where
|
||||||
|
dispatchID _ _ = return Nothing
|
||||||
|
|
||||||
|
instance (CryptoRoute ciphertext plaintext, Dispatch ciphertext ps) => Dispatch ciphertext (plaintext ': ps) where
|
||||||
|
dispatchID _ ciphertext = (<|>) <$> dispatchHead <*> dispatchTail
|
||||||
|
where
|
||||||
|
dispatchHead = (Just <$> cryptoIDRoute (Proxy :: Proxy plaintext) ciphertext) `catches` [ E.Handler handleHCError, E.Handler handleCryptoID ]
|
||||||
|
where
|
||||||
|
handleHCError :: HandlerContents -> Handler (Maybe a)
|
||||||
|
handleHCError (HCError NotFound) = return Nothing
|
||||||
|
handleHCError e = throwM e
|
||||||
|
handleCryptoID :: CryptoIDError -> Handler (Maybe a)
|
||||||
|
handleCryptoID _ = return Nothing
|
||||||
|
dispatchTail = dispatchID (Proxy :: Proxy ps) ciphertext
|
||||||
|
|
||||||
|
|
||||||
|
getCryptoUUIDDispatchR :: UUID -> Handler ()
|
||||||
|
getCryptoUUIDDispatchR uuid = dispatchID p uuid >>= maybe notFound (redirectWith found302)
|
||||||
|
where
|
||||||
|
p :: Proxy '[ SubmissionId
|
||||||
|
]
|
||||||
|
p = Proxy
|
||||||
@ -1,67 +0,0 @@
|
|||||||
{-# LANGUAGE NoImplicitPrelude #-}
|
|
||||||
{-# LANGUAGE OverloadedStrings #-}
|
|
||||||
{-# LANGUAGE TemplateHaskell #-}
|
|
||||||
{-# LANGUAGE MultiParamTypeClasses #-}
|
|
||||||
{-# LANGUAGE TypeFamilies #-}
|
|
||||||
module Handler.Home where
|
|
||||||
|
|
||||||
import Import
|
|
||||||
import Yesod.Form.Bootstrap3 (BootstrapFormLayout (..), renderBootstrap3)
|
|
||||||
import Text.Julius (RawJS (..))
|
|
||||||
|
|
||||||
-- Define our data that will be used for creating the form.
|
|
||||||
data FileForm = FileForm
|
|
||||||
{ fileInfo :: FileInfo
|
|
||||||
, fileDescription :: Text
|
|
||||||
}
|
|
||||||
|
|
||||||
-- This is a handler function for the GET request method on the HomeR
|
|
||||||
-- resource pattern. All of your resource patterns are defined in
|
|
||||||
-- config/routes
|
|
||||||
--
|
|
||||||
-- The majority of the code you will write in Yesod lives in these handler
|
|
||||||
-- functions. You can spread them across multiple files if you are so
|
|
||||||
-- inclined, or create a single monolithic file.
|
|
||||||
getHomeR :: Handler Html
|
|
||||||
getHomeR = do
|
|
||||||
(formWidget, formEnctype) <- generateFormPost sampleForm
|
|
||||||
let submission = Nothing :: Maybe FileForm
|
|
||||||
handlerName = "getHomeR" :: Text
|
|
||||||
defaultLayout $ do
|
|
||||||
let (commentFormId, commentTextareaId, commentListId) = commentIds
|
|
||||||
aDomId <- newIdent
|
|
||||||
setTitle "Welcome To Yesod!"
|
|
||||||
$(widgetFile "homepage")
|
|
||||||
|
|
||||||
postHomeR :: Handler Html
|
|
||||||
postHomeR = do
|
|
||||||
((result, formWidget), formEnctype) <- runFormPost sampleForm
|
|
||||||
let handlerName = "postHomeR" :: Text
|
|
||||||
submission = case result of
|
|
||||||
FormSuccess res -> Just res
|
|
||||||
_ -> Nothing
|
|
||||||
|
|
||||||
defaultLayout $ do
|
|
||||||
let (commentFormId, commentTextareaId, commentListId) = commentIds
|
|
||||||
aDomId <- newIdent
|
|
||||||
setTitle "Welcome To Yesod!"
|
|
||||||
$(widgetFile "homepage")
|
|
||||||
|
|
||||||
sampleForm :: Form FileForm
|
|
||||||
sampleForm = renderBootstrap3 BootstrapBasicForm $ FileForm
|
|
||||||
<$> fileAFormReq "Choose a file"
|
|
||||||
<*> areq textField textSettings Nothing
|
|
||||||
-- Add attributes like the placeholder and CSS classes.
|
|
||||||
where textSettings = FieldSettings
|
|
||||||
{ fsLabel = "What's on the file?"
|
|
||||||
, fsTooltip = Nothing
|
|
||||||
, fsId = Nothing
|
|
||||||
, fsName = Nothing
|
|
||||||
, fsAttrs =
|
|
||||||
[ ("class", "form-control")
|
|
||||||
, ("placeholder", "File description")
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
commentIds :: (Text, Text, Text)
|
|
||||||
commentIds = ("js-commentForm", "js-createCommentTextarea", "js-commentList")
|
|
||||||
@ -1,3 +1,6 @@
|
|||||||
|
{-# LANGUAGE NoImplicitPrelude #-}
|
||||||
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
{-# LANGUAGE RecordWildCards #-}
|
||||||
{-# LANGUAGE TemplateHaskell #-}
|
{-# LANGUAGE TemplateHaskell #-}
|
||||||
{-# LANGUAGE QuasiQuotes #-}
|
{-# LANGUAGE QuasiQuotes #-}
|
||||||
{-# LANGUAGE MultiParamTypeClasses #-}
|
{-# LANGUAGE MultiParamTypeClasses #-}
|
||||||
@ -8,17 +11,23 @@ module Handler.Sheet where
|
|||||||
|
|
||||||
import Import
|
import Import
|
||||||
import Handler.Utils
|
import Handler.Utils
|
||||||
|
import Handler.Utils.Zip
|
||||||
|
|
||||||
|
|
||||||
-- import Data.Time
|
-- import Data.Time
|
||||||
-- import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
-- import Data.Function ((&))
|
-- import Data.Function ((&))
|
||||||
-- import Yesod.Form.Bootstrap3
|
import Yesod.Form.Bootstrap3
|
||||||
--
|
--
|
||||||
-- import Colonnade hiding (fromMaybe)
|
import Colonnade -- hiding (fromMaybe)
|
||||||
-- import Yesod.Colonnade
|
import Yesod.Colonnade
|
||||||
--
|
--
|
||||||
-- import qualified Data.UUID.Cryptographic as UUID
|
import qualified Data.UUID.Cryptographic as UUID
|
||||||
|
import qualified Data.Conduit.List as C
|
||||||
|
|
||||||
|
import qualified Database.Esqueleto as E
|
||||||
|
|
||||||
|
import Network.Mime
|
||||||
|
|
||||||
{-
|
{-
|
||||||
* Implement Handlers
|
* Implement Handlers
|
||||||
@ -26,23 +35,203 @@ import Handler.Utils
|
|||||||
* Implement Access in Foundation
|
* Implement Access in Foundation
|
||||||
-}
|
-}
|
||||||
|
|
||||||
|
data SheetForm = SheetForm
|
||||||
|
{ sfName :: Text
|
||||||
|
, sfComment :: Maybe Html
|
||||||
|
, sfType :: SheetType
|
||||||
|
, sfMarkingText :: Maybe Html
|
||||||
|
, sfActiveFrom :: UTCTime
|
||||||
|
, sfActiveTo :: UTCTime
|
||||||
|
, sfSheetF :: Maybe FileInfo
|
||||||
|
, sfHintFrom :: Maybe UTCTime
|
||||||
|
, sfHintF :: Maybe FileInfo
|
||||||
|
, sfSolutionFrom :: Maybe UTCTime
|
||||||
|
, sfSolutionF :: Maybe FileInfo
|
||||||
|
}
|
||||||
|
|
||||||
getSheetListR :: TermIdentifier -> Text -> Handler Html
|
|
||||||
getSheetListR _ _ = defaultLayout [whamlet| Under Construction !!! |] -- TODO
|
|
||||||
|
|
||||||
getSheetNewR :: TermIdentifier -> Text -> Handler Html
|
makeSheetForm :: CourseId -> Maybe SheetForm -> Form SheetForm
|
||||||
getSheetNewR _ _ = defaultLayout [whamlet| Under Construction !!! |] -- TODO
|
makeSheetForm cid template = identForm FIDsheet $ \html -> do
|
||||||
|
-- TODO: Yesod.Form.MassInput.inputList arbeitet Server-seitig :(
|
||||||
|
-- Erstmal nur mit ZIP arbeiten
|
||||||
|
(result, widget) <- flip (renderBootstrap3 bsHorizontalDefault) html $ SheetForm
|
||||||
|
<$> areq textField (fsb "Name") (sfName <$> template)
|
||||||
|
<*> aopt htmlField (fsb "Hinweise für Teilnehmer") (sfMarkingText <$> template)
|
||||||
|
<*> sheetTypeAFormReq (fsb "Bewertung") (sfType <$> template)
|
||||||
|
--TODO: SICHTBARKEIT hinzunehmen
|
||||||
|
<*> aopt htmlField (fsb "Hinweise für Korrektoren") (sfMarkingText <$> template)
|
||||||
|
<*> areq utcTimeField (fsb "Abgabe ab") (sfActiveFrom <$> template)
|
||||||
|
<*> areq utcTimeField (fsb "Abgabefrist") (sfActiveTo <$> template)
|
||||||
|
<*> fileAFormOpt (fsb "Aufgaben")
|
||||||
|
<*> aopt utcTimeField (fsb "Hinweis ab") (sfHintFrom <$> template)
|
||||||
|
<*> fileAFormOpt (fsb "Hinweis")
|
||||||
|
<*> aopt utcTimeField (fsb "Lösung ab") (sfSolutionFrom <$> template)
|
||||||
|
<*> fileAFormOpt (fsb "Lösung")
|
||||||
|
return $ case result of
|
||||||
|
FormSuccess sheetResult
|
||||||
|
| errorMsgs <- validateSheet sheetResult
|
||||||
|
, not $ null errorMsgs ->
|
||||||
|
(FormFailure errorMsgs,
|
||||||
|
[whamlet|
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
<h4> Fehler:
|
||||||
|
<ul>
|
||||||
|
$forall errmsg <- errorMsgs
|
||||||
|
<li> #{errmsg}
|
||||||
|
^{widget}
|
||||||
|
|]
|
||||||
|
)
|
||||||
|
_ -> (result, widget)
|
||||||
|
where
|
||||||
|
validateSheet _ = [] -- TODO
|
||||||
|
|
||||||
getSheetShowR :: TermIdentifier -> Text -> SheetId -> Handler Html
|
|
||||||
getSheetShowR _ _ _ = defaultLayout [whamlet| Under Construction !!! |] -- TODO
|
|
||||||
|
|
||||||
getSheetEditR :: TermIdentifier -> Text -> SheetId -> Handler Html
|
fetchSheet :: TermId -> Text -> Text -> YesodDB UniWorX (Entity Sheet)
|
||||||
|
fetchSheet tid csh shn = do
|
||||||
|
-- TODO: More efficient with Esquleto?
|
||||||
|
(Entity cid _course) <- getBy404 $ CourseTermShort tid csh
|
||||||
|
getBy404 $ CourseSheet cid shn
|
||||||
|
|
||||||
|
-- List Sheets
|
||||||
|
getSheetListCID :: CourseId -> Handler Html
|
||||||
|
getSheetListCID cid = getSheetList =<<
|
||||||
|
(Entity cid) <$> (runDB $ get404 cid)
|
||||||
|
|
||||||
|
getSheetListR :: TermId -> Text -> Handler Html
|
||||||
|
getSheetListR tid csh = getSheetList =<<
|
||||||
|
(runDB $ getBy404 $ CourseTermShort tid csh)
|
||||||
|
|
||||||
|
getSheetList :: Entity Course -> Handler Html
|
||||||
|
getSheetList courseEnt = do
|
||||||
|
-- mbAid <- maybeAuthId
|
||||||
|
let cid = entityKey courseEnt
|
||||||
|
let course = entityVal courseEnt
|
||||||
|
let csh = courseShorthand course
|
||||||
|
let tid = courseTermId course
|
||||||
|
sheets <- runDB $ do
|
||||||
|
rawSheets <- selectList [SheetCourseId ==. cid] [Desc SheetActiveFrom]
|
||||||
|
forM rawSheets $ \(Entity sid sheet) -> do
|
||||||
|
let sheetsub = [SubmissionSheetId ==. sid]
|
||||||
|
submissions <- count sheetsub
|
||||||
|
rated <- count $ (SubmissionRatingTime !=. Nothing):sheetsub
|
||||||
|
return (sid, sheet, (submissions, rated))
|
||||||
|
let colSheets = mconcat
|
||||||
|
[ headed "Blatt" $ toWgt . sheetName . snd3
|
||||||
|
, headed "Abgabe ab" $ toWgt . formatTimeGerWD . sheetActiveFrom . snd3
|
||||||
|
, headed "Abgabe bis" $ toWgt . formatTimeGerWD . sheetActiveTo . snd3
|
||||||
|
, headed "Bewertung" $ toWgt . show . sheetType . snd3
|
||||||
|
, headed "Korrigiert" $ toWgt . snd . trd3
|
||||||
|
, headed "Eingereicht" $ toWgt . fst . trd3
|
||||||
|
-- TODO: only show edit button for allowed course assistants
|
||||||
|
, headed "" $ \s -> linkButton "Edit" BCLink $ SheetEditR tid csh $ fst3 s
|
||||||
|
]
|
||||||
|
defaultLayout $ do
|
||||||
|
setTitle $ toHtml $ T.append "Übungsblätter " csh
|
||||||
|
if null sheets
|
||||||
|
then [whamlet|Es wurden noch keine Übungsblätter angelegt.|]
|
||||||
|
else encodeWidgetTable tableDefault colSheets sheets
|
||||||
|
|
||||||
|
-- Show single sheet
|
||||||
|
getSheetShowR :: TermId -> Text -> Text -> Handler Html
|
||||||
|
getSheetShowR tid csh shn = getSheetShow =<<
|
||||||
|
(runDB $ fetchSheet tid csh shn)
|
||||||
|
|
||||||
|
{- Nur per UUID
|
||||||
|
getSheetIdShowR :: SheetId -> Handler Html
|
||||||
|
getSheetIdShowR sheetId = getSheetShow =<<
|
||||||
|
(Entity sheetId) <$> (runDB $ get404 sheetId)\
|
||||||
|
-}{-
|
||||||
|
getSheetUUIDShowR :: CryptoUUIDSheet -> Handler Html
|
||||||
|
getSheetUUIDShowR sUUID = do
|
||||||
|
cIDKey <- getsYesod appCryptoIDKey
|
||||||
|
sheetId <- UUID.decrypt cIDKey sUUID
|
||||||
|
sheetEnt <- runDB $ get404 sheetId
|
||||||
|
getSheetShow $ Entity sheetId sheetEnt
|
||||||
|
-}
|
||||||
|
|
||||||
|
getSheetShow :: (Entity Sheet) -> Handler Html
|
||||||
|
getSheetShow entSheet = do
|
||||||
|
let sheet = entityVal entSheet
|
||||||
|
defaultLayout $ do
|
||||||
|
setTitle $ toHtml $ T.append "Übung " $ sheetName sheet
|
||||||
|
[whamlet| Under Construction !!! |] -- TODO
|
||||||
|
$(widgetFile "sheetAdmin")
|
||||||
|
|
||||||
|
getSheetFileR :: TermId -> Text -> Text -> SheetFileType -> FilePath -> Handler TypedContent
|
||||||
|
getSheetFileR tid csh shn typ title = do
|
||||||
|
content <- runDB $ E.select $ E.from $
|
||||||
|
\(course `E.InnerJoin` sheet `E.InnerJoin` sheetFile `E.InnerJoin` file) -> do
|
||||||
|
-- Restrict to consistent rows that correspond to each other
|
||||||
|
E.on (file E.^. FileId E.==. sheetFile E.^. SheetFileFileId)
|
||||||
|
E.on (sheetFile E.^. SheetFileSheetId E.==. sheet E.^. SheetId)
|
||||||
|
E.on (sheet E.^. SheetCourseId E.==. course E.^. CourseId)
|
||||||
|
-- filter to requested file
|
||||||
|
E.where_ ((file E.^. FileTitle E.==. E.val title)
|
||||||
|
E.&&. (sheetFile E.^. SheetFileType E.==. E.val typ )
|
||||||
|
E.&&. (sheet E.^. SheetName E.==. E.val shn )
|
||||||
|
E.&&. (course E.^. CourseShorthand E.==. E.val csh )
|
||||||
|
E.&&. (course E.^. CourseTermId E.==. E.val tid )
|
||||||
|
)
|
||||||
|
-- return desired columns
|
||||||
|
return $ file E.^. FileContent
|
||||||
|
let mimeType = defaultMimeLookup $ pack title
|
||||||
|
case content of
|
||||||
|
[E.Value (Just nochmalContent)] -> do
|
||||||
|
addHeader "Content-Disposition" "attachment"
|
||||||
|
respond mimeType nochmalContent
|
||||||
|
[] -> notFound
|
||||||
|
_other -> error "Multiple matching files found."
|
||||||
|
|
||||||
|
getSheetNewR :: TermId -> Text -> Handler Html
|
||||||
|
getSheetNewR tid csh = do
|
||||||
|
(Entity cid course) <- runDB $ getBy404 $ CourseTermShort tid csh
|
||||||
|
let template = Nothing -- TODO: provide convenience by interpolating name/nr/dates+7days
|
||||||
|
((res,wdgt), enc) <- runFormPost $ makeSheetForm cid template
|
||||||
|
|
||||||
|
case res of
|
||||||
|
(FormSuccess SheetForm{..}) -> do
|
||||||
|
|
||||||
|
|
||||||
|
let sid = undefined -- TODO after first insert
|
||||||
|
let sname = undefined -- TODO after first insert
|
||||||
|
|
||||||
|
-- Prüfe, das FileTitle innerhalb des Sheets eindeutig ist für diesen SheetFileTpye
|
||||||
|
whenIsJust sfSheetF $ \sinfo -> do
|
||||||
|
let sheetInsert file = do
|
||||||
|
fid <- insert file
|
||||||
|
void . insert $ SheetFile sid fid SheetExercise
|
||||||
|
runDB . runConduit $ (sourceFiles sinfo) =$= C.mapM_ sheetInsert
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
addMessage "info" "Blatt angelegt"
|
||||||
|
redirect $ SheetShowR tid csh sname
|
||||||
|
(FormFailure msgs) -> forM_ msgs $ (addMessage "warning") . toHtml
|
||||||
|
_ -> return ()
|
||||||
|
defaultLayout $ do
|
||||||
|
$(widgetFile "newSheet")
|
||||||
|
|
||||||
|
postSheetNewR :: TermId -> Text -> Handler Html
|
||||||
|
postSheetNewR = getSheetNewR
|
||||||
|
|
||||||
|
getSheetEditR :: TermId -> Text -> SheetId -> Handler Html
|
||||||
getSheetEditR _ _ _ = defaultLayout [whamlet| Under Construction !!! |] -- TODO
|
getSheetEditR _ _ _ = defaultLayout [whamlet| Under Construction !!! |] -- TODO
|
||||||
|
|
||||||
|
postSheetEditR :: TermId -> Text -> SheetId -> Handler Html
|
||||||
|
postSheetEditR _ _ _ = defaultLayout [whamlet| Under Construction !!! |] -- TODO
|
||||||
|
|
||||||
|
|
||||||
|
getSheetDelR :: TermId -> Text -> SheetId -> Handler Html
|
||||||
|
getSheetDelR _ _ _ = defaultLayout [whamlet| Under Construction !!! |] -- TODO
|
||||||
|
-- Sicherheitsabfrage
|
||||||
|
|
||||||
|
postSheetDelR :: TermId -> Text -> SheetId -> Handler Html
|
||||||
|
postSheetDelR _ _ _ = defaultLayout [whamlet| Under Construction !!! |] -- TODO
|
||||||
|
-- Tatsächlich löschen
|
||||||
|
|
||||||
|
|
||||||
{-
|
{-
|
||||||
getCourseShowR :: TermIdentifier -> Text -> Handler Html
|
getCourseShowR :: TermId -> Text -> Handler Html
|
||||||
getCourseShowR tid csh = do
|
getCourseShowR tid csh = do
|
||||||
mbAid <- maybeAuthId
|
mbAid <- maybeAuthId
|
||||||
(courseEnt,(schoolMB,participants,mbRegistered)) <- runDB $ do
|
(courseEnt,(schoolMB,participants,mbRegistered)) <- runDB $ do
|
||||||
|
|||||||
@ -27,8 +27,6 @@ import qualified Data.Text as Text
|
|||||||
import qualified Data.Text.Encoding as Text
|
import qualified Data.Text.Encoding as Text
|
||||||
import qualified Data.CaseInsensitive as CI
|
import qualified Data.CaseInsensitive as CI
|
||||||
|
|
||||||
import qualified Data.UUID.Cryptographic as UUID
|
|
||||||
|
|
||||||
import qualified Database.Esqueleto as E
|
import qualified Database.Esqueleto as E
|
||||||
|
|
||||||
import qualified Data.Conduit.List as Conduit
|
import qualified Data.Conduit.List as Conduit
|
||||||
@ -38,7 +36,6 @@ import Data.Map (Map)
|
|||||||
import qualified Data.Map as Map
|
import qualified Data.Map as Map
|
||||||
|
|
||||||
import System.FilePath
|
import System.FilePath
|
||||||
import qualified System.FilePath.Cryptographic as FilePath (decrypt, encrypt)
|
|
||||||
|
|
||||||
import Colonnade
|
import Colonnade
|
||||||
import Yesod.Colonnade
|
import Yesod.Colonnade
|
||||||
@ -52,12 +49,11 @@ submissionTable = do
|
|||||||
|
|
||||||
return (sub, sheet, course)
|
return (sub, sheet, course)
|
||||||
|
|
||||||
cIDKey <- getsYesod appCryptoIDKey
|
|
||||||
cryptedSubs <- forM subs $ \s@(Entity submissionId _, _, _) ->
|
cryptedSubs <- forM subs $ \s@(Entity submissionId _, _, _) ->
|
||||||
(,,) <$> FilePath.encrypt cIDKey submissionId <*> UUID.encrypt cIDKey submissionId <*> pure s
|
(,,) <$> encrypt submissionId <*> encrypt submissionId <*> pure s
|
||||||
|
|
||||||
let
|
let
|
||||||
anchorCourse (_, _, (_, _, Entity _ Course{..})) = CourseShowR (unTermKey courseTermId) courseShorthand
|
anchorCourse (_, _, (_, _, Entity _ Course{..})) = CourseShowR courseTermId courseShorthand
|
||||||
courseText (_, _, (_, _, Entity _ Course{..})) = toWidget courseName
|
courseText (_, _, (_, _, Entity _ Course{..})) = toWidget courseName
|
||||||
anchorSubmission (_, cUUID, _) = SubmissionR cUUID
|
anchorSubmission (_, cUUID, _) = SubmissionR cUUID
|
||||||
submissionText (cID, _, _) = toWidget . toPathPiece . CI.foldedCase $ ciphertext cID
|
submissionText (cID, _, _) = toWidget . toPathPiece . CI.foldedCase $ ciphertext cID
|
||||||
@ -69,7 +65,7 @@ submissionTable = do
|
|||||||
toExternal :: (CryptoFileNameSubmission, CryptoUUIDSubmission, a) -> Handler CryptoUUIDSubmission
|
toExternal :: (CryptoFileNameSubmission, CryptoUUIDSubmission, a) -> Handler CryptoUUIDSubmission
|
||||||
toExternal (_, cID, _) = return cID
|
toExternal (_, cID, _) = return cID
|
||||||
fromExternal :: CryptoUUIDSubmission -> Handler SubmissionId
|
fromExternal :: CryptoUUIDSubmission -> Handler SubmissionId
|
||||||
fromExternal = UUID.decrypt cIDKey
|
fromExternal = decrypt
|
||||||
headedRowSelector toExternal fromExternal (HA.class_ "table table-striped table-hover") colonnade cryptedSubs
|
headedRowSelector toExternal fromExternal (HA.class_ "table table-striped table-hover") colonnade cryptedSubs
|
||||||
|
|
||||||
|
|
||||||
@ -104,10 +100,9 @@ postSubmissionListR = do
|
|||||||
sinks <- execStateC Map.empty . awaitForever $ \case
|
sinks <- execStateC Map.empty . awaitForever $ \case
|
||||||
v@(Right (sId, _)) -> lift $ feed sId v
|
v@(Right (sId, _)) -> lift $ feed sId v
|
||||||
(Left f@File{..}) -> case splitDirectories fileTitle of
|
(Left f@File{..}) -> case splitDirectories fileTitle of
|
||||||
(cID:rest)
|
(cID:rest)
|
||||||
| not (null rest) -> do
|
| not (null rest) -> do
|
||||||
cIDKey <- getsYesod appCryptoIDKey
|
sId <- decrypt (CryptoID $ CI.mk cID :: CryptoFileNameSubmission)
|
||||||
sId <- FilePath.decrypt cIDKey (CryptoID $ CI.mk cID :: CryptoFileNameSubmission)
|
|
||||||
lift . feed sId $ Left f{ fileTitle = joinPath rest }
|
lift . feed sId $ Left f{ fileTitle = joinPath rest }
|
||||||
| otherwise -> return ()
|
| otherwise -> return ()
|
||||||
[] -> invalidArgs ["Encountered file/directory with empty name"]
|
[] -> invalidArgs ["Encountered file/directory with empty name"]
|
||||||
@ -121,9 +116,8 @@ postSubmissionListR = do
|
|||||||
|
|
||||||
getSubmissionDownloadSingleR :: CryptoUUIDSubmission -> FilePath -> Handler TypedContent
|
getSubmissionDownloadSingleR :: CryptoUUIDSubmission -> FilePath -> Handler TypedContent
|
||||||
getSubmissionDownloadSingleR cID path = do
|
getSubmissionDownloadSingleR cID path = do
|
||||||
cIDKey <- getsYesod appCryptoIDKey
|
submissionID <- decrypt cID
|
||||||
submissionID <- UUID.decrypt cIDKey cID
|
cID' <- encrypt submissionID
|
||||||
cID' <- FilePath.encrypt cIDKey submissionID
|
|
||||||
|
|
||||||
runDB $ do
|
runDB $ do
|
||||||
isRating <- maybe False (== submissionID) <$> isRatingFile path
|
isRating <- maybe False (== submissionID) <$> isRatingFile path
|
||||||
@ -172,9 +166,7 @@ postSubmissionDownloadMultiArchiveR = do
|
|||||||
let
|
let
|
||||||
fileEntitySource' :: (Rating, Entity Submission) -> Source (YesodDB UniWorX) File
|
fileEntitySource' :: (Rating, Entity Submission) -> Source (YesodDB UniWorX) File
|
||||||
fileEntitySource' (rating, Entity submissionID Submission{..}) = do
|
fileEntitySource' (rating, Entity submissionID Submission{..}) = do
|
||||||
cID <- lift $ do
|
cID <- encrypt submissionID
|
||||||
cIDKey <- getsYesod appCryptoIDKey
|
|
||||||
FilePath.encrypt cIDKey submissionID
|
|
||||||
|
|
||||||
let
|
let
|
||||||
directoryName = CI.foldedCase $ ciphertext (cID :: CryptoFileNameSubmission)
|
directoryName = CI.foldedCase $ ciphertext (cID :: CryptoFileNameSubmission)
|
||||||
@ -202,9 +194,8 @@ getSubmissionDownloadArchiveR path = do
|
|||||||
cID :: CryptoFileNameSubmission
|
cID :: CryptoFileNameSubmission
|
||||||
cID = CryptoID $ CI.mk baseName
|
cID = CryptoID $ CI.mk baseName
|
||||||
unless (ext == ".zip") notFound
|
unless (ext == ".zip") notFound
|
||||||
cIDKey <- getsYesod appCryptoIDKey
|
submissionID <- decrypt cID
|
||||||
submissionID <- FilePath.decrypt cIDKey cID
|
cUUID <- encrypt submissionID
|
||||||
cUUID <- UUID.encrypt cIDKey submissionID
|
|
||||||
respondSourceDB "application/zip" $ do
|
respondSourceDB "application/zip" $ do
|
||||||
rating <- lift $ getRating submissionID
|
rating <- lift $ getRating submissionID
|
||||||
case rating of
|
case rating of
|
||||||
@ -218,8 +209,7 @@ getSubmissionDownloadArchiveR path = do
|
|||||||
getSubmissionR, postSubmissionR :: CryptoUUIDSubmission -> Handler Html
|
getSubmissionR, postSubmissionR :: CryptoUUIDSubmission -> Handler Html
|
||||||
getSubmissionR = postSubmissionR
|
getSubmissionR = postSubmissionR
|
||||||
postSubmissionR cID = do
|
postSubmissionR cID = do
|
||||||
cIDKey <- getsYesod appCryptoIDKey
|
submissionId <- decrypt cID
|
||||||
submissionId <- UUID.decrypt cIDKey cID
|
|
||||||
|
|
||||||
((uploadResult, uploadWidget), uploadEnctype) <- runFormPost . renderBootstrap3 BootstrapBasicForm $ (,)
|
((uploadResult, uploadWidget), uploadEnctype) <- runFormPost . renderBootstrap3 BootstrapBasicForm $ (,)
|
||||||
<$> areq checkBoxField (bfs ("Dies ist eine Korrektur" :: Text)) (Just False)
|
<$> areq checkBoxField (bfs ("Dies ist eine Korrektur" :: Text)) (Just False)
|
||||||
@ -260,7 +250,7 @@ postSubmissionR cID = do
|
|||||||
, ratingTime = submissionRatingTime submission
|
, ratingTime = submissionRatingTime submission
|
||||||
}
|
}
|
||||||
|
|
||||||
cID' <- FilePath.encrypt cIDKey submissionId
|
cID' <- encrypt submissionId
|
||||||
let
|
let
|
||||||
archiveBaseName = CI.foldedCase $ ciphertext (cID' :: CryptoFileNameSubmission)
|
archiveBaseName = CI.foldedCase $ ciphertext (cID' :: CryptoFileNameSubmission)
|
||||||
archiveName = archiveBaseName <.> "zip"
|
archiveName = archiveBaseName <.> "zip"
|
||||||
|
|||||||
@ -39,12 +39,12 @@ getTermShowR = do
|
|||||||
provideRep $ return $ toJSON $ map fst termData
|
provideRep $ return $ toJSON $ map fst termData
|
||||||
provideRep $ do
|
provideRep $ do
|
||||||
let colonnadeTerms = mconcat
|
let colonnadeTerms = mconcat
|
||||||
[ headed "Kürzel" $ \(Entity _ Term{..},_) -> do
|
[ headed "Kürzel" $ \(Entity tid Term{..},_) -> do
|
||||||
-- Scrap this if to slow, create term edit page instead
|
-- Scrap this if to slow, create term edit page instead
|
||||||
adminLink <- handlerToWidget $ isAuthorized (TermEditExistR termName) False
|
adminLink <- handlerToWidget $ isAuthorized (TermEditExistR tid) False
|
||||||
[whamlet|
|
[whamlet|
|
||||||
$if adminLink == Authorized
|
$if adminLink == Authorized
|
||||||
<a href=@{TermEditExistR termName}>
|
<a href=@{TermEditExistR tid}>
|
||||||
#{termToText termName}
|
#{termToText termName}
|
||||||
$else
|
$else
|
||||||
#{termToText termName}
|
#{termToText termName}
|
||||||
@ -55,9 +55,9 @@ getTermShowR = do
|
|||||||
fromString $ formatTimeGerWD termLectureEnd
|
fromString $ formatTimeGerWD termLectureEnd
|
||||||
, headed "Aktiv" $ \(Entity _ Term{..},_) ->
|
, headed "Aktiv" $ \(Entity _ Term{..},_) ->
|
||||||
bool "" tickmark termActive
|
bool "" tickmark termActive
|
||||||
, headed "Kursliste" $ \(Entity _ Term{..}, E.Value numCourses) ->
|
, headed "Kursliste" $ \(Entity tid Term{..}, E.Value numCourses) ->
|
||||||
[whamlet|
|
[whamlet|
|
||||||
<a href=@{CourseListTermR termName}>
|
<a href=@{CourseListTermR tid}>
|
||||||
#{show numCourses} Kurse
|
#{show numCourses} Kurse
|
||||||
|]
|
|]
|
||||||
, headed "Semesteranfang" $ \(Entity _ Term{..},_) ->
|
, headed "Semesteranfang" $ \(Entity _ Term{..},_) ->
|
||||||
@ -69,7 +69,7 @@ getTermShowR = do
|
|||||||
]
|
]
|
||||||
defaultLayout $ do
|
defaultLayout $ do
|
||||||
setTitle "Freigeschaltete Semester"
|
setTitle "Freigeschaltete Semester"
|
||||||
encodeHeadedWidgetTable tableDefault colonnadeTerms termData
|
encodeWidgetTable tableDefault colonnadeTerms termData
|
||||||
|
|
||||||
|
|
||||||
getTermEditR :: Handler Html
|
getTermEditR :: Handler Html
|
||||||
@ -80,9 +80,9 @@ getTermEditR = do
|
|||||||
postTermEditR :: Handler Html
|
postTermEditR :: Handler Html
|
||||||
postTermEditR = termEditHandler Nothing
|
postTermEditR = termEditHandler Nothing
|
||||||
|
|
||||||
getTermEditExistR :: TermIdentifier -> Handler Html
|
getTermEditExistR :: TermId -> Handler Html
|
||||||
getTermEditExistR tid = do
|
getTermEditExistR tid = do
|
||||||
term <- runDB $ get $ TermKey tid
|
term <- runDB $ get tid
|
||||||
termEditHandler term
|
termEditHandler term
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
45
src/Handler/Users.hs
Normal file
45
src/Handler/Users.hs
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
{-# LANGUAGE NoImplicitPrelude #-}
|
||||||
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
{-# LANGUAGE TemplateHaskell #-}
|
||||||
|
{-# LANGUAGE MultiParamTypeClasses #-}
|
||||||
|
{-# LANGUAGE TypeFamilies #-}
|
||||||
|
|
||||||
|
module Handler.Users where
|
||||||
|
|
||||||
|
import Import
|
||||||
|
-- import Data.Text
|
||||||
|
import Handler.Utils
|
||||||
|
|
||||||
|
import Colonnade hiding (fromMaybe)
|
||||||
|
import Yesod.Colonnade
|
||||||
|
|
||||||
|
-- import qualified Database.Esqueleto as E
|
||||||
|
-- import Database.Esqueleto ((^.))
|
||||||
|
|
||||||
|
|
||||||
|
getUsersR :: Handler Html
|
||||||
|
getUsersR = do
|
||||||
|
-- TODO: Esqueleto, combine the two queries into one
|
||||||
|
(users,schools) <- runDB $ (,)
|
||||||
|
<$> (selectList [] [Asc UserDisplayName]
|
||||||
|
>>= mapM (\usr -> (,,)
|
||||||
|
<$> pure usr
|
||||||
|
<*> selectList [UserAdminUser ==. entityKey usr] [Asc UserAdminSchool]
|
||||||
|
<*> selectList [UserLecturerUser ==. entityKey usr] [Asc UserLecturerSchool]
|
||||||
|
))
|
||||||
|
<*> selectList [] [Asc SchoolShorthand]
|
||||||
|
let schoolnames = entities2map schools
|
||||||
|
let getSchoolname = \sid ->
|
||||||
|
case lookup sid schoolnames of
|
||||||
|
Nothing -> "???"
|
||||||
|
(Just school) -> schoolShorthand school
|
||||||
|
let colonnadeUsers = mconcat $
|
||||||
|
[ headed "User" $ text2widget.userDisplayName.entityVal.fst3
|
||||||
|
, headed "Admin for Schools" $ (\u -> text2widget $ intercalate ", " $ map (getSchoolname.userAdminSchool .entityVal) $ snd3 u)
|
||||||
|
, headed "Lecturer at Schools" $ (\u -> text2widget $ intercalate ", " $ map (getSchoolname.userLecturerSchool.entityVal) $ trd3 u)
|
||||||
|
]
|
||||||
|
-- ++ map (\school -> headed (text2widget $ schoolName $ entityVal school) (\u -> "xx")) schools
|
||||||
|
defaultLayout $ do
|
||||||
|
setTitle "Comprehensive User List"
|
||||||
|
let userList = encodeWidgetTable tableDefault colonnadeUsers users
|
||||||
|
$(widgetFile "users")
|
||||||
@ -1,5 +1,7 @@
|
|||||||
|
{-# LANGUAGE NoImplicitPrelude #-}
|
||||||
{-# LANGUAGE OverloadedStrings #-}
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
{-# LANGUAGE FlexibleContexts #-}
|
{-# LANGUAGE FlexibleContexts #-}
|
||||||
|
{-# LANGUAGE QuasiQuotes #-}
|
||||||
|
|
||||||
module Handler.Utils
|
module Handler.Utils
|
||||||
( module Handler.Utils
|
( module Handler.Utils
|
||||||
@ -18,12 +20,44 @@ import Handler.Utils.Rating as Handler.Utils
|
|||||||
import Handler.Utils.Submission as Handler.Utils
|
import Handler.Utils.Submission as Handler.Utils
|
||||||
import Handler.Utils.Templates as Handler.Utils
|
import Handler.Utils.Templates as Handler.Utils
|
||||||
|
|
||||||
import Text.Blaze (Markup)
|
import Text.Blaze (Markup, ToMarkup)
|
||||||
|
|
||||||
|
import Data.Map (Map)
|
||||||
|
import qualified Data.Map as Map
|
||||||
|
|
||||||
tickmark :: IsString a => a
|
tickmark :: IsString a => a
|
||||||
tickmark = fromString "✔"
|
tickmark = fromString "✔"
|
||||||
|
|
||||||
|
text2Html :: Text -> Html
|
||||||
|
text2Html = toHtml -- prevents ambiguous types
|
||||||
|
|
||||||
|
toWgt :: (ToMarkup a, MonadBaseControl IO m, MonadThrow m, MonadIO m) =>
|
||||||
|
a -> WidgetT site m ()
|
||||||
|
toWgt = toWidget . toHtml
|
||||||
|
|
||||||
|
text2widget :: (MonadBaseControl IO m, MonadThrow m, MonadIO m) =>
|
||||||
|
Text -> WidgetT site m ()
|
||||||
|
text2widget t = [whamlet|#{t}|]
|
||||||
|
|
||||||
|
str2widget :: (MonadBaseControl IO m, MonadThrow m, MonadIO m) =>
|
||||||
|
String -> WidgetT site m ()
|
||||||
|
str2widget s = [whamlet|#{s}|]
|
||||||
|
|
||||||
|
|
||||||
withFragment :: ( Monad m
|
withFragment :: ( Monad m
|
||||||
) => MForm m (a, WidgetT site IO ()) -> Markup -> MForm m (a, WidgetT site IO ())
|
) => MForm m (a, WidgetT site IO ()) -> Markup -> MForm m (a, WidgetT site IO ())
|
||||||
withFragment form html = (flip fmap) form $ \(x, widget) -> (x, toWidget html >> widget)
|
withFragment form html = (flip fmap) form $ \(x, widget) -> (x, toWidget html >> widget)
|
||||||
|
|
||||||
|
-----------
|
||||||
|
-- Maybe --
|
||||||
|
-----------
|
||||||
|
whenIsJust :: Monad m => Maybe a -> (a -> m ()) -> m ()
|
||||||
|
whenIsJust (Just x) f = f x
|
||||||
|
whenIsJust Nothing _ = return ()
|
||||||
|
|
||||||
|
----------
|
||||||
|
-- Maps --
|
||||||
|
----------
|
||||||
|
entities2map :: PersistEntity record => [Entity record] -> Map (Key record) record
|
||||||
|
entities2map = foldl' (\m entity -> Map.insert (entityKey entity) (entityVal entity) m) Map.empty
|
||||||
|
|
||||||
|
|||||||
@ -28,7 +28,7 @@ import Web.PathPieces (showToPathPiece, readFromPathPiece)
|
|||||||
-- Unique Form Identifiers to avoid accidents --
|
-- Unique Form Identifiers to avoid accidents --
|
||||||
------------------------------------------------
|
------------------------------------------------
|
||||||
|
|
||||||
data FormIdentifier = FIDcourse
|
data FormIdentifier = FIDcourse | FIDsheet
|
||||||
deriving (Enum, Eq, Ord, Bounded, Read, Show)
|
deriving (Enum, Eq, Ord, Bounded, Read, Show)
|
||||||
|
|
||||||
|
|
||||||
@ -219,8 +219,25 @@ minIntField :: (Monad m, Integral i, Show i, RenderMessage (HandlerSite m) FormM
|
|||||||
minIntField m d = checkBool (>= m) (T.concat [d," muss größer als ", T.pack $ show m, " sein."]) $ intField
|
minIntField m d = checkBool (>= m) (T.concat [d," muss größer als ", T.pack $ show m, " sein."]) $ intField
|
||||||
|
|
||||||
|
|
||||||
schoolField :: (Monad m, RenderMessage (HandlerSite m) FormMessage) => Field m SchoolId
|
--termField: see Utils.Term
|
||||||
schoolField = undefined -- TODO
|
|
||||||
|
schoolField :: Field Handler SchoolId
|
||||||
|
schoolField = selectField schools
|
||||||
|
where
|
||||||
|
schools = optionsPersistKey [] [Asc SchoolName] schoolName
|
||||||
|
|
||||||
|
schoolEntField :: Field Handler (Entity School)
|
||||||
|
schoolEntField = selectField schools
|
||||||
|
where
|
||||||
|
schools = optionsPersist [] [Asc SchoolName] schoolName
|
||||||
|
|
||||||
|
sheetTypeAFormReq :: FieldSettings UniWorX -> Maybe SheetType -> AForm Handler SheetType
|
||||||
|
sheetTypeAFormReq d Nothing =
|
||||||
|
-- TODO, offer options to choose between Normal/Bonus/Pass
|
||||||
|
(Normal . toPoints) <$> areq (natField "Punkte") d Nothing
|
||||||
|
sheetTypeAFormReq d (Just (Normal p)) =
|
||||||
|
-- TODO, offer options to choose between Normal/Bonus/Pass
|
||||||
|
(Normal . toPoints) <$> areq (natField "Punkte") d (Just $ fromPoints p)
|
||||||
|
|
||||||
utcTimeField :: (Monad m, RenderMessage (HandlerSite m) FormMessage) => Field m UTCTime
|
utcTimeField :: (Monad m, RenderMessage (HandlerSite m) FormMessage) => Field m UTCTime
|
||||||
-- StackOverflow: dayToUTC <$> (areq (jqueryDayField def {...}) settings Nothing)
|
-- StackOverflow: dayToUTC <$> (areq (jqueryDayField def {...}) settings Nothing)
|
||||||
@ -229,19 +246,20 @@ utcTimeField = Field
|
|||||||
, fieldView = \theId name attrs val isReq ->
|
, fieldView = \theId name attrs val isReq ->
|
||||||
[whamlet|
|
[whamlet|
|
||||||
$newline never
|
$newline never
|
||||||
<input id="#{theId}" name="#{name}" *{attrs} type="text" :isReq:required value="#{either id showTime val}">
|
<input id="#{theId}" name="#{name}" *{attrs} type="datetime-local" :isReq:required value="#{either id showTime val}">
|
||||||
|]
|
|]
|
||||||
, fieldEnctype = UrlEncoded
|
, fieldEnctype = UrlEncoded
|
||||||
}
|
}
|
||||||
where
|
where
|
||||||
fieldTimeFormat :: String
|
fieldTimeFormat :: String
|
||||||
fieldTimeFormat = "%e.%m.%y %k:%M"
|
--fieldTimeFormat = "%e.%m.%y %k:%M"
|
||||||
|
fieldTimeFormat = "%Y-%m-%eT%H:%M"
|
||||||
|
|
||||||
readTime :: Text -> Either FormMessage UTCTime
|
readTime :: Text -> Either FormMessage UTCTime
|
||||||
readTime t =
|
readTime t =
|
||||||
case parseTimeM True germanTimeLocale fieldTimeFormat (T.unpack t) of
|
case parseTimeM True germanTimeLocale fieldTimeFormat (T.unpack t) of
|
||||||
(Just time) -> Right time
|
(Just time) -> Right time
|
||||||
Nothing -> Left $ MsgInvalidEntry "Datum/Zeit Format: tt.mm.yy hh:mm"
|
Nothing -> Left $ MsgInvalidEntry $ "Datum/Zeit Format: tt.mm.yy hh:mm " ++ t
|
||||||
|
|
||||||
showTime :: UTCTime -> Text
|
showTime :: UTCTime -> Text
|
||||||
showTime = fromString . (formatTime germanTimeLocale fieldTimeFormat)
|
showTime = fromString . (formatTime germanTimeLocale fieldTimeFormat)
|
||||||
|
|||||||
@ -109,7 +109,7 @@ sinkSubmission sheetId userId mExists = do
|
|||||||
| not (null collidingFiles) = any (/~ file) [ f | (Entity _ f, _) <- collidingFiles ]
|
| not (null collidingFiles) = any (/~ file) [ f | (Entity _ f, _) <- collidingFiles ]
|
||||||
| otherwise = True
|
| otherwise = True
|
||||||
matchesUnderlying
|
matchesUnderlying
|
||||||
| not (null underlyingFiles) = all (~~ file) [ f | (Entity _ f, Entity _ sf) <- underlyingFiles ]
|
| not (null underlyingFiles) = all (~~ file) [ f | (Entity _ f, Entity _ _sf) <- underlyingFiles ]
|
||||||
| otherwise = False
|
| otherwise = False
|
||||||
undoneDeletion = any submissionFileIsDeletion [ sf | (_, Entity _ sf) <- collidingFiles ]
|
undoneDeletion = any submissionFileIsDeletion [ sf | (_, Entity _ sf) <- collidingFiles ]
|
||||||
|
|
||||||
|
|||||||
@ -36,7 +36,7 @@ pairColonnade a b = mconcat [ lmap fst a, lmap snd b]
|
|||||||
-- Table Modification
|
-- Table Modification
|
||||||
encodeHeadedWidgetTableNumbered :: Attribute -> Colonnade Headed a (WidgetT site IO ()) -> [a] -> WidgetT site IO ()
|
encodeHeadedWidgetTableNumbered :: Attribute -> Colonnade Headed a (WidgetT site IO ()) -> [a] -> WidgetT site IO ()
|
||||||
encodeHeadedWidgetTableNumbered attrs colo tdata =
|
encodeHeadedWidgetTableNumbered attrs colo tdata =
|
||||||
encodeHeadedWidgetTable attrs (mconcat [numberCol, lmap snd colo]) (zip [1..] tdata)
|
encodeWidgetTable attrs (mconcat [numberCol, lmap snd colo]) (zip [1..] tdata)
|
||||||
where
|
where
|
||||||
numberCol :: Colonnade Headed (Int,a) (WidgetT site IO ())
|
numberCol :: Colonnade Headed (Int,a) (WidgetT site IO ())
|
||||||
numberCol = headed "Nr" (fromString.show.fst)
|
numberCol = headed "Nr" (fromString.show.fst)
|
||||||
@ -86,5 +86,5 @@ headedRowSelector toExternal fromExternal attrs colonnade tdata = do
|
|||||||
collectResult (FormSuccess x:rs) = (x :) <$> collectResult rs
|
collectResult (FormSuccess x:rs) = (x :) <$> collectResult rs
|
||||||
|
|
||||||
return ( catMaybes <$> collectResult selectionResults
|
return ( catMaybes <$> collectResult selectionResults
|
||||||
, encodeHeadedCellTable attrs (pairColonnade selColonnade colonnade) (zip [0..] tdata)
|
, encodeCellTable attrs (pairColonnade selColonnade colonnade) (zip [0..] tdata)
|
||||||
)
|
)
|
||||||
|
|||||||
@ -13,15 +13,14 @@ import Model.Types
|
|||||||
-- import Data.Maybe
|
-- import Data.Maybe
|
||||||
|
|
||||||
|
|
||||||
termExistsField :: Field Handler TermIdentifier
|
termActiveField :: Field Handler TermId
|
||||||
termExistsField = termField True
|
termActiveField = selectField $ optionsPersistKey [TermActive ==. True] [Desc TermStart] termName
|
||||||
-- TODO: Change this to an option list of active terms
|
|
||||||
|
termActiveOld :: Field Handler TermIdentifier
|
||||||
|
termActiveOld = convertField unTermKey TermKey $ selectField $ optionsPersistKey [TermActive ==. True] [Desc TermStart] termName
|
||||||
|
|
||||||
termNewField :: Field Handler TermIdentifier
|
termNewField :: Field Handler TermIdentifier
|
||||||
termNewField = termField False
|
termNewField = checkMMap checkTerm termToText textField
|
||||||
|
|
||||||
termField :: Bool -> Field Handler TermIdentifier
|
|
||||||
termField mustexist = checkMMap checkTerm termToText textField
|
|
||||||
where
|
where
|
||||||
errTextParse :: Text
|
errTextParse :: Text
|
||||||
errTextParse = "Semester: S oder W gefolgt von Jahreszahl"
|
errTextParse = "Semester: S oder W gefolgt von Jahreszahl"
|
||||||
@ -31,12 +30,8 @@ termField mustexist = checkMMap checkTerm termToText textField
|
|||||||
|
|
||||||
checkTerm :: Text -> HandlerT UniWorX IO (Either Text TermIdentifier)
|
checkTerm :: Text -> HandlerT UniWorX IO (Either Text TermIdentifier)
|
||||||
checkTerm t = case termFromText t of
|
checkTerm t = case termFromText t of
|
||||||
Left _ -> return $ Left errTextParse
|
Left _ -> return $ Left errTextParse
|
||||||
res@(Right ti) -> do
|
res@(Right _) -> return res
|
||||||
term <- runDB $ get $ TermKey ti -- TODO: membershiptest instead?
|
|
||||||
return $ if mustexist && isNothing term
|
|
||||||
then Left $ errTextFreigabe ti
|
|
||||||
else res
|
|
||||||
|
|
||||||
validateTerm :: Term -> [Text]
|
validateTerm :: Term -> [Text]
|
||||||
validateTerm (Term{..}) =
|
validateTerm (Term{..}) =
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
{-# LANGUAGE NoImplicitPrelude #-}
|
{-# LANGUAGE NoImplicitPrelude #-}
|
||||||
{-# LANGUAGE RecordWildCards #-}
|
{-# LANGUAGE RecordWildCards #-}
|
||||||
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
{-# LANGUAGE DeriveGeneric, DeriveDataTypeable #-}
|
{-# LANGUAGE DeriveGeneric, DeriveDataTypeable #-}
|
||||||
{-# OPTIONS_GHC -fno-warn-missing-fields #-} -- This concerns zipEntrySize in produceZip
|
{-# OPTIONS_GHC -fno-warn-missing-fields #-} -- This concerns zipEntrySize in produceZip
|
||||||
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
||||||
@ -10,6 +11,7 @@ module Handler.Utils.Zip
|
|||||||
, produceZip
|
, produceZip
|
||||||
, consumeZip
|
, consumeZip
|
||||||
, modifyFileTitle
|
, modifyFileTitle
|
||||||
|
, sourceFiles
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import Import
|
import Import
|
||||||
@ -29,6 +31,8 @@ import Data.Time
|
|||||||
|
|
||||||
import Data.List (dropWhileEnd)
|
import Data.List (dropWhileEnd)
|
||||||
|
|
||||||
|
import Network.Mime
|
||||||
|
|
||||||
|
|
||||||
instance Default ZipInfo where
|
instance Default ZipInfo where
|
||||||
def = ZipInfo
|
def = ZipInfo
|
||||||
@ -93,3 +97,16 @@ produceZip info = mapC toZipData =$= void (zipStream zipOptions)
|
|||||||
|
|
||||||
modifyFileTitle :: Monad m => (FilePath -> FilePath) -> Conduit File m File
|
modifyFileTitle :: Monad m => (FilePath -> FilePath) -> Conduit File m File
|
||||||
modifyFileTitle f = mapC $ \x@File{..} -> x{ fileTitle = f fileTitle }
|
modifyFileTitle f = mapC $ \x@File{..} -> x{ fileTitle = f fileTitle }
|
||||||
|
|
||||||
|
-- Takes FileInfo and if it is a ZIP-Archive, extract files, otherwiese yield fileinfo
|
||||||
|
sourceFiles :: (MonadResource m, MonadThrow m, MonadIO m) => FileInfo -> Source m File
|
||||||
|
sourceFiles fInfo
|
||||||
|
| mimeType == "application/zip" = fileSource fInfo =$= void consumeZip
|
||||||
|
| otherwise = do
|
||||||
|
let fileTitle = unpack $ fileName fInfo
|
||||||
|
fileModified <- liftIO getCurrentTime
|
||||||
|
yieldM $ do
|
||||||
|
fileContent <- Just <$> runConduit (fileSource fInfo =$= foldC)
|
||||||
|
return File{..}
|
||||||
|
where
|
||||||
|
mimeType = defaultMimeLookup (fileName fInfo)
|
||||||
|
|||||||
@ -11,6 +11,8 @@ import Yesod.Auth as Import
|
|||||||
import Yesod.Core.Types as Import (loggerSet)
|
import Yesod.Core.Types as Import (loggerSet)
|
||||||
import Yesod.Default.Config2 as Import
|
import Yesod.Default.Config2 as Import
|
||||||
|
|
||||||
|
|
||||||
import Data.Fixed as Import
|
import Data.Fixed as Import
|
||||||
|
|
||||||
import CryptoID as Import
|
import CryptoID as Import
|
||||||
|
import Data.UUID as Import (UUID)
|
||||||
|
|||||||
@ -4,6 +4,9 @@
|
|||||||
{-# LANGUAGE NoImplicitPrelude #-}
|
{-# LANGUAGE NoImplicitPrelude #-}
|
||||||
{-# LANGUAGE OverloadedStrings #-}
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
{-# LANGUAGE DeriveGeneric, DeriveDataTypeable #-}
|
{-# LANGUAGE DeriveGeneric, DeriveDataTypeable #-}
|
||||||
|
{-# LANGUAGE FlexibleInstances, MultiParamTypeClasses #-}
|
||||||
|
|
||||||
|
|
||||||
module Model.Types where
|
module Model.Types where
|
||||||
|
|
||||||
import ClassyPrelude
|
import ClassyPrelude
|
||||||
@ -22,6 +25,7 @@ import Data.Text (Text)
|
|||||||
import qualified Data.Text as Text
|
import qualified Data.Text as Text
|
||||||
|
|
||||||
import Text.Read (readMaybe)
|
import Text.Read (readMaybe)
|
||||||
|
import Text.Shakespeare.I18N
|
||||||
|
|
||||||
-- import Data.CaseInsensitive (CI)
|
-- import Data.CaseInsensitive (CI)
|
||||||
import qualified Data.CaseInsensitive as CI
|
import qualified Data.CaseInsensitive as CI
|
||||||
@ -36,6 +40,12 @@ import Data.Typeable (Typeable)
|
|||||||
|
|
||||||
type Points = Centi
|
type Points = Centi
|
||||||
|
|
||||||
|
toPoints :: Integral a => a -> Points
|
||||||
|
toPoints = MkFixed . fromIntegral
|
||||||
|
|
||||||
|
fromPoints :: Integral a => Points -> a
|
||||||
|
fromPoints = error "TODO: Types.fromPoints not yet implemented"
|
||||||
|
|
||||||
data SheetType
|
data SheetType
|
||||||
= Bonus { maxPoints :: Points }
|
= Bonus { maxPoints :: Points }
|
||||||
| Normal { maxPoints :: Points }
|
| Normal { maxPoints :: Points }
|
||||||
@ -53,6 +63,16 @@ data SheetFileType = SheetExercise | SheetHint | SheetSolution | SheetMarking
|
|||||||
deriving (Show, Read, Eq, Ord, Enum, Bounded)
|
deriving (Show, Read, Eq, Ord, Enum, Bounded)
|
||||||
derivePersistField "SheetFileType"
|
derivePersistField "SheetFileType"
|
||||||
|
|
||||||
|
instance PathPiece SheetFileType where
|
||||||
|
toPathPiece SheetExercise = "file"
|
||||||
|
toPathPiece SheetHint = "hint"
|
||||||
|
toPathPiece SheetSolution = "solution"
|
||||||
|
toPathPiece SheetMarking = "marking"
|
||||||
|
fromPathPiece t =
|
||||||
|
lookup (CI.mk t) [(CI.mk $ toPathPiece ty,ty) | ty <- [minBound..maxBound]]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
data Load = ByTutorial | ByProportion Double
|
data Load = ByTutorial | ByProportion Double
|
||||||
deriving (Show, Read, Eq)
|
deriving (Show, Read, Eq)
|
||||||
derivePersistField "Load"
|
derivePersistField "Load"
|
||||||
@ -113,6 +133,9 @@ instance ToJSON TermIdentifier where
|
|||||||
instance FromJSON TermIdentifier where
|
instance FromJSON TermIdentifier where
|
||||||
parseJSON = withText "Term" $ either (fail . Text.unpack) return . termFromText
|
parseJSON = withText "Term" $ either (fail . Text.unpack) return . termFromText
|
||||||
|
|
||||||
|
instance RenderMessage site TermIdentifier where -- TODO: I18N
|
||||||
|
renderMessage _ _ = termToText
|
||||||
|
|
||||||
{- Must be defined in a later module:
|
{- Must be defined in a later module:
|
||||||
termField :: Field (HandlerT UniWorX IO) TermIdentifier
|
termField :: Field (HandlerT UniWorX IO) TermIdentifier
|
||||||
termField = checkMMap (return . termFromText) termToText textField
|
termField = checkMMap (return . termFromText) termToText textField
|
||||||
|
|||||||
@ -5,7 +5,9 @@ let
|
|||||||
in haskell.lib.buildStackProject {
|
in haskell.lib.buildStackProject {
|
||||||
inherit ghc;
|
inherit ghc;
|
||||||
name = "stackenv";
|
name = "stackenv";
|
||||||
buildInputs = with pkgs;
|
buildInputs = (with pkgs;
|
||||||
[ postgresql zlib openldap cyrus_sasl.dev
|
[ postgresql zlib openldap cyrus_sasl.dev
|
||||||
];
|
]) ++ (with haskell.packages."ghc${builtins.replaceStrings ["."] [""] ghc.version}";
|
||||||
|
[ yesod-bin
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
58
stack.yaml
58
stack.yaml
@ -1,4 +1,5 @@
|
|||||||
flags: {}
|
flags: {}
|
||||||
|
|
||||||
docker:
|
docker:
|
||||||
enable: false
|
enable: false
|
||||||
image: uniworx
|
image: uniworx
|
||||||
@ -6,33 +7,40 @@ nix:
|
|||||||
packages: []
|
packages: []
|
||||||
pure: false
|
pure: false
|
||||||
shell-file: ./stack.nix
|
shell-file: ./stack.nix
|
||||||
|
|
||||||
extra-package-dbs: []
|
extra-package-dbs: []
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
- .
|
- .
|
||||||
- location:
|
- location:
|
||||||
git: https://github.com/pngwjpgh/zip-stream.git
|
git: https://github.com/pngwjpgh/zip-stream.git
|
||||||
commit: 9272bbed000928d500febad1cdc98d1da29d399e
|
commit: 9272bbed000928d500febad1cdc98d1da29d399e
|
||||||
extra-dep: true
|
extra-dep: true
|
||||||
- location:
|
- location:
|
||||||
git: https://github.com/mlitchard/yesod-auth-ldap.git
|
git: https://github.com/mlitchard/yesod-auth-ldap.git
|
||||||
commit: 69e08ef687ab96df3352ff4267562135453c6f02
|
commit: 69e08ef687ab96df3352ff4267562135453c6f02
|
||||||
extra-dep: true
|
extra-dep: true
|
||||||
- location:
|
- location:
|
||||||
git: https://github.com/mlitchard/authenticate-ldap.git
|
git: https://github.com/mlitchard/authenticate-ldap.git
|
||||||
commit: cc2770024766a8fa29d3086688df60aaf65fb954
|
commit: cc2770024766a8fa29d3086688df60aaf65fb954
|
||||||
extra-dep: true
|
extra-dep: true
|
||||||
|
- location:
|
||||||
|
git: https://github.com/pngwjpgh/encoding.git
|
||||||
|
commit: 67bb87ceff53f0178c988dd4e15eeb2daee92b84
|
||||||
|
extra-dep: true
|
||||||
|
|
||||||
extra-deps:
|
extra-deps:
|
||||||
- colonnade-1.1.1
|
- colonnade-1.2.0
|
||||||
- yesod-colonnade-1.1.0
|
- yesod-colonnade-1.2.0
|
||||||
# - zip-stream-0.1.0.1
|
|
||||||
- conduit-resumablesink-0.2
|
|
||||||
- uuid-crypto-1.3.1.0
|
|
||||||
- filepath-crypto-0.0.0.0
|
|
||||||
- cryptoids-0.4.0.0
|
|
||||||
- cryptoids-types-0.0.0
|
|
||||||
|
|
||||||
- encoding-0.8.2
|
- conduit-resumablesink-0.2
|
||||||
- regex-compat-0.93.1
|
|
||||||
|
|
||||||
- LDAP-0.6.11
|
- uuid-crypto-1.4.0.0
|
||||||
resolver: lts-9.3
|
- filepath-crypto-0.1.0.0
|
||||||
|
- cryptoids-0.5.0.0
|
||||||
|
- cryptoids-types-0.0.0
|
||||||
|
- cryptoids-class-0.0.0
|
||||||
|
|
||||||
|
- LDAP-0.6.11
|
||||||
|
|
||||||
|
resolver: lts-10.5
|
||||||
|
|||||||
10
start.sh
10
start.sh
@ -1,2 +1,8 @@
|
|||||||
#!/bin/bash
|
#!/usr/bin/env bash
|
||||||
env DUMMY_LOGIN=true stack exec -- yesod devel
|
|
||||||
|
unset HOST
|
||||||
|
export DETAILED_LOGGING=true
|
||||||
|
export LOG_ALL=true
|
||||||
|
export DUMMY_LOGIN=true
|
||||||
|
|
||||||
|
exec -- stack exec -- yesod devel
|
||||||
|
|||||||
@ -24,6 +24,11 @@
|
|||||||
#{participants}
|
#{participants}
|
||||||
$maybe capacity <- courseCapacity course
|
$maybe capacity <- courseCapacity course
|
||||||
\ von #{capacity}
|
\ von #{capacity}
|
||||||
|
<br>
|
||||||
|
$maybe regFrom <- courseRegisterFrom course
|
||||||
|
Anmeldezeitraum: #{formatTimeGerWD regFrom}
|
||||||
|
$maybe regTo <- courseRegisterTo course
|
||||||
|
\ bis #{formatTimeGerWD regTo}
|
||||||
<form method=post action=@{CourseShowR tid csh} enctype=#{regEnctype}>
|
<form method=post action=@{CourseShowR tid csh} enctype=#{regEnctype}>
|
||||||
^{regWidget}
|
^{regWidget}
|
||||||
|
|
||||||
|
|||||||
@ -27,16 +27,23 @@
|
|||||||
<h2>Teilweise funktionierende Abschnitte
|
<h2>Teilweise funktionierende Abschnitte
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li .list-group-item>
|
||||||
|
<a href=@{UsersR}>Benutzer Verwaltung
|
||||||
|
|
||||||
|
<li .list-group-item>
|
||||||
<a href=@{TermShowR}>Semester Verwaltung
|
<a href=@{TermShowR}>Semester Verwaltung
|
||||||
<li>
|
|
||||||
|
<li .list-group-item>
|
||||||
<a href=@{CourseEditR}>Kurse anlegen, editieren und anzeigen
|
<a href=@{CourseEditR}>Kurse anlegen, editieren und anzeigen
|
||||||
|
|
||||||
|
<li .list-group-item>
|
||||||
|
<a href=@{SubmissionListR}>Dateien hochladen und abrufen
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h2>Funktionen zum Testen
|
<h2>Funktionen zum Testen
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
Institut einmalig in Datenbank einfügen:
|
Knopf-Test:
|
||||||
<form .form-inline method=post action=@{HomeR} enctype=#{btnEnctype}>
|
<form .form-inline method=post action=@{HomeR} enctype=#{btnEnctype}>
|
||||||
^{btnWdgt}
|
^{btnWdgt}
|
||||||
|
|||||||
20
templates/newSheet.hamlet
Normal file
20
templates/newSheet.hamlet
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<div .container>
|
||||||
|
<div .bs-docs-section>
|
||||||
|
<div .row>
|
||||||
|
<div .col-lg-12>
|
||||||
|
<div .page-header>
|
||||||
|
<h1 #forms>Neuen Blatt anlegen:
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Bitte alles ausfüllen!
|
||||||
|
|
||||||
|
<div .row>
|
||||||
|
<div .col-lg-6>
|
||||||
|
<div .bs-callout bs-callout-info well>
|
||||||
|
<form .form-horizontal method=post #forms enctype=#{enc}>
|
||||||
|
^{wdgt}
|
||||||
|
|
||||||
|
<button .btn.btn-primary type="submit">
|
||||||
|
Blatt anlegen
|
||||||
|
|
||||||
|
|
||||||
30
templates/sheetAdmin.hamlet
Normal file
30
templates/sheetAdmin.hamlet
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<div .masthead>
|
||||||
|
<div .container>
|
||||||
|
<div .row>
|
||||||
|
<h1 .header>
|
||||||
|
#{sheetName sheet}
|
||||||
|
<div .container>
|
||||||
|
<div .bs-docs-section>
|
||||||
|
<div .row>
|
||||||
|
<div .col-lg-12>
|
||||||
|
<div .page-header>
|
||||||
|
|
||||||
|
$maybe descr <- sheetDescription sheet
|
||||||
|
<h2 #description>Hinweise
|
||||||
|
<p> #{descr}
|
||||||
|
<h3>Bewertung
|
||||||
|
<p> #{show $ sheetType sheet}
|
||||||
|
$maybe marking <- sheetMarkingText sheet
|
||||||
|
<p> #{marking}
|
||||||
|
<br>
|
||||||
|
Freigeschaltet ab:
|
||||||
|
\ #{formatTimeGerWD $ sheetActiveFrom sheet}
|
||||||
|
\ Abgabe bis:
|
||||||
|
\ #{formatTimeGerWD $ sheetActiveTo sheet}
|
||||||
|
|
||||||
|
<div .row>
|
||||||
|
<div .col-lg-12>
|
||||||
|
<h2>Abgaben
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
8
templates/users.hamlet
Normal file
8
templates/users.hamlet
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<div .ui.container>
|
||||||
|
|
||||||
|
<p .bg-danger>
|
||||||
|
This page is only for development purposes.
|
||||||
|
|
||||||
|
<h1>
|
||||||
|
User list
|
||||||
|
^{userList}
|
||||||
Reference in New Issue
Block a user