Merge branch 'master' into feat/datepicker

This commit is contained in:
Felix Hamann 2018-06-09 19:20:47 +02:00
commit 07e5ca8b79
72 changed files with 5069 additions and 1180 deletions

View File

@ -1,7 +1,7 @@
** Sicherheitsabfragen? ** Sicherheitsabfragen?
- Verschlüsselung des Zugriffs? - Verschlüsselung des Zugriffs?
- SheetDelR tid csh sn : GET zeigt Sicherheitsabfrage - SDelR tid csh sn : GET zeigt Sicherheitsabfrage
POST löscht. POST löscht.
Ist das so sinnvoll? Ist das so sinnvoll?
Sicherheitsabfrage als PopUpMessage? Sicherheitsabfrage als PopUpMessage?
@ -9,7 +9,7 @@
- Utils.getKeyBy404 effiziente Variante, welche nur den Key liefert? Esq? - Utils.getKeyBy404 effiziente Variante, welche nur den Key liefert? Esq?
(Sheet.hs -> fetchSheet) (Sheet.hs -> fetchSheet)
- Handler.Sheet.postSheetDelR: deleteCascade für Files? Klappt das? - Handler.Sheet.postSDelR: deleteCascade für Files? Klappt das?
Kann man abfragen, was bei deleteCascade alles gelöscht wird? Kann man abfragen, was bei deleteCascade alles gelöscht wird?
@ -19,7 +19,7 @@
Links -> MenuItems verwenden wie bisher Links -> MenuItems verwenden wie bisher
Page Titles -> setTitleI Page Titles -> setTitleI
Buttons? -> Kann leicht geändert werden! Buttons? -> Kann leicht geändert werden!
Was ist mit einfachen Text Feldern, z.B. die Beschriftung von Knöpfen wie in Handler.Course.getCourseListTermR, Zeile 66 "pageActions" für menuItemLabel? Was ist mit einfachen Text Feldern, z.B. die Beschriftung von Knöpfen wie in Handler.Course.getTermCourseListR, Zeile 66 "pageActions" für menuItemLabel?
** Page pageActions - Berechtigungen prüfen? ** Page pageActions - Berechtigungen prüfen?
=> Eigener Constructor statt NavbarLeft/Right?! => Eigener Constructor statt NavbarLeft/Right?!

View File

@ -109,7 +109,7 @@ TABLE "user";
DROP TABLE "course" CASCADE; DROP TABLE "course" CASCADE;
-- UserId 5 zum Lecturer in SchoolId 1 machen (27 ist laufende Nummer) -- UserId 5 zum Lecturer in SchoolId 1 machen (27 ist laufende Nummer)
INSERT INTO "user_lecturer" (id,"user",school) VALUES (27,5,1); INSERT INTO "user_lecturer" (id,"user","school") VALUES (27,5,1);
-- Beenden: -- Beenden:
\q \q

View File

@ -89,8 +89,8 @@ main = db $ do
, courseDescription = Nothing , courseDescription = Nothing
, courseLinkExternal = Nothing , courseLinkExternal = Nothing
, courseShorthand = "ffp" , courseShorthand = "ffp"
, courseTermId = TermKey summer2018 , courseTerm = TermKey summer2018
, courseSchoolId = ifi , courseSchool = ifi
, courseCapacity = Just 20 , courseCapacity = Just 20
, courseHasRegistration = True , courseHasRegistration = True
, courseRegisterFrom = Just now , courseRegisterFrom = Just now
@ -104,8 +104,11 @@ main = db $ do
void . insert $ DegreeCourse ffp sdMst sdInf void . insert $ DegreeCourse ffp sdMst sdInf
void . insert $ Lecturer jost ffp void . insert $ Lecturer jost ffp
void . insert $ Lecturer gkleen ffp void . insert $ Lecturer gkleen ffp
insert_ $ Corrector gkleen ffp (ByProportion 1) sheetkey <- insert $ Sheet ffp "AdHoc-Gruppen" Nothing NotGraded (Arbitrary 3) Nothing Nothing now now Nothing Nothing
sheetkey <- insert $ Sheet ffp "Blatt 1" Nothing NotGraded NoGroups Nothing Nothing now now Nothing Nothing insert_ $ SheetEdit gkleen now sheetkey
sheetkey <- insert $ Sheet ffp "Feste Gruppen" Nothing NotGraded RegisteredGroups Nothing Nothing now now Nothing Nothing
insert_ $ SheetEdit gkleen now sheetkey
sheetkey <- insert $ Sheet ffp "Keine Gruppen" Nothing NotGraded NoGroups Nothing Nothing now now Nothing Nothing
insert_ $ SheetEdit gkleen now sheetkey insert_ $ SheetEdit gkleen now sheetkey
-- EIP -- EIP
eip <- insert Course eip <- insert Course
@ -113,8 +116,8 @@ main = db $ do
, courseDescription = Nothing , courseDescription = Nothing
, courseLinkExternal = Nothing , courseLinkExternal = Nothing
, courseShorthand = "eip" , courseShorthand = "eip"
, courseTermId = TermKey summer2017 , courseTerm = TermKey summer2017
, courseSchoolId = ifi , courseSchool = ifi
, courseCapacity = Just 20 , courseCapacity = Just 20
, courseHasRegistration = False , courseHasRegistration = False
, courseRegisterFrom = Nothing , courseRegisterFrom = Nothing
@ -132,8 +135,8 @@ main = db $ do
, courseDescription = Nothing , courseDescription = Nothing
, courseLinkExternal = Nothing , courseLinkExternal = Nothing
, courseShorthand = "ixd" , courseShorthand = "ixd"
, courseTermId = TermKey summer2018 , courseTerm = TermKey summer2018
, courseSchoolId = ifi , courseSchool = ifi
, courseCapacity = Just 20 , courseCapacity = Just 20
, courseHasRegistration = True , courseHasRegistration = True
, courseRegisterFrom = Just now , courseRegisterFrom = Just now
@ -151,8 +154,8 @@ main = db $ do
, courseDescription = Nothing , courseDescription = Nothing
, courseLinkExternal = Nothing , courseLinkExternal = Nothing
, courseShorthand = "ux3" , courseShorthand = "ux3"
, courseTermId = TermKey winter2017 , courseTerm = TermKey winter2017
, courseSchoolId = ifi , courseSchool = ifi
, courseCapacity = Just 30 , courseCapacity = Just 30
, courseHasRegistration = False , courseHasRegistration = False
, courseRegisterFrom = Nothing , courseRegisterFrom = Nothing
@ -170,8 +173,8 @@ main = db $ do
, courseDescription = Nothing , courseDescription = Nothing
, courseLinkExternal = Nothing , courseLinkExternal = Nothing
, courseShorthand = "pmo" , courseShorthand = "pmo"
, courseTermId = TermKey summer2017 , courseTerm = TermKey summer2017
, courseSchoolId = ifi , courseSchool = ifi
, courseCapacity = Just 50 , courseCapacity = Just 50
, courseHasRegistration = False , courseHasRegistration = False
, courseRegisterFrom = Nothing , courseRegisterFrom = Nothing
@ -189,8 +192,8 @@ main = db $ do
, courseDescription = Nothing , courseDescription = Nothing
, courseLinkExternal = Nothing , courseLinkExternal = Nothing
, courseShorthand = "dbs" , courseShorthand = "dbs"
, courseTermId = TermKey summer2018 , courseTerm = TermKey summer2018
, courseSchoolId = ifi , courseSchool = ifi
, courseCapacity = Just 50 , courseCapacity = Just 50
, courseHasRegistration = False , courseHasRegistration = False
, courseRegisterFrom = Nothing , courseRegisterFrom = Nothing

View File

@ -1,15 +1,17 @@
SummerTerm year@Integer: Sommersemester #{tshow year} SummerTerm year@Integer: Sommersemester #{tshow year}
WinterTerm year@Integer: Wintersemester #{tshow year}/#{tshow $ succ year} WinterTerm year@Integer: Wintersemester #{tshow year}/#{tshow $ succ year}
PSLimitNonPositive: “pagesize” muss größer als null sein PSLimitNonPositive: “pagesize” muss größer als null sein
Page n@Int64 num@Int64: Seite #{tshow n} von #{tshow num} Page n@Int64: #{tshow n}
TermEdited tid@TermIdentifier: Semester #{termToText tid} erfolgreich editiert. TermEdited tid@TermIdentifier: Semester #{termToText tid} erfolgreich editiert.
TermNewTitle: Semester editiere/anlegen. TermNewTitle: Semester editiere/anlegen.
InvalidInput: Eingaben bitte korrigieren. InvalidInput: Eingaben bitte korrigieren.
CourseNewOk tid@TermIdentifier courseShortHand@Text: Kurs #{termToText tid}-#{courseShortHand} wurde erfolgreich erstellt. CourseNewOk tid@TermIdentifier courseShortHand@Text: Kurs #{termToText tid}-#{courseShortHand} wurde erfolgreich erstellt.
CourseEditOk tid@TermIdentifier courseShortHand@Text: Kurs #{termToText tid}-#{courseShortHand} wurde erfolgreich geändert. CourseEditOk tid@TermIdentifier courseShortHand@Text: Kurs #{termToText tid}-#{courseShortHand} wurde erfolgreich geändert.
CourseNewDupShort tid@TermIdentifier courseShortHand@Text: Kurs #{termToText tid}-#{courseShortHand} konnte nicht erstellt werden: Es gibt bereits einen anderen Kurs mit dem Kürzel #{courseShortHand} in diesem Semester. CourseNewDupShort tid@TermIdentifier courseShortHand@Text: Kurs #{termToText tid}-#{courseShortHand} konnte nicht erstellt werden: Es gibt bereits einen anderen Kurs mit dem Kürzel #{courseShortHand} in diesem Semester.
CourseEditDupShort tid@TermIdentifier courseShortHand@Text: Kurs #{termToText tid}-#{courseShortHand} konnte nicht geändert werden: Es gibt bereits einen anderen Kurs mit dem Kürzel #{courseShortHand} in diesem Semester. CourseEditDupShort tid@TermIdentifier courseShortHand@Text: Kurs #{termToText tid}-#{courseShortHand} konnte nicht geändert werden: Es gibt bereits einen anderen Kurs mit dem Kürzel #{courseShortHand} in diesem Semester.
FFSheetName: Name FFSheetName: Name
SheetNewOk tid@TermIdentifier courseShortHand@Text sheetName@Text: Neues Übungsblatt #{sheetName} wurde im Kurs #{termToText tid}-#{courseShortHand} erfolgreich erstellt. SheetNewOk tid@TermIdentifier courseShortHand@Text sheetName@Text: Neues Übungsblatt #{sheetName} wurde im Kurs #{termToText tid}-#{courseShortHand} erfolgreich erstellt.
SheetTitle tid@TermIdentifier courseShortHand@Text sheetName@Text: #{termToText tid}-#{courseShortHand} #{sheetName} SheetTitle tid@TermIdentifier courseShortHand@Text sheetName@Text: #{termToText tid}-#{courseShortHand} #{sheetName}
SheetTitleNew tid@TermIdentifier courseShortHand@Text : #{termToText tid}-#{courseShortHand}: Neues Übungsblatt SheetTitleNew tid@TermIdentifier courseShortHand@Text : #{termToText tid}-#{courseShortHand}: Neues Übungsblatt
@ -18,9 +20,37 @@ SheetNameDup tid@TermIdentifier courseShortHand@Text sheetName@Text: Es gi
SheetDelTitle tid@TermIdentifier courseShortHand@Text sheetName@Text: Übungsblatt #{sheetName} wirklich aus Kurs #{termToText tid}-#{courseShortHand} herauslöschen? SheetDelTitle tid@TermIdentifier courseShortHand@Text sheetName@Text: Übungsblatt #{sheetName} wirklich aus Kurs #{termToText tid}-#{courseShortHand} herauslöschen?
SheetDelText submissionNo@Int: Dies kann nicht mehr rückgängig gemacht werden! Alle Einreichungen gehen ebenfalls verloren! Es gibt #{show submissionNo} Abgaben. SheetDelText submissionNo@Int: Dies kann nicht mehr rückgängig gemacht werden! Alle Einreichungen gehen ebenfalls verloren! Es gibt #{show submissionNo} Abgaben.
SheetDelOk tid@TermIdentifier courseShortHand@Text sheetName@Text: #{termToText tid}-#{courseShortHand}: Übungsblatt #{sheetName} gelöscht. SheetDelOk tid@TermIdentifier courseShortHand@Text sheetName@Text: #{termToText tid}-#{courseShortHand}: Übungsblatt #{sheetName} gelöscht.
Unauthorized: Sie haben hierfür keine explizite Berechtigung.
UnauthorizedAnd l@Text r@Text: "#{l}" und "#{r}"
UnauthorizedOr l@Text r@Text: "#{l}" oder "#{r}"
UnauthorizedSchoolAdmin: Sie sind nicht als Administrator für dieses Institut eingetragen. UnauthorizedSchoolAdmin: Sie sind nicht als Administrator für dieses Institut eingetragen.
UnauthorizedSchoolLecturer: Sie sind nicht als Veranstalter für dieses Institut eingetragen. UnauthorizedSchoolLecturer: Sie sind nicht als Veranstalter für dieses Institut eingetragen.
UnauthorizedLecturer: Sie sind nicht als Veranstalter für diese Veranstaltung eingetragen. UnauthorizedLecturer: Sie sind nicht als Veranstalter für diese Veranstaltung eingetragen.
UnauthorizedCorrector: Sie sind nicht als Korrektor für diese Veranstaltung eingetragen. UnauthorizedCorrector: Sie sind nicht als Korrektor für diese Veranstaltung eingetragen.
UnauthorizedSheetCorrector: Sie sind nicht als Korrektor für dieses Übungsblatt eingetragen.
UnauthorizedCorrectorAny: Sie sind nicht als Korrektor für eine Veranstaltung eingetragen.
UnauthorizedParticipant: Sie sind nicht als Teilnehmer für diese Veranstaltung registriert. UnauthorizedParticipant: Sie sind nicht als Teilnehmer für diese Veranstaltung registriert.
UnauthorizedSheetTime: Dieses Übungsblatt ist momentan nicht freigegeben.
UnauthorizedSubmissionOwner: Sie sind an dieser Abgabe nicht beteiligt.
UnauthorizedSubmissionCorrector: Sie sind nicht der Korrektor für diese Abgabe.
OnlyUploadOneFile: Bitte nur eine Datei hochladen. OnlyUploadOneFile: Bitte nur eine Datei hochladen.
DeprecatedRoute: Diese Ansicht ist obsolet und könnte in Zukunft entfallen.
UnfreeMaterials: Die Materialien für diese Veranstaltung sind nicht allgemein freigegeben.
UnauthorizedWrite: Sie haben hierfür keine Schreibberechtigung
SubmissionWrongSheet: Abgabenummer gehört nicht zum angegebenen Übungsblatt.
SubmissionAlreadyExists: Sie haben bereits eine Abgabe zu diesem Übungsblatt.
SubmissionTitle tid@TermIdentifier courseShortHand@Text sheetName@Text: #{termToText tid}-#{courseShortHand} #{sheetName}: Abgabe editieren/anlegen
SubmissionMember g@Int: Mitabgebende(r) ##{tshow g}
SubmissionArchive: Zip-Archiv der Abgabedatei(en)
SubmissionFile: Datei zur Abgabe
SubmissionAlreadyExistsFor user@Text: #{user} hat bereits eine Abgabe zu diesem Übungsblatt.
EMailUnknown email@Text: E-Mail #{email} gehört zu keinem bekannten Benutzer.
NotAParticipant user@Text tid@TermIdentifier csh@Text: #{user} ist nicht im Kurs #{termToText tid}-#{csh} angemeldet.
HomeHeading: Startseite
TermsHeading: Semesterübersicht
NumCourses n@Int64: #{tshow n} Kurse

119
models
View File

@ -1,11 +1,12 @@
User User
plugin Text plugin Text
ident Text ident Text
matrikelnummer Text Maybe matrikelnummer Text Maybe
email Text email Text
displayName Text displayName Text
maxFavourites Int default=12 maxFavourites Int default=12
UniqueAuthentication plugin ident UniqueAuthentication plugin ident
UniqueEmail email
UserAdmin UserAdmin
user UserId user UserId
school SchoolId school SchoolId
@ -51,20 +52,20 @@ DegreeCourse json
terms StudyTermsId terms StudyTermsId
UniqueDegreeCourse course degree terms UniqueDegreeCourse course degree terms
Course Course
name Text name Text
description Html Maybe description Html Maybe
linkExternal Text Maybe linkExternal Text Maybe
shorthand Text shorthand Text
termId TermId term TermId
schoolId SchoolId school SchoolId
capacity Int Maybe capacity Int Maybe
hasRegistration Bool -- canRegisterNow = hasRegistration && maybe False (<= currentTime) registerFrom && maybe True (>= currentTime) registerTo hasRegistration Bool -- canRegisterNow = hasRegistration && maybe False (<= currentTime) registerFrom && maybe True (>= currentTime) registerTo
registerFrom UTCTime Maybe registerFrom UTCTime Maybe
registerTo UTCTime Maybe registerTo UTCTime Maybe
deregisterUntil UTCTime Maybe deregisterUntil UTCTime Maybe
registerSecret Text Maybe -- Falls ein Passwort erforderlich ist registerSecret Text Maybe -- Falls ein Passwort erforderlich ist
materialFree Bool default=true materialFree Bool default=true
CourseTermShort termId shorthand CourseTermShort term shorthand
CourseEdit CourseEdit
user UserId user UserId
time UTCTime time UTCTime
@ -75,29 +76,28 @@ CourseFavourite
course CourseId course CourseId
UniqueCourseFavourite user course UniqueCourseFavourite user course
Lecturer Lecturer
userId UserId user UserId
courseId CourseId course CourseId
UniqueLecturer userId courseId UniqueLecturer user course
Corrector Corrector -- deprecated
userId UserId user UserId
courseId CourseId course CourseId
load Load load Load
-- SELECT submissionID FROM Tutorial, TutorialUser, Submission, Sheet -- SELECT submissionID FROM Tutorial, TutorialUser, Submission, Sheet
-- WHERE ( tutorialTutor = correctorUserId -- WHERE ( tutorialTutor = correctorUserId
-- && tutorialCourse = correctorCourseId -- && tutorialCourse = correctorCourseId
-- && tutorialUserTutorial = tutorialId -- && tutorialUserTutorial = tutorialId
-- && submissionUser = tutorialUserUser -- && submissionUser = tutorialUserUser
-- && sheetId = submissionSheetId -- && sheetId = SubmissionSheet
-- && sheetCourse = correctorCourseId -- && sheetCourse = correctorCourseId
-- ) -- )
UniqueCorrector userId courseId
CourseParticipant CourseParticipant
courseId CourseId course CourseId
userId UserId user UserId
registration UTCTime registration UTCTime
UniqueParticipant userId courseId UniqueParticipant user course
Sheet Sheet
courseId CourseId course CourseId
name Text name Text
description Html Maybe description Html Maybe
type SheetType type SheetType
@ -108,23 +108,28 @@ Sheet
activeTo UTCTime activeTo UTCTime
hintFrom UTCTime Maybe hintFrom UTCTime Maybe
solutionFrom UTCTime Maybe solutionFrom UTCTime Maybe
CourseSheet courseId name CourseSheet course name
SheetEdit SheetEdit
user UserId user UserId
time UTCTime time UTCTime
sheet SheetId sheet SheetId
SheetCorrector
user UserId
sheet SheetId
load Load
UniqueSheetCorrector user sheet
SheetFile SheetFile
sheetId SheetId sheet SheetId
fileId FileId file FileId
type SheetFileType type SheetFileType
UniqueSheetFile fileId sheetId type UniqueSheetFile file sheet type
File File
title FilePath title FilePath
content ByteString Maybe -- Nothing iff this is a directory content ByteString Maybe -- Nothing iff this is a directory
modified UTCTime modified UTCTime
deriving Show Eq deriving Show Eq
Submission Submission
sheetId SheetId sheet SheetId
ratingPoints Points Maybe ratingPoints Points Maybe
ratingComment Text Maybe ratingComment Text Maybe
ratingBy UserId Maybe ratingBy UserId Maybe
@ -135,37 +140,37 @@ SubmissionEdit
time UTCTime time UTCTime
submission SubmissionId submission SubmissionId
SubmissionFile SubmissionFile
submissionId SubmissionId submission SubmissionId
fileId FileId file FileId
isUpdate Bool isUpdate Bool -- is this the file updated by a corrector (original will always be retained)
isDeletion Bool isDeletion Bool -- only set if isUpdate is also set, but file was deleted by corrector
UniqueSubmissionFile fileId submissionId isUpdate UniqueSubmissionFile file submission isUpdate
deriving Show deriving Show
SubmissionUser SubmissionUser
userId UserId user UserId
submissionId SubmissionId submission SubmissionId
UniqueSubmissionUser userId submissionId UniqueSubmissionUser user submission
SubmissionGroup SubmissionGroup
courseId CourseId course CourseId
name Text name Text Maybe
SubmissionGroupEdit SubmissionGroupEdit
user UserId user UserId
time UTCTime time UTCTime
submissionGroup SubmissionGroupId submissionGroup SubmissionGroupId
SubmissionGroupUser SubmissionGroupUser
submissionGroupId SubmissionGroupId submissionGroup SubmissionGroupId
userId UserId user UserId
UniqueSubmissionGroupUser submissionGroupId userId UniqueSubmissionGroupUser submissionGroup user
Tutorial json Tutorial json
name Text name Text
tutor UserId tutor UserId
course CourseId course CourseId
TutorialUser TutorialUser
userId UserId user UserId
tutorialId TutorialId tutorial TutorialId
UniqueTutorialUser userId tutorialId UniqueTutorialUser user tutorial
Booking Booking
termId TermId term TermId
begin UTCTime begin UTCTime
end UTCTime end UTCTime
weekly Bool weekly Bool
@ -182,17 +187,17 @@ Room
building Text Maybe building Text Maybe
-- BookingRoom -- BookingRoom
-- subject RoomForId -- subject RoomForId
-- roomId RoomId -- room RoomId
-- bookingId BookingId -- booking BookingId
-- UniqueRoomCourse subject roomId bookingId -- UniqueRoomCourse subject room booking
+RoomFor +RoomFor
courseId CourseId course CourseId
tutorialId TutorialId tutorial TutorialId
examId ExamId exam ExamId
-- data RoomFor = RoomForCourseIdSum CourseId | RoomForTutorialIdSum TutorialId ... -- data RoomFor = RoomForCourseSum CourseId | RoomForTutorialSum TutorialId ...
-- EXAMS ARE TODO: -- EXAMS ARE TODO:
Exam Exam
courseId CourseId course CourseId
name Text name Text
description Text description Text
begin UTCTime begin UTCTime
@ -207,8 +212,8 @@ Exam
-- time UTCTime -- time UTCTime
-- exam ExamId -- exam ExamId
--ExamUser --ExamUser
-- userId UserId -- user UserId
-- examId ExamId -- examId ExamId
-- -- CONTINUE HERE: Include rating in this table or separately? -- -- CONTINUE HERE: Include rating in this table or separately?
-- UniqueExamUser userId examId -- UniqueExamUser user examId
-- By default this file is used in Model.hs (which is imported by Foundation.hs) -- By default this file is used in Model.hs (which is imported by Foundation.hs)

View File

@ -79,6 +79,8 @@ dependencies:
- parsec - parsec
- uuid - uuid
- exceptions - exceptions
- lens
- MonadRandom
# 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.
@ -91,6 +93,7 @@ library:
- -Wall - -Wall
- -fwarn-tabs - -fwarn-tabs
- -O0 - -O0
- -ddump-splices
cpp-options: -DDEVELOPMENT cpp-options: -DDEVELOPMENT
else: else:
ghc-options: ghc-options:

92
routes
View File

@ -1,41 +1,69 @@
/static StaticR Static appStatic --
/auth AuthR Auth getAuth -- Accesss granted via tags; default is no accesss.
-- Permission must be explicitly granted.
--
-- Access permission is the disjunction of permit tags
-- Tags are split on "AND" to encode conjunction.
--
-- Note that nested routes automatically inherit all tags from the parent.
--
-- Admins always have access to entities within their assigned schools.
--
-- Access Tags:
-- !free -- free for all
-- !lecturer -- lecturer for this course (or the school, if route is not connected to a course)
-- !corrector -- corrector for this sheet (or the submission, if route is connected to a submission, or the course, if route is not connected to a sheet, or any course, if route is not connected to a course)
-- !registered -- participant for this course (no effect outside of courses)
-- !owner -- part of the group of owners of this submission
--
-- !materials -- only if course allows all materials to be free (no meaning outside of courses)
-- !time -- access depends on time somehow
-- !isRead -- only if it is read-only access (i.e. GET but not POST)
-- !isWrite -- only if it is write access (i.e. POST only) why needed???
--
-- !deprecated -- like free, but logs and gives a warning
--
/favicon.ico FaviconR GET /static StaticR Static appStatic !free
/robots.txt RobotsR GET /auth AuthR Auth getAuth !free
/ HomeR GET POST /favicon.ico FaviconR GET !free
/profile ProfileR GET /robots.txt RobotsR GET !free
/users UsersR GET !adminAny
/term TermShowR GET / HomeR GET POST !free
/term/edit TermEditR GET POST !adminAny /profile ProfileR GET !free
/term/#TermId/edit TermEditExistR GET !adminAny /users UsersR GET -- no tags, i.e. admins only
/course/ CourseListR GET /terms TermShowR GET !free
!/course/new CourseNewR GET POST !lecturerAny /terms/current TermCurrentR GET !free
!/course/#TermId CourseListTermR GET /terms/edit TermEditR GET POST
/course/#TermId/#Text CourseR !updateFavourite: /terms/#TermId/edit TermEditExistR GET
/show CourseShowR GET POST !/terms/#TermId TermCourseListR GET !free
/edit CourseEditR GET POST !lecturer
/ex SheetR !registered: -- For Pattern Synonyms see Foundation
/ SheetListR GET /course/ CourseListR GET !free
/#Text/show SheetShowR GET !time !/course/new CourseNewR GET POST !lecturer
/#Text/#SheetFileType/#FilePath SheetFileR GET !time /course/#TermId/#Text CourseR !lecturer:
/new SheetNewR GET POST !lecturer /show CShowR GET POST !free
/#Text/edit SheetEditR GET POST !lecturer /edit CEditR GET POST
/#Text/delete SheetDelR GET POST !lecturer /ex SheetListR GET !registered !materials
!/ex/new SheetNewR GET POST
/ex/#Text SheetR:
/show SShowR GET !timeANDregistered !timeANDmaterials !corrector
/#SheetFileType/#FilePath SFileR GET !timeANDregistered !timeANDmaterials !corrector
/edit SEditR GET POST
/delete SDelR GET POST
!/sub/new SubmissionNewR GET POST !timeANDregistered
!/sub/own SubmissionOwnR GET !free
!/sub/#CryptoUUIDSubmission SubmissionR GET POST !owner !corrector
!/#UUID CryptoUUIDDispatchR GET !free -- just redirect
-- TODO below -- TODO below
/submission SubmissionListR GET POST !/#{ZIPArchiveName SubmissionId} SubmissionDownloadArchiveR GET !deprecated
/submission/#CryptoUUIDSubmission SubmissionR GET POST !/#CryptoUUIDSubmission/#FilePath SubmissionDownloadSingleR GET !deprecated
/submissions.zip SubmissionDownloadMultiArchiveR POST
!/submission/archive/#FilePath SubmissionDownloadArchiveR GET
!/submission/#CryptoUUIDSubmission/#FilePath SubmissionDownloadSingleR GET
!/#UUID CryptoUUIDDispatchR GET /submission SubmissionListR GET !deprecated
/submission/#CryptoUUIDSubmission SubmissionDemoR GET POST !deprecated
-- For demonstration /submissions.zip SubmissionDownloadMultiArchiveR POST !deprecated
/course/#CryptoUUIDCourse/edit CourseEditIDR GET

View File

@ -24,13 +24,12 @@ let
override = oldAttrs: { override = oldAttrs: {
nativeBuildInputs = oldAttrs.nativeBuildInputs ++ (with pkgs; [ postgresql cabal-install ]) ++ (with haskellPackages; [ stack yesod-bin ]); nativeBuildInputs = oldAttrs.nativeBuildInputs ++ (with pkgs; [ postgresql cabal-install ]) ++ (with haskellPackages; [ stack yesod-bin ]);
shellHook = '' shellHook = ''
${oldAttrs.shellHook}
export PROMPT_INFO="${oldAttrs.name}" export PROMPT_INFO="${oldAttrs.name}"
pgDir=$(mktemp -d) pgDir=$(mktemp -d)
pgSockDir=$(mktemp -d) pgSockDir=$(mktemp -d)
pgLogFile=$(mktemp) pgLogFile=$(mktemp)
pg_ctl init -D ''${pgDir} initdb --no-locale -D ''${pgDir}
pg_ctl start -D ''${pgDir} -l ''${pgLogFile} -w -o "-k ''${pgSockDir} -c listen_addresses=''' -c hba_file='${postgresHba}' -c unix_socket_permissions=0700" pg_ctl start -D ''${pgDir} -l ''${pgLogFile} -w -o "-k ''${pgSockDir} -c listen_addresses=''' -c hba_file='${postgresHba}' -c unix_socket_permissions=0700"
export PGHOST=''${pgSockDir} PGLOG=''${pgLogFile} export PGHOST=''${pgSockDir} PGLOG=''${pgLogFile}
psql -f ${postgresSchema} postgres psql -f ${postgresSchema} postgres
@ -42,6 +41,8 @@ let
} }
trap cleanup EXIT trap cleanup EXIT
${oldAttrs.shellHook}
''; '';
}; };

View File

@ -1,8 +1,11 @@
{-# LANGUAGE DataKinds #-} {-# LANGUAGE DataKinds #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE NoImplicitPrelude #-} {-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE FlexibleInstances, TypeSynonymInstances #-} {-# LANGUAGE FlexibleInstances, TypeSynonymInstances #-}
{-# LANGUAGE RecordWildCards, ViewPatterns, PatternGuards #-}
{-# LANGUAGE PatternSynonyms #-}
{-# OPTIONS_GHC -fno-warn-orphans #-} {-# OPTIONS_GHC -fno-warn-orphans #-}
module CryptoID module CryptoID
@ -24,6 +27,9 @@ import System.FilePath.Cryptographic.ImplicitNamespace
import Data.UUID.Types import Data.UUID.Types
import Web.PathPieces import Web.PathPieces
import Data.CaseInsensitive (CI)
import qualified Data.CaseInsensitive as CI
instance PathPiece UUID where instance PathPiece UUID where
fromPathPiece = fromString . unpack fromPathPiece = fromString . unpack
@ -35,5 +41,33 @@ decCryptoIDs [ ''SubmissionId
, ''CourseId , ''CourseId
, ''SheetId , ''SheetId
, ''FileId , ''FileId
, ''UserId
] ]
{- TODO: Do we need/want CryptoUUIDs for Sheet numbers? -} {- TODO: Do we need/want CryptoUUIDs for Sheet numbers? -}
newtype SubmissionMode = SubmissionMode (Maybe CryptoUUIDSubmission)
deriving (Show, Read, Eq)
pattern NewSubmission :: SubmissionMode
pattern NewSubmission = SubmissionMode Nothing
pattern ExistingSubmission :: CryptoUUIDSubmission -> SubmissionMode
pattern ExistingSubmission cID = SubmissionMode (Just cID)
instance PathPiece SubmissionMode where
fromPathPiece "new" = Just $ SubmissionMode Nothing
fromPathPiece s = SubmissionMode . Just <$> fromPathPiece s
toPathPiece (SubmissionMode Nothing) = "new"
toPathPiece (SubmissionMode (Just x)) = toPathPiece x
newtype ZIPArchiveName objID = ZIPArchiveName (CryptoID (CI FilePath) objID)
deriving (Show, Read, Eq)
instance PathPiece (ZIPArchiveName objID) where
fromPathPiece (map CI.mk . unpack -> s)
| Just s' <- stripSuffix (map CI.mk ".zip") s = Just . ZIPArchiveName . CryptoID . CI.mk $ map CI.original s'
| otherwise = Nothing
toPathPiece (ZIPArchiveName CryptoID{..}) = pack (CI.foldedCase ciphertext) <> ".zip"

View File

@ -1,14 +1,17 @@
{-# LANGUAGE NoImplicitPrelude #-} {-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE TemplateHaskell, QuasiQuotes #-}
{-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE ViewPatterns #-} {-# LANGUAGE ViewPatterns #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE DataKinds #-} {-# LANGUAGE DataKinds #-}
{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE PatternGuards #-} {-# LANGUAGE PatternGuards #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE FlexibleInstances, UndecidableInstances #-} {-# LANGUAGE FlexibleInstances, UndecidableInstances #-}
module Foundation where module Foundation where
@ -29,6 +32,7 @@ import LDAP.Search (LDAPEntry(..))
import Yesod.Default.Util (addStaticContentExternal) import Yesod.Default.Util (addStaticContentExternal)
import Yesod.Core.Types (Logger) import Yesod.Core.Types (Logger)
import qualified Yesod.Core.Unsafe as Unsafe import qualified Yesod.Core.Unsafe as Unsafe
import Data.CaseInsensitive (CI)
import qualified Data.CaseInsensitive as CI import qualified Data.CaseInsensitive as CI
import qualified Data.Text.Encoding as TE import qualified Data.Text.Encoding as TE
@ -44,6 +48,13 @@ import qualified Data.ByteString.Lazy as Lazy.ByteString
import qualified Data.Text as Text import qualified Data.Text as Text
import qualified Data.Text.Encoding as Text import qualified Data.Text.Encoding as Text
import Data.List (foldr1)
import Data.Set (Set)
import qualified Data.Set as Set
import Data.Map (Map, (!?))
import qualified Data.Map as Map
import Data.Conduit (($$)) import Data.Conduit (($$))
import Data.Conduit.List (sourceList) import Data.Conduit.List (sourceList)
@ -51,12 +62,14 @@ import qualified Database.Esqueleto as E
import Control.Monad.Except (MonadError(..), runExceptT) import Control.Monad.Except (MonadError(..), runExceptT)
import Control.Monad.Trans.Maybe (MaybeT(..)) import Control.Monad.Trans.Maybe (MaybeT(..))
import Control.Monad.Trans.Reader (runReader)
import System.FilePath import System.FilePath
import Handler.Utils.Templates import Handler.Utils.Templates
import Handler.Utils.StudyFeatures import Handler.Utils.StudyFeatures
import Control.Lens
import Utils.Lens
-- infixl 9 :$: -- infixl 9 :$:
-- pattern a :$: b = a b -- pattern a :$: b = a b
@ -88,17 +101,29 @@ data UniWorX = UniWorX
-- type Widget = WidgetT UniWorX IO () -- type Widget = WidgetT UniWorX IO ()
mkYesodData "UniWorX" $(parseRoutesFile "routes") mkYesodData "UniWorX" $(parseRoutesFile "routes")
-- | Convenient Type Synonyms:
type DB a = YesodDB UniWorX a
type Form x = Html -> MForm (HandlerT UniWorX IO) (FormResult x, Widget)
type MsgRenderer = MsgRendererS UniWorX -- see Utils
-- Pattern Synonyms for convenience -- Pattern Synonyms for convenience
pattern CSheetR tid csh ptn = CourseR tid csh (SheetR ptn) pattern CSheetR tid csh shn ptn
= CourseR tid csh (SheetR shn ptn)
-- Menus and Favourites
data MenuItem = MenuItem data MenuItem = MenuItem
{ menuItemLabel :: Text { menuItemLabel :: Text
, menuItemIcon :: Maybe Text , menuItemIcon :: Maybe Text
, menuItemRoute :: Route UniWorX , menuItemRoute :: Route UniWorX
, menuItemAccessCallback :: Handler Bool , menuItemAccessCallback' :: Handler Bool -- Check whether action is shown in ADDITION to authorization (which is always checked)
} }
menuItemAccessCallback :: MenuItem -> Handler Bool
menuItemAccessCallback MenuItem{..} = (&&) <$> ((==) Authorized <$> authCheck) <*> menuItemAccessCallback'
where
authCheck = handleAny (\_ -> return . Unauthorized $ error "authCheck caught exception") $ isAuthorized menuItemRoute False
data MenuTypes -- Semantische Rolle: data MenuTypes -- Semantische Rolle:
= NavbarAside { menuItem :: MenuItem } -- TODO = NavbarAside { menuItem :: MenuItem } -- TODO
| NavbarExtra { menuItem :: MenuItem } -- TODO | NavbarExtra { menuItem :: MenuItem } -- TODO
@ -107,10 +132,7 @@ data MenuTypes -- Semantische Rolle:
| PageActionPrime { menuItem :: MenuItem } -- Seitenspezifische Aktion, häufig | PageActionPrime { menuItem :: MenuItem } -- Seitenspezifische Aktion, häufig
| PageActionSecondary { menuItem :: MenuItem } -- Seitenspezifische Aktion, selten | PageActionSecondary { menuItem :: MenuItem } -- Seitenspezifische Aktion, selten
-- | Convenient Type Synonyms: -- Messages
type DB a = YesodDB UniWorX a
type Form x = Html -> MForm (HandlerT UniWorX IO) (FormResult x, Widget)
mkMessage "UniWorX" "messages" "de" mkMessage "UniWorX" "messages" "de"
-- This instance is required to use forms. You can modify renderMessage to -- This instance is required to use forms. You can modify renderMessage to
@ -125,6 +147,217 @@ instance RenderMessage UniWorX TermIdentifier where
where renderMessage' = renderMessage foundation ls where renderMessage' = renderMessage foundation ls
-- Access Control
data AccessPredicate
= APPure (Route UniWorX -> Reader MsgRenderer AuthResult)
| APHandler (Route UniWorX -> Handler AuthResult)
| APDB (Route UniWorX -> DB AuthResult)
orAR, andAR :: MsgRenderer -> AuthResult -> AuthResult -> AuthResult
orAR _ Authorized _ = Authorized
orAR _ _ Authorized = Authorized
orAR _ AuthenticationRequired _ = AuthenticationRequired
orAR _ _ AuthenticationRequired = AuthenticationRequired
orAR mr (Unauthorized x) (Unauthorized y) = Unauthorized . render mr $ MsgUnauthorizedOr x y
-- and
andAR mr (Unauthorized x) (Unauthorized y) = Unauthorized . render mr $ MsgUnauthorizedAnd x y
andAR _ reason@(Unauthorized x) _ = reason
andAR _ _ reason@(Unauthorized x) = reason
andAR _ Authorized other = other
andAR _ AuthenticationRequired _ = AuthenticationRequired
orAP,andAP :: AccessPredicate -> AccessPredicate -> AccessPredicate
orAP = liftAR orAR (== Authorized)
andAP = liftAR andAR (const False)
liftAR :: (MsgRenderer -> AuthResult -> AuthResult -> AuthResult)
-> (AuthResult -> Bool) -- ^ Predicate to Short-Circuit on first argument
-> AccessPredicate -> AccessPredicate -> AccessPredicate
-- Ensure to first evaluate Pure conditions, then Handler before DB
liftAR op sc (APPure f) (APPure g) = APPure $ \r -> shortCircuitM sc (f r) (g r) . op =<< ask
liftAR op sc (APHandler f) (APHandler g) = APHandler $ \r -> shortCircuitM sc (f r) (g r) . op =<< getMsgRenderer
liftAR op sc (APDB f) (APDB g) = APDB $ \r -> shortCircuitM sc (f r) (g r) . op =<< getMsgRenderer
liftAR op sc (APPure f) apg = liftAR op sc (APHandler $ \r -> runReader (f r) <$> getMsgRenderer) apg
liftAR op sc apf apg@(APPure _) = liftAR op sc apg apf
liftAR op sc (APHandler f) apdb = liftAR op sc (APDB $ lift . f) apdb
liftAR op sc apdb apg@(APHandler _) = liftAR op sc apg apdb
trueAP,falseAP :: AccessPredicate
trueAP = APPure . const $ return Authorized
falseAP = APPure . const $ Unauthorized . ($ MsgUnauthorized) . render <$> ask
-- TODO: I believe falseAP := adminAP
adminAP :: AccessPredicate -- access for admins (of appropriate school in case of course-routes)
adminAP = APDB $ \case
-- Courses: access only to school admins
CourseR tid csh _ -> exceptT return return $ do
authId <- maybeExceptT AuthenticationRequired $ lift maybeAuthId
[E.Value c] <- lift . E.select . E.from $ \(course `E.InnerJoin` userAdmin) -> do
E.on $ course E.^. CourseSchool E.==. userAdmin E.^. UserAdminSchool
E.where_ $ userAdmin E.^. UserAdminUser E.==. E.val authId
E.&&. course E.^. CourseTerm E.==. E.val tid
E.&&. course E.^. CourseShorthand E.==. E.val csh
return (E.countRows :: E.SqlExpr (E.Value Int64))
guardMExceptT (c > 0) (unauthorizedI MsgUnauthorizedSchoolAdmin)
return Authorized
-- other routes: access to any admin is granted here
_other -> exceptT return return $ do
authId <- maybeExceptT AuthenticationRequired $ lift maybeAuthId
adrights <- lift $ selectFirst [UserAdminUser ==. authId] []
guardMExceptT (isJust adrights) (unauthorizedI $ MsgUnauthorized)
return Authorized
knownTags :: Map (CI Text) AccessPredicate
knownTags = -- should not throw exceptions, i.e. no getBy404 or requireAuthId
[("free", trueAP)
,("deprecated", APHandler $ \r -> do
$logWarnS "AccessControl" ("deprecated route: " <> tshow r)
addMessageI "error" MsgDeprecatedRoute
return Authorized
)
,("lecturer", APDB $ \case
CourseR tid csh _ -> exceptT return return $ do
authId <- maybeExceptT AuthenticationRequired $ lift maybeAuthId
[E.Value c] <- lift . E.select . E.from $ \(course `E.InnerJoin` lecturer) -> do
E.on $ course E.^. CourseId E.==. lecturer E.^. LecturerCourse
E.where_ $ lecturer E.^. LecturerUser E.==. E.val authId
E.&&. course E.^. CourseTerm E.==. E.val tid
E.&&. course E.^. CourseShorthand E.==. E.val csh
return (E.countRows :: E.SqlExpr (E.Value Int64))
guardMExceptT (c>0) (unauthorizedI MsgUnauthorizedLecturer)
return Authorized
_ -> exceptT return return $ do
authId <- maybeExceptT AuthenticationRequired $ lift maybeAuthId
void . maybeMExceptT (unauthorizedI MsgUnauthorizedSchoolLecturer) $ selectFirst [UserLecturerUser ==. authId] []
return Authorized
)
,("corrector", APDB $ \route -> exceptT return return $ do
authId <- maybeExceptT AuthenticationRequired $ lift maybeAuthId
resList <- lift . E.select . E.from $ \(course `E.InnerJoin` sheet `E.InnerJoin` sheetCorrector) -> do
E.on $ sheetCorrector E.^. SheetCorrectorSheet E.==. sheet E.^. SheetId
E.on $ sheet E.^. SheetCourse E.==. course E.^. CourseId
E.where_ $ sheetCorrector E.^. SheetCorrectorUser E.==. E.val authId
return (course E.^. CourseId, sheet E.^. SheetId)
let
resMap :: Map CourseId (Set SheetId)
resMap = Map.fromListWith Set.union [ (cid, Set.singleton sid) | (E.Value cid, E.Value sid) <- resList ]
case route of
CSheetR _ _ _ (SubmissionR cID) -> maybeT (unauthorizedI MsgUnauthorizedSubmissionCorrector) $ do
sid <- catchIfMaybeT (const True :: CryptoIDError -> Bool) $ decrypt cID
Submission{..} <- MaybeT . lift $ get sid
guard $ maybe False (== authId) submissionRatingBy
return Authorized
CSheetR tid csh shn _ -> maybeT (unauthorizedI MsgUnauthorizedSheetCorrector) $ do
Entity cid _ <- MaybeT . lift . getBy $ CourseTermShort tid csh
Entity sid _ <- MaybeT . lift . getBy $ CourseSheet cid shn
guard $ sid `Set.member` fromMaybe Set.empty (resMap !? cid)
return Authorized
CourseR tid csh _ -> maybeT (unauthorizedI MsgUnauthorizedCorrector) $ do
Entity cid _ <- MaybeT . lift . getBy $ CourseTermShort tid csh
guard $ cid `Set.member` Map.keysSet resMap
return Authorized
_ -> do
guardMExceptT (not $ Map.null resMap) (unauthorizedI MsgUnauthorizedCorrectorAny)
return Authorized
)
,("time", APDB $ \case
CSheetR tid csh shn subRoute -> maybeT (unauthorizedI MsgUnauthorizedSheetTime) $ do
Entity cid _ <- MaybeT . getBy $ CourseTermShort tid csh
Entity sid Sheet{..} <- MaybeT . getBy $ CourseSheet cid shn
cTime <- liftIO getCurrentTime
case subRoute of
SFileR SheetExercise _ -> guard $ maybe False (<= cTime) sheetVisibleFrom
SFileR SheetHint _ -> guard $ maybe False (<= cTime) sheetHintFrom
SFileR SheetSolution _ -> guard $ maybe False (<= cTime) sheetSolutionFrom
SFileR SheetMarking _ -> mzero -- only for correctors and lecturers
SubmissionNewR -> guard $ sheetActiveFrom <= cTime && cTime <= sheetActiveTo
_ -> guard $ maybe False (<= cTime) sheetVisibleFrom
return Authorized
r -> do
$logErrorS "AccessControl" $ "'!time' used on route that doesn't support it: " <> tshow r
unauthorizedI MsgUnauthorized
)
,("registered", APDB $ \case
CourseR tid csh _ -> exceptT return return $ do
authId <- maybeExceptT AuthenticationRequired $ lift maybeAuthId
[E.Value c] <- lift . E.select . E.from $ \(course `E.InnerJoin` courseParticipant) -> do
E.on $ course E.^. CourseId E.==. courseParticipant E.^. CourseParticipantCourse
E.where_ $ courseParticipant E.^. CourseParticipantUser E.==. E.val authId
E.&&. course E.^. CourseTerm E.==. E.val tid
E.&&. course E.^. CourseShorthand E.==. E.val csh
return (E.countRows :: E.SqlExpr (E.Value Int64))
guardMExceptT (c > 0) (unauthorizedI MsgUnauthorizedParticipant)
return Authorized
r -> do
$logErrorS "AccessControl" $ "'!registered' used on route that doesn't support it: " <> tshow r
unauthorizedI MsgUnauthorized
)
,("materials", APDB $ \case
CourseR tid csh _ -> maybeT (unauthorizedI MsgUnfreeMaterials) $ do
Entity _ Course{..} <- MaybeT . getBy $ CourseTermShort tid csh
guard courseMaterialFree
return Authorized
r -> do
$logErrorS "AccessControl" $ "'!materials' used on route that doesn't support it: " <> tshow r
unauthorizedI MsgUnauthorized
)
,("owner", APDB $ \case
CSheetR _ _ _ (SubmissionR cID) -> exceptT return return $ do
sid <- catchIfMExceptT (const $ unauthorizedI MsgUnauthorizedSubmissionOwner) (const True :: CryptoIDError -> Bool) $ decrypt cID
authId <- maybeExceptT AuthenticationRequired $ lift maybeAuthId
void . maybeMExceptT (unauthorizedI MsgUnauthorizedSubmissionOwner) . getBy $ UniqueSubmissionUser authId sid
return Authorized
CSheetR _ _ _ SubmissionNewR -> unauthorizedI MsgUnauthorizedSubmissionOwner
r -> do
$logErrorS "AccessControl" $ "'!owner' used on route that doesn't support it: " <> tshow r
unauthorizedI MsgUnauthorized
)
,("isRead", APHandler $ \route ->
bool <$> return Authorized
<*> unauthorizedI MsgUnauthorizedWrite
<*> isWriteRequest route
)
,("isWrite", APHandler $ \route -> do
write <- isWriteRequest route
if write
then return Authorized
else unauthorizedI MsgUnauthorized
)
]
tag2ap :: Text -> AccessPredicate
tag2ap t = case Map.lookup (CI.mk t) knownTags of
(Just acp) -> acp
Nothing -> APHandler $ \_route -> do -- Can this be pure like falseAP? GK: not if we want to log a message (which we definitely should)
$logWarnS "AccessControl" $ "'" <> t <> "' not known to access control"
unauthorizedI MsgUnauthorized
route2ap :: Route UniWorX -> AccessPredicate
route2ap r = foldr orAP adminAP attrsAND -- adminAP causes all to be in DB!!! GK: Due to shortCircuitM this (while still true) is no longer costly (we do a `runDB` but then only actually send off queries, if needed)
where
attrsAND = map splitAND $ Set.toList $ routeAttrs r
splitAND = foldr1 andAP . map tag2ap . Text.splitOn "AND"
evalAccessDB :: Route UniWorX -> DB AuthResult -- all requests, regardless of POST/GET, use isWriteRequest otherwise
evalAccessDB r = case route2ap r of
(APPure p) -> lift $ runReader (p r) <$> getMsgRenderer
(APHandler p) -> lift $ p r
(APDB p) -> p r
evalAccess :: Route UniWorX -> Handler AuthResult
evalAccess r = case route2ap r of
(APPure p) -> runReader (p r) <$> getMsgRenderer
(APHandler p) -> p r
(APDB p) -> runDB $ p r
-- TODO: isAuthorized = evalAccess'
-- Please see the documentation for the Yesod typeclass. There are a number -- Please see the documentation for the Yesod typeclass. There are a number
-- of settings which can be configured by overriding methods here. -- of settings which can be configured by overriding methods here.
instance Yesod UniWorX where instance Yesod UniWorX where
@ -151,9 +384,9 @@ instance Yesod UniWorX where
yesodMiddleware handler = do yesodMiddleware handler = do
res <- defaultYesodMiddleware handler res <- defaultYesodMiddleware handler
void . runMaybeT $ do void . runMaybeT $ do
route@(routeAttrs -> attrs) <- MaybeT getCurrentRoute route <- MaybeT getCurrentRoute
case route of case route of -- update Course Favourites here
CourseR tid csh _ | "updateFavourite" `elem` attrs -> do CourseR tid csh _ -> do
uid <- MaybeT maybeAuthId uid <- MaybeT maybeAuthId
$(logDebug) "Favourites save" $(logDebug) "Favourites save"
now <- liftIO $ getCurrentTime now <- liftIO $ getCurrentTime
@ -161,7 +394,7 @@ instance Yesod UniWorX where
cid <- MaybeT . getKeyBy $ CourseTermShort tid csh cid <- MaybeT . getKeyBy $ CourseTermShort tid csh
user <- MaybeT $ get uid user <- MaybeT $ get uid
-- update Favourites -- update Favourites
lift $ upsertBy void . lift $ upsertBy
(UniqueCourseFavourite uid cid) (UniqueCourseFavourite uid cid)
(CourseFavourite uid now cid) (CourseFavourite uid now cid)
[CourseFavouriteTime =. now] [CourseFavouriteTime =. now]
@ -176,26 +409,84 @@ instance Yesod UniWorX where
_other -> return () _other -> return ()
return res return res
defaultLayout = defaultLinkLayout [] defaultLayout widget = do
master <- getYesod
mmsgs <- getMessages
mcurrentRoute <- getCurrentRoute
-- Get the breadcrumbs, as defined in the YesodBreadcrumbs instance.
(title, parents) <- breadcrumbs
let
menu = defaultLinks ++ maybe [] pageActions mcurrentRoute
menuTypes <- filterM (menuItemAccessCallback . menuItem) menu
-- Lookup Favourites if possible
favourites' <- do
muid <- maybeAuthId
case muid of
Nothing -> return []
(Just uid) -> runDB . E.select . E.from $ \(course `E.InnerJoin` courseFavourite) -> do
E.on (course E.^. CourseId E.==. courseFavourite E.^. CourseFavouriteCourse)
E.where_ (courseFavourite E.^. CourseFavouriteUser E.==. E.val uid)
E.orderBy [ E.asc $ course E.^. CourseShorthand ]
return course
favourites <- forM favourites' $ \(Entity _ c@Course{..})
-> let
courseRoute = CourseR courseTerm courseShorthand CShowR
in (c, courseRoute, ) <$> filterM (menuItemAccessCallback . menuItem) (pageActions courseRoute)
-- We break up the default layout into two components:
-- default-layout is the contents of the body tag, and
-- default-layout-wrapper is the entire page. Since the final
-- value passed to hamletToRepHtml cannot be a widget, this allows
-- you to use normal widget features in default-layout.
let
navbar :: Widget
navbar = $(widgetFile "widgets/navbar")
asidenav :: Widget
asidenav = $(widgetFile "widgets/asidenav")
contentHeadline :: Maybe Widget
contentHeadline = pageHeading =<< mcurrentRoute
breadcrumbs :: Widget
breadcrumbs = $(widgetFile "widgets/breadcrumbs")
pageactionprime :: Widget
pageactionprime = $(widgetFile "widgets/pageactionprime")
-- functions to determine if there are page-actions
isPageActionPrime :: MenuTypes -> Bool
isPageActionPrime (PageActionPrime _) = True
isPageActionPrime _ = False
hasPageActions :: Bool
hasPageActions = any isPageActionPrime menuTypes
pc <- widgetToPageContent $ do
addStylesheetRemote "https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,600,800,900"
addScript $ StaticR js_zepto_js
addScript $ StaticR js_fetchPolyfill_js
addScript $ StaticR js_urlPolyfill_js
addScript $ StaticR js_featureChecker_js
addScript $ StaticR js_flatpickr_js
addScript $ StaticR js_tabber_js
addStylesheet $ StaticR css_flatpickr_css
addStylesheet $ StaticR css_tabber_css
addStylesheet $ StaticR css_fonts_css
addStylesheet $ StaticR css_icons_css
$(widgetFile "default-layout")
$(widgetFile "standalone/modal")
$(widgetFile "standalone/showHide")
$(widgetFile "standalone/inputs")
$(widgetFile "standalone/tabber")
withUrlRenderer $(hamletFile "templates/default-layout-wrapper.hamlet")
-- The page to be redirected to when authentication is required. -- The page to be redirected to when authentication is required.
authRoute _ = Just $ AuthR LoginR authRoute _ = Just $ AuthR LoginR
isAuthorized (AuthR _) _ = return Authorized isAuthorized route _isWrite = evalAccess route
isAuthorized HomeR _ = return Authorized
isAuthorized FaviconR _ = return Authorized
isAuthorized RobotsR _ = return Authorized
isAuthorized (StaticR _) _ = return Authorized
isAuthorized ProfileR _ = isAuthenticated
isAuthorized TermShowR _ = return Authorized
isAuthorized CourseListR _ = return Authorized
isAuthorized (CourseListTermR _) _ = return Authorized
isAuthorized (CourseR _ _ CourseShowR) _ = return Authorized
isAuthorized (CryptoUUIDDispatchR _) _ = return Authorized
isAuthorized SubmissionListR _ = isAuthenticated
isAuthorized SubmissionDownloadMultiArchiveR _ = isAuthenticated
-- isAuthorized TestR _ = return Authorized
isAuthorized route isWrite = runDB $ isAuthorizedDB route isWrite
-- This function creates static content files in the static folder -- This function creates static content files in the static folder
-- and names them based on a hash of their content. This allows -- and names them based on a hash of their content. This allows
@ -236,92 +527,8 @@ instance Yesod UniWorX where
makeLogger = return . appLogger makeLogger = return . appLogger
isAuthorizedDB :: Route UniWorX -> Bool -> YesodDB UniWorX AuthResult
isAuthorizedDB route@(routeAttrs -> attrs) writeable
| "adminAny" `member` attrs = adminAccess Nothing
| "lecturerAny" `member` attrs = lecturerAccess Nothing
isAuthorizedDB UsersR _ = adminAccess Nothing
isAuthorizedDB (SubmissionR cID) _ = submissionAccess $ Right cID
isAuthorizedDB (SubmissionDownloadSingleR cID _) _ = submissionAccess $ Right cID
isAuthorizedDB (SubmissionDownloadArchiveR (splitExtension -> (baseName, _))) _ = submissionAccess . Left . CryptoID $ CI.mk baseName
isAuthorizedDB TermEditR _ = adminAccess Nothing
isAuthorizedDB (TermEditExistR _) _ = adminAccess Nothing
isAuthorizedDB CourseNewR _ = lecturerAccess Nothing
isAuthorizedDB (CourseR t c CourseEditR) _ = courseLecturerAccess . entityKey =<< getBy404 (CourseTermShort t c)
isAuthorizedDB (CourseR t c (SheetR SheetListR)) False = return Authorized --
isAuthorizedDB (CourseR t c (SheetR SheetListR)) _ = courseLecturerAccess . entityKey =<< getBy404 (CourseTermShort t c)
isAuthorizedDB (CourseR t c (SheetR (SheetShowR s))) _ = return Authorized -- TODO: nur für angemeldete Kursteilnehmer falls sichtbar, sonst nur Lectrurer oder Korrektor
isAuthorizedDB (CourseR t c (SheetR (SheetFileR s _ _))) _ = return Authorized -- TODO: nur für angemeldete Kursteilnehmer falls sichtbar, sonst nur Lectrurer oder Korrektor
isAuthorizedDB (CourseR t c (SheetR SheetNewR)) _ = courseLecturerAccess . entityKey =<< getBy404 (CourseTermShort t c)
isAuthorizedDB (CourseR t c (SheetR (SheetEditR s))) _ = courseLecturerAccess . entityKey =<< getBy404 (CourseTermShort t c)
isAuthorizedDB (CourseR t c (SheetR (SheetDelR s))) _ = courseLecturerAccess . entityKey =<< getBy404 (CourseTermShort t c)
isAuthorizedDB (CourseEditIDR cID) _ = do
courseId <- decrypt cID
courseLecturerAccess courseId
isAuthorizedDB _route _isWrite = return $ Unauthorized "No access to this route." -- Calling isAuthorized here creates infinite loop!
submissionAccess :: Either CryptoFileNameSubmission CryptoUUIDSubmission -> YesodDB UniWorX AuthResult
submissionAccess cID = do
authId <- lift requireAuthId
submissionId <- either decrypt decrypt cID
Submission{..} <- get404 submissionId
submissionUsers <- map (submissionUserUserId . entityVal) <$> selectList [SubmissionUserSubmissionId ==. submissionId] []
let auth = authId `elem` submissionUsers || Just authId == submissionRatingBy
return $ case auth of
True -> Authorized
False -> Unauthorized "No access to this submission"
adminAccess :: Maybe SchoolId -- ^ If @Just@, matched exactly against 'userAdminSchool'
-> YesodDB UniWorX AuthResult
adminAccess school = do
authId <- lift requireAuthId
adrights <- selectList ((UserAdminUser ==. authId) : maybe [] (\s -> [UserAdminSchool ==. s]) school) []
return $ if (not $ null adrights)
then Authorized
else Unauthorized "No admin access" -- TODO internationalize
lecturerAccess :: Maybe SchoolId
-> YesodDB UniWorX AuthResult
lecturerAccess school = do
authId <- lift requireAuthId
lecrights <- selectList ((UserLecturerUser ==. authId) : maybe [] (\s -> [UserLecturerSchool ==. s]) school) []
return $ if (not $ null lecrights)
then Authorized
else Unauthorized "No lecturer access" -- TODO internationalize
lecturerAccess' :: SchoolId -> YesodDB UniWorX AuthResult
lecturerAccess' = authorizedFor UniqueSchoolLecturer MsgUnauthorizedSchoolLecturer
courseLecturerAccess :: CourseId -> YesodDB UniWorX AuthResult
courseLecturerAccess = authorizedFor UniqueLecturer MsgUnauthorizedLecturer
courseCorrectorAccess :: CourseId -> YesodDB UniWorX AuthResult
courseCorrectorAccess = authorizedFor UniqueCorrector MsgUnauthorizedCorrector
courseParticipantAccess :: CourseId -> YesodDB UniWorX AuthResult
courseParticipantAccess = authorizedFor UniqueParticipant MsgUnauthorizedParticipant
authorizedFor :: ( PersistEntityBackend record ~ BaseBackend backend
, PersistEntity record, PersistUniqueRead backend
, YesodAuth master, RenderMessage master msg
)
=> (AuthId master -> t -> Unique record) -> msg -> t -> ReaderT backend (HandlerT master IO) AuthResult
authorizedFor authType msg courseId = do
authId <- lift requireAuthId
access <- getBy $ authType authId courseId
case access of
(Just _) -> return Authorized
Nothing -> unauthorizedI msg
isAuthorizedDB' :: Route UniWorX -> Bool -> YesodDB UniWorX Bool
isAuthorizedDB' route isWrite = (== Authorized) <$> isAuthorizedDB route isWrite
isAuthorized' :: Route UniWorX -> Bool -> Handler Bool
isAuthorized' route isWrite = runDB $ isAuthorizedDB' route isWrite
-- Define breadcrumbs. -- Define breadcrumbs.
instance YesodBreadcrumbs UniWorX where instance YesodBreadcrumbs UniWorX where
breadcrumb TermShowR = return ("Semester", Just HomeR) breadcrumb TermShowR = return ("Semester", Just HomeR)
@ -329,25 +536,89 @@ 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 (toPathPiece term, Just TermShowR) breadcrumb (TermCourseListR term) = return (toPathPiece term, Just TermShowR)
breadcrumb (CourseR term course CourseShowR) = return (course, Just $ CourseListTermR term) breadcrumb (CourseR term course CShowR) = return (course, Just $ TermCourseListR term)
breadcrumb CourseNewR = return ("Neu", Just CourseListR) breadcrumb CourseNewR = return ("Neu", Just CourseListR)
breadcrumb (CourseR _ _ CourseEditR) = return ("Editieren", Just CourseListR) breadcrumb (CourseR _ _ CEditR) = return ("Editieren", Just CourseListR)
breadcrumb (CourseR tid csh (SheetR SheetListR)) = return ("Übungen",Just $ CourseR tid csh CourseShowR) breadcrumb (CourseR tid csh SheetListR) = return ("Übungen",Just $ CourseR tid csh CShowR)
breadcrumb (CourseR tid csh (SheetR SheetNewR )) = return ("Neu", Just $ CourseR tid csh $ SheetR SheetListR) breadcrumb (CourseR tid csh SheetNewR ) = return ("Neu", Just $ CourseR tid csh SheetListR)
breadcrumb (CourseR tid csh (SheetR (SheetShowR shn))) = return (shn, Just $ CourseR tid csh $ SheetR SheetListR) breadcrumb (CSheetR tid csh shn SShowR) = return (shn, Just $ CourseR tid csh SheetListR)
breadcrumb (CourseR tid csh (SheetR (SheetEditR shn))) = return ("Edit", Just $ CourseR tid csh $ SheetR $ SheetShowR shn) breadcrumb (CSheetR tid csh shn SEditR) = return ("Edit", Just $ CSheetR tid csh shn SShowR)
breadcrumb (CourseR tid csh (SheetR (SheetDelR shn))) = return ("DELETE", Just $ CourseR tid csh $ SheetR $ SheetShowR shn) breadcrumb (CSheetR tid csh shn SDelR ) = return ("DELETE", Just $ CSheetR tid csh shn SShowR)
breadcrumb (CSheetR tid csh shn (SubmissionR _)) = return ("Abgabe", Just $ CSheetR tid csh shn SShowR)
breadcrumb SubmissionListR = return ("Abgaben", Just HomeR) breadcrumb SubmissionListR = return ("Abgaben", Just HomeR)
breadcrumb (SubmissionR _) = return ("Abgabe", Just SubmissionListR)
breadcrumb HomeR = return ("UniworkY", Nothing)
breadcrumb HomeR = return ("Uniworky", Nothing)
breadcrumb (AuthR _) = return ("Login", Just HomeR) breadcrumb (AuthR _) = return ("Login", Just HomeR)
breadcrumb ProfileR = return ("Profile", Just HomeR) breadcrumb ProfileR = return ("Profile", Just HomeR)
breadcrumb _ = return ("home", Nothing) breadcrumb _ = return ("home", Nothing)
pageActions :: Route UniWorX -> [MenuTypes]
pageActions (CourseR tid csh CShowR) =
[ PageActionPrime $ MenuItem
{ menuItemLabel = "Übungsblätter"
, menuItemIcon = Nothing
, menuItemRoute = CourseR tid csh SheetListR
, menuItemAccessCallback' = return True
}
, PageActionPrime $ MenuItem
{ menuItemLabel = "Kurs Editieren"
, menuItemIcon = Nothing
, menuItemRoute = CourseR tid csh CEditR
, menuItemAccessCallback' = return True
}
]
pageActions (CourseR tid csh SheetListR) =
[ PageActionPrime $ MenuItem
{ menuItemLabel = "Neues Übungsblatt"
, menuItemIcon = Nothing
, menuItemRoute = CourseR tid csh SheetNewR
, menuItemAccessCallback' = return True
}
]
pageActions (CSheetR tid csh shn SShowR) =
[ PageActionPrime $ MenuItem
{ menuItemLabel = "Abgabe anlegen"
, menuItemIcon = Nothing
, menuItemRoute = CSheetR tid csh shn SubmissionNewR
, menuItemAccessCallback' = return True -- TODO: check that no submission already exists
}
, PageActionPrime $ MenuItem
{ menuItemLabel = "Abgabe"
, menuItemIcon = Nothing
, menuItemRoute = CSheetR tid csh shn SubmissionOwnR
, menuItemAccessCallback' = return True -- TODO: check that a submission already exists
}
]
pageActions TermShowR =
[ PageActionPrime $ MenuItem
{ menuItemLabel = "Neues Semester"
, menuItemIcon = Nothing
, menuItemRoute = TermEditR
, menuItemAccessCallback' = return True
}
]
pageActions (TermCourseListR _) =
[ PageActionPrime $ MenuItem
{ menuItemLabel = "Neuer Kurs"
, menuItemIcon = Just "book"
, menuItemRoute = CourseNewR
, menuItemAccessCallback' = return True
}
]
pageActions _ = []
pageHeading :: Route UniWorX -> Maybe Widget
pageHeading HomeR
= Just [whamlet|_{MsgHomeHeading}|]
pageHeading TermShowR
= Just [whamlet|_{MsgTermsHeading}|]
pageHeading _
= Nothing
defaultLinks :: [MenuTypes] defaultLinks :: [MenuTypes]
defaultLinks = -- Define the menu items of the header. defaultLinks = -- Define the menu items of the header.
@ -355,114 +626,46 @@ defaultLinks = -- Define the menu items of the header.
{ menuItemLabel = "Home" { menuItemLabel = "Home"
, menuItemIcon = Just "home" , menuItemIcon = Just "home"
, menuItemRoute = HomeR , menuItemRoute = HomeR
, menuItemAccessCallback = return True , menuItemAccessCallback' = return True
} }
, NavbarRight $ MenuItem , NavbarRight $ MenuItem
{ menuItemLabel = "Profile" { menuItemLabel = "Profile"
, menuItemIcon = Just "profile" , menuItemIcon = Just "profile"
, menuItemRoute = ProfileR , menuItemRoute = ProfileR
, menuItemAccessCallback = isJust <$> maybeAuthPair , menuItemAccessCallback' = isJust <$> maybeAuthPair
} }
, NavbarSecondary $ MenuItem , NavbarSecondary $ MenuItem
{ menuItemLabel = "Login" { menuItemLabel = "Login"
, menuItemIcon = Just "login" , menuItemIcon = Just "login"
, menuItemRoute = AuthR LoginR , menuItemRoute = AuthR LoginR
, menuItemAccessCallback = isNothing <$> maybeAuthPair , menuItemAccessCallback' = isNothing <$> maybeAuthPair
} }
, NavbarSecondary $ MenuItem , NavbarSecondary $ MenuItem
{ menuItemLabel = "Logout" { menuItemLabel = "Logout"
, menuItemIcon = Just "logout" , menuItemIcon = Just "logout"
, menuItemRoute = AuthR LogoutR , menuItemRoute = AuthR LogoutR
, menuItemAccessCallback = isJust <$> maybeAuthPair , menuItemAccessCallback' = isJust <$> maybeAuthPair
}
, NavbarAside $ MenuItem
{ menuItemLabel = "Aktuelle Veranstaltungen"
, menuItemIcon = Just "book"
, menuItemRoute = CourseListR -- should be CourseListActiveR or similar in the future
, menuItemAccessCallback = return True
}
, NavbarAside $ MenuItem
{ menuItemLabel = "Alte Veranstaltungen"
, menuItemIcon = Just "book"
, menuItemRoute = CourseListR -- should be CourseListInactiveR or similar in the future
, menuItemAccessCallback = return True
} }
, NavbarAside $ MenuItem , NavbarAside $ MenuItem
{ menuItemLabel = "Veranstaltungen" { menuItemLabel = "Veranstaltungen"
, menuItemIcon = Just "book" , menuItemIcon = Just "book"
, menuItemRoute = CourseListR , menuItemRoute = CourseListR -- should be CourseListActiveR or similar in the future
, menuItemAccessCallback = return True , menuItemAccessCallback' = return True
}
, NavbarAside $ MenuItem
{ menuItemLabel = "Semester"
, menuItemIcon = Nothing
, menuItemRoute = CourseListR -- should be TermListR ,,,
, menuItemAccessCallback' = return True
} }
, NavbarAside $ MenuItem , NavbarAside $ MenuItem
{ menuItemLabel = "Benutzer" { menuItemLabel = "Benutzer"
, menuItemIcon = Just "user" , menuItemIcon = Just "user"
, menuItemRoute = UsersR , menuItemRoute = UsersR
, menuItemAccessCallback = return True -- Creates a LOOP: (Authorized ==) <$> isAuthorized UsersR False , menuItemAccessCallback' = return True -- Creates a LOOP: (Authorized ==) <$> isAuthorized UsersR False
} }
] ]
defaultLinkLayout :: [MenuTypes] -> Widget -> Handler Html
defaultLinkLayout = defaultMenuLayout . (defaultLinks ++)
defaultMenuLayout :: [MenuTypes] -> Widget -> Handler Html
defaultMenuLayout menu widget = do
master <- getYesod
mmsgs <- getMessages
mcurrentRoute <- getCurrentRoute
-- Get the breadcrumbs, as defined in the YesodBreadcrumbs instance.
(title, parents) <- breadcrumbs
menuTypes <- filterM (menuItemAccessCallback . menuItem) menu
-- Lookup Favourites if possible
favourites <- do
muid <- maybeAuthId
case muid of
Nothing -> return []
(Just uid) -> runDB . E.select . E.from $ \(course `E.InnerJoin` courseFavourite) -> do
E.on (course E.^. CourseId E.==. courseFavourite E.^. CourseFavouriteCourse)
E.where_ (courseFavourite E.^. CourseFavouriteUser E.==. E.val uid)
E.orderBy [ E.asc $ course E.^. CourseShorthand ]
return course
-- We break up the default layout into two components:
-- default-layout is the contents of the body tag, and
-- default-layout-wrapper is the entire page. Since the final
-- value passed to hamletToRepHtml cannot be a widget, this allows
-- you to use normal widget features in default-layout.
let
navbar :: Widget
navbar = $(widgetFile "widgets/navbar")
asidenav :: Widget
asidenav = $(widgetFile "widgets/asidenav")
breadcrumbs :: Widget
breadcrumbs = $(widgetFile "widgets/breadcrumbs")
pageactionprime :: Widget
pageactionprime = $(widgetFile "widgets/pageactionprime")
-- functions to determine if there are page-actions
isPageActionPrime :: MenuTypes -> Bool
isPageActionPrime (PageActionPrime _) = True
isPageActionPrime _ = False
hasPageActions :: Bool
hasPageActions = any isPageActionPrime menuTypes
pc <- widgetToPageContent $ do
addStylesheetRemote "https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,600,800,900"
addScript $ StaticR js_featureChecker_js
addScript $ StaticR js_flatpickr_js
addStylesheet $ StaticR css_fonts_css
addStylesheet $ StaticR css_icons_css
addStylesheet $ StaticR css_flatpickr_css
$(widgetFile "default-layout")
$(widgetFile "standalone/modal")
$(widgetFile "standalone/showHide")
$(widgetFile "standalone/sortable")
$(widgetFile "standalone/inputs")
withUrlRenderer $(hamletFile "templates/default-layout-wrapper.hamlet")
-- How to run database actions. -- How to run database actions.
instance YesodPersist UniWorX where instance YesodPersist UniWorX where
type YesodPersistBackend UniWorX = SqlBackend type YesodPersistBackend UniWorX = SqlBackend

View File

@ -1,6 +1,7 @@
{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE NoImplicitPrelude #-} {-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ViewPatterns #-}
{-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE MultiParamTypeClasses #-}
@ -26,11 +27,18 @@ import qualified Data.UUID.Cryptographic as UUID
getCourseListR :: Handler TypedContent getCourseListR :: Handler TypedContent
getCourseListR = redirect TermShowR getCourseListR = redirect TermShowR
getCourseListTermR :: TermId -> Handler Html getTermCurrentR :: Handler Html
getCourseListTermR tidini = do getTermCurrentR = do
termIds <- runDB $ selectKeysList [TermActive ==. True] [] -- [Desc TermName] does not work, since database representation has wrong ordering
case fromNullable termIds of
Nothing -> notFound
(Just (maximum -> tid)) -> getTermCourseListR tid
getTermCourseListR :: TermId -> Handler Html
getTermCourseListR tidini = do
(term,courses) <- runDB $ (,) (term,courses) <- runDB $ (,)
<$> get tidini <$> get tidini
<*> selectList [CourseTermId ==. tidini] [Asc CourseShorthand] <*> selectList [CourseTerm ==. tidini] [Asc CourseShorthand]
when (isNothing term) $ do when (isNothing term) $ do
addMessage "warning" [shamlet| Semester #{toPathPiece tidini} nicht gefunden. |] addMessage "warning" [shamlet| Semester #{toPathPiece tidini} nicht gefunden. |]
redirect TermShowR redirect TermShowR
@ -39,51 +47,43 @@ getCourseListTermR tidini = do
[ headed "Kürzel" $ (\ckv -> [ headed "Kürzel" $ (\ckv ->
let c = entityVal ckv let c = entityVal ckv
shd = courseShorthand c shd = courseShorthand c
tid = courseTermId c tid = courseTerm c
in [whamlet| <a href=@{CourseR tid shd CourseShowR}>#{shd} |] ) in [whamlet| <a href=@{CourseR tid shd CShowR}>#{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
, headed "Ende Anmeldung" $ fromString.(maybe "" formatTimeGerWD).courseRegisterTo.entityVal , headed "Ende Anmeldung" $ fromString.(maybe "" formatTimeGerWD).courseRegisterTo.entityVal
, headed "Teilnehmer" $ (\ckv -> do , headed "Teilnehmer" $ (\ckv -> do
let cid = entityKey ckv let cid = entityKey ckv
partiNum <- handlerToWidget $ runDB $ count [CourseParticipantCourseId ==. cid] partiNum <- handlerToWidget $ runDB $ count [CourseParticipantCourse ==. cid]
[whamlet| #{show partiNum} |] [whamlet| #{show partiNum} |]
) )
, headed " " $ (\ckv -> , headed " " $ (\ckv ->
let c = entityVal ckv let c = entityVal ckv
shd = courseShorthand c shd = courseShorthand c
tid = courseTermId c tid = courseTerm c
in do in do
adminLink <- handlerToWidget $ isAuthorized (CourseR tid shd CourseEditR) False adminLink <- handlerToWidget $ isAuthorized (CourseR tid shd CEditR) False
-- if (adminLink==Authorized) then linkButton "Ändern" BCWarning (CourseEditR tid shd) else "" -- if (adminLink==Authorized) then linkButton "Ändern" BCWarning (CEditR tid shd) else ""
[whamlet| [whamlet|
$if adminLink == Authorized $if adminLink == Authorized
<a href=@{CourseR tid shd CourseEditR}> <a href=@{CourseR tid shd CEditR}>
editieren editieren
|] |]
) )
] ]
let pageLinks =
[ PageActionPrime $ MenuItem
{ menuItemLabel = "Neuer Kurs"
, menuItemIcon = Just "book"
, menuItemRoute = CourseNewR
, menuItemAccessCallback = (== Authorized) <$> isAuthorized CourseNewR False
}
]
let coursesTable = encodeWidgetTable tableSortable colonnadeTerms courses let coursesTable = encodeWidgetTable tableSortable colonnadeTerms courses
defaultLinkLayout pageLinks $ do defaultLayout $ do
setTitle "Semesterkurse" setTitle "Semesterkurse"
$(widgetFile "courses") $(widgetFile "courses")
getCourseShowR :: TermId -> Text -> Handler Html getCShowR :: TermId -> Text -> Handler Html
getCourseShowR tid csh = do getCShowR 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 tid csh courseEnt@(Entity cid course) <- getBy404 $ CourseTermShort tid csh
dependent <- (,,) dependent <- (,,)
<$> get (courseSchoolId course) -- join <$> get (courseSchool course) -- join
<*> count [CourseParticipantCourseId ==. cid] -- join <*> count [CourseParticipantCourse ==. cid] -- join
<*> (case mbAid of -- TODO: Someone please refactor this late-night mess here! <*> (case mbAid of -- TODO: Someone please refactor this late-night mess here!
Nothing -> return False Nothing -> return False
(Just aid) -> do (Just aid) -> do
@ -92,15 +92,7 @@ getCourseShowR tid csh = do
return $ (courseEnt,dependent) return $ (courseEnt,dependent)
let course = entityVal courseEnt let course = entityVal courseEnt
(regWidget, regEnctype) <- generateFormPost $ identifyForm "registerBtn" $ registerButton $ mbRegistered (regWidget, regEnctype) <- generateFormPost $ identifyForm "registerBtn" $ registerButton $ mbRegistered
let pageActions = defaultLayout $ do
[ PageActionPrime $ MenuItem
{ menuItemLabel = "Übungsblätter"
, menuItemIcon = Nothing
, menuItemRoute = CSheetR tid csh SheetListR
, menuItemAccessCallback = (== Authorized) <$> isAuthorized (CSheetR tid csh SheetListR) False
}
]
defaultLinkLayout pageActions $ do
setTitle $ [shamlet| #{toPathPiece tid} - #{csh}|] setTitle $ [shamlet| #{toPathPiece tid} - #{csh}|]
$(widgetFile "course") $(widgetFile "course")
@ -111,8 +103,8 @@ registerButton registered = renderAForm FormStandard $
msg = if registered then "Abmelden" else "Anmelden" msg = if registered then "Abmelden" else "Anmelden"
regMsg = msg :: BootstrapSubmit Text regMsg = msg :: BootstrapSubmit Text
postCourseShowR :: TermId -> Text -> Handler Html postCShowR :: TermId -> Text -> Handler Html
postCourseShowR tid csh = do postCShowR tid csh = do
aid <- requireAuthId aid <- requireAuthId
(cid, registered) <- runDB $ do (cid, registered) <- runDB $ do
(Entity cid _) <- getBy404 $ CourseTermShort tid csh (Entity cid _) <- getBy404 $ CourseTermShort tid csh
@ -130,7 +122,7 @@ postCourseShowR tid csh = do
when (isJust regOk) $ addMessage "success" "Erfolgreich angemeldet!" when (isJust regOk) $ addMessage "success" "Erfolgreich angemeldet!"
(_other) -> return () -- TODO check this! (_other) -> return () -- TODO check this!
-- redirect or not?! I guess not, since we want GET now -- redirect or not?! I guess not, since we want GET now
getCourseShowR tid csh getCShowR tid csh
getCourseNewR :: Handler Html getCourseNewR :: Handler Html
getCourseNewR = do getCourseNewR = do
@ -140,13 +132,13 @@ getCourseNewR = do
postCourseNewR :: Handler Html postCourseNewR :: Handler Html
postCourseNewR = courseEditHandler Nothing postCourseNewR = courseEditHandler Nothing
getCourseEditR :: TermId -> Text -> Handler Html getCEditR :: TermId -> Text -> Handler Html
getCourseEditR tid csh = do getCEditR tid csh = do
course <- runDB $ getBy $ CourseTermShort tid csh course <- runDB $ getBy $ CourseTermShort tid csh
courseEditHandler course courseEditHandler course
postCourseEditR :: TermId -> Text -> Handler Html postCEditR :: TermId -> Text -> Handler Html
postCourseEditR = getCourseEditR postCEditR = getCEditR
getCourseEditIDR :: CryptoUUIDCourse -> Handler Html getCourseEditIDR :: CryptoUUIDCourse -> Handler Html
getCourseEditIDR cID = do getCourseEditIDR cID = do
@ -163,7 +155,7 @@ courseDeleteHandler = undefined
runDB $ deleteCascade cid -- TODO Sicherheitsabfrage einbauen! runDB $ deleteCascade cid -- TODO Sicherheitsabfrage einbauen!
let cti = toPathPiece $ 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 $ TermCourseListR $ cfTerm res
-} -}
courseEditHandler :: Maybe (Entity Course) -> Handler Html courseEditHandler :: Maybe (Entity Course) -> Handler Html
@ -183,8 +175,8 @@ courseEditHandler course = do
, courseDescription = cfDesc res , courseDescription = cfDesc res
, courseLinkExternal = cfLink res , courseLinkExternal = cfLink res
, courseShorthand = cfShort res , courseShorthand = cfShort res
, courseTermId = cfTerm res , courseTerm = cfTerm res
, courseSchoolId = cfSchool res , courseSchool = cfSchool res
, courseCapacity = cfCapacity res , courseCapacity = cfCapacity res
, courseHasRegistration = cfHasReg res , courseHasRegistration = cfHasReg res
, courseRegisterFrom = cfRegFrom res , courseRegisterFrom = cfRegFrom res
@ -199,7 +191,7 @@ courseEditHandler course = do
insert_ $ CourseEdit aid now cid insert_ $ CourseEdit aid now cid
insert_ $ Lecturer aid cid insert_ $ Lecturer aid cid
addMessageI "info" $ MsgCourseNewOk tident csh addMessageI "info" $ MsgCourseNewOk tident csh
redirect $ CourseListTermR tid redirect $ TermCourseListR tid
Nothing -> Nothing ->
addMessageI "danger" $ MsgCourseNewDupShort tident csh addMessageI "danger" $ MsgCourseNewDupShort tident csh
@ -226,8 +218,8 @@ courseEditHandler course = do
-- , CourseDescription =. cfDesc res -- , CourseDescription =. cfDesc res
-- , CourseLinkExternal =. cfLink res -- , CourseLinkExternal =. cfLink res
-- , CourseShorthand =. cfShort res -- TODO: change here should generate a warning, or only allowed for Admins?! -- , CourseShorthand =. cfShort res -- TODO: change here should generate a warning, or only allowed for Admins?!
-- , CourseTermId =. tid -- TODO: change here should generate a warning, or only allowed for Admins?! -- , CourseTerm =. tid -- TODO: change here should generate a warning, or only allowed for Admins?!
-- , CourseSchoolId =. cfSchool res -- , CourseSchool =. cfSchool res
-- , CourseCapacity =. cfCapacity res -- , CourseCapacity =. cfCapacity res
-- , CourseRegisterFrom =. cfRegFrom res -- , CourseRegisterFrom =. cfRegFrom res
-- , CourseRegisterTo =. cfRegTo res -- , CourseRegisterTo =. cfRegTo res
@ -239,8 +231,8 @@ courseEditHandler course = do
, courseDescription = cfDesc res , courseDescription = cfDesc res
, courseLinkExternal = cfLink res , courseLinkExternal = cfLink res
, courseShorthand = cfShort res , courseShorthand = cfShort res
, courseTermId = cfTerm res , courseTerm = cfTerm res
, courseSchoolId = cfSchool res , courseSchool = cfSchool res
, courseCapacity = cfCapacity res , courseCapacity = cfCapacity res
, courseHasRegistration = cfHasReg res , courseHasRegistration = cfHasReg res
, courseRegisterFrom = cfRegFrom res , courseRegisterFrom = cfRegFrom res
@ -254,7 +246,7 @@ courseEditHandler course = do
-- if (isNothing updOkay) -- if (isNothing updOkay)
-- then do -- then do
addMessageI "info" $ MsgCourseEditOk tident csh addMessageI "info" $ MsgCourseEditOk tident csh
-- redirect $ CourseListTermR tid -- redirect $ TermCourseListR tid
-- else addMessageI "danger" $ MsgCourseEditDupShort tident csh -- else addMessageI "danger" $ MsgCourseEditDupShort tident csh
(FormFailure _) -> addMessageI "warning" MsgInvalidInput (FormFailure _) -> addMessageI "warning" MsgInvalidInput
@ -291,8 +283,8 @@ courseToForm cEntity = CourseForm
, cfDesc = courseDescription course , cfDesc = courseDescription course
, cfLink = courseLinkExternal course , cfLink = courseLinkExternal course
, cfShort = courseShorthand course , cfShort = courseShorthand course
, cfTerm = courseTermId course , cfTerm = courseTerm course
, cfSchool = courseSchoolId course , cfSchool = courseSchool course
, cfCapacity = courseCapacity course , cfCapacity = courseCapacity course
, cfHasReg = courseHasRegistration course , cfHasReg = courseHasRegistration course
, cfRegFrom = courseRegisterFrom course , cfRegFrom = courseRegisterFrom course

View File

@ -20,6 +20,8 @@ import Import hiding (Proxy)
import Data.Proxy import Data.Proxy
import Handler.Utils
import Yesod.Core.Types (HandlerContents(..), ErrorResponse(..)) import Yesod.Core.Types (HandlerContents(..), ErrorResponse(..))
import qualified Control.Monad.Catch as E (Handler(..)) import qualified Control.Monad.Catch as E (Handler(..))
@ -30,9 +32,13 @@ class CryptoRoute ciphertext plaintext where
instance CryptoRoute UUID SubmissionId where instance CryptoRoute UUID SubmissionId where
cryptoIDRoute _ (CryptoID -> cID) = do cryptoIDRoute _ (CryptoID -> cID) = do
(_ :: SubmissionId) <- decrypt cID (smid :: SubmissionId) <- decrypt cID
(tid,csh,shn) <- runDB $ do
return $ SubmissionR cID shid <- submissionSheet <$> get404 smid
Sheet{..} <- get404 shid
Course{..} <- get404 sheetCourse
return (courseTerm, courseShorthand, sheetName)
return $ CSheetR tid csh shn $ SubmissionR cID
class Dispatch ciphertext (x :: [*]) where class Dispatch ciphertext (x :: [*]) where

View File

@ -43,7 +43,7 @@ getHomeR :: Handler Html
getHomeR = do getHomeR = do
(btnWdgt, btnEnctype) <- generateFormPost (buttonForm :: Form CreateButton) (btnWdgt, btnEnctype) <- generateFormPost (buttonForm :: Form CreateButton)
defaultLayout $ do defaultLayout $ do
setTitle "Willkommen zum UniworkY Test!" setTitle "Willkommen zum Uniworky Test!"
$(widgetFile "home") $(widgetFile "home")

View File

@ -1,5 +1,6 @@
{-# LANGUAGE NoImplicitPrelude #-} {-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE QuasiQuotes #-}
@ -7,6 +8,7 @@
{-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE TypeOperators #-}
module Handler.Sheet where module Handler.Sheet where
@ -61,6 +63,7 @@ data SheetForm = SheetForm
, sfSolutionFrom :: Maybe UTCTime , sfSolutionFrom :: Maybe UTCTime
, sfSolutionF :: Maybe FileInfo , sfSolutionF :: Maybe FileInfo
-- Keine SheetId im Formular! -- Keine SheetId im Formular!
, sfCorrectors :: [(UserId,Load)]
} }
@ -68,8 +71,8 @@ makeSheetForm :: Maybe SheetId -> Maybe SheetForm -> Form SheetForm
makeSheetForm msId template = identForm FIDsheet $ \html -> do makeSheetForm msId template = identForm FIDsheet $ \html -> do
let oldFileIds fType let oldFileIds fType
| Just sId <- msId = fmap setFromList . fmap (map E.unValue) . runDB . E.select . E.from $ \(file `E.InnerJoin` sheetFile) -> do | Just sId <- msId = fmap setFromList . fmap (map E.unValue) . runDB . E.select . E.from $ \(file `E.InnerJoin` sheetFile) -> do
E.on $ file E.^. FileId E.==. sheetFile E.^. SheetFileFileId E.on $ file E.^. FileId E.==. sheetFile E.^. SheetFileFile
E.where_ $ sheetFile E.^. SheetFileSheetId E.==. E.val sId E.where_ $ sheetFile E.^. SheetFileSheet E.==. E.val sId
E.&&. sheetFile E.^. SheetFileType E.==. E.val fType E.&&. sheetFile E.^. SheetFileType E.==. E.val fType
return (file E.^. FileId) return (file E.^. FileId)
| otherwise = return Set.empty | otherwise = return Set.empty
@ -88,7 +91,8 @@ makeSheetForm msId template = identForm FIDsheet $ \html -> do
<*> fileAFormOpt (fsb "Hinweis") <*> fileAFormOpt (fsb "Hinweis")
<*> aopt utcTimeField (fsb "Lösung ab") (sfSolutionFrom <$> template) <*> aopt utcTimeField (fsb "Lösung ab") (sfSolutionFrom <$> template)
<*> fileAFormOpt (fsb "Lösung") <*> fileAFormOpt (fsb "Lösung")
<* submitButton <*> formToAForm (correctorForm msId (maybe [] sfCorrectors template))
-- <* submitButton
return $ case result of return $ case result of
FormSuccess sheetResult FormSuccess sheetResult
| errorMsgs <- validateSheet sheetResult | errorMsgs <- validateSheet sheetResult
@ -117,36 +121,15 @@ makeSheetForm msId template = identForm FIDsheet $ \html -> do
-- TODO: continue validation here!!! -- TODO: continue validation here!!!
] ] ] ]
correctorForm :: Maybe SheetId -> [(UserId,Load)] -> MForm Handler (FormResult [(UserId,Load)], [FieldView UniWorX])
correctorForm _msid templates = return mempty -- TODO deprecated
-- Datenbank UserId -> UserName
-- Eingabelist für Colonnade
-- enthält die benötigten Felder
-- FormResult konstruieren
-- Eingabebox für Korrektor hinzufügen
-- Eingabe für Korrekt ausgefüllt: FormMissing zurückschicken um dann Feld hinzuzufügen
fetchSheetAux :: ( BaseBackend backend ~ SqlBackend
, E.SqlSelect b a
, Typeable a, MonadHandler m, IsPersistBackend backend
, PersistQueryRead backend, PersistUniqueRead backend
)
=> (E.SqlExpr (Entity Sheet) -> b)
-> Key Term -> Text -> Text -> ReaderT backend m a
fetchSheetAux prj tid csh shn =
let cachId = encodeUtf8 $ tshow (tid,csh,shn)
in cachedBy cachId $ do
-- Mit Yesod:
-- cid <- getKeyBy404 $ CourseTermShort tid csh
-- getBy404 $ CourseSheet cid shn
-- Mit Esqueleto:
sheetList <- E.select . E.from $ \(course `E.InnerJoin` sheet) -> do
E.on $ course E.^. CourseId E.==. sheet E.^. SheetCourseId
E.where_ $ course E.^. CourseTermId E.==. E.val tid
E.&&. course E.^. CourseShorthand E.==. E.val csh
E.&&. sheet E.^. SheetName E.==. E.val shn
return $ prj sheet
case sheetList of
[sheet] -> return sheet
_other -> notFound
fetchSheet :: TermId -> Text -> Text -> YesodDB UniWorX (Entity Sheet)
fetchSheet = fetchSheetAux id
fetchSheetId :: TermId -> Text -> Text -> YesodDB UniWorX (Key Sheet)
fetchSheetId tid cid shn = E.unValue <$> fetchSheetAux (E.^. SheetId) tid cid shn
-- List Sheets -- List Sheets
getSheetListCID :: CourseId -> Handler Html getSheetListCID :: CourseId -> Handler Html
@ -163,16 +146,16 @@ getSheetList courseEnt = do
let cid = entityKey courseEnt let cid = entityKey courseEnt
let course = entityVal courseEnt let course = entityVal courseEnt
let csh = courseShorthand course let csh = courseShorthand course
let tid = courseTermId course let tid = courseTerm course
sheets <- runDB $ do sheets <- runDB $ do
rawSheets <- selectList [SheetCourseId ==. cid] [Desc SheetActiveFrom] rawSheets <- selectList [SheetCourse ==. cid] [Desc SheetActiveFrom]
forM rawSheets $ \(Entity sid sheet) -> do forM rawSheets $ \(Entity sid sheet) -> do
let sheetsub = [SubmissionSheetId ==. sid] let sheetsub = [SubmissionSheet ==. sid]
submissions <- count sheetsub submissions <- count sheetsub
rated <- count $ (SubmissionRatingTime !=. Nothing):sheetsub rated <- count $ (SubmissionRatingTime !=. Nothing):sheetsub
return (sid, sheet, (submissions, rated)) return (sid, sheet, (submissions, rated))
let colBase = mconcat let colBase = mconcat
[ headed "Blatt" $ \(sid,sheet,_) -> linkButton (toWgt $ sheetName sheet) BCLink $ CourseR tid csh $ SheetR $ SheetShowR $ sheetName sheet [ headed "Blatt" $ \(sid,sheet,_) -> simpleLink (toWgt $ sheetName sheet) $ CSheetR tid csh (sheetName sheet) SShowR
, headed "Abgabe ab" $ toWgt . formatTimeGerWD . sheetActiveFrom . snd3 , headed "Abgabe ab" $ toWgt . formatTimeGerWD . sheetActiveFrom . snd3
, headed "Abgabe bis" $ toWgt . formatTimeGerWD . sheetActiveTo . snd3 , headed "Abgabe bis" $ toWgt . formatTimeGerWD . sheetActiveTo . snd3
, headed "Bewertung" $ toWgt . show . sheetType . snd3 , headed "Bewertung" $ toWgt . show . sheetType . snd3
@ -180,69 +163,93 @@ getSheetList courseEnt = do
let colAdmin = mconcat -- only show edit button for allowed course assistants let colAdmin = mconcat -- only show edit button for allowed course assistants
[ headed "Korrigiert" $ toWgt . snd . trd3 [ headed "Korrigiert" $ toWgt . snd . trd3
, headed "Eingereicht" $ toWgt . fst . trd3 , headed "Eingereicht" $ toWgt . fst . trd3
, headed "" $ \s -> linkButton "Edit" BCLink $ CourseR tid csh $ SheetR $ SheetEditR $ sheetName $ snd3 s , headed "" $ \s -> simpleLink "Edit" $ CSheetR tid csh (sheetName $ snd3 s) SEditR
, headed "" $ \s -> linkButton "Delete" BCLink $ CourseR tid csh $ SheetR $ SheetDelR $ sheetName $ snd3 s , headed "" $ \s -> simpleLink "Delete" $ CSheetR tid csh (sheetName $ snd3 s) SDelR
] ]
showAdmin <- case sheets of showAdmin <- case sheets of
((_,firstSheet,_):_) -> do ((_,firstSheet,_):_) -> do
setUltDestCurrent setUltDestCurrent
(Authorized ==) <$> isAuthorized (CourseR tid csh $ SheetR $ SheetEditR $ sheetName firstSheet) False (Authorized ==) <$> isAuthorized (CSheetR tid csh (sheetName firstSheet) SEditR) False
_otherwise -> return False _otherwise -> return False
let colSheets = if showAdmin let colSheets = if showAdmin
then colBase `mappend` colAdmin then colBase `mappend` colAdmin
else colBase else colBase
let pageActions = defaultLayout $ do
[ PageActionPrime $ MenuItem
{ menuItemLabel = "Neues Übungsblatt"
, menuItemIcon = Nothing
, menuItemRoute = CSheetR tid csh SheetNewR
, menuItemAccessCallback = (== Authorized) <$> isAuthorized CourseNewR False
}
]
defaultLinkLayout pageActions $ do
setTitle $ toHtml $ T.append "Übungsblätter " csh setTitle $ toHtml $ T.append "Übungsblätter " csh
if null sheets if null sheets
then [whamlet|Es wurden noch keine Übungsblätter angelegt.|] then [whamlet|Es wurden noch keine Übungsblätter angelegt.|]
else encodeWidgetTable tableDefault colSheets sheets else encodeWidgetTable tableDefault colSheets sheets
-- Show single sheet -- Show single sheet
getSheetShowR :: TermId -> Text -> Text -> Handler Html getSShowR :: TermId -> Text -> Text -> Handler Html
getSheetShowR tid csh shn = do getSShowR tid csh shn = do
entSheet <- runDB $ fetchSheet tid csh shn entSheet <- runDB $ fetchSheet tid csh shn
let sheet = entityVal entSheet let sheet = entityVal entSheet
sid = entityKey entSheet sid = entityKey entSheet
-- -- without Colonnade
fileNameTypes <- runDB $ E.select $ E.from $ -- fileNameTypes <- runDB $ E.select $ E.from $
\(sheet `E.InnerJoin` sheetFile `E.InnerJoin` file) -> do -- \(sheet `E.InnerJoin` sheetFile `E.InnerJoin` file) -> do
-- Restrict to consistent rows that correspond to each other -- -- Restrict to consistent rows that correspond to each other
E.on (file E.^. FileId E.==. sheetFile E.^. SheetFileFileId) -- E.on (file E.^. FileId E.==. sheetFile E.^. SheetFileFile)
E.on (sheetFile E.^. SheetFileSheetId E.==. sheet E.^. SheetId) -- E.on (sheetFile E.^. SheetFileSheet E.==. sheet E.^. SheetId)
-- filter to requested file -- -- filter to requested file
E.where_ (sheet E.^. SheetId E.==. E.val sid ) -- E.where_ (sheet E.^. SheetId E.==. E.val sid )
-- return desired columns -- -- return desired columns
return $ (file E.^. FileTitle, sheetFile E.^. SheetFileType) -- return $ (file E.^. FileTitle, file E.^. FileModified, sheetFile E.^. SheetFileType)
let fileLinks = map (\(E.Value fName, E.Value fType) -> CSheetR tid csh (SheetFileR shn fType fName)) fileNameTypes -- let fileLinks = map (\(E.Value fName, E.Value modified, E.Value fType) -> (CSheetR tid csh (SheetFileR shn fType fName),modified)) fileNameTypes
-- with Colonnade
let fileData (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.^. SheetFileFile)
E.on (sheetFile E.^. SheetFileSheet E.==. sheet E.^. SheetId)
-- filter to requested file
E.where_ $ sheet E.^. SheetId E.==. E.val sid
E.&&. E.not_ (E.isNothing $ file E.^. FileContent)
-- return desired columns
return $ (file E.^. FileTitle, file E.^. FileModified, sheetFile E.^. SheetFileType)
let colonnadeFiles = mconcat
[ sortable (Just "type") "Typ" $ \(_,_, E.Value ftype) -> textCell $ toPathPiece ftype
, sortable (Just "path") "Dateiname" $ anchorCell (\(E.Value fName,_,E.Value fType) -> CSheetR tid csh shn (SFileR fType fName))
(\(E.Value fName,_,_) -> str2widget fName)
, sortable (Just "time") "Modifikation" $ \(_,E.Value modified,_) -> stringCell $ formatTimeGerWDT (modified :: UTCTime)
]
fileTable <- dbTable def $ DBTable
{ dbtSQLQuery = fileData
, dbtColonnade = colonnadeFiles
, dbtAttrs = tableDefault
, dbtIdent = "files" :: Text
, dbtSorting = [ ( "type"
, SortColumn $ \(sheet `E.InnerJoin` sheetFile `E.InnerJoin` file) -> sheetFile E.^. SheetFileType
)
, ( "path"
, SortColumn $ \(sheet `E.InnerJoin` sheetFile `E.InnerJoin` file) -> file E.^. FileTitle
)
, ( "time"
, SortColumn $ \(sheet `E.InnerJoin` sheetFile `E.InnerJoin` file) -> file E.^. FileModified
)
]
}
defaultLayout $ do defaultLayout $ do
setTitle $ toHtml $ T.append "Übung " $ sheetName sheet setTitle $ toHtml $ T.append "Übung " $ sheetName sheet
$(widgetFile "sheetShow") $(widgetFile "sheetShow")
[whamlet| Under Construction !!! |] -- TODO [whamlet| Under Construction !!! |] -- TODO
getSFileR :: TermId -> Text -> Text -> SheetFileType -> FilePath -> Handler TypedContent
getSheetFileR :: TermId -> Text -> Text -> SheetFileType -> FilePath -> Handler TypedContent getSFileR tid csh shn typ title = do
getSheetFileR tid csh shn typ title = do
content <- runDB $ E.select $ E.from $ content <- runDB $ E.select $ E.from $
\(course `E.InnerJoin` sheet `E.InnerJoin` sheetFile `E.InnerJoin` file) -> do \(course `E.InnerJoin` sheet `E.InnerJoin` sheetFile `E.InnerJoin` file) -> do
-- Restrict to consistent rows that correspond to each other -- Restrict to consistent rows that correspond to each other
E.on (file E.^. FileId E.==. sheetFile E.^. SheetFileFileId) E.on (file E.^. FileId E.==. sheetFile E.^. SheetFileFile)
E.on (sheetFile E.^. SheetFileSheetId E.==. sheet E.^. SheetId) E.on (sheetFile E.^. SheetFileSheet E.==. sheet E.^. SheetId)
E.on (sheet E.^. SheetCourseId E.==. course E.^. CourseId) E.on (sheet E.^. SheetCourse E.==. course E.^. CourseId)
-- filter to requested file -- filter to requested file
E.where_ ((file E.^. FileTitle E.==. E.val title) E.where_ ((file E.^. FileTitle E.==. E.val title)
E.&&. (sheetFile E.^. SheetFileType E.==. E.val typ ) E.&&. (sheetFile E.^. SheetFileType E.==. E.val typ )
E.&&. (sheet E.^. SheetName E.==. E.val shn ) E.&&. (sheet E.^. SheetName E.==. E.val shn )
E.&&. (course E.^. CourseShorthand E.==. E.val csh ) E.&&. (course E.^. CourseShorthand E.==. E.val csh )
E.&&. (course E.^. CourseTermId E.==. E.val tid ) E.&&. (course E.^. CourseTerm E.==. E.val tid )
) )
-- return desired columns -- return desired columns
return $ file E.^. FileContent return $ file E.^. FileContent
@ -266,13 +273,13 @@ postSheetNewR :: TermId -> Text -> Handler Html
postSheetNewR = getSheetNewR postSheetNewR = getSheetNewR
getSheetEditR :: TermId -> Text -> Text -> Handler Html getSEditR :: TermId -> Text -> Text -> Handler Html
getSheetEditR tid csh shn = do getSEditR tid csh shn = do
(sheetEnt, sheetFileIds) <- runDB $ do (sheetEnt, sheetFileIds) <- runDB $ do
ent <- fetchSheet tid csh shn ent <- fetchSheet tid csh shn
fIds <- fmap setFromList . fmap (map E.unValue) . E.select . E.from $ \(file `E.InnerJoin` sheetFile) -> do fIds <- fmap setFromList . fmap (map E.unValue) . E.select . E.from $ \(file `E.InnerJoin` sheetFile) -> do
E.on $ file E.^. FileId E.==. sheetFile E.^. SheetFileFileId E.on $ file E.^. FileId E.==. sheetFile E.^. SheetFileFile
E.where_ $ sheetFile E.^. SheetFileSheetId E.==. E.val (entityKey ent) E.where_ $ sheetFile E.^. SheetFileSheet E.==. E.val (entityKey ent)
E.&&. sheetFile E.^. SheetFileType E.==. E.val SheetExercise E.&&. sheetFile E.^. SheetFileType E.==. E.val SheetExercise
return (file E.^. FileId) return (file E.^. FileId)
return (ent, fIds) return (ent, fIds)
@ -292,6 +299,7 @@ getSheetEditR tid csh shn = do
, sfHintF = Nothing -- TODO , sfHintF = Nothing -- TODO
, sfSolutionFrom = sheetSolutionFrom , sfSolutionFrom = sheetSolutionFrom
, sfSolutionF = Nothing -- TODO , sfSolutionF = Nothing -- TODO
, sfCorrectors = [] -- TODO read correctors from list
} }
let action newSheet = do let action newSheet = do
replaceRes <- myReplaceUnique sid $ newSheet replaceRes <- myReplaceUnique sid $ newSheet
@ -300,8 +308,8 @@ getSheetEditR tid csh shn = do
(Just _err) -> return $ Nothing -- More specific error message for edit old sheet could go here (Just _err) -> return $ Nothing -- More specific error message for edit old sheet could go here
handleSheetEdit tid csh (Just sid) template action handleSheetEdit tid csh (Just sid) template action
postSheetEditR :: TermId -> Text -> Text -> Handler Html postSEditR :: TermId -> Text -> Text -> Handler Html
postSheetEditR = getSheetEditR postSEditR = getSEditR
handleSheetEdit :: TermId -> Text -> Maybe SheetId -> Maybe SheetForm -> (Sheet -> YesodDB UniWorX (Maybe SheetId)) -> Handler Html handleSheetEdit :: TermId -> Text -> Maybe SheetId -> Maybe SheetForm -> (Sheet -> YesodDB UniWorX (Maybe SheetId)) -> Handler Html
handleSheetEdit tid csh msId template dbAction = do handleSheetEdit tid csh msId template dbAction = do
@ -315,7 +323,7 @@ handleSheetEdit tid csh msId template dbAction = do
actTime <- liftIO getCurrentTime actTime <- liftIO getCurrentTime
cid <- getKeyBy404 $ CourseTermShort tid csh cid <- getKeyBy404 $ CourseTermShort tid csh
let newSheet = Sheet let newSheet = Sheet
{ sheetCourseId = cid { sheetCourse = cid
, sheetName = sfName , sheetName = sfName
, sheetDescription = sfDescription , sheetDescription = sfDescription
, sheetType = sfType , sheetType = sfType
@ -337,44 +345,44 @@ handleSheetEdit tid csh msId template dbAction = do
insert_ $ SheetEdit aid actTime sid insert_ $ SheetEdit aid actTime sid
addMessageI "info" $ MsgSheetEditOk tident csh sfName addMessageI "info" $ MsgSheetEditOk tident csh sfName
return True return True
when saveOkay $ redirect $ CSheetR tid csh $ SheetShowR sfName -- redirect must happen outside of runDB when saveOkay $ redirect $ CSheetR tid csh sfName SShowR -- redirect must happen outside of runDB
(FormFailure msgs) -> forM_ msgs $ (addMessage "warning") . toHtml (FormFailure msgs) -> forM_ msgs $ (addMessage "warning") . toHtml
_ -> return () _ -> return ()
let pageTitle = maybe (MsgSheetTitleNew tident csh) let pageTitle = maybe (MsgSheetTitleNew tident csh)
(MsgSheetTitle tident csh) mbshn (MsgSheetTitle tident csh) mbshn
let formTitle = pageTitle let formTitle = pageTitle
let formText = Nothing :: Maybe UniWorXMessage let formText = Nothing :: Maybe UniWorXMessage
actionUrl <- fromMaybe (CSheetR tid csh SheetNewR) <$> getCurrentRoute actionUrl <- fromMaybe (CourseR tid csh SheetNewR) <$> getCurrentRoute
defaultLayout $ do defaultLayout $ do
setTitleI pageTitle setTitleI pageTitle
$(widgetFile "formPageI18n") $(widgetFile "formPageI18n")
getSheetDelR :: TermId -> Text -> Text -> Handler Html getSDelR :: TermId -> Text -> Text -> Handler Html
getSheetDelR tid csh shn = do getSDelR tid csh shn = do
let tident = unTermKey tid let tident = unTermKey tid
((result,formWidget), formEnctype) <- runFormPost (buttonForm :: Form BtnDelete) ((result,formWidget), formEnctype) <- runFormPost (buttonForm :: Form BtnDelete)
case result of case result of
(FormSuccess BtnAbort) -> redirectUltDest $ CSheetR tid csh $ SheetShowR shn (FormSuccess BtnAbort) -> redirectUltDest $ CSheetR tid csh shn SShowR
(FormSuccess BtnDelete) -> do (FormSuccess BtnDelete) -> do
runDB $ fetchSheetId tid csh shn >>= deleteCascade runDB $ fetchSheetId tid csh shn >>= deleteCascade
-- TODO: deleteCascade löscht aber nicht die hochgeladenen Dateien!!! -- TODO: deleteCascade löscht aber nicht die hochgeladenen Dateien!!!
setMessageI $ MsgSheetDelOk tident csh shn addMessageI "info" $ MsgSheetDelOk tident csh shn
redirect $ CSheetR tid csh SheetListR redirect $ CourseR tid csh SheetListR
_other -> do _other -> do
submissionno <- runDB $ do submissionno <- runDB $ do
sid <- fetchSheetId tid csh shn sid <- fetchSheetId tid csh shn
count [SubmissionSheetId ==. sid] count [SubmissionSheet ==. sid]
let formTitle = MsgSheetDelTitle tident csh shn let formTitle = MsgSheetDelTitle tident csh shn
let formText = Just $ MsgSheetDelText submissionno let formText = Just $ MsgSheetDelText submissionno
let actionUrl = CSheetR tid csh $ SheetDelR shn let actionUrl = CSheetR tid csh shn SDelR
defaultLayout $ do defaultLayout $ do
setTitleI $ MsgSheetTitle tident csh shn setTitleI $ MsgSheetTitle tident csh shn
$(widgetFile "formPageI18n") $(widgetFile "formPageI18n")
postSheetDelR :: TermId -> Text -> Text -> Handler Html postSDelR :: TermId -> Text -> Text -> Handler Html
postSheetDelR = getSheetDelR postSDelR = getSDelR
@ -389,8 +397,8 @@ insertSheetFile sid ftype finfo = do
insertSheetFile' :: SheetId -> SheetFileType -> Source Handler (Either FileId File) -> YesodDB UniWorX () insertSheetFile' :: SheetId -> SheetFileType -> Source Handler (Either FileId File) -> YesodDB UniWorX ()
insertSheetFile' sid ftype fs = do insertSheetFile' sid ftype fs = do
oldFileIds <- fmap setFromList . fmap (map E.unValue) . E.select . E.from $ \(file `E.InnerJoin` sheetFile) -> do oldFileIds <- fmap setFromList . fmap (map E.unValue) . E.select . E.from $ \(file `E.InnerJoin` sheetFile) -> do
E.on $ file E.^. FileId E.==. sheetFile E.^. SheetFileFileId E.on $ file E.^. FileId E.==. sheetFile E.^. SheetFileFile
E.where_ $ sheetFile E.^. SheetFileSheetId E.==. E.val sid E.where_ $ sheetFile E.^. SheetFileSheet E.==. E.val sid
E.&&. sheetFile E.^. SheetFileType E.==. E.val ftype E.&&. sheetFile E.^. SheetFileType E.==. E.val ftype
return (file E.^. FileId) return (file E.^. FileId)
keep <- execWriterT . runConduit $ transPipe (lift . lift) fs =$= C.mapM_ finsert keep <- execWriterT . runConduit $ transPipe (lift . lift) fs =$= C.mapM_ finsert

View File

@ -1,6 +1,8 @@
{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE NoImplicitPrelude #-} {-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE ParallelListComp #-}
{-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE MultiParamTypeClasses #-}
@ -9,6 +11,8 @@
{-# LANGUAGE TupleSections #-} {-# LANGUAGE TupleSections #-}
{-# LANGUAGE LambdaCase #-} {-# LANGUAGE LambdaCase #-}
{-# LANGUAGE ViewPatterns #-} {-# LANGUAGE ViewPatterns #-}
{-# LANGUAGE PatternGuards #-}
{-# LANGUAGE TypeOperators #-}
module Handler.Submission where module Handler.Submission where
@ -24,8 +28,11 @@ import Control.Monad.Trans.Maybe
import Control.Monad.State.Class import Control.Monad.State.Class
import Control.Monad.Trans.State.Strict (StateT) import Control.Monad.Trans.State.Strict (StateT)
import qualified Data.Maybe
import qualified Data.Text as Text import qualified Data.Text as Text
import qualified Data.Text.Encoding as Text import qualified Data.Text.Encoding as Text
import Data.CaseInsensitive (CI)
import qualified Data.CaseInsensitive as CI import qualified Data.CaseInsensitive as CI
import qualified Database.Esqueleto as E import qualified Database.Esqueleto as E
@ -33,20 +40,310 @@ import qualified Database.Esqueleto as E
import qualified Data.Conduit.List as Conduit import qualified Data.Conduit.List as Conduit
import Data.Conduit.ResumableSink import Data.Conduit.ResumableSink
import Data.Set (Set)
import qualified Data.Set as Set
import Data.Map (Map) import Data.Map (Map)
import qualified Data.Map as Map import qualified Data.Map as Map
import Data.Bifunctor
import System.FilePath import System.FilePath
import Colonnade import Colonnade hiding (bool)
import Yesod.Colonnade import Yesod.Colonnade
import qualified Text.Blaze.Html5.Attributes as HA import qualified Text.Blaze.Html5.Attributes as HA
numberOfSubmissionEditDates :: Int64
numberOfSubmissionEditDates = 3 -- for debugging only, should be 1 in production.
makeSubmissionForm :: Maybe SubmissionId -> Bool -> SheetGroup -> [Text] -> Form (Maybe (Source Handler File), [Text])
makeSubmissionForm msmid unpackZips grouping buddies = identForm FIDsubmission $ \html -> do
flip (renderAForm FormStandard) html $ (,)
<$> (bool (\f fs _ -> Just <$> areq f fs Nothing) aopt $ isJust msmid) (zipFileField unpackZips) (fsm $ bool MsgSubmissionFile MsgSubmissionArchive unpackZips) Nothing
<*> (catMaybes <$> sequenceA [bool aforced' aopt editableBuddies textField (fsm $ MsgSubmissionMember g) buddy
| g <- [1..(max groupNr $ length buddies)] -- groupNr might have decreased meanwhile
| buddy <- map (Just . Just) buddies ++ repeat Nothing -- show current buddies
])
<* submitButton
where
(groupNr, editableBuddies)
| Arbitrary{..} <- grouping = (pred maxParticipants, True) -- pred to account for the person submitting
| otherwise = (0, False)
aforced' f fs (Just (Just v)) = Just <$> aforced f fs v
aforced' _ _ _ = error "Cannot happen since groupNr==0 if grouping/=Arbitrary"
getSubmissionNewR, postSubmissionNewR :: TermId -> Text -> Text -> Handler Html
getSubmissionNewR = postSubmissionNewR
postSubmissionNewR tid csh shn = submissionHelper tid csh shn NewSubmission
getSubmissionR, postSubmissionR :: TermId -> Text -> Text -> CryptoUUIDSubmission -> Handler Html
getSubmissionR = postSubmissionR
postSubmissionR tid csh shn cid = submissionHelper tid csh shn $ ExistingSubmission cid
getSubmissionOwnR :: TermId -> Text -> Text -> Handler Html
getSubmissionOwnR tid csh shn = do
authId <- requireAuthId
sid <- runDB $ do
shid <- fetchSheetId tid csh shn
submissions <- E.select . E.from $ \(submission `E.InnerJoin` submissionUser) -> do
E.on (submission E.^. SubmissionId E.==. submissionUser E.^. SubmissionUserSubmission)
E.where_ $ submissionUser E.^. SubmissionUserUser E.==. E.val authId
E.&&. submission E.^. SubmissionSheet E.==. E.val shid
return $ submission E.^. SubmissionId
case submissions of
((E.Value sid):_) -> return sid
[] -> notFound
cID <- encrypt sid
redirect . CourseR tid csh . SheetR shn $ SubmissionR cID
submissionHelper :: TermId -> Text -> Text -> SubmissionMode -> Handler Html
submissionHelper tid csh shn (SubmissionMode mcid) = do
uid <- requireAuthId
msmid <- traverse decrypt mcid
(Entity shid Sheet{..}, buddies, lastEdits) <- runDB $ do
sheet@(Entity shid Sheet{..}) <- fetchSheet tid csh shn
case msmid of
Nothing -> do
submissions <- E.select . E.from $ \(submission `E.InnerJoin` submissionUser) -> do
E.on (submission E.^. SubmissionId E.==. submissionUser E.^. SubmissionUserSubmission)
E.where_ $ submissionUser E.^. SubmissionUserUser E.==. E.val uid
E.&&. submission E.^. SubmissionSheet E.==. E.val shid
return $ submission E.^. SubmissionId
-- $logDebugS "Submission.DUPLICATENEW" (tshow submissions)
case submissions of
[] -> do
-- fetch buddies from previous submission in this course
buddies <- E.select . E.from $ \(submissionUser `E.InnerJoin` user) -> do
E.on (submissionUser E.^. SubmissionUserUser E.==. user E.^. UserId)
let oldids = E.subList_select . E.from $ \(sheet `E.InnerJoin` submission `E.InnerJoin` submissionUser `E.InnerJoin` submissionEdit) -> do
E.on (submissionEdit E.^. SubmissionEditSubmission E.==. submission E.^. SubmissionId)
E.on (submissionUser E.^. SubmissionUserSubmission E.==. submission E.^. SubmissionId)
E.on (sheet E.^. SheetId E.==. submission E.^. SubmissionSheet)
E.where_ $ submissionUser E.^. SubmissionUserUser E.==. E.val uid
E.&&. sheet E.^. SheetCourse E.==. E.val sheetCourse
E.orderBy [E.desc $ submissionEdit E.^. SubmissionEditTime]
E.limit 1
return $ submission E.^. SubmissionId
E.where_ $ submissionUser E.^. SubmissionUserSubmission `E.in_` oldids
E.&&. submissionUser E.^. SubmissionUserUser E.!=. E.val uid
E.orderBy [E.asc $ user E.^. UserEmail]
return $ user E.^. UserEmail
return (sheet,buddies,[])
(E.Value smid:_) -> do
cID <- encrypt smid
addMessageI "info" $ MsgSubmissionAlreadyExists
redirect $ CSheetR tid csh shn $ SubmissionR cID
(Just smid) -> do
shid' <- submissionSheet <$> get404 smid
when (shid /= shid') $ invalidArgsI [MsgSubmissionWrongSheet]
-- fetch buddies from current submission
buddies <- E.select . E.from $ \(submissionUser `E.InnerJoin` user) -> do
E.on (submissionUser E.^. SubmissionUserUser E.==. user E.^. UserId)
E.where_ $ submissionUser E.^. SubmissionUserSubmission E.==. E.val smid
E.&&. submissionUser E.^. SubmissionUserUser E.!=. E.val uid
E.orderBy [E.asc $ user E.^. UserEmail]
return $ user E.^. UserEmail
-- mLastEdit <- selectFirst [SubmissionEditSubmission ==. smid] [Desc SubmissionEditTime]
lastEditValues <- E.select . E.from $ \(user `E.InnerJoin` submissionEdit) -> do
E.on (user E.^. UserId E.==. submissionEdit E.^. SubmissionEditUser)
E.where_ $ submissionEdit E.^. SubmissionEditSubmission E.==. E.val smid
E.orderBy [E.desc $ submissionEdit E.^. SubmissionEditTime]
E.limit numberOfSubmissionEditDates
return $ (user E.^. UserDisplayName, submissionEdit E.^. SubmissionEditTime)
let lastEdits = map (bimap E.unValue E.unValue) lastEditValues
return (sheet,buddies,lastEdits)
let unpackZips = True -- undefined -- TODO
((res,formWidget), formEnctype) <- runFormPost $ makeSubmissionForm msmid unpackZips sheetGrouping $ map E.unValue buddies
mCID <- runDB $ do
res' <- case res of
(FormMissing ) -> return $ FormMissing
(FormFailure failmsgs) -> return $ FormFailure failmsgs
(FormSuccess (mFiles,[])) -> return $ FormSuccess (mFiles,[]) -- Type change
(FormSuccess (mFiles, (map CI.mk -> gEMails@(_:_)))) -- Validate AdHoc Group Members
| (Arbitrary {..}) <- sheetGrouping
, length gEMails < maxParticipants -> do -- < since submitting user is already accounted for
let gemails = map CI.foldedCase gEMails
prep :: [(E.Value Text, (E.Value UserId, E.Value Bool, E.Value Bool))] -> Map (CI Text) (Maybe (UserId, Bool, Bool))
prep ps = Map.fromList $ map (, Nothing) gEMails ++ [(CI.mk m, Just (i,p,s))|(E.Value m, (E.Value i, E.Value p, E.Value s)) <- ps]
participants <- fmap prep . E.select . E.from $ \user -> do
E.where_ $ (E.lower_ $ user E.^. UserEmail) `E.in_` E.valList gemails
let
isParticipant = E.sub_select . E.from $ \courseParticipant -> do
E.where_ $ user E.^. UserId E.==. courseParticipant E.^. CourseParticipantUser
E.&&. courseParticipant E.^. CourseParticipantCourse E.==. E.val sheetCourse
return $ E.countRows E.>. E.val (0 :: Int64)
hasSubmitted = E.sub_select . E.from $ \(submissionUser `E.InnerJoin` submission) -> do
E.on $ submissionUser E.^. SubmissionUserSubmission E.==. submission E.^. SubmissionId
E.where_ $ submissionUser E.^. SubmissionUserUser E.==. user E.^. UserId
E.&&. submission E.^. SubmissionSheet E.==. E.val shid
return $ E.countRows E.>. E.val (0 :: Int64)
return (user E.^. UserEmail, (user E.^. UserId, isParticipant, hasSubmitted))
$logDebugS "SUBMISSION.AdHocGroupValidation" $ tshow participants
mr <- getMessageRender
let failmsgs = flip Map.foldMapWithKey participants $ \email -> \case
Nothing -> [mr $ MsgEMailUnknown $ CI.original email]
(Just (_,False,_)) -> [mr $ MsgNotAParticipant (CI.original email) (unTermKey tid) csh]
(Just (_,_, True)) -> [mr $ MsgSubmissionAlreadyExistsFor (CI.original email)]
_other -> mempty
return $ if null failmsgs
then FormSuccess (mFiles, foldMap (\(Just (i,_,_)) -> [i]) participants)
else FormFailure failmsgs
| otherwise -> return $ FormFailure ["Mismatching number of group participants"]
case res' of
(FormSuccess (mFiles,(setFromList -> adhocIds))) -> do
now <- liftIO $ getCurrentTime
smid <- do
smid <- case (mFiles, msmid) of
(Nothing, Just smid) -- no new files, existing submission partners updated
-> return smid
(Just files, _) -- new files
-> runConduit $ transPipe lift files .| extractRatings .| sinkSubmission shid uid ((,False) <$> msmid)
_ -> error "Impossible, because of definition of `makeSubmissionForm`"
-- Determine members of pre-registered group
groupUids <- fmap (setFromList . map E.unValue) . E.select . E.from $ \(submissionGroupUser `E.InnerJoin` submissionGroup `E.InnerJoin` submissionGroupUser') -> do
E.on $ submissionGroup E.^. SubmissionGroupId E.==. submissionGroupUser' E.^. SubmissionGroupUserSubmissionGroup
E.on $ submissionGroupUser E.^. SubmissionGroupUserSubmissionGroup E.==. submissionGroup E.^. SubmissionGroupId
E.where_ $ submissionGroupUser E.^. SubmissionGroupUserUser E.==. E.val uid
E.&&. submissionGroup E.^. SubmissionGroupCourse E.==. E.val sheetCourse
return $ submissionGroupUser' E.^. SubmissionGroupUserUser
-- SubmissionUser for all group members (pre-registered & ad-hoc)
let subUsers = Set.insert uid $ groupUids `Set.union` adhocIds
-- remove obsolete old entries
deleteWhere [SubmissionUserSubmission ==. smid, SubmissionUserUser /<-. setToList subUsers]
-- maybe add current users
forM_ subUsers $ \uid' -> void . insertUnique $ SubmissionUser uid' smid
return smid
cID <- encrypt smid
return $ Just cID
(FormFailure msgs) -> Nothing <$ forM_ msgs (addMessage "warning" . toHtml)
_other -> return Nothing
case mCID of
Just cID -> redirect $ CSheetR tid csh shn $ SubmissionR cID
Nothing -> return ()
mArCid <- fmap ZIPArchiveName <$> traverse encrypt msmid
let pageTitle = MsgSubmissionTitle (unTermKey tid) csh shn
let formTitle = pageTitle
let formText = Nothing :: Maybe UniWorXMessage
actionUrl <- Data.Maybe.fromJust <$> getCurrentRoute
-- Maybe construct a table to display uploaded archive files
let colonnadeFiles cid = mconcat
-- [ sortable (Just "type") "Typ" $ \(_,_, E.Value ftype) -> textCell $ toPathPiece ftype
[ sortable (Just "path") "Dateiname" $ anchorCell (\(Entity _ File{..}) -> SubmissionDownloadSingleR cid fileTitle)
(\(Entity _ File{..}) -> str2widget fileTitle)
, sortable (Just "time") "Modifikation" $ \(Entity _ File{..}) -> stringCell $ formatTimeGerWDT fileModified
]
smid2ArchiveTable (smid,cid) = DBTable
{ dbtSQLQuery = submissionFileQuery smid
, dbtColonnade = colonnadeFiles cid
, dbtAttrs = tableDefault
, dbtIdent = "files" :: Text
, dbtSorting = [ ( "path"
, SortColumn $ \(sf `E.InnerJoin` f) -> f E.^. FileTitle
)
, ( "time"
, SortColumn $ \(sf `E.InnerJoin` f) -> f E.^. FileModified
)
]
, dbtFilter = []
}
mFileTable <- traverse (dbTable def) . fmap smid2ArchiveTable $ (,) <$> msmid <*> mcid
defaultLayout $ do
setTitleI pageTitle
$(widgetFile "formPageI18n")
[whamlet|
$maybe arCid <- mArCid
<hr>
<h2>
<a href=@{SubmissionDownloadArchiveR arCid}>Archiv
$forall (name,time) <- lastEdits
<div>last edited by #{name} at #{formatTimeGerDTlong time}
$maybe fileTable <- mFileTable
<h3>Enthaltene Dateien:
^{fileTable}
|]
submissionFileSource :: SubmissionId -> Source (YesodDB UniWorX) (Entity File)
submissionFileSource = E.selectSource . E.from . submissionFileQuery
submissionFileQuery :: SubmissionId -> E.SqlExpr (Entity SubmissionFile) `E.InnerJoin` E.SqlExpr (Entity File)
-> E.SqlQuery (E.SqlExpr (Entity File))
submissionFileQuery submissionID (sf `E.InnerJoin` f) = E.distinctOnOrderBy [E.asc $ f E.^. FileTitle] $ do
E.on (f E.^. FileId E.==. sf E.^. SubmissionFileFile)
E.where_ $ sf E.^. SubmissionFileSubmission E.==. E.val submissionID
E.where_ . E.not_ $ sf E.^. SubmissionFileIsDeletion -- TODO@gk: won't work as intended! Fix with refactor
E.orderBy [E.desc $ sf E.^. SubmissionFileIsUpdate] -- E.desc returns corrector updated data first
return f
getSubmissionDownloadSingleR :: CryptoUUIDSubmission -> FilePath -> Handler TypedContent
getSubmissionDownloadSingleR cID path = do
submissionID <- decrypt cID
cID' <- encrypt submissionID
runDB $ do
isRating <- maybe False (== submissionID) <$> isRatingFile path
case isRating of
True -> do
file <- runMaybeT $ lift . ratingFile cID' =<< MaybeT (getRating submissionID)
maybe notFound (return . toTypedContent . Text.decodeUtf8) $ fileContent =<< file
False -> do
results <- E.select . E.from $ \(sf `E.InnerJoin` f) -> E.distinctOnOrderBy [E.asc $ f E.^. FileTitle] $ do
E.on (f E.^. FileId E.==. sf E.^. SubmissionFileFile)
E.where_ (sf E.^. SubmissionFileSubmission E.==. E.val submissionID)
E.where_ (f E.^. FileTitle E.==. E.val path)
E.where_ . E.not_ . E.isNothing $ f E.^. FileContent
E.where_ . E.not_ $ sf E.^. SubmissionFileIsDeletion
E.orderBy [E.desc $ sf E.^. SubmissionFileIsUpdate]
return f
let fileName = Text.pack $ takeFileName path
case results of
[Entity _ File{ fileContent = Just c }] -> return $ TypedContent (defaultMimeLookup fileName) (toContent c)
_ -> notFound
getSubmissionDownloadArchiveR :: ZIPArchiveName SubmissionId -> Handler TypedContent
getSubmissionDownloadArchiveR (ZIPArchiveName cID) = do
submissionID <- decrypt cID
cUUID <- encrypt submissionID
respondSourceDB "application/zip" $ do
rating <- lift $ getRating submissionID
case rating of
Nothing -> lift notFound
Just rating' -> do
let fileEntitySource' :: Source (YesodDB UniWorX) File
fileEntitySource' = submissionFileSource submissionID =$= Conduit.map entityVal >> yieldM (ratingFile cID rating')
info = ZipInfo { zipComment = Text.encodeUtf8 . tshow $ ciphertext (cUUID :: CryptoUUIDSubmission) }
fileEntitySource' =$= produceZip info =$= Conduit.map toFlushBuilder
-----------------------------------------------------------------------------------------------
------------------------- DEMO BELOW
submissionTable :: MForm Handler (FormResult [SubmissionId], Widget) submissionTable :: MForm Handler (FormResult [SubmissionId], Widget)
submissionTable = do submissionTable = do
subs <- lift . runDB $ E.select . E.from $ \(sub `E.InnerJoin` sheet `E.InnerJoin` course) -> do subs <- lift . runDB $ E.select . E.from $ \(sub `E.InnerJoin` sheet `E.InnerJoin` course) -> do
E.on $ course E.^. CourseId E.==. sheet E.^. SheetCourseId E.on $ course E.^. CourseId E.==. sheet E.^. SheetCourse
E.on $ sheet E.^. SheetId E.==. sub E.^. SubmissionSheetId E.on $ sheet E.^. SheetId E.==. sub E.^. SubmissionSheet
return (sub, sheet, course) return (sub, sheet, course)
@ -54,9 +351,9 @@ submissionTable = do
(,,) <$> encrypt submissionId <*> encrypt submissionId <*> pure s (,,) <$> encrypt submissionId <*> encrypt submissionId <*> pure s
let let
anchorCourse (_, _, (_, _, Entity _ Course{..})) = CourseR courseTermId courseShorthand CourseShowR anchorCourse (_, _, (_, _, Entity _ Course{..})) = CourseR courseTerm courseShorthand CShowR
courseText (_, _, (_, _, Entity _ Course{..})) = toWidget courseName courseText (_, _, (_, _, Entity _ Course{..})) = toWidget courseName
anchorSubmission (_, cUUID, _) = SubmissionR cUUID anchorSubmission (_, cUUID, _) = SubmissionDemoR cUUID
submissionText (cID, _, _) = toWidget . toPathPiece . CI.foldedCase $ ciphertext cID submissionText (cID, _, _) = toWidget . toPathPiece . CI.foldedCase $ ciphertext cID
colonnade = mconcat colonnade = mconcat
[ headed "Abgabe-ID" $ anchorCell anchorSubmission submissionText [ headed "Abgabe-ID" $ anchorCell anchorSubmission submissionText
@ -91,7 +388,7 @@ postSubmissionListR = do
Just sink -> return sink Just sink -> return sink
Nothing -> do Nothing -> do
Submission{..} <- lift $ get404 sId Submission{..} <- lift $ get404 sId
return . newResumableSink $ sinkSubmission submissionSheetId userId (Just (sId, isUpdate)) return . newResumableSink $ sinkSubmission submissionSheet userId (Just (sId, isUpdate))
sink' <- lift $ yield val ++$$ sink sink' <- lift $ yield val ++$$ sink
case sink' of case sink' of
Left _ -> error "sinkSubmission returned prematurely" Left _ -> error "sinkSubmission returned prematurely"
@ -115,39 +412,7 @@ postSubmissionListR = do
defaultLayout $(widgetFile "submission-list") defaultLayout $(widgetFile "submission-list")
getSubmissionDownloadSingleR :: CryptoUUIDSubmission -> FilePath -> Handler TypedContent
getSubmissionDownloadSingleR cID path = do
submissionID <- decrypt cID
cID' <- encrypt submissionID
runDB $ do
isRating <- maybe False (== submissionID) <$> isRatingFile path
case isRating of
True -> do
file <- runMaybeT $ lift . ratingFile cID' =<< MaybeT (getRating submissionID)
maybe notFound (return . toTypedContent . Text.decodeUtf8) $ fileContent =<< file
False -> do
results <- E.select . E.from $ \(sf `E.InnerJoin` f) -> E.distinctOnOrderBy [E.asc $ f E.^. FileTitle] $ do
E.on (f E.^. FileId E.==. sf E.^. SubmissionFileFileId)
E.where_ (sf E.^. SubmissionFileSubmissionId E.==. E.val submissionID)
E.where_ (f E.^. FileTitle E.==. E.val path)
E.where_ . E.not_ . E.isNothing $ f E.^. FileContent
E.where_ . E.not_ $ sf E.^. SubmissionFileIsDeletion
E.orderBy [E.desc $ sf E.^. SubmissionFileIsUpdate]
return f
let fileName = Text.pack $ takeFileName path
case results of
[Entity _ File{ fileContent = Just c }] -> return $ TypedContent (defaultMimeLookup fileName) (toContent c)
_ -> notFound
submissionFileSource :: SubmissionId -> Source (YesodDB UniWorX) (Entity File)
submissionFileSource submissionID = E.selectSource . E.from $ \(sf `E.InnerJoin` f) -> E.distinctOnOrderBy [E.asc $ f E.^. FileTitle] $ do
E.on (f E.^. FileId E.==. sf E.^. SubmissionFileFileId)
E.where_ $ sf E.^. SubmissionFileSubmissionId E.==. E.val submissionID
E.where_ . E.not_ $ sf E.^. SubmissionFileIsDeletion
E.orderBy [E.desc $ sf E.^. SubmissionFileIsUpdate]
return f
postSubmissionDownloadMultiArchiveR :: Handler TypedContent postSubmissionDownloadMultiArchiveR :: Handler TypedContent
postSubmissionDownloadMultiArchiveR = do postSubmissionDownloadMultiArchiveR = do
@ -193,27 +458,12 @@ postSubmissionDownloadMultiArchiveR = do
mapM_ fileEntitySource' ratedSubmissions =$= produceZip def =$= Conduit.map toFlushBuilder mapM_ fileEntitySource' ratedSubmissions =$= produceZip def =$= Conduit.map toFlushBuilder
getSubmissionDownloadArchiveR :: FilePath -> Handler TypedContent
getSubmissionDownloadArchiveR path = do
let (baseName, ext) = splitExtension path
cID :: CryptoFileNameSubmission
cID = CryptoID $ CI.mk baseName
unless (ext == ".zip") notFound
submissionID <- decrypt cID
cUUID <- encrypt submissionID
respondSourceDB "application/zip" $ do
rating <- lift $ getRating submissionID
case rating of
Nothing -> lift notFound
Just rating' -> do
let fileEntitySource' :: Source (YesodDB UniWorX) File
fileEntitySource' = submissionFileSource submissionID =$= Conduit.map entityVal >> yieldM (ratingFile cID rating')
info = ZipInfo { zipComment = Text.encodeUtf8 . tshow $ ciphertext (cUUID :: CryptoUUIDSubmission) }
fileEntitySource' =$= produceZip info =$= Conduit.map toFlushBuilder
getSubmissionR, postSubmissionR :: CryptoUUIDSubmission -> Handler Html
getSubmissionR = postSubmissionR
postSubmissionR cID = do getSubmissionDemoR, postSubmissionDemoR :: CryptoUUIDSubmission -> Handler Html
getSubmissionDemoR = postSubmissionDemoR
postSubmissionDemoR cID = do
submissionId <- decrypt cID submissionId <- decrypt cID
((uploadResult, uploadWidget), uploadEnctype) <- runFormPost . renderAForm FormStandard $ (,) ((uploadResult, uploadWidget), uploadEnctype) <- runFormPost . renderAForm FormStandard $ (,)
@ -238,12 +488,12 @@ postSubmissionR cID = do
yieldM $ do yieldM $ do
fileContent <- Just <$> runConduit (fileSource fInfo =$= foldC) fileContent <- Just <$> runConduit (fileSource fInfo =$= foldC)
return File{..} return File{..}
submissionId' <- runConduit $ source =$= extractRatings =$= sinkSubmission submissionSheetId userId (Just (submissionId, isUpdate)) submissionId' <- runConduit $ source =$= extractRatings =$= sinkSubmission submissionSheet userId (Just (submissionId, isUpdate))
get404 submissionId' get404 submissionId'
files <- E.select . E.from $ \(sf `E.InnerJoin` f) -> E.distinctOnOrderBy [E.asc $ f E.^. FileTitle] $ do files <- E.select . E.from $ \(sf `E.InnerJoin` f) -> E.distinctOnOrderBy [E.asc $ f E.^. FileTitle] $ do
E.on (f E.^. FileId E.==. sf E.^. SubmissionFileFileId) E.on (f E.^. FileId E.==. sf E.^. SubmissionFileFile)
E.where_ (sf E.^. SubmissionFileSubmissionId E.==. E.val submissionId) E.where_ (sf E.^. SubmissionFileSubmission E.==. E.val submissionId)
E.orderBy [E.desc $ sf E.^. SubmissionFileIsUpdate] E.orderBy [E.desc $ sf E.^. SubmissionFileIsUpdate]
return (f, sf) return (f, sf)
return (submission, files) return (submission, files)

View File

@ -1,11 +1,13 @@
{-# LANGUAGE NoImplicitPrelude #-} {-# LANGUAGE NoImplicitPrelude
{-# LANGUAGE OverloadedStrings #-} , OverloadedStrings
{-# LANGUAGE RecordWildCards #-} , OverloadedLists
{-# LANGUAGE TemplateHaskell #-} , RecordWildCards
{-# LANGUAGE QuasiQuotes #-} , TemplateHaskell
{-# LANGUAGE MultiParamTypeClasses #-} , QuasiQuotes
{-# LANGUAGE TypeFamilies #-} , MultiParamTypeClasses
{-# LANGUAGE FlexibleContexts #-} , TypeFamilies
, FlexibleContexts
#-}
module Handler.Term where module Handler.Term where
@ -29,18 +31,18 @@ getTermShowR = do
-- return term -- return term
-- --
let let
termData = E.from $ \term -> do termData :: E.SqlExpr (Entity Term) -> E.SqlQuery (E.SqlExpr (Entity Term), E.SqlExpr (E.Value Int64))
termData term = do
-- E.orderBy [E.desc $ term E.^. TermStart ] -- E.orderBy [E.desc $ term E.^. TermStart ]
let courseCount :: E.SqlExpr (E.Value Int) let courseCount = E.sub_select . E.from $ \course -> do
courseCount = E.sub_select . E.from $ \course -> do E.where_ $ term E.^. TermId E.==. course E.^. CourseTerm
E.where_ $ term E.^. TermId E.==. course E.^. CourseTermId
return E.countRows return E.countRows
return (term, courseCount) return (term, courseCount)
selectRep $ do selectRep $ do
provideRep $ toJSON . map fst <$> runDB (E.select termData) provideRep $ toJSON . map fst <$> runDB (E.select $ E.from termData)
provideRep $ do provideRep $ do
let colonnadeTerms = mconcat let colonnadeTerms = mconcat
[ headed "Kürzel" $ \(Entity tid Term{..},_) -> cell $ do [ sortable Nothing "Kürzel" $ \(Entity tid Term{..},_) -> cell $ 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 tid) False adminLink <- handlerToWidget $ isAuthorized (TermEditExistR tid) False
[whamlet| [whamlet|
@ -50,40 +52,46 @@ getTermShowR = do
$else $else
#{termToText termName} #{termToText termName}
|] |]
, headed "Beginn Vorlesungen" $ \(Entity _ Term{..},_) -> , sortable (Just "lecture-start") "Beginn Vorlesungen" $ \(Entity _ Term{..},_) ->
stringCell $ formatTimeGerWD termLectureStart stringCell $ formatTimeGerWD termLectureStart
, headed "Ende Vorlesungen" $ \(Entity _ Term{..},_) -> , sortable (Just "lecture-end") "Ende Vorlesungen" $ \(Entity _ Term{..},_) ->
stringCell $ formatTimeGerWD termLectureEnd stringCell $ formatTimeGerWD termLectureEnd
, headed "Aktiv" $ \(Entity _ Term{..},_) -> , sortable Nothing "Aktiv" $ \(Entity _ Term{..},_) ->
textCell $ bool "" tickmark termActive textCell $ bool "" tickmark termActive
, headed "Kursliste" $ \(Entity tid Term{..}, E.Value numCourses) -> , sortable Nothing "Kursliste" $ anchorCell
cell [whamlet| (\(Entity tid _, _) -> TermCourseListR tid)
<a href=@{CourseListTermR tid}> (\(_, E.Value numCourses) -> [whamlet|_{MsgNumCourses numCourses}|])
#{show numCourses} Kurse , sortable (Just "start") "Semesteranfang" $ \(Entity _ Term{..},_) ->
|]
, headed "Semesteranfang" $ \(Entity _ Term{..},_) ->
stringCell $ formatTimeGerWD termStart stringCell $ formatTimeGerWD termStart
, headed "Semesterende" $ \(Entity _ Term{..},_) -> , sortable (Just "end") "Semesterende" $ \(Entity _ Term{..},_) ->
stringCell $ formatTimeGerWD termEnd stringCell $ formatTimeGerWD termEnd
, headed "Feiertage im Semester" $ \(Entity _ Term{..},_) -> , sortable Nothing "Feiertage im Semester" $ \(Entity _ Term{..},_) ->
stringCell $ (intercalate ", ") $ map formatTimeGerWD termHolidays stringCell $ (intercalate ", ") $ map formatTimeGerWD termHolidays
] ]
table <- dbTable def $ DBTable table <- dbTable def $ DBTable
{ dbtSQLQuery = termData { dbtSQLQuery = termData
, dbtColonnade = colonnadeTerms , dbtColonnade = colonnadeTerms
, dbtSorting = mempty , dbtSorting = [ ( "start"
, SortColumn $ \term -> term E.^. TermStart
)
, ( "end"
, SortColumn $ \term -> term E.^. TermEnd
)
, ( "lecture-start"
, SortColumn $ \term -> term E.^. TermLectureStart
)
, ( "lecture-end"
, SortColumn $ \term -> term E.^. TermLectureEnd
)
]
, dbtFilter = [ ( "active"
, FilterColumn $ \term -> (term E.^. TermActive :: E.SqlExpr (E.Value Bool))
)
]
, dbtAttrs = tableDefault , dbtAttrs = tableDefault
, dbtIdent = "terms" :: Text , dbtIdent = "terms" :: Text
} }
let pageActions = defaultLayout $ do
[ PageActionPrime $ MenuItem
{ menuItemLabel = "Neues Semester"
, menuItemIcon = Nothing
, menuItemRoute = TermEditR
, menuItemAccessCallback = (== Authorized) <$> isAuthorized TermEditR True
}
]
defaultLinkLayout pageActions $ do
setTitle "Freigeschaltete Semester" setTitle "Freigeschaltete Semester"
$(widgetFile "terms") $(widgetFile "terms")

View File

@ -16,7 +16,8 @@ import Handler.Utils.Form as Handler.Utils
import Handler.Utils.Table as Handler.Utils import Handler.Utils.Table as Handler.Utils
import Handler.Utils.Table.Pagination as Handler.Utils import Handler.Utils.Table.Pagination as Handler.Utils
import Handler.Utils.Zip as Handler.Utils import Handler.Utils.Zip as Handler.Utils
import Handler.Utils.Rating as Handler.Utils 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.Sheet as Handler.Utils
import Handler.Utils.Templates as Handler.Utils

View File

@ -15,9 +15,11 @@ module Handler.Utils.Form where
import Handler.Utils.Form.Types import Handler.Utils.Form.Types
import Handler.Utils.DateTime
import Import import Import
import qualified Data.Char as Char import qualified Data.Char as Char
import Handler.Utils.DateTime
import Data.String (IsString(..)) import Data.String (IsString(..))
import qualified Data.Foldable as Foldable import qualified Data.Foldable as Foldable
@ -40,11 +42,13 @@ import qualified Database.Esqueleto.Internal.Sql as E
import qualified Data.Set as Set import qualified Data.Set as Set
import Control.Monad.Writer.Class
------------------------------------------------ ------------------------------------------------
-- Unique Form Identifiers to avoid accidents -- -- Unique Form Identifiers to avoid accidents --
------------------------------------------------ ------------------------------------------------
data FormIdentifier = FIDcourse | FIDsheet data FormIdentifier = FIDcourse | FIDsheet | FIDsubmission
deriving (Enum, Eq, Ord, Bounded, Read, Show) deriving (Enum, Eq, Ord, Bounded, Read, Show)
@ -131,6 +135,8 @@ linkButton lbl cls url = [whamlet| <a href=@{url} .btn .#{bcc2txt cls} role=butt
-- |] -- |]
-- <input .btn .#{bcc2txt cls} type="submit" value=^{lbl}> -- <input .btn .#{bcc2txt cls} type="submit" value=^{lbl}>
simpleLink :: Widget -> Route UniWorX -> Widget
simpleLink lbl url = [whamlet| <a href=@{url}>^{lbl} |]
buttonField :: Button a => a -> Field Handler a buttonField :: Button a => a -> Field Handler a
buttonField btn = Field {fieldParse, fieldView, fieldEnctype} buttonField btn = Field {fieldParse, fieldView, fieldEnctype}
@ -232,7 +238,6 @@ posIntField d = checkBool (>= 1) (T.append d " muss eine positive Zahl sein.")
minIntField :: (Monad m, Integral i, Show i, RenderMessage (HandlerSite m) FormMessage) => i -> Text -> Field m i minIntField :: (Monad m, Integral i, Show i, RenderMessage (HandlerSite m) FormMessage) => i -> Text -> Field m i
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
--termField: see Utils.Term --termField: see Utils.Term
schoolField :: Field Handler SchoolId schoolField :: Field Handler SchoolId
@ -327,6 +332,25 @@ sheetGroupAFormReq d _other = -- TODO
-- TODO, offer options to choose between Arbitrary/Registered/NoGroups -- TODO, offer options to choose between Arbitrary/Registered/NoGroups
Arbitrary <$> areq (natField "Abgabegruppengröße") d (Just 1) Arbitrary <$> areq (natField "Abgabegruppengröße") d (Just 1)
{-
dayTimeField :: FieldSettings UniWorX -> Maybe UTCTime -> Form Handler UTCTime
dayTimeField fs mutc = do
let (mbDay,mbTime) = case mutcs of
Nothing -> return (Nothing,Nothing)
(Just utc) ->
(dayResult, dayView) <- mreq dayField fs
(result, view) <- (,) <$> dayField <*> timeField
where
(mbDay,mbTime)
| (Just utc) <- mutc =
let lt = utcToLocalTime ??? utcs
in (Just $ localDay lt, Just $ localTimeOfDay lt)
| otherwise = (Nothing,Nothing)
-}
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)
-- TODO: Verify whether this is UTC or local time from Browser -- TODO: Verify whether this is UTC or local time from Browser
@ -355,7 +379,7 @@ utcTimeField = Field
fsm :: RenderMessage UniWorX msg => msg -> FieldSettings UniWorX fsm :: RenderMessage UniWorX msg => msg -> FieldSettings UniWorX
fsm = bfs fsm = bfs -- TODO: get rid of Bootstrap
fsb :: Text -> FieldSettings site fsb :: Text -> FieldSettings site
fsb = bfs -- Just to avoid annoying Ambiguous Type Errors fsb = bfs -- Just to avoid annoying Ambiguous Type Errors
@ -426,3 +450,26 @@ optionsPersistCryptoId filts ords toDisplay = fmap mkOptionList $ do
, optionInternalValue = key , optionInternalValue = key
, optionExternalValue = toPathPiece (cId :: CryptoID UUID (Key a)) , optionExternalValue = toPathPiece (cId :: CryptoID UUID (Key a))
}) cPairs }) cPairs
mforced :: (site ~ HandlerSite m, MonadHandler m)
=> Field m a -> FieldSettings site -> a -> MForm m (FormResult a, FieldView site)
mforced Field{..} FieldSettings{..} val = do
tell fieldEnctype
name <- maybe newFormIdent return fsName
theId <- lift $ maybe newIdent return fsId
mr <- getMessageRender
let fsAttrs' = fsAttrs <> [("disabled", "")]
return ( FormSuccess val
, FieldView
{ fvLabel = toHtml $ mr fsLabel
, fvTooltip = toHtml <$> fmap mr fsTooltip
, fvId = theId
, fvInput = fieldView theId name fsAttrs' (Right val) False
, fvErrors = Nothing
, fvRequired = False
}
)
aforced :: (RenderMessage site FormMessage, HandlerSite m ~ site, MonadHandler m)
=> Field m a -> FieldSettings site -> a -> AForm m a
aforced field settings val = formToAForm $ second pure <$> mforced field settings val

View File

@ -90,8 +90,8 @@ instance Exception RatingException
getRating :: SubmissionId -> YesodDB UniWorX (Maybe Rating) getRating :: SubmissionId -> YesodDB UniWorX (Maybe Rating)
getRating submissionId = runMaybeT $ do getRating submissionId = runMaybeT $ do
let query = E.select . E.from $ \(submission `E.InnerJoin` sheet `E.InnerJoin` course) -> do let query = E.select . E.from $ \(submission `E.InnerJoin` sheet `E.InnerJoin` course) -> do
E.on $ course E.^. CourseId E.==. sheet E.^. SheetCourseId E.on $ course E.^. CourseId E.==. sheet E.^. SheetCourse
E.on $ sheet E.^. SheetId E.==. submission E.^. SubmissionSheetId E.on $ sheet E.^. SheetId E.==. submission E.^. SubmissionSheet
E.where_ $ submission E.^. SubmissionId E.==. E.val submissionId E.where_ $ submission E.^. SubmissionId E.==. E.val submissionId

View File

@ -0,0 +1,52 @@
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
module Handler.Utils.Sheet where
import Import
import qualified Database.Esqueleto as E
import qualified Database.Esqueleto.Internal.Sql as E
fetchSheetAux :: ( BaseBackend backend ~ SqlBackend
, E.SqlSelect b a
, Typeable a, MonadHandler m, IsPersistBackend backend
, PersistQueryRead backend, PersistUniqueRead backend
)
=> (E.SqlExpr (Entity Sheet) -> b)
-> TermId -> Text -> Text -> ReaderT backend m a
fetchSheetAux prj tid csh shn =
let cachId = encodeUtf8 $ tshow (tid,csh,shn)
in cachedBy cachId $ do
-- Mit Yesod:
-- cid <- getKeyBy404 $ CourseTermShort tid csh
-- getBy404 $ CourseSheet cid shn
-- Mit Esqueleto:
sheetList <- E.select . E.from $ \(course `E.InnerJoin` sheet) -> do
E.on $ course E.^. CourseId E.==. sheet E.^. SheetCourse
E.where_ $ course E.^. CourseTerm E.==. E.val tid
E.&&. course E.^. CourseShorthand E.==. E.val csh
E.&&. sheet E.^. SheetName E.==. E.val shn
return $ prj sheet
case sheetList of
[sheet] -> return sheet
_other -> notFound
fetchSheet :: TermId -> Text -> Text -> YesodDB UniWorX (Entity Sheet)
fetchSheet = fetchSheetAux id
fetchSheetId :: TermId -> Text -> Text -> YesodDB UniWorX (Key Sheet)
fetchSheetId tid cid shn = E.unValue <$> fetchSheetAux (E.^. SheetId) tid cid shn
fetchSheetIdCourseId :: TermId -> Text -> Text -> YesodDB UniWorX (Key Sheet, Key Course)
fetchSheetIdCourseId tid cid shn = bimap E.unValue E.unValue <$> fetchSheetAux ((,) <$> (E.^. SheetId) <*> (E.^. SheetCourse)) tid cid shn

View File

@ -15,12 +15,23 @@ module Handler.Utils.Submission
, sinkSubmission , sinkSubmission
) where ) where
import Import import Import hiding ((.=))
import Control.Lens
import Control.Lens.Extras (is)
import Utils.Lens
import Control.Monad.State hiding (forM_) import Control.Monad.State hiding (forM_)
import qualified Control.Monad.Random as Rand
import Data.Maybe
import qualified Data.List as List
import Data.Set (Set) import Data.Set (Set)
import qualified Data.Set as Set import qualified Data.Set as Set
import Data.Map (Map, (!?))
import qualified Data.Map as Map
import Data.Monoid (Monoid, Any(..)) import Data.Monoid (Monoid, Any(..))
import Generics.Deriving.Monoid (memptydefault, mappenddefault) import Generics.Deriving.Monoid (memptydefault, mappenddefault)
@ -32,6 +43,70 @@ import qualified Database.Esqueleto as E
import qualified Data.Conduit.List as Conduit import qualified Data.Conduit.List as Conduit
data AssignSubmissionException = NoCorrectorsByProportion
deriving (Typeable, Show)
instance Exception AssignSubmissionException
-- | Assigns all submissions according to sheet corrector loads
assignSubmissions ::
SheetId -- ^ Sheet do distribute to correction
-> YesodDB UniWorX (Set SubmissionId -- ^ assigned submissions
,Set SubmissionId -- ^ unassigend submissions (no tutors by load)
)
assignSubmissions sid = do
correctors <- selectList [SheetCorrectorSheet ==. sid] []
let corrsGroup = filter hasTutorialLoad correctors -- needed as List within Esqueleto
let corrsProp = filter hasPositiveLoad correctors
let countsToLoad' :: UserId -> Bool
countsToLoad' uid = -- refactor by simply using Map.(!)
fromMaybe (error "Called `countsToLoad'` on entity not element of `corrsGroup`") $
Map.lookup uid loadMap
loadMap :: Map UserId Bool
loadMap = Map.fromList [(sheetCorrectorUser,b) | Entity _ SheetCorrector{ sheetCorrectorLoad = (Load {byTutorial = Just b}), .. } <- corrsGroup]
subs <- E.select . E.from $ \(submission `E.LeftOuterJoin` user) -> do
let tutors = E.subList_select . E.from $ \(submissionUser `E.InnerJoin` tutorialUser `E.InnerJoin` tutorial) -> do
-- Uncomment next line for equal chance between tutors, irrespective of the number of students per tutor per submission group
-- E.distinctOn [E.don $ tutorial E.^. TutorialTutor] $ do
E.on (tutorial E.^. TutorialId E.==. tutorialUser E.^. TutorialUserTutorial)
E.on (submissionUser E.^. SubmissionUserUser E.==. tutorialUser E.^. TutorialUserUser)
E.where_ (tutorial E.^. TutorialTutor `E.in_` E.valList (map (sheetCorrectorUser . entityVal) corrsGroup))
return $ tutorial E.^. TutorialTutor
E.on $ user E.?. UserId `E.in_` E.justList tutors
E.where_ $ submission E.^. SubmissionSheet E.==. E.val sid
E.orderBy [E.rand] -- randomize for fair tutor distribution
return (submission E.^. SubmissionId, user) -- , listToMaybe tutors)
queue <- liftIO . Rand.evalRandIO . sequence . repeat $ Rand.weightedMay [ (sheetCorrectorUser, byProportion sheetCorrectorLoad) | Entity _ SheetCorrector{..} <- corrsProp]
let subTutor' :: Map SubmissionId (Maybe UserId)
subTutor' = Map.fromListWith (<|>) $ map (over (_2.traverse) entityKey . over _1 E.unValue) subs
subTutor <- fmap fst . flip execStateT (Map.empty, queue) . forM_ (Map.toList subTutor') $ \case
(smid, Just tutid) -> do
_1 %= Map.insert smid tutid
when (any ((== tutid) . sheetCorrectorUser . entityVal) corrsProp && countsToLoad' tutid) $
_2 %= List.delete (Just tutid)
(smid, Nothing) -> do
(q:qs) <- use _2
_2 .= qs
case q of
Just q -> _1 %= Map.insert smid q
Nothing -> return () -- NOTE: throwM NoCorrectorsByProportion
forM_ (Map.toList subTutor) $ \(smid, tutid) -> update smid [SubmissionRatingBy =. Just tutid]
let assignedSubmissions = Map.keysSet subTutor
unassigendSubmissions = Map.keysSet subTutor' \\ assignedSubmissions
return (assignedSubmissions, unassigendSubmissions)
where
hasPositiveLoad = (> 0) . byProportion . sheetCorrectorLoad . entityVal
hasTutorialLoad = isJust . byTutorial . sheetCorrectorLoad . entityVal
data SubmissionSinkState = SubmissionSinkState data SubmissionSinkState = SubmissionSinkState
{ sinkSeenRating :: Any { sinkSeenRating :: Any
, sinkSubmissionTouched :: Any , sinkSubmissionTouched :: Any
@ -65,7 +140,7 @@ sinkSubmission :: SheetId
sinkSubmission sheetId userId mExists = do sinkSubmission sheetId userId mExists = do
now <- liftIO getCurrentTime now <- liftIO getCurrentTime
let let
submissionSheetId = sheetId submissionSheet = sheetId
submissionRatingPoints = Nothing submissionRatingPoints = Nothing
submissionRatingComment = Nothing submissionRatingComment = Nothing
submissionRatingBy = Nothing submissionRatingBy = Nothing
@ -90,8 +165,8 @@ sinkSubmission sheetId userId mExists = do
tell $ mempty{ sinkFilenames = Set.singleton fileTitle } tell $ mempty{ sinkFilenames = Set.singleton fileTitle }
otherVersions <- lift . E.select . E.from $ \(sf `E.InnerJoin` f) -> do otherVersions <- lift . E.select . E.from $ \(sf `E.InnerJoin` f) -> do
E.on $ sf E.^. SubmissionFileFileId E.==. f E.^. FileId E.on $ sf E.^. SubmissionFileFile E.==. f E.^. FileId
E.where_ $ sf E.^. SubmissionFileSubmissionId E.==. E.val submissionId E.where_ $ sf E.^. SubmissionFileSubmission E.==. E.val submissionId
-- E.where_ $ sf E.^. SubmissionFileIsUpdate E.==. E.val isUpdate -- E.where_ $ sf E.^. SubmissionFileIsUpdate E.==. E.val isUpdate
E.where_ $ f E.^. FileTitle E.==. E.val fileTitle -- 'Zip.hs' normalises filenames already, so this should work E.where_ $ f E.^. FileTitle E.==. E.val fileTitle -- 'Zip.hs' normalises filenames already, so this should work
return (f, sf) return (f, sf)
@ -121,8 +196,8 @@ sinkSubmission sheetId userId mExists = do
_ -> do _ -> do
fileId <- insert file fileId <- insert file
insert_ $ SubmissionFile insert_ $ SubmissionFile
{ submissionFileSubmissionId = submissionId { submissionFileSubmission = submissionId
, submissionFileFileId = fileId , submissionFileFile = fileId
, submissionFileIsUpdate = isUpdate , submissionFileIsUpdate = isUpdate
, submissionFileIsDeletion = False , submissionFileIsDeletion = False
} }
@ -189,8 +264,8 @@ sinkSubmission sheetId userId mExists = do
finalize :: SubmissionSinkState -> YesodDB UniWorX () finalize :: SubmissionSinkState -> YesodDB UniWorX ()
finalize SubmissionSinkState{..} = do finalize SubmissionSinkState{..} = do
missingFiles <- E.select . E.from $ \(sf `E.InnerJoin` f) -> E.distinctOnOrderBy [E.asc $ f E.^. FileTitle] $ do missingFiles <- E.select . E.from $ \(sf `E.InnerJoin` f) -> E.distinctOnOrderBy [E.asc $ f E.^. FileTitle] $ do
E.on $ sf E.^. SubmissionFileFileId E.==. f E.^. FileId E.on $ sf E.^. SubmissionFileFile E.==. f E.^. FileId
E.where_ $ sf E.^. SubmissionFileSubmissionId E.==. E.val submissionId E.where_ $ sf E.^. SubmissionFileSubmission E.==. E.val submissionId
when (not isUpdate) $ when (not isUpdate) $
E.where_ $ sf E.^. SubmissionFileIsUpdate E.==. E.val isUpdate E.where_ $ sf E.^. SubmissionFileIsUpdate E.==. E.val isUpdate
E.where_ $ f E.^. FileTitle `E.notIn` E.valList (Set.toList sinkFilenames) E.where_ $ f E.^. FileTitle `E.notIn` E.valList (Set.toList sinkFilenames)
@ -202,8 +277,8 @@ sinkSubmission sheetId userId mExists = do
False -> deleteCascadeWhere [ FileId <-. [ fileId | (Entity fileId _, _) <- missingFiles ] ] False -> deleteCascadeWhere [ FileId <-. [ fileId | (Entity fileId _, _) <- missingFiles ] ]
True -> forM_ missingFiles $ \(Entity fileId File{..}, Entity sfId SubmissionFile{..}) -> do True -> forM_ missingFiles $ \(Entity fileId File{..}, Entity sfId SubmissionFile{..}) -> do
shadowing <- E.select . E.from $ \(sf `E.InnerJoin` f) -> do shadowing <- E.select . E.from $ \(sf `E.InnerJoin` f) -> do
E.on $ sf E.^. SubmissionFileFileId E.==. f E.^. FileId E.on $ sf E.^. SubmissionFileFile E.==. f E.^. FileId
E.where_ $ sf E.^. SubmissionFileSubmissionId E.==. E.val submissionId E.where_ $ sf E.^. SubmissionFileSubmission E.==. E.val submissionId
E.where_ $ sf E.^. SubmissionFileIsUpdate E.==. E.val (not isUpdate) E.where_ $ sf E.^. SubmissionFileIsUpdate E.==. E.val (not isUpdate)
E.where_ $ f E.^. FileTitle E.==. E.val fileTitle E.where_ $ f E.^. FileTitle E.==. E.val fileTitle
return $ f E.^. FileId return $ f E.^. FileId
@ -212,13 +287,13 @@ sinkSubmission sheetId userId mExists = do
([], _) -> deleteCascade fileId ([], _) -> deleteCascade fileId
(E.Value f:_, False) -> do (E.Value f:_, False) -> do
insert_ $ SubmissionFile insert_ $ SubmissionFile
{ submissionFileSubmissionId = submissionId { submissionFileSubmission = submissionId
, submissionFileFileId = f , submissionFileFile = f
, submissionFileIsUpdate = True , submissionFileIsUpdate = True
, submissionFileIsDeletion = True , submissionFileIsDeletion = True
} }
(E.Value f:_, True) -> do (E.Value f:_, True) -> do
update sfId [ SubmissionFileFileId =. f, SubmissionFileIsDeletion =. True ] update sfId [ SubmissionFileFile =. f, SubmissionFileIsDeletion =. True ]
deleteCascade fileId deleteCascade fileId
when (isUpdate && not (getAny sinkSeenRating)) $ when (isUpdate && not (getAny sinkSeenRating)) $

View File

@ -6,30 +6,50 @@
, QuasiQuotes , QuasiQuotes
, LambdaCase , LambdaCase
, ViewPatterns , ViewPatterns
, FlexibleContexts
, FlexibleInstances
, MultiParamTypeClasses
, TypeFamilies
#-} #-}
module Handler.Utils.Table.Pagination module Handler.Utils.Table.Pagination
( SortColumn(..), SortDirection(..) ( SortColumn(..), SortDirection(..)
, FilterColumn(..), IsFilterColumn
, DBRow(..), DBOutput
, DBTable(..) , DBTable(..)
, PaginationSettings(..) , PaginationSettings(..)
, PSValidator(..) , PSValidator(..)
, Sortable(..), sortable
, dbTable , dbTable
) where ) where
import Handler.Utils.Table.Pagination.Types
import Import import Import
import qualified Database.Esqueleto as E import qualified Database.Esqueleto as E
import qualified Database.Esqueleto.Internal.Sql as E (SqlSelect) import qualified Database.Esqueleto.Internal.Sql as E (SqlSelect,unsafeSqlValue)
import qualified Database.Esqueleto.Internal.Language as E (From)
import Text.Blaze (Attribute) import Text.Blaze (Attribute)
import qualified Text.Blaze.Html5.Attributes as Html5 import qualified Text.Blaze.Html5.Attributes as Html5
import qualified Text.Blaze.Html5 as Html5
import Yesod.Core.Types (Body(..),GWData(..),WidgetT(..))
import qualified Data.Binary.Builder as Builder
import qualified Network.Wai as Wai
import Data.CaseInsensitive (CI) import Data.CaseInsensitive (CI)
import qualified Data.CaseInsensitive as CI import qualified Data.CaseInsensitive as CI
import Control.Monad.RWS hiding ((<>), Foldable(..), mapM_) import Control.Monad.RWS hiding ((<>), Foldable(..), mapM_, forM_)
import Data.Map (Map, (!)) import Data.Map (Map, (!))
import qualified Data.Map as Map
import Colonnade hiding (bool, fromMaybe) import Data.Profunctor (lmap)
import Colonnade hiding (bool, fromMaybe, singleton)
import Colonnade.Encode
import Yesod.Colonnade import Yesod.Colonnade
import Text.Hamlet (hamletFile) import Text.Hamlet (hamletFile)
@ -37,7 +57,7 @@ import Text.Hamlet (hamletFile)
import Data.Ratio ((%)) import Data.Ratio ((%))
data SortColumn = forall a. PersistField a => SortColumn { getSortColumn :: E.SqlExpr (E.Value a) } data SortColumn t = forall a. PersistField a => SortColumn { getSortColumn :: t -> E.SqlExpr (E.Value a) }
data SortDirection = SortAsc | SortDesc data SortDirection = SortAsc | SortDesc
deriving (Eq, Ord, Enum, Show, Read) deriving (Eq, Ord, Enum, Show, Read)
@ -49,24 +69,68 @@ instance PathPiece SortDirection where
| t == "desc" = Just SortDesc | t == "desc" = Just SortDesc
| otherwise = Nothing | otherwise = Nothing
sqlSortDirection :: (SortColumn, SortDirection) -> E.SqlExpr E.OrderBy sqlSortDirection :: t -> (SortColumn t, SortDirection) -> E.SqlExpr E.OrderBy
sqlSortDirection (SortColumn e, SortAsc ) = E.asc e sqlSortDirection t (SortColumn e, SortAsc ) = E.asc $ e t
sqlSortDirection (SortColumn e, SortDesc) = E.desc e sqlSortDirection t (SortColumn e, SortDesc) = E.desc $ e t
data DBTable = forall a r h i.
( Headedness h data FilterColumn t = forall a. IsFilterColumn t a => FilterColumn a
, E.SqlSelect a r
filterColumn :: FilterColumn t -> [Text] -> t -> E.SqlExpr (E.Value Bool)
filterColumn (FilterColumn f) = filterColumn' f
class IsFilterColumn t a where
filterColumn' :: a -> [Text] -> t -> E.SqlExpr (E.Value Bool)
instance IsFilterColumn t (E.SqlExpr (E.Value Bool)) where
filterColumn' fin _ _ = fin
instance IsFilterColumn t cont => IsFilterColumn t (t -> cont) where
filterColumn' cont is t = filterColumn' (cont t) is t
instance {-# OVERLAPPABLE #-} (PathPiece (Element l), IsFilterColumn t cont, MonoPointed l, Monoid l) => IsFilterColumn t (l -> cont) where
filterColumn' cont is t = filterColumn' (cont input) is' t
where
(input, ($ []) -> is') = go (mempty, id) is
go acc [] = acc
go (acc, is') (i:is)
| Just i' <- fromPathPiece i = go (acc `mappend` singleton i', is') is
| otherwise = go (acc, is' . (i:)) is
data DBRow r = DBRow
{ dbrIndex, dbrCount :: Int64
, dbrOutput :: r
}
class DBOutput r r' where
dbProj :: r -> r'
instance DBOutput r r where
dbProj = id
instance DBOutput (DBRow r) r where
dbProj = dbrOutput
instance DBOutput (DBRow r) (Int64, r) where
dbProj = (,) <$> dbrIndex <*> dbrOutput
data DBTable = forall a r r' h i t.
( ToSortable h, Functor h
, E.SqlSelect a r, DBOutput (DBRow r) r'
, PathPiece i , PathPiece i
, E.From E.SqlQuery E.SqlExpr E.SqlBackend t
) => DBTable ) => DBTable
{ dbtSQLQuery :: E.SqlQuery a { dbtSQLQuery :: t -> E.SqlQuery a
, dbtColonnade :: Colonnade h r (Cell UniWorX) , dbtColonnade :: Colonnade h r' (Cell UniWorX)
, dbtSorting :: Map Text SortColumn , dbtSorting :: Map Text (SortColumn t)
, dbtFilter :: Map Text (FilterColumn t)
, dbtAttrs :: Attribute , dbtAttrs :: Attribute
, dbtIdent :: i , dbtIdent :: i
} }
data PaginationSettings = PaginationSettings data PaginationSettings = PaginationSettings
{ psSorting :: [(Text, SortDirection)] { psSorting :: [(Text, SortDirection)]
, psFilter :: Map Text [Text]
, psLimit :: Int64 , psLimit :: Int64
, psPage :: Int64 , psPage :: Int64
, psShortcircuit :: Bool , psShortcircuit :: Bool
@ -75,15 +139,16 @@ data PaginationSettings = PaginationSettings
instance Default PaginationSettings where instance Default PaginationSettings where
def = PaginationSettings def = PaginationSettings
{ psSorting = [] { psSorting = []
, psFilter = Map.empty
, psLimit = 50 , psLimit = 50
, psPage = 0 , psPage = 0
, psShortcircuit = False , psShortcircuit = False
} }
newtype PSValidator = PSValidator { runPSValidator :: Maybe PaginationSettings -> ([SomeMessage UniWorX], PaginationSettings) } newtype PSValidator = PSValidator { runPSValidator :: DBTable -> Maybe PaginationSettings -> ([SomeMessage UniWorX], PaginationSettings) }
instance Default PSValidator where instance Default PSValidator where
def = PSValidator $ \case def = PSValidator $ \DBTable{..} -> \case
Nothing -> def Nothing -> def
Just ps -> swap . (\act -> execRWS act () ps) $ do Just ps -> swap . (\act -> execRWS act () ps) $ do
l <- gets psLimit l <- gets psLimit
@ -91,8 +156,9 @@ instance Default PSValidator where
modify $ \ps -> ps { psLimit = psLimit def } modify $ \ps -> ps { psLimit = psLimit def }
tell . pure $ SomeMessage MsgPSLimitNonPositive tell . pure $ SomeMessage MsgPSLimitNonPositive
dbTable :: PSValidator -> DBTable -> Handler Widget dbTable :: PSValidator -> DBTable -> Handler Widget
dbTable PSValidator{..} DBTable{ dbtIdent = (toPathPiece -> dbtIdent), .. } = do dbTable PSValidator{..} dbtable@(DBTable{ dbtIdent = (toPathPiece -> dbtIdent), dbtColonnade = (lmap dbProj -> dbtColonnade), .. }) = do
let let
sortingOptions = mkOptionList sortingOptions = mkOptionList
[ Option t' (t, d) t' [ Option t' (t, d) t'
@ -100,46 +166,96 @@ dbTable PSValidator{..} DBTable{ dbtIdent = (toPathPiece -> dbtIdent), .. } = do
, d <- [SortAsc, SortDesc] , d <- [SortAsc, SortDesc]
, let t' = t <> "-" <> toPathPiece d , let t' = t <> "-" <> toPathPiece d
] ]
(_, defPS) = runPSValidator Nothing (_, defPS) = runPSValidator dbtable Nothing
wIdent n wIdent n
| not $ null dbtIdent = dbtIdent <> "-" <> n | not $ null dbtIdent = dbtIdent <> "-" <> n
| otherwise = n | otherwise = n
dbtAttrs' dbtAttrs'
| not $ null dbtIdent = Html5.id (fromString $ unpack dbtIdent) <> dbtAttrs | not $ null dbtIdent = Html5.id (fromString $ unpack dbtIdent) <> dbtAttrs
| otherwise = dbtAttrs | otherwise = dbtAttrs
multiTextField = Field
{ fieldParse = \ts _ -> return . Right $ Just ts
, fieldView = undefined
, fieldEnctype = UrlEncoded
}
psResult <- runInputGetResult $ PaginationSettings psResult <- runInputGetResult $ PaginationSettings
<$> ireq (multiSelectField $ return sortingOptions) (wIdent "sorting") <$> (fromMaybe [] <$> iopt (multiSelectField $ return sortingOptions) (wIdent "sorting"))
<*> (Map.mapMaybe ((\args -> args <$ guard (not $ null args)) =<<) <$> Map.traverseWithKey (\k _ -> iopt multiTextField $ wIdent k) dbtFilter)
<*> (fromMaybe (psLimit defPS) <$> iopt intField (wIdent "pagesize")) <*> (fromMaybe (psLimit defPS) <$> iopt intField (wIdent "pagesize"))
<*> (fromMaybe (psPage defPS) <$> iopt intField (wIdent "page")) <*> (fromMaybe (psPage defPS) <$> iopt intField (wIdent "page"))
<*> ireq checkBoxField (wIdent "table-only") <*> ireq checkBoxField (wIdent "table-only")
$(logDebug) . tshow $ (,,,) <$> (length . psSorting <$> psResult) $(logDebug) . tshow $ (,,,,) <$> (length . psSorting <$> psResult)
<*> (psLimit <$> psResult) <*> (Map.keys . psFilter <$> psResult)
<*> (psPage <$> psResult) <*> (psLimit <$> psResult)
<*> (psShortcircuit <$> psResult) <*> (psPage <$> psResult)
<*> (psShortcircuit <$> psResult)
let let
(errs, PaginationSettings{..}) = case psResult of (errs, PaginationSettings{..}) = case psResult of
FormSuccess ps -> runPSValidator $ Just ps FormSuccess ps -> runPSValidator dbtable $ Just ps
FormFailure errs -> first (map SomeMessage errs <>) $ runPSValidator Nothing FormFailure errs -> first (map SomeMessage errs <>) $ runPSValidator dbtable Nothing
FormMissing -> runPSValidator Nothing FormMissing -> runPSValidator dbtable Nothing
psSorting' = map (first (dbtSorting !)) psSorting psSorting' = map (first (dbtSorting !)) psSorting
sqlQuery' = dbtSQLQuery sqlQuery' = E.from $ \t -> dbtSQLQuery t
<* E.orderBy (map sqlSortDirection psSorting') <* E.orderBy (map (sqlSortDirection t) psSorting')
<* E.limit psLimit <* E.limit psLimit
<* E.offset (psPage * psLimit) <* E.offset (psPage * psLimit)
<* Map.foldrWithKey (\key args expr -> E.where_ (filterColumn (dbtFilter ! key) args t) >> expr) (return ()) psFilter
mapM_ (addMessageI "warning") errs mapM_ (addMessageI "warning") errs
(rows, [E.Value rowCount]) <- runDB $ (,) <$> E.select sqlQuery' <*> E.select (E.countRows <$ dbtSQLQuery :: E.SqlQuery (E.SqlExpr (E.Value Int64))) rows' <- runDB . E.select $ (,) <$> pure (E.unsafeSqlValue "row_number() OVER ()" :: E.SqlExpr (E.Value Int64), E.unsafeSqlValue "count(*) OVER ()" :: E.SqlExpr (E.Value Int64)) <*> sqlQuery'
let
rowCount
| ((_, E.Value n), _):_ <- rows' = n
| otherwise = 0
rows = map (\((E.Value i, E.Value n), r) -> DBRow i n r) rows'
bool return (sendResponse <=< tblLayout) psShortcircuit $ do bool return (sendResponse <=< tblLayout) psShortcircuit $ do
let table = encodeCellTable dbtAttrs' dbtColonnade rows getParams <- handlerToWidget $ queryToQueryText . Wai.queryString . reqWaiRequest <$> getRequest
let table = $(widgetFile "table/colonnade")
pageCount = max 1 . ceiling $ rowCount % psLimit pageCount = max 1 . ceiling $ rowCount % psLimit
$(widgetFile "table-layout") pageNumbers = [0..pred pageCount]
tblLink f = decodeUtf8 . Builder.toLazyByteString . renderQueryText True $ f getParams
withSortLinks Sortable{ sortableContent = Cell{..}, .. } = Cell
{ cellContents = $(widgetFile "table/sortable-header")
, cellAttrs = maybe mempty (const sortableAttr) sortableKey <> cellAttrs
}
where
directions = [dir | (k, dir) <- psSorting, Just k == sortableKey ]
sortableAttr = Html5.class_ . fromString . unwords $ "sortable" : foldMap toAttr directions
toAttr SortAsc = ["sorted-asc"]
toAttr SortDesc = ["sorted-desc"]
$(widgetFile "table/layout")
where where
tblLayout :: Widget -> Handler Html tblLayout :: Widget -> Handler Html
tblLayout tbl' = do tblLayout tbl' = do
tbl <- widgetToPageContent tbl' tbl <- widgetToPageContent tbl'
withUrlRenderer $(hamletFile "templates/table-layout-wrapper.hamlet") withUrlRenderer $(hamletFile "templates/table/layout-wrapper.hamlet")
setParam :: Text -> Maybe Text -> QueryText -> QueryText
setParam key v qt = (key, v) : [ i | i@(key', _) <- qt, key' /= key ]
widgetFromCell ::
(Attribute -> WidgetT site IO () -> WidgetT site IO ())
-> Cell site
-> WidgetT site IO ()
widgetFromCell f (Cell attrs contents) =
f attrs contents
td,th ::
Attribute -> WidgetT site IO () -> WidgetT site IO ()
td = liftParent Html5.td
th = liftParent Html5.th
liftParent :: (Html -> Html) -> Attribute -> WidgetT site IO a -> WidgetT site IO a
liftParent el attrs (WidgetT f) = WidgetT $ \hdata -> do
(a,gwd) <- f hdata
let Body bodyFunc = gwdBody gwd
newBodyFunc render =
el Html5.! attrs $ (bodyFunc render)
return (a,gwd { gwdBody = Body newBodyFunc })

View File

@ -0,0 +1,43 @@
{-# LANGUAGE NoImplicitPrelude
, ExistentialQuantification
, RankNTypes
, RecordWildCards
#-}
module Handler.Utils.Table.Pagination.Types where
import Import hiding (singleton)
import Colonnade
import Colonnade.Encode
data Sortable a = Sortable
{ sortableKey :: Maybe Text
, sortableContent :: a
}
sortable :: Maybe Text -> c -> (a -> c) -> Colonnade Sortable a c
sortable k h = singleton (Sortable k h)
instance Headedness Sortable where
headednessPure = Sortable Nothing
headednessExtract = Just $ \(Sortable _ x) -> x
headednessExtractForall = Just $ ExtractForall (\(Sortable _ x) -> x)
instance Functor Sortable where
fmap f Sortable{..} = Sortable { sortableContent = f sortableContent, .. }
newtype SortableP s = SortableP { toSortable :: forall a. s a -> Sortable a}
class Headedness s => ToSortable s where
pSortable :: Maybe (SortableP s)
instance ToSortable Sortable where
pSortable = Just $ SortableP id
instance ToSortable Headed where
pSortable = Just $ SortableP (\(Headed x) -> Sortable Nothing x)
instance ToSortable Headless where
pSortable = Nothing

View File

@ -54,7 +54,7 @@ deriveJSON defaultOptions ''SheetType
derivePersistFieldJSON "SheetType" derivePersistFieldJSON "SheetType"
data SheetGroup data SheetGroup
= Arbitrary { maxParticipants :: Int } -- Distinguish Limited/Arbitrary = Arbitrary { maxParticipants :: Int }
| RegisteredGroups | RegisteredGroups
| NoGroups | NoGroups
deriving (Show, Read, Eq) deriving (Show, Read, Eq)
@ -77,10 +77,33 @@ data ExamStatus = Attended | NoShow | Voided
deriving (Show, Read, Eq, Ord, Enum, Bounded) deriving (Show, Read, Eq, Ord, Enum, Bounded)
derivePersistField "ExamStatus" derivePersistField "ExamStatus"
data Load = ByTutorial | ByProportion Rational -- | Specify a corrector's workload
data Load -- = ByTutorial { countsToLoad :: Bool } | ByProportion { load :: Rational }
= Load { byTutorial :: Maybe Bool -- ^ Just all from Tutorial, True if counting towards overall workload
, byProportion :: Rational -- ^ workload proportion of all submission not assigned to tutorial leaders
}
deriving (Show, Read, Eq) deriving (Show, Read, Eq)
derivePersistField "Load" derivePersistField "Load"
instance Semigroup Load where
(Load byTut prop) <> (Load byTut' prop') = Load byTut'' (prop + prop')
where
byTut''
| Nothing <- byTut = byTut'
| Nothing <- byTut' = byTut
| Just a <- byTut
, Just b <- byTut' = Just $ a || b
instance Monoid Load where
mempty = Load Nothing 0
mappend = (<>)
{- Use (is _ByTutorial) instead of this unneeded definition:
isByTutorial :: Load -> Bool
isByTutorial (ByTutorial {}) = True
isByTutorial _ = False
-}
data Season = Summer | Winter data Season = Summer | Winter
deriving (Show, Read, Eq, Ord, Enum, Bounded, Generic, Typeable) deriving (Show, Read, Eq, Ord, Enum, Bounded, Generic, Typeable)
@ -158,3 +181,6 @@ time `withinTerm` term = timeYear `mod` 100 == termYear `mod` 100
data StudyFieldType = FieldPrimary | FieldSecondary data StudyFieldType = FieldPrimary | FieldSecondary
deriving (Eq, Ord, Enum, Show, Read, Bounded) deriving (Eq, Ord, Enum, Show, Read, Bounded)
derivePersistField "StudyFieldType" derivePersistField "StudyFieldType"

View File

@ -1,5 +1,7 @@
{-# LANGUAGE NoImplicitPrelude #-} {-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TypeFamilies, FlexibleContexts, ConstraintKinds #-} {-# LANGUAGE TypeFamilies, FlexibleContexts, ConstraintKinds #-}
{-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE QuasiQuotes #-}
@ -14,10 +16,30 @@ import Utils.Common as Utils
import Text.Blaze (Markup, ToMarkup) import Text.Blaze (Markup, ToMarkup)
import Data.Map (Map) -- import Data.Map (Map)
import qualified Data.Map as Map -- import qualified Data.Map as Map
import qualified Data.List as List -- import qualified Data.List as List
import Control.Monad.Trans.Except (ExceptT(..), throwE, runExceptT)
import Control.Monad.Trans.Maybe (MaybeT(..))
import Control.Monad.Catch
-----------
-- Yesod --
-----------
newtype MsgRendererS site = MsgRenderer { render :: (forall msg. RenderMessage site msg => msg -> Text) }
getMsgRenderer :: forall m site. (MonadHandler m, HandlerSite m ~ site) => m (MsgRendererS site)
getMsgRenderer = do
mr <- getMessageRender
return $ MsgRenderer (mr . SomeMessage :: forall msg. RenderMessage site msg => msg -> Text)
---------------------
-- Text and String --
---------------------
tickmark :: IsString a => a tickmark :: IsString a => a
tickmark = fromString "" tickmark = fromString ""
@ -42,6 +64,16 @@ 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)
------------
-- Tuples --
------------
----------
-- Maps --
----------
----------- -----------
-- Maybe -- -- Maybe --
----------- -----------
@ -49,8 +81,48 @@ whenIsJust :: Monad m => Maybe a -> (a -> m ()) -> m ()
whenIsJust (Just x) f = f x whenIsJust (Just x) f = f x
whenIsJust Nothing _ = return () whenIsJust Nothing _ = return ()
maybeT :: Monad m => m a -> MaybeT m a -> m a
maybeT x m = runMaybeT m >>= maybe x return
catchIfMaybeT :: (MonadCatch m, Exception e) => (e -> Bool) -> m a -> MaybeT m a
catchIfMaybeT p act = catchIf p (lift act) (const mzero)
---------------
-- Exception --
---------------
maybeExceptT :: Monad m => e -> m (Maybe b) -> ExceptT e m b
maybeExceptT err act = lift act >>= maybe (throwE err) return
maybeMExceptT :: Monad m => (m e) -> m (Maybe b) -> ExceptT e m b
maybeMExceptT err act = lift act >>= maybe (lift err >>= throwE) return
whenExceptT :: Monad m => Bool -> e -> ExceptT e m ()
whenExceptT b err = when b $ throwE err
whenMExceptT :: Monad m => Bool -> (m e) -> ExceptT e m ()
whenMExceptT b err = when b $ lift err >>= throwE
guardExceptT :: Monad m => Bool -> e -> ExceptT e m ()
guardExceptT b err = unless b $ throwE err
guardMExceptT :: Monad m => Bool -> (m e) -> ExceptT e m ()
guardMExceptT b err = unless b $ lift err >>= throwE
exceptT :: Monad m => (e -> m b) -> (a -> m b) -> ExceptT e m a -> m b
exceptT f g = either f g <=< runExceptT
catchIfMExceptT :: (MonadCatch m, Exception e) => (e -> m e') -> (e -> Bool) -> m a -> ExceptT e' m a
catchIfMExceptT err p act = catchIf p (lift act) (throwE <=< lift . err)
---------- ------------
-- Maps -- -- Monads --
---------- ------------
shortCircuitM :: Monad m => (a -> Bool) -> m a -> m a -> (a -> a -> a) -> m a
shortCircuitM sc mx my op = do
x <- mx
case sc x of
True -> return x
False -> op <$> pure x <*> my

View File

@ -5,7 +5,10 @@ module Utils.Common where
-- Common Utility Functions -- Common Utility Functions
import Language.Haskell.TH import Language.Haskell.TH
-- import Control.Monad
-- import Control.Monad.Trans.Class
-- import Control.Monad.Trans.Maybe
-- import Control.Monad.Trans.Except
------------ ------------
-- Tuples -- -- Tuples --
@ -50,3 +53,4 @@ altFun perm = lamE pat rhs
ps = [ xs !! (j-1) | j <- perm ] ps = [ xs !! (j-1) | j <- perm ]
fn = mkName "fn" fn = mkName "fn"

15
src/Utils/Lens.hs Normal file
View File

@ -0,0 +1,15 @@
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FunctionalDependencies #-}
module Utils.Lens where
import Import.NoFoundation
import Control.Lens
makeClassy_ ''Entity
makeClassy_ ''SheetCorrector
-- makeClassy_ ''Load

View File

@ -32,3 +32,6 @@
.glyphicon--logout::before { .glyphicon--logout::before {
content: '\e163'; content: '\e163';
} }
.glyphicon--none::before {
content: '';
}

39
static/css/tabber.css Normal file
View File

@ -0,0 +1,39 @@
.tab-group {
border-top: 2px solid #dcdcdc;
padding-top: 30px;
}
.tab-group-openers {
display: flex;
justify-content: stretch;
line-height: 40px;
font-size: 14px;
margin-bottom: 40px;
}
.tab-opener {
display: inline-block;
flex: 1;
text-align: center;
padding: 0 13px;
margin: 0 2px;
background-color: #b3b7c1;
color: white;
font-size: 16px;
text-transform: uppercase;
font-weight: 600;
transition: all .1s ease;
border-bottom: 5px solid rgba(100, 100, 100, 0.2);
}
.tab-opener:not(.tab-visible):hover {
cursor: pointer;
background-color: transparent;
color: rgb(52, 48, 58);
border-bottom-color: grey;
}
.tab-opener.tab-visible {
background-color: transparent;
color: rgb(52, 48, 58);
border-bottom-color: #5F98C2;
}

View File

@ -1,7 +1,4 @@
window.addEventListener('touchstart', function onFirstTouch() { window.addEventListener('touchstart', function onFirstTouch() {
// we could use a class
document.body.classList.add('touch-supported'); document.body.classList.add('touch-supported');
// we only need to know once that a human touched the screen, so we can stop listening now
window.removeEventListener('touchstart', onFirstTouch, false); window.removeEventListener('touchstart', onFirstTouch, false);
}, false); }, false);

466
static/js/fetchPolyfill.js Normal file
View File

@ -0,0 +1,466 @@
(function(self) {
'use strict';
if (self.fetch) {
return
}
var support = {
searchParams: 'URLSearchParams' in self,
iterable: 'Symbol' in self && 'iterator' in Symbol,
blob: 'FileReader' in self && 'Blob' in self && (function() {
try {
new Blob()
return true
} catch(e) {
return false
}
})(),
formData: 'FormData' in self,
arrayBuffer: 'ArrayBuffer' in self
}
if (support.arrayBuffer) {
var viewClasses = [
'[object Int8Array]',
'[object Uint8Array]',
'[object Uint8ClampedArray]',
'[object Int16Array]',
'[object Uint16Array]',
'[object Int32Array]',
'[object Uint32Array]',
'[object Float32Array]',
'[object Float64Array]'
]
var isDataView = function(obj) {
return obj && DataView.prototype.isPrototypeOf(obj)
}
var isArrayBufferView = ArrayBuffer.isView || function(obj) {
return obj && viewClasses.indexOf(Object.prototype.toString.call(obj)) > -1
}
}
function normalizeName(name) {
if (typeof name !== 'string') {
name = String(name)
}
if (/[^a-z0-9\-#$%&'*+.\^_`|~]/i.test(name)) {
throw new TypeError('Invalid character in header field name')
}
return name.toLowerCase()
}
function normalizeValue(value) {
if (typeof value !== 'string') {
value = String(value)
}
return value
}
// Build a destructive iterator for the value list
function iteratorFor(items) {
var iterator = {
next: function() {
var value = items.shift()
return {done: value === undefined, value: value}
}
}
if (support.iterable) {
iterator[Symbol.iterator] = function() {
return iterator
}
}
return iterator
}
function Headers(headers) {
this.map = {}
if (headers instanceof Headers) {
headers.forEach(function(value, name) {
this.append(name, value)
}, this)
} else if (Array.isArray(headers)) {
headers.forEach(function(header) {
this.append(header[0], header[1])
}, this)
} else if (headers) {
Object.getOwnPropertyNames(headers).forEach(function(name) {
this.append(name, headers[name])
}, this)
}
}
Headers.prototype.append = function(name, value) {
name = normalizeName(name)
value = normalizeValue(value)
var oldValue = this.map[name]
this.map[name] = oldValue ? oldValue+','+value : value
}
Headers.prototype['delete'] = function(name) {
delete this.map[normalizeName(name)]
}
Headers.prototype.get = function(name) {
name = normalizeName(name)
return this.has(name) ? this.map[name] : null
}
Headers.prototype.has = function(name) {
return this.map.hasOwnProperty(normalizeName(name))
}
Headers.prototype.set = function(name, value) {
this.map[normalizeName(name)] = normalizeValue(value)
}
Headers.prototype.forEach = function(callback, thisArg) {
for (var name in this.map) {
if (this.map.hasOwnProperty(name)) {
callback.call(thisArg, this.map[name], name, this)
}
}
}
Headers.prototype.keys = function() {
var items = []
this.forEach(function(value, name) { items.push(name) })
return iteratorFor(items)
}
Headers.prototype.values = function() {
var items = []
this.forEach(function(value) { items.push(value) })
return iteratorFor(items)
}
Headers.prototype.entries = function() {
var items = []
this.forEach(function(value, name) { items.push([name, value]) })
return iteratorFor(items)
}
if (support.iterable) {
Headers.prototype[Symbol.iterator] = Headers.prototype.entries
}
function consumed(body) {
if (body.bodyUsed) {
return Promise.reject(new TypeError('Already read'))
}
body.bodyUsed = true
}
function fileReaderReady(reader) {
return new Promise(function(resolve, reject) {
reader.onload = function() {
resolve(reader.result)
}
reader.onerror = function() {
reject(reader.error)
}
})
}
function readBlobAsArrayBuffer(blob) {
var reader = new FileReader()
var promise = fileReaderReady(reader)
reader.readAsArrayBuffer(blob)
return promise
}
function readBlobAsText(blob) {
var reader = new FileReader()
var promise = fileReaderReady(reader)
reader.readAsText(blob)
return promise
}
function readArrayBufferAsText(buf) {
var view = new Uint8Array(buf)
var chars = new Array(view.length)
for (var i = 0; i < view.length; i++) {
chars[i] = String.fromCharCode(view[i])
}
return chars.join('')
}
function bufferClone(buf) {
if (buf.slice) {
return buf.slice(0)
} else {
var view = new Uint8Array(buf.byteLength)
view.set(new Uint8Array(buf))
return view.buffer
}
}
function Body() {
this.bodyUsed = false
this._initBody = function(body) {
this._bodyInit = body
if (!body) {
this._bodyText = ''
} else if (typeof body === 'string') {
this._bodyText = body
} else if (support.blob && Blob.prototype.isPrototypeOf(body)) {
this._bodyBlob = body
} else if (support.formData && FormData.prototype.isPrototypeOf(body)) {
this._bodyFormData = body
} else if (support.searchParams && URLSearchParams.prototype.isPrototypeOf(body)) {
this._bodyText = body.toString()
} else if (support.arrayBuffer && support.blob && isDataView(body)) {
this._bodyArrayBuffer = bufferClone(body.buffer)
// IE 10-11 can't handle a DataView body.
this._bodyInit = new Blob([this._bodyArrayBuffer])
} else if (support.arrayBuffer && (ArrayBuffer.prototype.isPrototypeOf(body) || isArrayBufferView(body))) {
this._bodyArrayBuffer = bufferClone(body)
} else {
throw new Error('unsupported BodyInit type')
}
if (!this.headers.get('content-type')) {
if (typeof body === 'string') {
this.headers.set('content-type', 'text/plain;charset=UTF-8')
} else if (this._bodyBlob && this._bodyBlob.type) {
this.headers.set('content-type', this._bodyBlob.type)
} else if (support.searchParams && URLSearchParams.prototype.isPrototypeOf(body)) {
this.headers.set('content-type', 'application/x-www-form-urlencoded;charset=UTF-8')
}
}
}
if (support.blob) {
this.blob = function() {
var rejected = consumed(this)
if (rejected) {
return rejected
}
if (this._bodyBlob) {
return Promise.resolve(this._bodyBlob)
} else if (this._bodyArrayBuffer) {
return Promise.resolve(new Blob([this._bodyArrayBuffer]))
} else if (this._bodyFormData) {
throw new Error('could not read FormData body as blob')
} else {
return Promise.resolve(new Blob([this._bodyText]))
}
}
this.arrayBuffer = function() {
if (this._bodyArrayBuffer) {
return consumed(this) || Promise.resolve(this._bodyArrayBuffer)
} else {
return this.blob().then(readBlobAsArrayBuffer)
}
}
}
this.text = function() {
var rejected = consumed(this)
if (rejected) {
return rejected
}
if (this._bodyBlob) {
return readBlobAsText(this._bodyBlob)
} else if (this._bodyArrayBuffer) {
return Promise.resolve(readArrayBufferAsText(this._bodyArrayBuffer))
} else if (this._bodyFormData) {
throw new Error('could not read FormData body as text')
} else {
return Promise.resolve(this._bodyText)
}
}
if (support.formData) {
this.formData = function() {
return this.text().then(decode)
}
}
this.json = function() {
return this.text().then(JSON.parse)
}
return this
}
// HTTP methods whose capitalization should be normalized
var methods = ['DELETE', 'GET', 'HEAD', 'OPTIONS', 'POST', 'PUT']
function normalizeMethod(method) {
var upcased = method.toUpperCase()
return (methods.indexOf(upcased) > -1) ? upcased : method
}
function Request(input, options) {
options = options || {}
var body = options.body
if (input instanceof Request) {
if (input.bodyUsed) {
throw new TypeError('Already read')
}
this.url = input.url
this.credentials = input.credentials
if (!options.headers) {
this.headers = new Headers(input.headers)
}
this.method = input.method
this.mode = input.mode
if (!body && input._bodyInit != null) {
body = input._bodyInit
input.bodyUsed = true
}
} else {
this.url = String(input)
}
this.credentials = options.credentials || this.credentials || 'omit'
if (options.headers || !this.headers) {
this.headers = new Headers(options.headers)
}
this.method = normalizeMethod(options.method || this.method || 'GET')
this.mode = options.mode || this.mode || null
this.referrer = null
if ((this.method === 'GET' || this.method === 'HEAD') && body) {
throw new TypeError('Body not allowed for GET or HEAD requests')
}
this._initBody(body)
}
Request.prototype.clone = function() {
return new Request(this, { body: this._bodyInit })
}
function decode(body) {
var form = new FormData()
body.trim().split('&').forEach(function(bytes) {
if (bytes) {
var split = bytes.split('=')
var name = split.shift().replace(/\+/g, ' ')
var value = split.join('=').replace(/\+/g, ' ')
form.append(decodeURIComponent(name), decodeURIComponent(value))
}
})
return form
}
function parseHeaders(rawHeaders) {
var headers = new Headers()
// Replace instances of \r\n and \n followed by at least one space or horizontal tab with a space
// https://tools.ietf.org/html/rfc7230#section-3.2
var preProcessedHeaders = rawHeaders.replace(/\r?\n[\t ]+/g, ' ')
preProcessedHeaders.split(/\r?\n/).forEach(function(line) {
var parts = line.split(':')
var key = parts.shift().trim()
if (key) {
var value = parts.join(':').trim()
headers.append(key, value)
}
})
return headers
}
Body.call(Request.prototype)
function Response(bodyInit, options) {
if (!options) {
options = {}
}
this.type = 'default'
this.status = options.status === undefined ? 200 : options.status
this.ok = this.status >= 200 && this.status < 300
this.statusText = 'statusText' in options ? options.statusText : 'OK'
this.headers = new Headers(options.headers)
this.url = options.url || ''
this._initBody(bodyInit)
}
Body.call(Response.prototype)
Response.prototype.clone = function() {
return new Response(this._bodyInit, {
status: this.status,
statusText: this.statusText,
headers: new Headers(this.headers),
url: this.url
})
}
Response.error = function() {
var response = new Response(null, {status: 0, statusText: ''})
response.type = 'error'
return response
}
var redirectStatuses = [301, 302, 303, 307, 308]
Response.redirect = function(url, status) {
if (redirectStatuses.indexOf(status) === -1) {
throw new RangeError('Invalid status code')
}
return new Response(null, {status: status, headers: {location: url}})
}
self.Headers = Headers
self.Request = Request
self.Response = Response
self.fetch = function(input, init) {
return new Promise(function(resolve, reject) {
var request = new Request(input, init)
var xhr = new XMLHttpRequest()
xhr.onload = function() {
var options = {
status: xhr.status,
statusText: xhr.statusText,
headers: parseHeaders(xhr.getAllResponseHeaders() || '')
}
options.url = 'responseURL' in xhr ? xhr.responseURL : options.headers.get('X-Request-URL')
var body = 'response' in xhr ? xhr.response : xhr.responseText
resolve(new Response(body, options))
}
xhr.onerror = function() {
reject(new TypeError('Network request failed'))
}
xhr.ontimeout = function() {
reject(new TypeError('Network request failed'))
}
xhr.open(request.method, request.url, true)
if (request.credentials === 'include') {
xhr.withCredentials = true
} else if (request.credentials === 'omit') {
xhr.withCredentials = false
}
if ('responseType' in xhr && support.blob) {
xhr.responseType = 'blob'
}
request.headers.forEach(function(value, name) {
xhr.setRequestHeader(name, value)
})
xhr.send(typeof request._bodyInit === 'undefined' ? null : request._bodyInit)
})
}
self.fetch.polyfill = true
})(typeof self !== 'undefined' ? self : this);

88
static/js/tabber.js Normal file
View File

@ -0,0 +1,88 @@
(function($) {
document.addEventListener('DOMContentLoaded', function() {
'use strict';
// define plugin
$.fn.tabgroup = function() {
var $this = $(this);
var $openers = $('<div class="tab-group-openers"></div>');
$this.prepend($openers);
var openedByDefault = $this.data('tab-open') || 0;
var tabs = [];
var currentTab = {};
$this.find('.tab').each(function(i, t) {
var tab = $(t);
tab.data('tab-index', i);
var tabName = tab.data('tab-name') || 'Tab '+i;
var tabFile = tab.data('tab-file') || false;
var $opener = makeOpener(tabName, i);
$openers.append($opener);
if (tab.find('.tab-title')) {
tab.find('.tab-title').remove();
}
tab.hide();
var loaded = false;
tabs.push({index: i, name: tabName, file: tabFile, dom: tab, opener: $opener, loaded: false});
});
$this.on('click', 'a[href^="#"]', function(event) {
var $target = $(event.currentTarget);
var tab = getTabByName($target.attr('href').replace('#', ''));
if ( tab ) {
showTab(tab.index);
}
event.preventDefault();
});
function getTabByName(name) {
var it = -1;
$.each(tabs, function(i, t) {
if ( t.name.toLowerCase() === name.toLowerCase() ) {
it = i;
}
});
if ( it >= 0 ) {
return tabs[it];
} else {
return false;
}
}
function makeOpener(tabName, i) {
return $('<span class="tab-opener">'+tabName+'</span>').
on('click', function() {
showTab(i);
});
}
function showTab(i) {
tabs.forEach(function(t) {
t.dom.hide();
t.opener.removeClass('tab-visible');
});
currentTab = tabs[i];
if ( !currentTab.loaded && currentTab.file ){
$.get(currentTab.file, function(res) {
currentTab.dom.html(res);
currentTab.loaded = true;
});
}
currentTab.opener.addClass('tab-visible');
currentTab.dom.show();
}
showTab(openedByDefault);
currentTab = tabs[openedByDefault];
};
// apply plugin to all available tab-groups
$('.tab-group').each(function(i, t) {
$(t).tabgroup();
})
});
})($);

348
static/js/urlPolyfill.js Normal file
View File

@ -0,0 +1,348 @@
(function(global) {
/**
* Polyfill URLSearchParams
*
* Inspired from : https://github.com/WebReflection/url-search-params/blob/master/src/url-search-params.js
*/
var checkIfIteratorIsSupported = function() {
try {
return !!Symbol.iterator;
} catch(error) {
return false;
}
};
var iteratorSupported = checkIfIteratorIsSupported();
var createIterator = function(items) {
var iterator = {
next: function() {
var value = items.shift();
return { done: value === void 0, value: value };
}
};
if(iteratorSupported) {
iterator[Symbol.iterator] = function() {
return iterator;
};
}
return iterator;
};
/**
* Search param name and values should be encoded according to https://url.spec.whatwg.org/#urlencoded-serializing
* encodeURIComponent() produces the same result except encoding spaces as `%20` instead of `+`.
*/
var serializeParam = function(value) {
return encodeURIComponent(value).replace(/%20/g, '+');
};
var deserializeParam = function(value) {
return decodeURIComponent(value).replace(/\+/g, ' ');
};
var polyfillURLSearchParams= function() {
var URLSearchParams = function(searchString) {
Object.defineProperty(this, '_entries', { value: {} });
if(typeof searchString === 'string') {
if(searchString !== '') {
searchString = searchString.replace(/^\?/, '');
var attributes = searchString.split('&');
var attribute;
for(var i = 0; i < attributes.length; i++) {
attribute = attributes[i].split('=');
this.append(
deserializeParam(attribute[0]),
(attribute.length > 1) ? deserializeParam(attribute[1]) : ''
);
}
}
} else if(searchString instanceof URLSearchParams) {
var _this = this;
searchString.forEach(function(value, name) {
_this.append(value, name);
});
}
};
var proto = URLSearchParams.prototype;
proto.append = function(name, value) {
if(name in this._entries) {
this._entries[name].push(value.toString());
} else {
this._entries[name] = [value.toString()];
}
};
proto.delete = function(name) {
delete this._entries[name];
};
proto.get = function(name) {
return (name in this._entries) ? this._entries[name][0] : null;
};
proto.getAll = function(name) {
return (name in this._entries) ? this._entries[name].slice(0) : [];
};
proto.has = function(name) {
return (name in this._entries);
};
proto.set = function(name, value) {
this._entries[name] = [value.toString()];
};
proto.forEach = function(callback, thisArg) {
var entries;
for(var name in this._entries) {
if(this._entries.hasOwnProperty(name)) {
entries = this._entries[name];
for(var i = 0; i < entries.length; i++) {
callback.call(thisArg, entries[i], name, this);
}
}
}
};
proto.keys = function() {
var items = [];
this.forEach(function(value, name) { items.push(name); });
return createIterator(items);
};
proto.values = function() {
var items = [];
this.forEach(function(value) { items.push(value); });
return createIterator(items);
};
proto.entries = function() {
var items = [];
this.forEach(function(value, name) { items.push([name, value]); });
return createIterator(items);
};
if(iteratorSupported) {
proto[Symbol.iterator] = proto.entries;
}
proto.toString = function() {
var searchString = '';
this.forEach(function(value, name) {
if(searchString.length > 0) searchString+= '&';
searchString += serializeParam(name) + '=' + serializeParam(value);
});
return searchString;
};
global.URLSearchParams = URLSearchParams;
};
if(!('URLSearchParams' in global) || (new URLSearchParams('?a=1').toString() !== 'a=1')) {
polyfillURLSearchParams();
}
// HTMLAnchorElement
})(
(typeof global !== 'undefined') ? global
: ((typeof window !== 'undefined') ? window
: ((typeof self !== 'undefined') ? self : this))
);
(function(global) {
/**
* Polyfill URL
*
* Inspired from : https://github.com/arv/DOM-URL-Polyfill/blob/master/src/url.js
*/
var checkIfURLIsSupported = function() {
try {
var u = new URL('b', 'http://a');
u.pathname = 'c%20d';
return (u.href === 'http://a/c%20d') && u.searchParams;
} catch(e) {
return false;
}
};
var polyfillURL = function() {
var _URL = global.URL;
var URL = function(url, base) {
if(typeof url !== 'string') url = String(url);
var doc = document.implementation.createHTMLDocument('');
window.doc = doc;
if(base) {
var baseElement = doc.createElement('base');
baseElement.href = base;
doc.head.appendChild(baseElement);
}
var anchorElement = doc.createElement('a');
anchorElement.href = url;
doc.body.appendChild(anchorElement);
anchorElement.href = anchorElement.href; // force href to refresh
if(anchorElement.protocol === ':' || !/:/.test(anchorElement.href)) {
throw new TypeError('Invalid URL');
}
Object.defineProperty(this, '_anchorElement', {
value: anchorElement
});
};
var proto = URL.prototype;
var linkURLWithAnchorAttribute = function(attributeName) {
Object.defineProperty(proto, attributeName, {
get: function() {
return this._anchorElement[attributeName];
},
set: function(value) {
this._anchorElement[attributeName] = value;
},
enumerable: true
});
};
['hash', 'host', 'hostname', 'port', 'protocol', 'search']
.forEach(function(attributeName) {
linkURLWithAnchorAttribute(attributeName);
});
Object.defineProperties(proto, {
'toString': {
get: function() {
var _this = this;
return function() {
return _this.href;
};
}
},
'href' : {
get: function() {
return this._anchorElement.href.replace(/\?$/,'');
},
set: function(value) {
this._anchorElement.href = value;
},
enumerable: true
},
'pathname' : {
get: function() {
return this._anchorElement.pathname.replace(/(^\/?)/,'/');
},
set: function(value) {
this._anchorElement.pathname = value;
},
enumerable: true
},
'origin': {
get: function() {
// get expected port from protocol
var expectedPort = {'http:': 80, 'https:': 443, 'ftp:': 21}[this._anchorElement.protocol];
// add port to origin if, expected port is different than actual port
// and it is not empty f.e http://foo:8080
// 8080 != 80 && 8080 != ''
var addPortToOrigin = this._anchorElement.port != expectedPort &&
this._anchorElement.port !== ''
return this._anchorElement.protocol +
'//' +
this._anchorElement.hostname +
(addPortToOrigin ? (':' + this._anchorElement.port) : '');
},
enumerable: true
},
'password': { // TODO
get: function() {
return '';
},
set: function(value) {
},
enumerable: true
},
'username': { // TODO
get: function() {
return '';
},
set: function(value) {
},
enumerable: true
},
'searchParams': {
get: function() {
var searchParams = new URLSearchParams(this.search);
var _this = this;
['append', 'delete', 'set'].forEach(function(methodName) {
var method = searchParams[methodName];
searchParams[methodName] = function() {
method.apply(searchParams, arguments);
_this.search = searchParams.toString();
};
});
return searchParams;
},
enumerable: true
}
});
URL.createObjectURL = function(blob) {
return _URL.createObjectURL.apply(_URL, arguments);
};
URL.revokeObjectURL = function(url) {
return _URL.revokeObjectURL.apply(_URL, arguments);
};
global.URL = URL;
};
if(!checkIfURLIsSupported()) {
polyfillURL();
}
if((global.location !== void 0) && !('origin' in global.location)) {
var getOrigin = function() {
return global.location.protocol + '//' + global.location.hostname + (global.location.port ? (':' + global.location.port) : '');
};
try {
Object.defineProperty(global.location, 'origin', {
get: getOrigin,
enumerable: true
});
} catch(e) {
setInterval(function() {
global.location.origin = getOrigin();
}, 100);
}
}
})(
(typeof global !== 'undefined') ? global
: ((typeof window !== 'undefined') ? window
: ((typeof self !== 'undefined') ? self : this))
);

1650
static/js/zepto.js Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,34 +1,100 @@
<div .course-header> <div .container>
<div .course-header__info> <h2>#{courseName course}
<table> <table>
<tr>
<th>Teilnehmer
<td>
#{participants}
$maybe capacity <- courseCapacity course
\ von #{capacity}
<tr>
<th>Anmeldezeitraum
<td>
$maybe regFrom <- courseRegisterFrom course
#{formatTimeGerWD regFrom}
$maybe regTo <- courseRegisterTo course
\ bis #{formatTimeGerWD regTo}
<div>
<form method=post action=@{CourseR tid csh CourseShowR} enctype=#{regEnctype}>
^{regWidget}
<div .course-header__title>
<h1>#{courseName course}
$maybe school <- schoolMB $maybe school <- schoolMB
<h4>#{schoolName school} <tr>
<th #school>Fakultät/Institut
<td>
#{schoolName school}
$maybe descr <- courseDescription course
<tr>
<th #description>Beschreibung
<td>
<p>#{descr}
$maybe link <- courseLinkExternal course
<tr>
<th #website>Website
<td>
<a href=#{link}>#{link}
<tr>
<th #participants>Teilnehmer
<td>
#{participants}
$maybe capacity <- courseCapacity course
\ von #{capacity}
<tr>
<th #registration>Anmeldezeitraum
<td>
$maybe regFrom <- courseRegisterFrom course
#{formatTimeGerWD regFrom}
$maybe regTo <- courseRegisterTo course
\ bis #{formatTimeGerWD regTo}
<tr>
<th>
<td>
$# if allowed to register
<div .course__registration.container>
<button class="btn btn-primary">
<a href="#">Anmelden
$# <form method=post action=@{CourseR tid csh CShow} enctype=#{regEnctype}>
$# ^{regWidget}
<div .container> <div .container>
$maybe descr <- courseDescription course <div .tab-group>
<h2 #description>Beschreibung <div .tab data-tab-name="Übungsblätter">
<p> #{descr} ^{modal "#modal-toggler__new-sheet" Nothing}
$maybe link <- courseLinkExternal course <h3 .tab-title>Übungsblätter
<h4 #linl>Homepage <table .table.table-striped.table-hover>
<a href=#{link}>#{link} <thead>
<tr>
<th>Blatt
<th>Abgabe ab
<th>Abgabe bis
<th>Bewertung</th>
<tbody>
<tr>
<td>
<a href="http://localhost:3000/course/S2018/ffp/ex/Blatt%201/show" role="button">Blatt 1
<td>Do 08.04.18
<td>Do 11.04.18
<td>NotGraded
<tr>
<td>
<a href="http://localhost:3000/course/S2018/ffp/ex/Blatt%201/show" role="button">Blatt 2
<td>Do 15.04.18
<td>Do 18.04.18
<td>NotGraded
<tr .no-hover.no-stripe>
<td colspan="4">
<a href="/course/S2018/ffp/ex/new" #modal-toggler__new-sheet>Neues Übungsblatt anlegen
<div .tab data-tab-name="Übungsgruppen">
<h3 .tab-title>Übungsgruppen
<table .table.table-striped.table-hover>
<thead>
<tr>
<th>Name
<th>Termin
<th>Raum
<th>Studenten
<th>Tutor
<th>Anmeldung bis
<tbody>
<tr>
<td>Gruppe 1
<td>Montag 10:00 - 12:00
<td>N/A
<td>2/10
<td>Tutor1 Tutoren
<td>Do 21.02.2019, 19:00
<tr>
<td>Gruppe 2
<td>Montag 12:00 - 14:00
<td>N/A
<td>0/10
<td>Assistant1 Assistant
<td>Di 21.02.2017, 19:00
<div .tab data-tab-name="Klausuren">
<h3 .tab-title>Klausuren
<div>...

View File

@ -1,19 +1,3 @@
.course-header { .course__registration {
/*display: flex; margin-top: 20px;
flex-direction: row;
justify-content: space-between;*/
}
.course-header__title {
align-self: baseline;
}
.course-header__info {
border: 1px solid var(--greybase);
padding: 13px;
align-self: center;
float: right;
}
.course-header__info table {
margin: 0;
} }

View File

@ -13,8 +13,17 @@
$with status2 <- bool status "info" (status == "") $with status2 <- bool status "info" (status == "")
<div class="alert alert-#{status2}">#{msg} <div class="alert alert-#{status2}">#{msg}
<!-- breadcrumbs -->
$if not $ Just HomeR == mcurrentRoute
^{breadcrumbs}
$maybe headline <- contentHeadline
<h1>
^{headline}
<!-- prime page actions --> <!-- prime page actions -->
^{pageactionprime} ^{pageactionprime}
<!-- actual content --> <!-- actual content -->
^{widget} ^{widget}

View File

@ -1,46 +1,18 @@
:root { :root {
/* THEME 1 */
--base00: #72a85b;
--base-bg-color: #1d1c1d;
--base-font-color: #fff;
--sec-font-color: #fff;
--box-bg-color: #3c3c3c;
/* THEME 2 */
--base00: #38428a;
--base-bg-color: #ffffff;
--base-font-color: rgb(53, 53, 53);
--sec-font-color: #eaf2ff;
--box-bg-color: #dddddd;
/* THEME 3 */
--darkbase: #364B60;
--lightbase: #2490E8;
--lighterbase: #60C2FF;
--whitebase: #FCFFFA;
--greybase: #B1B5C0;
--fontbase: #34303a;
--fontsec: #5b5861;
/* THEME 4 */
--darkbase: #263C4C;
--lightbase: #598EB5;
--lighterbase: #5F98C2;
--whitebase: #FCFFFA;
--greybase: #B1B5C0;
--lightgreybase: #D9DEDB;
--blackbase: #1A2A36;
--fontbase: #34303a;
--fontsec: #5b5861;
--primarybase: #4C7A9C;
/* THEME INDEPENDENT COLORS */ /* THEME INDEPENDENT COLORS */
--errorbase: red; --color-error: red;
--warningbase: #fe7700; --color-warning: #fe7700;
--validbase: #2dcc35; --color-success: #2dcc35;
--infobase: var(--darkbase); --color-info: #c4c4c4;
--color-lightblack: #1A2A36;
--color-lightwhite: #FCFFFA;
--color-grey: #B1B5C0;
--color-font: #34303a;
--color-fontsec: #5b5861;
/* FONTS */ /* FONTS */
--fontfamilybase: "Source Sans Pro", Helvetica, sans-serif; --font-base: "Source Sans Pro", Helvetica, sans-serif;
/* DIMENSIONS */ /* DIMENSIONS */
--header-height: 80px; --header-height: 80px;
@ -55,13 +27,64 @@
body { body {
background-color: white; background-color: white;
color: var(--fontbase); color: var(--color-font);
font-family: var(--fontfamilybase); font-family: var(--font-base);
font-weight: 400; font-weight: 400;
font-size: 16px; font-size: 16px;
overflow-y: scroll; overflow-y: scroll;
} }
/* THEMES */
body {
/* DEFAULT THEME */
--color-primary: #4C7A9C;
--color-light: #598EB5;
--color-lighter: #5F98C2;
--color-dark: #425d79;
--color-darker: #274a65;
--color-link: var(--color-dark);
--color-link-hover: var(--color-darker);
&.theme--neutral-blue {
--color-primary: #3E606F;
--color-light: rgb(189, 201, 219);
--color-lighter: rgb(145, 159, 170);
--color-dark: #3E606F;
--color-darker: #193441;
}
&.theme--aberdeen-reds {
--color-primary: #820333;
--color-light: #C9283E;
--color-lighter: #F0433A;
--color-dark: #540032;
--color-darker: #2E112D;
}
&.theme--mint-green {
--color-primary: #5C996B;
--color-light: #7ACC8F;
--color-lighter: #99FFB2;
--color-dark: #3D6647;
--color-darker: #1F3324;
}
&.theme--sky-love {
--color-primary: #87ABE5;
--color-light: #A0C6F2;
--color-lighter: #BAE2FF;
--color-dark: #7A95DE;
--color-darker: #6B7BC9;
--color-link: var(--color-lightblack);
--color-link-hover: var(--color-darker);
}
}
/* END THEMES */
a, a,
a:visited { a:visited {
text-decoration: none; text-decoration: none;
@ -69,6 +92,15 @@ a:visited {
transition: color .2s ease, background-color .2s ease; transition: color .2s ease, background-color .2s ease;
} }
a {
color: var(--color-link);
}
a:hover {
color: var(--color-link-hover);
}
ul { ul {
list-style-type: none; list-style-type: none;
} }
@ -105,18 +137,35 @@ table {
overflow: auto; overflow: auto;
} }
.table-striped {
tbody {
tr:not(.no-stripe):nth-child(even) {
background-color: #e8e8e8;
}
}
}
.table-hover {
tbody {
tr:not(.no-hover):hover {
background-color: #d8d8d8;
}
}
}
th, td { th, td {
text-align: left; text-align: left;
padding: 0 13px 0 7px; padding: 7px;
vertical-align: baseline; vertical-align: baseline;
} }
th:first-child, th:first-child,
td:first-child { td:first-child {
padding-left: 0;
border-left: 0; border-left: 0;
} }
th { th {
border-left: 2px solid var(--greybase); border-left: 2px solid var(--color-grey);
} }
/* LAYOUT */ /* LAYOUT */
.main { .main {
@ -140,18 +189,10 @@ th {
p { p {
margin: 10px 0; margin: 10px 0;
} }
a {
color: var(--darkbase);
}
a:hover {
color: var(--lightbase);
}
} }
.pseudo-focus { .pseudo-focus {
outline: 5px auto var(--lightbase); outline: 5px auto var(--color-light);
outline: 5px auto -webkit-focus-ring-color; outline: 5px auto -webkit-focus-ring-color;
} }
@ -163,28 +204,22 @@ button,
outline: 0; outline: 0;
border: 0; border: 0;
box-shadow: 0; box-shadow: 0;
background-color: var(--lightbase); background-color: var(--color-dark);
color: white; color: white;
padding: 10px 17px; padding: 10px 17px;
min-width: 100px; min-width: 100px;
transition: all .1s; transition: all .1s;
font-size: 16px; font-size: 16px;
cursor: pointer; cursor: pointer;
border-radius: 4px;
display: inline-block; display: inline-block;
}
input.btn-primary,
button.btn-primary,
a.btn.btn-primary,
.btn.btn-primary {
background-color: var(--primarybase);
}
input.btn-info, a {
button.btn-info, color: white;
a.btn.btn-info, }
.btn.btn-info {
background-color: var(--infobase) a:hover {
color: white;
}
} }
input[type="submit"][disabled], input[type="submit"][disabled],
@ -193,7 +228,7 @@ button[disabled],
a.btn[disabled], a.btn[disabled],
.btn[disabled] { .btn[disabled] {
opacity: 0.3; opacity: 0.3;
background-color: var(--greybase); background-color: var(--color-grey);
cursor: default; cursor: default;
} }
@ -202,20 +237,33 @@ input[type="button"]:not([disabled]):hover,
button:not([disabled]):hover, button:not([disabled]):hover,
a.btn:not([disabled]):hover, a.btn:not([disabled]):hover,
.btn:not([disabled]):hover { .btn:not([disabled]):hover {
background-color: var(--lighterbase); background-color: var(--color-light);
text-decoration: underline; text-decoration: underline;
color: white; color: white;
} }
input.btn-primary,
button.btn-primary,
a.btn.btn-primary,
.btn.btn-primary {
background-color: var(--color-primary);
}
input.btn-info,
button.btn-info,
a.btn.btn-info,
.btn.btn-info {
background-color: var(--color-info)
}
input[type="submit"].btn-info:hover, input[type="submit"].btn-info:hover,
input[type="button"].btn-info:hover, input[type="button"].btn-info:hover,
button.btn-info:hover, button.btn-info:hover,
a.btn.btn-info:hover, a.btn.btn-info:hover,
.btn.btn-info:hover { .btn.btn-info:hover {
background-color: var(--greybase) background-color: var(--color-grey)
} }
.alert-debug { .alert-debug {
background-color: rgb(240, 30, 240); background-color: rgb(240, 30, 240);
} }

View File

@ -1,5 +1,5 @@
<div .container> <div .container>
<h1>UniworkY - Demo <h1>Uniworky - Demo
<h3> <h3>
Testumgebung für die Re-Implementierung von <a href="https://uniworx.ifi.lmu.de/">UniWorX</a> Testumgebung für die Re-Implementierung von <a href="https://uniworx.ifi.lmu.de/">UniWorX</a>
<p> <p>
@ -49,24 +49,6 @@
^{modal ".toggler1" Nothing} ^{modal ".toggler1" Nothing}
<a href="/" .btn.toggler1>Klick mich für Ajax-Test <a href="/" .btn.toggler1>Klick mich für Ajax-Test
<noscript>(Für Modals bitte JS aktivieren)</noscript> <noscript>(Für Modals bitte JS aktivieren)</noscript>
^{modal ".toggler2" (Just "Test wegen Modal")} ^{modal ".toggler2" (Just "Test Inhalt für Modal")}
<div .btn.toggler2>Klick mich für Content-Test <div .btn.toggler2>Klick mich für Content-Test
<noscript>(Für Modals bitte JS aktivieren)</noscript> <noscript>(Für Modals bitte JS aktivieren)</noscript>
<li><br>
Multi-File-Input für bereits hochgeladene Dateien:
<form>
<div .form-group>
<label .form-group__label>Datei(en)
$# file 1
<div .file-checkbox__container>
<label .file-checkbox__label.reactive-label.btn for="f2-1">Datenschutz.txt
<div .checkbox>
<input .file-checkbox id="f2-1" name="f2" value="Datenschutz.txt" type="checkbox">
<label for="f2-1">
$# file 2
<div .file-checkbox__container>
<label .file-checkbox__label.reactive-label.btn for="f2-2">fill-db.hs
<div .checkbox>
<input .file-checkbox id="f2-2" name="f2" value="fill-db.hs" type="checkbox">
<label for="f2-2">

View File

@ -6,13 +6,6 @@ $forall FileUploadInfo{..} <- fileInfos
<input .file-checkbox.js-file-checkbox id=#{fuiHtmlId} name=#{fieldName} :fuiChecked:checked value=#{toPathPiece fuiId} type="checkbox"> <input .file-checkbox.js-file-checkbox id=#{fuiHtmlId} name=#{fieldName} :fuiChecked:checked value=#{toPathPiece fuiId} type="checkbox">
<label for=#{fuiHtmlId}> <label for=#{fuiHtmlId}>
<div .file-checkbox__container.file-checkbox__container--checked>
<label .file-checkbox__label.reactive-label.btn for=fi1>file1.txt
<div .checkbox>
<input .file-checkbox.js-file-checkbox id=fi1 name=file checked value="file1.txt" type="checkbox">
<label for=fi1>
$# new files $# new files
<input type="file" name=#{fieldName} multiple> <input type="file" name=#{fieldName} multiple>

View File

@ -25,15 +25,6 @@
<div .row> <div .row>
<div .col-lg-12> <div .col-lg-12>
<h2>Dateien <h2>Dateien
<ul> ^{fileTable}
$forall fileLink <- fileLinks
<li>
$case fileLink
$of CourseR _ _ (SheetR (SheetFileR _ typ name))
#{toPathPiece typ}
<a href=@{fileLink}>#{name}
$of other
<a href=@{fileLink}>@{fileLink}
<hr> <hr>

View File

@ -63,7 +63,6 @@
if (formGroup.classList.contains('form-group')) { if (formGroup.classList.contains('form-group')) {
formGroup.classList.add('form-group--valid') formGroup.classList.add('form-group--valid')
} }
submitBtn.removeAttribute('disabled');
if (isMulti) { if (isMulti) {
addNextInput(); addNextInput();
} }
@ -71,7 +70,6 @@
if (formGroup.classList.contains('form-group')) { if (formGroup.classList.contains('form-group')) {
formGroup.classList.remove('form-group--valid') formGroup.classList.remove('form-group--valid')
} }
submitBtn.setAttribute('disabled', 'disabled');
} }
} }
// addseventlistener destInput // addseventlistener destInput

View File

@ -13,21 +13,9 @@ form {
grid-gap: 5px; grid-gap: 5px;
justify-content: flex-start; justify-content: flex-start;
align-items: center; align-items: center;
margin: 10px 0; margin: 17px 0;
padding-left: 10px; padding-left: 10px;
border-left: 8px solid transparent; border-left: 2px solid transparent;
}
.form-group--required {
border-left: 8px solid var(--lighterbase);
}
.form-group--valid {
border-left: 8px solid var(--validbase);
}
.form-group--has-error {
border-left: 8px solid var(--errorbase) !important;
} }
.form-group__label { .form-group__label {
@ -56,16 +44,54 @@ input[type="password"],
input[type="url"], input[type="url"],
input[type="number"], input[type="number"],
input[type="email"] { input[type="email"] {
background-color: rgba(0, 0, 0, 0.05); /* from bulma.css */
padding: 7px 3px 7px; background-color: #fff;
outline: 0; border-color: #dbdbdb;
border: 0; color: #363636;
border-bottom: 2px solid var(--darkbase); box-shadow: inset 0 2px 3px 1px rgba(50,50,50,.1);
box-shadow: 0 2px 13px rgba(0, 0, 0, 0.05);
color: var(--fontbase);
transition: all .1s;
font-size: 16px;
min-width: 400px; min-width: 400px;
-webkit-appearance: none;
align-items: center;
border: 1px solid transparent;
border-radius: 4px;
display: inline-flex;
font-size: 1rem;
height: 2.25em;
justify-content: flex-start;
line-height: 1.5;
padding-bottom: calc(.375em - 1px);
padding-left: calc(.625em - 1px);
padding-right: calc(.625em - 1px);
padding-top: calc(.375em - 1px);
position: relative;
vertical-align: top;
}
.form-group--required {
.form-group__label::before {
content: '*';
position: absolute;
left: -14px;
}
input, textarea {
border-bottom-color: var(--color-lighter);
}
}
.form-group--valid {
input, textarea {
border-bottom-color: var(--color-success);
}
}
.form-group--has-error {
input, textarea {
border-bottom-color: var(--color-error);
}
} }
input[type="text"]:focus, input[type="text"]:focus,
@ -73,30 +99,42 @@ input[type="password"]:focus,
input[type="url"]:focus, input[type="url"]:focus,
input[type="number"]:focus, input[type="number"]:focus,
input[type="email"]:focus { input[type="email"]:focus {
border-bottom-color: var(--lightbase); /* border-bottom-color: var(--color-light);
background-color: transparent; background-color: transparent;
box-shadow: 0 0 13px var(--color-lighter); */
border-color: #3273dc;
box-shadow: 0 0 0 0.125em rgba(50,115,220,.25);
outline: 0;
} }
/* BUTTON STYLE SEE default-layout.lucius */ /* BUTTON STYLE SEE default-layout.lucius */
/* TEXTAREAS */ /* TEXTAREAS */
textarea { textarea {
outline: 0; -webkit-appearance: none;
border: 0; border: 1px solid transparent;
padding: 7px 4px; border-radius: 2px;
min-width: 400px; display: inline-flex;
min-height: 100px; font-size: 1rem;
font-family: var(--fontfamilybase); height: 170px;
font-size: 16px; width: 400px;
color: var(--fontbase); line-height: 1.5;
background-color: rgba(0, 0, 0, 0.05); padding-bottom: calc(.375em - 1px);
box-shadow: 0 2px 13px rgba(0, 0, 0, 0.05); padding-left: calc(.625em - 1px);
border-bottom: 2px solid var(--darkbase); padding-right: calc(.625em - 1px);
padding-top: calc(.375em - 1px);
position: relative;
vertical-align: top;
background-color: #fff;
border-color: #dbdbdb;
color: #363636;
box-shadow: inset 0 1px 2px rgba(10,10,10,.1);
} }
textarea:focus { textarea:focus {
background-color: transparent; border-color: #3273dc;
border-bottom-color: var(--lightbase); box-shadow: 0 0 0 0.125em rgba(50,115,220,.25);
outline: 0;
} }
/* CUSTOM LEGACY CHECKBOX AND RADIO BOXES */ /* CUSTOM LEGACY CHECKBOX AND RADIO BOXES */
@ -112,14 +150,14 @@ input[type="checkbox"]::before {
position: absolute; position: absolute;
width: 20px; width: 20px;
height: 20px; height: 20px;
background-color: var(--lighterbase); background-color: var(--color-lighter);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
border-radius: 2px; border-radius: 2px;
} }
input[type="checkbox"]:checked::before { input[type="checkbox"]:checked::before {
background-color: var(--lightbase); background-color: var(--color-light);
} }
input[type="checkbox"]:checked::after { input[type="checkbox"]:checked::after {
content: '✓'; content: '✓';
@ -148,7 +186,7 @@ input[type="checkbox"]:checked::after {
display: block; display: block;
height: 30px; height: 30px;
width: 30px; width: 30px;
background-color: var(--greybase); background-color: var(--color-grey);
border-radius: 4px; border-radius: 4px;
color: white; color: white;
cursor: pointer; cursor: pointer;
@ -180,12 +218,12 @@ input[type="checkbox"]:checked::after {
} }
> :checked + label { > :checked + label {
background-color: var(--lightbase); background-color: var(--color-light);
text-decoration: underline; text-decoration: underline;
} }
&:hover > label { &:hover > label {
background-color: var(--lighterbase); background-color: var(--color-lighter);
} }
&:hover > label::before { &:hover > label::before {
@ -231,13 +269,13 @@ input[type="checkbox"]:checked::after {
/* REACTIVE LABELS */ /* REACTIVE LABELS */
.reactive-label { .reactive-label {
cursor: text; cursor: text;
color: var(--fontsec); color: var(--color-fontsec);
transform: translate(0, 0); transform: translate(0, 0);
transition: all .1s; transition: all .1s;
} }
.reactive-label--small { .reactive-label--small {
cursor: default; cursor: default;
color: var(--fontbase); color: var(--color-font);
} }
@media (max-width: 999px) { @media (max-width: 999px) {
.reactive-label { .reactive-label {
@ -246,7 +284,7 @@ input[type="checkbox"]:checked::after {
} }
.reactive-label--small { .reactive-label--small {
transform: translate(2px, 0px); transform: translate(2px, 0px);
color: var(--fontsec); color: var(--color-fontsec);
/*font-size: 14px;*/ /*font-size: 14px;*/
} }
} }
@ -281,7 +319,7 @@ input[type="file"].js-file-input {
display: block; display: block;
border-radius: 2px; border-radius: 2px;
padding: 5px 13px; padding: 5px 13px;
color: var(--whitebase); color: var(--color-lightwhite);
cursor: pointer; cursor: pointer;
} }
.file-input__label, .file-input__label,
@ -291,7 +329,7 @@ input[type="file"].js-file-input {
height: 30px; height: 30px;
} }
.file-checkbox__label { .file-checkbox__label {
background-color: var(--greybase); background-color: var(--color-grey);
text-decoration: line-through; text-decoration: line-through;
} }
.file-input__label.btn, .file-input__label.btn,
@ -325,7 +363,7 @@ input[type="file"].js-file-input {
width: 40px; width: 40px;
height: 30px; height: 30px;
text-align: center; text-align: center;
background-color: var(--warningbase); background-color: var(--color-warning);
position: relative; position: relative;
margin-left: 10px; margin-left: 10px;
} }
@ -339,14 +377,14 @@ input[type="file"].js-file-input {
background-color: white; background-color: white;
} }
.file-input__container--valid > .file-input__label { .file-input__container--valid > .file-input__label {
background-color: var(--lightbase); background-color: var(--color-light);
} }
.file-checkbox__container--checked > .file-checkbox__label { .file-checkbox__container--checked > .file-checkbox__label {
text-decoration: none; text-decoration: none;
background-color: var(--lighterbase); background-color: var(--color-lighter);
&.btn:hover { &.btn:hover {
background-color: var(--lighterbase); background-color: var(--color-lighter);
text-decoration: line-through; text-decoration: line-through;
} }
} }

View File

@ -10,6 +10,8 @@
var origParent = modal.parentNode; var origParent = modal.parentNode;
function open(event) { function open(event) {
// disable modals for narrow screens
if (window.innerWidth < 768) return true;
if (event) { if (event) {
event.preventDefault(); event.preventDefault();
} }
@ -64,7 +66,9 @@
replaceMe.classList.remove('replace-me'); replaceMe.classList.remove('replace-me');
replaceMe.innerText = '...loading'; replaceMe.innerText = '...loading';
if (replaceWith.length > 0) { if (replaceWith.length > 0) {
fetch(replaceWith).then(function(response) { fetch(replaceWith, {
credentials: 'same-origin'
}).then(function(response) {
return response.text(); return response.text();
}).then(function(body) { }).then(function(body) {
var modalContent = document.createElement('div'); var modalContent = document.createElement('div');

View File

@ -10,7 +10,7 @@
max-height: calc(100vh - 30px); max-height: calc(100vh - 30px);
border-radius: 7px; border-radius: 7px;
z-index: -1; z-index: -1;
color: var(--fontbase); color: var(--color-font);
padding: 20px; padding: 20px;
overflow: auto; overflow: auto;
opacity: 0; opacity: 0;
@ -70,7 +70,7 @@
justify-content: center; justify-content: center;
width: 30px; width: 30px;
height: 30px; height: 30px;
background-color: var(--darkbase); background-color: var(--color-darker);
border-radius: 2px; border-radius: 2px;
cursor: pointer; cursor: pointer;
z-index: 20; z-index: 20;

View File

@ -24,12 +24,12 @@
left: -28px; left: -28px;
top: 10px; top: 10px;
border-left: 8px solid transparent; border-left: 8px solid transparent;
border-top: 8px solid var(--lightbase); border-top: 8px solid var(--color-light);
} }
.js-show-hide__toggle:hover::before, .js-show-hide__toggle:hover::before,
.js-show-hide--collapsed .js-show-hide__toggle::before { .js-show-hide--collapsed .js-show-hide__toggle::before {
border-left: 8px solid var(--lightbase); border-left: 8px solid var(--color-light);
border-top: 8px solid transparent; border-top: 8px solid transparent;
top: 5px; top: 5px;
left: -22px; left: -22px;

View File

@ -1 +0,0 @@
<!-- only here to be able to include sortable using `toWidget` -->

View File

@ -1,107 +0,0 @@
/**
* delcare a table as sortable by adding class 'js-sortable'
*/
(function() {
'use strict';
window.utils = window.utils || {};
window.utils.sortable = function(table) {
var ASC = 1;
var DESC = -1;
var trs, ths, sortBy, sortDir, trContents;
function setup() {
trs = table.querySelectorAll('tr');
ths = table.querySelectorAll('th');
sortBy = 0;
sortDir = ASC;
trContents = [];
Array.from(trs).forEach(function(tr, rowIndex) {
if (rowIndex === 0) {
// register table headers as sort-listener
Array.from(tr.querySelectorAll('th')).forEach(function(th, thIndex) {
th.addEventListener('click', function(el) {
sortTableBy(thIndex);
});
});
} else {
// register table rows
trContents.push(Array.from(tr.querySelectorAll('td')).map(function(td) {
return td.innerHTML;
}));
}
});
}
setup();
function updateThs(thIndex, sortOrder) {
Array.from(ths).forEach(function (th) {
th.classList.remove('sorted-asc', 'sorted-desc');
});
var suffix = sortOrder > 0 ? 'asc' : 'desc';
ths[thIndex].classList.add('sorted-' + suffix);
}
function sortTableBy(thIndex) {
var sortKey = thIndex;
var sortOrder = ASC;
if (sortBy === sortKey) {
sortOrder = sortDir === ASC ? DESC : ASC;
}
trContents.sort(dynamicSortByType(sortKey, sortOrder));
trContents.sort(dynamicSortByKey(sortKey, sortOrder));
sortBy = thIndex;
sortDir = sortOrder;
updateThs(thIndex, sortOrder);
Array.from(trs).forEach(function(tr, trIndex) {
if (trIndex > 0) {
Array.from(tr.querySelectorAll('td')).forEach(function (td, tdIndex) {
td.innerHTML = trContents[trIndex - 1][tdIndex];
});
}
});
}
function dynamicSortByKey(key, order) {
return function (a,b) {
var aVal = parseInt(a[key]);
var bVal = parseInt(b[key]);
if ((isNaN(aVal) && !isNaN(bVal)) || (!isNaN(aVal) && isNaN(bVal))) {
return 1;
}
aVal = isNaN(aVal) ? a[key] : aVal;
bVal = isNaN(bVal) ? b[key] : bVal;
var result = (aVal < bVal) ? -1 : (aVal > bVal) ? 1 : 0;
return result * order;
}
}
function dynamicSortByType(key, order) {
return function (a,b) {
var aVal = parseInt(a[key]);
var bVal = parseInt(b[key]);
aVal = isNaN(aVal) ? a[key] : aVal;
bVal = isNaN(bVal) ? b[key] : bVal;
var res = (aVal < bVal ? -1 : aVal > bVal ? 1 : 0);
if (isNaN(aVal) && !isNaN(bVal)) {
res = -1;
}
if (!isNaN(aVal) && isNaN(bVal)) {
res = 1;
}
return res * order;
}
}
};
})();
document.addEventListener('DOMContentLoaded', function() {
Array.from(document.querySelectorAll('.js-sortable')).forEach(function(table) {
utils.sortable(table);
});
});

View File

@ -1,31 +0,0 @@
table.js-sortable th {
cursor: pointer;
position: relative;
padding-right: 20px;
}
table.js-sortable th.sorted-asc,
table.js-sortable th.sorted-desc {
color: var(--darkbase);
}
table.js-sortable th.sorted-asc::after,
table.js-sortable th.sorted-desc::after {
content: '';
position: absolute;
right: 0;
top: 15px;
width: 0;
height: 0;
transform: translateY(-100%);
border-left: 8px solid transparent;
border-right: 8px solid transparent;
}
table.js-sortable th.sorted-asc::after {
border-top: 8px solid var(--lightbase);
}
table.js-sortable th.sorted-desc::after {
border-bottom: 8px solid var(--lightbase);
}

View File

@ -0,0 +1 @@
<!-- only here to be able to include tabber using `toWidget` -->

View File

@ -0,0 +1,7 @@
.tab-opener {
background-color: var(--color-dark);
&.tab-visible {
border-bottom-color: var(--color-primary);
}
}

View File

@ -27,13 +27,13 @@
<div .panel-heading> <div .panel-heading>
Abgabe herunterladen Abgabe herunterladen
<div .panel-body .text-center> <div .panel-body .text-center>
<a href=@{SubmissionDownloadArchiveR archiveName} download .btn .btn-lg .btn-default> $#<a href=@{SubmissionDownloadArchiveR archiveName} download .btn .btn-lg .btn-default>
<span .glyphicon .glyphicon-cloud-download aria-hidden="true"> ZIP-Archive <span .glyphicon .glyphicon-cloud-download aria-hidden="true"> ZIP-Archive
<div .col-md-6> <div .col-md-6>
<div .panel .panel-default> <div .panel .panel-default>
<div .panel-heading> <div .panel-heading>
Abgabe ersetzen Abgabe ersetzen
<form role=form method=post action=@{SubmissionR cID} enctype=#{uploadEnctype} .panel-body> <form role=form method=post action=@{SubmissionDemoR cID} enctype=#{uploadEnctype} .panel-body>
^{uploadWidget} ^{uploadWidget}
<div .panel .panel-default> <div .panel .panel-default>

View File

@ -1,4 +0,0 @@
<div .table>
^{table}
<p style="text-align:center">
_{MsgPage (succ psPage) pageCount}

View File

@ -0,0 +1,12 @@
<table id="#{dbtIdent}">
$maybe sortableP <- pSortable
$with toSortable <- toSortable sortableP
<thead>
$forall OneColonnade{..} <- getColonnade dbtColonnade
^{widgetFromCell th $ withSortLinks $ toSortable oneColonnadeHead}
$nothing
<tbody>
$forall row <- rows
<tr>
$forall OneColonnade{..} <- getColonnade dbtColonnade
^{widgetFromCell td $ oneColonnadeEncode row}

View File

@ -0,0 +1,45 @@
table th {
position: relative;
padding-right: 20px;
&.sortable {
cursor: pointer;
}
a {
font-weight: 800;
}
}
table th.sorted-asc,
table th.sorted-desc {
color: var(--color-light);
}
table th.sortable::after,
table th.sortable::before {
content: '';
position: absolute;
right: 0;
width: 0;
height: 0;
transform: translateY(-100%);
border-left: 8px solid transparent;
border-right: 8px solid transparent;
}
table th.sortable::before {
top: 21px;
border-top: 8px solid rgba(0, 0, 0, 0.1);
}
table th.sortable::after {
top: 9px;
border-bottom: 8px solid rgba(0, 0, 0, 0.1);
}
table th.sorted-asc::before {
border-top: 8px solid var(--color-light);
}
table th.sorted-desc::after {
border-bottom: 8px solid var(--color-light);
}

View File

@ -0,0 +1,10 @@
$newline never
<div ##{dbtIdent}-table-wrapper>
<div .scrolltable>
^{table}
$if pageCount > 1
<ul ##{dbtIdent}-pagination .pagination>
$forall p <- pageNumbers
<li .pagination-link :p == psPage:.current>
<a href=#{tblLink $ setParam (wIdent "page") (Just $ tshow p)}>
_{MsgPage (succ p)}

View File

@ -0,0 +1,70 @@
(function collonadeClosure() {
'use strict';
document.addEventListener('DOMContentLoaded', function DOMContentLoaded() {
var ASC = 'asc';
var DESC = 'desc';
function setupAsync(wrapper) {
var table = wrapper.querySelector('#' + #{String $ dbtIdent});
var ths = Array.from(table.querySelectorAll('th.sortable'));
if (ths) {
// attach click handler to each sortable column if any
ths.forEach(function(th) {
th.addEventListener('click', clickHandler);
});
}
var pagination = wrapper.querySelector('#' + #{String $ dbtIdent} + '-pagination');
if (pagination) {
var paginationLinks = Array.from(pagination.querySelectorAll('.pagination-link'));
// attach click handler to pagination links if any
paginationLinks.forEach(function(p) {
p.addEventListener('click', clickHandler);
});
}
function clickHandler(event) {
event.preventDefault();
var url = new URL(window.location.origin + window.location.pathname + getClickDestination(this));
url.searchParams.set(#{String $ wIdent "table-only"}, 'yes');
updateTableFrom(url);
}
function getClickDestination(el) {
var link = el.querySelector('a');
if (!link) { return false; }
return link.getAttribute('href');
}
// fetches new sorted table from url with params and replaces contents of current table
function updateTableFrom(url) {
fetch(url, {
credentials: 'same-origin',
headers: {
'Accept': 'text/html'
}
}).then(function(response) {
var contentType = response.headers.get("content-type");
if (!response.ok) {
throw ('Looks like there was a problem fetching ' + url.toString() + '. Status Code: ' + response.status);
}
return response.text();
}).then(function(data) {
// replace contents of table body
wrapper.innerHTML = data;
// set up async functionality again
setupAsync(wrapper);
table.querySelector('tbody').innerHTML = data;
}).catch(function(err) {
console.error(err);
});
}
}
var selector = '#' + #{String $ dbtIdent} + '-table-wrapper';
setupAsync(document.querySelector(selector));
});
})();

View File

@ -0,0 +1,38 @@
.pagination {
margin-top: 20px;
text-align: center;
.pagination-link {
margin: 0 7px;
display: inline-block;
background-color: var(--color-grey);
a {
color: var(--color-lightwhite);
padding: 7px 13px;
display: inline-block;
}
&:not(.current):hover {
background-color: var(--color-lighter);
a {
color: var(--color-lightwhite);
}
}
&.current {
pointer-events: none;
background-color: var(--color-light);
a {
text-decoration: underline;
pointer-events: none;
}
}
&:last-child {
margin-right: 0;
}
}
}

View File

@ -0,0 +1,10 @@
$maybe flag <- sortableKey
$case directions
$of [SortAsc]
<a href=#{tblLink $ setParam (wIdent "sorting") (Just $ flag <> "-desc")}>
^{cellContents}
$of _
<a href=#{tblLink $ setParam (wIdent "sorting") (Just $ flag <> "-asc")}>
^{cellContents}
$nothing
^{cellContents}

View File

@ -1,5 +1,2 @@
<div .container> <div .container>
<h1>Semesterübersicht ^{table}
<div .scrolltable>
^{table}

View File

@ -8,8 +8,7 @@ $newline never
$of NavbarAside (MenuItem label mIcon route _) $of NavbarAside (MenuItem label mIcon route _)
<li .asidenav__list-item :Just route == mcurrentRoute:.asidenav__list-item--active> <li .asidenav__list-item :Just route == mcurrentRoute:.asidenav__list-item--active>
<a .asidenav__link-wrapper href=@{route}> <a .asidenav__link-wrapper href=@{route}>
$if isJust mIcon <div .glyphicon.glyphicon--#{fromMaybe "none" mIcon}>
<div .glyphicon.glyphicon--#{fromMaybe "" mIcon}>
<div .asidenav__link-label>#{label} <div .asidenav__link-label>#{label}
$of _ $of _
@ -17,22 +16,28 @@ $newline never
<h3 .asidenav__box-title> <h3 .asidenav__box-title>
WiSe 17/18 WiSe 17/18
<ul .asidenav__list> <ul .asidenav__list>
$forall (Entity _ Course{..}) <- favourites $forall (Course{..}, courseRoute, pageActions) <- favourites
<li .asidenav__list-item> <li .asidenav__list-item>
<a .asidenav__link-wrapper href=@{CourseR courseTermId courseShorthand CourseShowR}> <a .asidenav__link-wrapper href=@{courseRoute}>
<div .asidenav__link-shorthand>#{courseShorthand} <div .asidenav__link-shorthand>#{courseShorthand}
<div .asidenav__link-label>#{courseName} <div .asidenav__link-label>#{courseName}
<ul .asidenav__nested-list>
$forall action <- pageActions
$case action
$of PageActionPrime (MenuItem{..})
<li .asidenav__nested-list-item>
<a .asidenav__link-wrapper href=@{menuItemRoute}>#{menuItemLabel}
$of _
<div .asidenav__box>
<h3 .asidenav__box-title>
Themes (dev)
<select #theme-selector>
<option value="default">Default Blue
<option value="neutral-blue">Neutral Blue
<option value="aberdeen-reds">Aberdeen Reds
<option value="mint-green">Mint Green
<option value="sky-love">Sky Love
<li .asidenav__list-item>
<a .asidenav__link-wrapper href="/course/S2018/ixd/show">
<div .asidenav__link-shorthand>EXAMPLE
<div .asidenav__link-label>Beispiel-Kurs
<ul .asidenav__nested-list>
<li .asidenav__list-item>
<a .asidenav__link-wrapper href="/course/S2018/ixd/ex">Übungsblätter
<li .asidenav__list-item>
<a .asidenav__link-wrapper href="/course/S2018/ixd/show">Klausuren
<li .asidenav__list-item>
<a .asidenav__link-wrapper href="/course/S2018/ixd/show">Übungsgruppen
<div .asidenav__toggler> <div .asidenav__toggler>

View File

@ -4,7 +4,7 @@
window.utils = window.utils || {}; window.utils = window.utils || {};
// Defines a function to turn an element into an interactive aside-navigation. // Defines a function to turn an element into an interactive aside-navigation.
// If the small is smaller than 999px the navigation is automatically // If the screen is smaller than 999px the navigation is automatically
// collapsed - even when dynamically resized (e.g. switching from portatit // collapsed - even when dynamically resized (e.g. switching from portatit
// to landscape). // to landscape).
// The can user may also manually collapse and expand the navigation by // The can user may also manually collapse and expand the navigation by
@ -16,7 +16,7 @@
// (potentially happening) initial collapse of the asidenav // (potentially happening) initial collapse of the asidenav
// goes unnoticed by the user. // goes unnoticed by the user.
var animClass = 'main__aside--transitioning'; var animClass = 'main__aside--transitioning';
var aboveCollapsedNav = false; var hoveringAboveCollapsedNav = false;
init(); init();
function init() { function init() {
@ -62,17 +62,21 @@
if (!hasCollapsedClass()) { if (!hasCollapsedClass()) {
return false; return false;
} }
aboveCollapsedNav = true; hoveringAboveCollapsedNav = true;
window.setTimeout(function() { window.setTimeout(function() {
if (aboveCollapsedNav && !document.body.classList.contains('touch-supported')) { if (hoveringAboveCollapsedNav && !document.body.classList.contains('touch-supported')) {
asideEl.classList.add('pseudo-hover'); asideEl.classList.add('pseudo-hover');
} }
}, 800); }, 200);
}, false); }, false);
asideEl.addEventListener('mouseleave', function(event) { asideEl.addEventListener('mouseleave', function(event) {
aboveCollapsedNav = false; hoveringAboveCollapsedNav = false;
asideEl.classList.remove('pseudo-hover'); window.setTimeout(function() {
if (!hoveringAboveCollapsedNav) {
asideEl.classList.remove('pseudo-hover');
}
}, 200);
}, false); }, false);
} }
}; };
@ -82,4 +86,36 @@ document.addEventListener('DOMContentLoaded', function() {
utils.aside(document.querySelector('.main__aside')); utils.aside(document.querySelector('.main__aside'));
// remove me before flight:
// EXPERIMENTAL
var selector = document.querySelector('#theme-selector');
var options = Array.from(selector.querySelectorAll('option'))
.reduce(function(acc, optEl) {
if (!acc.includes(optEl.value)) {
acc.push(optEl.value);
}
return acc;
},
[]);
selector.addEventListener('change', function(event) {
setTheme(event.target.value);
});
function setTheme(theme) {
document.body.className = 'theme--' + theme;
}
// random theme on loading and again every 20 seconds
// setInterval(function() {
// setTheme(randomOption());
// }, 20000);
// function randomOption() {
// return options[Math.floor(Math.random() * options.length)];
// }
// // initial theme
// setTheme(randomOption());
}); });

View File

@ -1,6 +1,6 @@
.main__aside { .main__aside {
position: relative; position: relative;
background-color: var(--darkbase); background-color: var(--color-dark);
box-shadow: 0 0 10px rgba(0, 0, 0, 0.3); box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
z-index: 1; z-index: 1;
flex: 0 0 300px; flex: 0 0 300px;
@ -12,48 +12,8 @@
transition: opacity .2s ease; transition: opacity .2s ease;
} }
.main__aside--collapsed.pseudo-hover {
overflow: visible;
}
.main__aside--collapsed {
width: 50px;
flex-basis: 50px;
overflow: hidden;
.asidenav__box-title {
width: 50px;
padding: 0;
}
.asidenav__link-wrapper {
.asidenav__link-shorthand {
display: flex;
position: static;
background-color: var(--darkbase);
color: var(--whitebase);
height: 50px;
width: 50px;
text-align: center;
opacity: 1;
font-size: 16px;
line-height: 1em;
margin-right: 13px;
flex-shrink: 0;
outline: 1px solid white;
text-transform: uppercase;
word-break: break-all;
align-items: center;
justify-content: center;
}
.asidenav__link-label {
padding-left: 0;
}
}
}
.asidenav { .asidenav {
width: 300px; width: 300px;
margin-top: 20px;
color: white; color: white;
.js-show-hide__target { .js-show-hide__target {
@ -78,82 +38,50 @@
.asidenav__box-title { .asidenav__box-title {
padding: 7px 13px; padding: 7px 13px;
margin-top: 13px;
a { background-color: transparent;
color: white; transition: all .2s ease;
}
}
/* hover sub-menus */
.asidenav__nested-list {
position: absolute;
top: 0;
right: 0;
color: var(--fontbase);
transform: translateX(0);
opacity: 0;
transition: all .2s ease-out;
width: 0;
overflow: hidden;
z-index: -1;
.asidenav__list-item {
background-color: var(--darkbase);
color: white;
&:first-child {
margin-top: 0;
}
}
.asidenav__link-wrapper {
padding-left: 13px;
padding-right: 13px;
border-left: 20px solid white;
transition: all .2s ease;
&:hover {
background-color: white;
color: var(--darkbase) !important;
border-left: 20px solid var(--darkbase);
}
}
} }
.asidenav__list-item { .asidenav__list-item {
position: relative; position: relative;
background-color: white; color: var(--color-lightwhite);
color: var(--darkbase);
margin: 4px 0; margin: 4px 0;
&:not(.asidenav__list-item--active):hover { &:hover {
color: white; color: var(--color-link);
background-color: var(--darkbase); background-color: var(--color-lightwhite);
.asidenav__link-shorthand {
transform: scale(1.05, 1.0);
transform-origin: right;
text-shadow: none;
}
> .asidenav__link-wrapper {
color: var(--color-link);
}
.asidenav__nested-list { .asidenav__nested-list {
transform: translateX(100%); transform: translateX(100%);
opacity: 1; opacity: 1;
width: 200px; width: 200px;
} }
.asidenav__link-wrapper,
.asidenav__link-label {
color: white;
}
.asidenav__link-shorthand {
transform: scale(1.05, 1.0);
transform-origin: right;
}
} }
} }
.asidenav__list-item--active { .asidenav__list-item--active {
background-color: var(--darkbase); background-color: var(--color-lightwhite);
color: white;
.asidenav__link-wrapper { .asidenav__link-wrapper {
pointer-events: none; pointer-events: none;
color: white; color: var(--color-link);
}
.asidenav__link-shorthand {
transform: scale(1.05, 1.0);
transform-origin: right;
text-shadow: none;
} }
} }
@ -163,35 +91,81 @@
height: 50px; height: 50px;
align-items: center; align-items: center;
justify-content: flex-start; justify-content: flex-start;
color: var(--darkbase); color: var(--color-lightwhite);
z-index: 1; z-index: 1;
.glyphicon { .glyphicon {
width: 50px; width: 50px;
} }
.asidenav__link-shorthand {
display: block;
position: absolute;
color: var(--greybase);
line-height: 50px;
opacity: 0.3;
right: 10px;
top: 0;
font-size: 40px;
text-transform: uppercase;
transition: transform .2s ease;
}
.asidenav__link-label {
padding-left: 13px;
}
.glyphicon + .asidenav__link-label { .glyphicon + .asidenav__link-label {
padding-left: 0; padding-left: 0;
} }
} }
.asidenav__link-shorthand {
display: block;
position: absolute;
color: var(--color-grey);
line-height: 50px;
opacity: 0.2;
right: 10px;
top: 0;
font-size: 40px;
text-transform: uppercase;
transition: transform .2s ease;
text-shadow: 1px 1px 4px rgba(30, 30, 30, 0.8);
}
.asidenav__link-label {
padding-left: 13px;
}
/* hover sub-menus */
.asidenav__nested-list {
position: absolute;
top: 0;
right: 0;
color: var(--color-font);
transform: translateX(0);
opacity: 0;
transition: all .2s ease-out;
width: 0;
overflow: hidden;
z-index: -1;
}
.asidenav__nested-list-item {
position: relative;
color: var(--color-lightwhite);
background-color: var(--color-dark);
&:hover {
color: var(--color-link);
background-color: var(--color-lightwhite);
.asidenav__link-wrapper {
background-color: white;
color: var(--color-link);
}
.asidenav__nested-list {
transform: translateX(100%);
opacity: 1;
width: 200px;
}
}
.asidenav__link-wrapper {
padding-left: 13px;
padding-right: 13px;
border-left: 20px solid white;
transition: all .2s ease;
color: var(--color-lightwhite);
}
}
.asidenav__toggler { .asidenav__toggler {
position: absolute; position: absolute;
bottom: 20px; bottom: 20px;
@ -201,22 +175,78 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
transition: background-color .2s ease; transition: background-color .2s ease;
border-top: 1px solid var(--whitebase); border-top: 1px solid var(--color-lightwhite);
border-bottom: 1px solid var(--whitebase); border-bottom: 1px solid var(--color-lightwhite);
cursor: pointer; cursor: pointer;
&::before { &::before {
content: '\e079'; content: '\e079';
display: block; display: block;
font-family: 'Glyphicons Halflings'; font-family: 'Glyphicons Halflings';
color: var(--whitebase); color: var(--color-lightwhite);
} }
&:hover { &:hover {
background-color: var(--lightbase); background-color: var(--color-light);
} }
} }
.main__aside--collapsed .asidenav__toggler::before { .main__aside--collapsed {
content: '\e080'; width: 50px;
flex-basis: 50px;
overflow: hidden;
&.pseudo-hover {
overflow: visible;
}
.asidenav__toggler::before {
content: '\e080';
}
.asidenav__box-title {
width: 50px;
padding: 1px;
font-size: 18px;
text-align: center;
margin-bottom: 0;
}
.asidenav__link-shorthand {
display: flex;
position: static;
background-color: var(--color-dark);
color: var(--color-lightwhite);
height: 50px;
width: 50px;
text-align: center;
opacity: 1;
font-size: 15px;
line-height: 1em;
margin-right: 13px;
flex-shrink: 0;
padding: 1px;
outline: 1px solid white;
text-transform: uppercase;
word-break: break-all;
align-items: center;
justify-content: center;
}
.asidenav__list-item:hover {
> .asidenav__link-wrapper {
color: var(--color-dark);
background-color: var(--color-lightwhite);
}
}
.asidenav__link-wrapper {
color: var(--color-lightwhite);
background-color: var(--color-dark);
}
.asidenav__link-label {
padding-left: 0;
}
} }

View File

@ -1,7 +1,7 @@
$newline never
<div .breadcrumbs__container> <div .breadcrumbs__container>
<ul .breadcrumbs__list.list--inline> <ul .breadcrumbs__list.list--inline>
$forall bc <- parents $forall bc <- parents
<li .breadcrumbs__item> <li .breadcrumbs__item>
<a .breadcrumbs__link href="@{fst bc}">#{snd bc} <a .breadcrumbs__link href="@{fst bc}">#{snd bc}
&gt; <li .breadcrumbs__item.breadcrumbs__item--active>#{title}
<li .breadcrumbs__item--active>#{title}

View File

@ -3,12 +3,82 @@
color: white; color: white;
z-index: 10; z-index: 10;
align-self: flex-end; align-self: flex-end;
margin-bottom: 20px; margin-bottom: 10px;
transition: margin-bottom .2s ease; transition: margin-bottom .2s ease;
margin-left: -40px;
margin-right: -40px;
background-color: var(--color-dark);
} }
.breadcrumbs__container--animated { .breadcrumbs__container--animated {
transition: left .2s ease; transition: left .2s ease;
} }
.breadcrumbs__container .breadcrumbs__link { .breadcrumbs__link {
color: white; color: var(--color-lightwhite);
z-index: 2;
}
.breadcrumbs__item {
padding-left: 10px;
padding-right: 4px;
position: relative;
line-height: 28px;
&:nth-child(even) {
background-color: var(--color-light);
}
&:first-child {
padding-left: 20px;
}
&:nth-child(even) {
background-color: var(--color-light);
&::before {
border-left-color: var(--color-primary) !important;
}
&::after {
border-left-color: var(--color-light) !important;
}
}
&:nth-child(odd) {
background-color: var(--color-primary);
&::before {
border-left-color: var(--color-light) !important;
}
&::after {
border-left-color: var(--color-primary) !important;
}
}
&:not(:first-child) {
padding-left: 25px;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
border-top: 14px solid transparent;
border-bottom: 14px solid transparent;
border-left: 14px solid var(--color-light);
}
}
&:last-child {
&::after {
content: '';
position: absolute;
top: 0;
right: -14px;
border-top: 14px solid transparent;
border-bottom: 14px solid transparent;
border-left: 14px solid var(--color-light);
}
}
} }

View File

@ -4,8 +4,8 @@
window.utils = window.utils || {}; window.utils = window.utils || {};
// registers input-listener for each element in <elements> (array) and // registers input-listener for each element in <elements> (array) and
// enables <button> if <fn> for these elements returns true // enables <button> if <validation> for these elements returns true
window.utils.reactiveButton = function(elements, button, fn) { window.utils.reactiveButton = function(elements, button, validation) {
if (elements.length == 0) { if (elements.length == 0) {
return false; return false;
} }
@ -19,7 +19,7 @@
}); });
function updateButtonState() { function updateButtonState() {
if (fn.call(null, elements)) { if (validation.call(null, elements) === true) {
button.removeAttribute('disabled'); button.removeAttribute('disabled');
} else { } else {
button.setAttribute('disabled', 'true'); button.setAttribute('disabled', 'true');
@ -36,7 +36,7 @@ document.addEventListener('DOMContentLoaded', function() {
var requireds = form.querySelectorAll('[required]'); var requireds = form.querySelectorAll('[required]');
var submitBtn = form.querySelector('[type=submit]'); var submitBtn = form.querySelector('[type=submit]');
if (submitBtn && requireds) { if (submitBtn && requireds) {
window.utils.reactiveButton(Array.from(requireds), submitBtn, function(inputs) { window.utils.reactiveButton(Array.from(requireds), submitBtn, function validateForm(inputs) {
var done = true; var done = true;
inputs.forEach(function(inp) { inputs.forEach(function(inp) {
var len = inp.value.trim().length; var len = inp.value.trim().length;

View File

@ -1,18 +1,8 @@
<div .modal.js-modal #modal-#{modalId} data-trigger=#{modalTrigger} data-closeable=true> <div .modal.js-modal #modal-#{modalId} data-trigger=#{modalTrigger} data-closeable=true>
$# primitive way of checking if this is supposed to be add a placeholder for async data.
$# modalContent is 'placeholder' if there should be a placeholder only.
$# 'placeholder' has length 11.
$if 11 == length modalContent $if 11 == length modalContent
<div .replace-me> <div .replace-me>
$else $else
<h2>Neue Veranstaltung
#{modalContent} #{modalContent}
<form>
<div .form-group>
<label .reactive-label for="inp1">Name
<input type="text" id="inp1">
<div .form-group>
<label .reactive-label for="inp2">Kürzel
<input type="text" id="inp2">
<div .form-group>
<label .reactive-label for="inp3">Semester
<input type="text" id="inp3">
<div .form-group>
<input type="submit" value="Submit">

View File

@ -2,10 +2,6 @@ $newline never
<div .navbar-container> <div .navbar-container>
<nav .navbar.js-sticky-navbar> <nav .navbar.js-sticky-navbar>
<!-- breadcrumbs -->
$if not $ Just HomeR == mcurrentRoute
^{breadcrumbs}
<ul .navbar__list.list--inline> <ul .navbar__list.list--inline>
$forall menuType <- menuTypes $forall menuType <- menuTypes
$case menuType $case menuType

View File

@ -7,18 +7,17 @@
width: 100%; width: 100%;
height: var(--header-height); height: var(--header-height);
padding-right: 5vw; padding-right: 5vw;
padding-left: 340px; background: var(--color-darker); /* Old browsers */
background: var(--darkbase); /* Old browsers */ background: -moz-linear-gradient(bottom, var(--color-dark) 0%, var(--color-darker) 100%); /* FF3.6-15 */
background: -moz-linear-gradient(bottom, var(--darkbase) 0%, #425d79 100%); /* FF3.6-15 */ background: -webkit-linear-gradient(bottom, var(--color-dark) 0%,var(--color-darker) 100%); /* Chrome10-25,Safari5.1-6 */
background: -webkit-linear-gradient(bottom, var(--darkbase) 0%,#425d79 100%); /* Chrome10-25,Safari5.1-6 */ background: linear-gradient(to top, var(--color-dark) 0%,var(--color-darker) 100%); /* W3C, IE10+, FF16+, Chrome26+, Opera12+, Safari7+ */
background: linear-gradient(to top, var(--darkbase) 0%,#425d79 100%); /* W3C, IE10+, FF16+, Chrome26+, Opera12+, Safari7+ */
color: white; color: white;
box-shadow: 0 1px 10px rgba(0, 0, 0, 0.1);
z-index: 10; z-index: 10;
top: 0; top: 0;
left: 0; left: 0;
overflow: hidden; overflow: hidden;
transition: height 0.2s ease; transition: height 0.2s ease;
box-shadow: 0 0 4px rgba(0, 0, 0, 0.2);
} }
.navbar__list { .navbar__list {
@ -50,7 +49,7 @@
justify-content: center; justify-content: center;
align-items: center; align-items: center;
height: 80px; height: 80px;
color: var(--whitebase); color: var(--color-lightwhite);
transition: height .2s ease; transition: height .2s ease;
} }
@ -63,7 +62,7 @@
.navbar__list-item--secondary { .navbar__list-item--secondary {
margin-left: 20px; margin-left: 20px;
color: var(--greybase); color: var(--color-grey);
} }
.navbar__list-item--secondary + .navbar__list-item--secondary { .navbar__list-item--secondary + .navbar__list-item--secondary {
margin-left: 0; margin-left: 0;
@ -72,32 +71,31 @@
.navbar__list-item--active { .navbar__list-item--active {
background-color: white; background-color: white;
color: var(--darkbase); color: var(--color-dark);
.navbar__link-wrapper { .navbar__link-wrapper {
color: var(--darkbase); color: var(--color-dark);
} }
} }
.navbar__list-item--active .navbar__link-wrapper { .navbar__list-item--active .navbar__link-wrapper {
pointer-events: none; pointer-events: none;
} }
.navbar__list-item--active .navbar__link-label { .navbar__list-item--active .navbar__link-label {
color: var(--darkbase); color: var(--color-dark);
} }
.navbar .navbar__list-item:not(.navbar__list-item--active):hover { .navbar .navbar__list-item:not(.navbar__list-item--active):hover {
background-color: var(--darkbase); background-color: var(--color-darker);
color: var(--whitebase);
} }
.navbar .navbar__list-item:not(.navbar__list-item--active):hover .navbar__link-wrapper { .navbar .navbar__list-item:not(.navbar__list-item--active):hover .navbar__link-wrapper {
color: var(--whitebase); color: var(--color-lightwhite);
} }
.navbar .navbar__list-item:not(.navbar__list-item--active):hover .navbar__link-label { .navbar .navbar__list-item:not(.navbar__list-item--active):hover .navbar__link-label {
color: var(--whitebase); color: var(--color-lightwhite);
} }
.navbar__list-item--secondary .navbar__link-wrapper, .navbar__list-item--secondary .navbar__link-wrapper,
.navbar__list-item--secondary .navbar__link-label { .navbar__list-item--secondary .navbar__link-label {
color: var(--greybase); color: var(--color-grey);
} }
.navbar--sticky { .navbar--sticky {

View File

@ -1,7 +1,6 @@
$newline never $newline never
$if hasPageActions $if hasPageActions
<div .page-nav-prime> <div .page-nav-prime>
<h3>Aktionen:
<ul .pagenav__list> <ul .pagenav__list>
$forall menuType <- menuTypes $forall menuType <- menuTypes
$case menuType $case menuType

View File

@ -1,21 +1,13 @@
.page-nav-prime { .page-nav-prime {
background-color: var(--lightgreybase); background-color: rgba(200, 200, 200, 0.2);
box-shadow: -20px -20px 0 20px var(--lightgreybase), padding: 13px;
20px -20px 0 20px var(--lightgreybase); margin-top: 30px;
padding: 13px 0;
} }
.page-nav-prime .pagenav__list { .page-nav-prime .pagenav__list {
margin: 7px 0 0;
display: block; display: block;
} }
.page-nav-prime .pagenav__list-item { .page-nav-prime .pagenav__list-item {
display: inline-block; display: inline-block;
border-bottom: 2px solid var(--lightbase);
margin-right: 7px; margin-right: 7px;
transition: border-bottom-color .2s ease;
&:hover {
border-bottom-color: var(--lighterbase);
}
} }