Merge branch 'master' into feat/assign-correctors
This commit is contained in:
commit
6e0558d094
@ -19,6 +19,7 @@ should-log-all: "_env:LOG_ALL:false"
|
|||||||
# mutable-static: false
|
# mutable-static: false
|
||||||
# skip-combining: false
|
# skip-combining: false
|
||||||
auth-dummy-login: "_env:DUMMY_LOGIN:false"
|
auth-dummy-login: "_env:DUMMY_LOGIN:false"
|
||||||
|
allow-deprecated: "_env:ALLOW_DEPRECATED:false"
|
||||||
|
|
||||||
# NB: If you need a numeric value (e.g. 123) to parse as a String, wrap it in single quotes (e.g. "_env:PGPASS:'123'")
|
# NB: If you need a numeric value (e.g. 123) to parse as a String, wrap it in single quotes (e.g. "_env:PGPASS:'123'")
|
||||||
# See https://github.com/yesodweb/yesod/wiki/Configuration#parsing-numeric-values-as-strings
|
# See https://github.com/yesodweb/yesod/wiki/Configuration#parsing-numeric-values-as-strings
|
||||||
|
|||||||
@ -25,6 +25,7 @@ main = db $ do
|
|||||||
, userEmail = "G.Kleen@campus.lmu.de"
|
, userEmail = "G.Kleen@campus.lmu.de"
|
||||||
, userDisplayName = "Gregor Kleen"
|
, userDisplayName = "Gregor Kleen"
|
||||||
, userMaxFavourites = 6
|
, userMaxFavourites = 6
|
||||||
|
, userTheme = AberdeenReds
|
||||||
}
|
}
|
||||||
fhamann <- insert User
|
fhamann <- insert User
|
||||||
{ userPlugin = "LDAP"
|
{ userPlugin = "LDAP"
|
||||||
@ -33,6 +34,7 @@ main = db $ do
|
|||||||
, userEmail = "felix.hamann@campus.lmu.de"
|
, userEmail = "felix.hamann@campus.lmu.de"
|
||||||
, userDisplayName = "Felix Hamann"
|
, userDisplayName = "Felix Hamann"
|
||||||
, userMaxFavourites = defaultFavourites
|
, userMaxFavourites = defaultFavourites
|
||||||
|
, userTheme = Default
|
||||||
}
|
}
|
||||||
jost <- insert User
|
jost <- insert User
|
||||||
{ userPlugin = "LDAP"
|
{ userPlugin = "LDAP"
|
||||||
@ -41,6 +43,7 @@ main = db $ do
|
|||||||
, userEmail = "jost@tcs.ifi.lmu.de"
|
, userEmail = "jost@tcs.ifi.lmu.de"
|
||||||
, userDisplayName = "Steffen Jost"
|
, userDisplayName = "Steffen Jost"
|
||||||
, userMaxFavourites = 14
|
, userMaxFavourites = 14
|
||||||
|
, userTheme = MintGreen
|
||||||
}
|
}
|
||||||
void . insert $ Term
|
void . insert $ Term
|
||||||
{ termName = summer2017
|
{ termName = summer2017
|
||||||
|
|||||||
@ -1,3 +1,12 @@
|
|||||||
|
BtnSubmit: Senden
|
||||||
|
BtnAbort: Abbrechen
|
||||||
|
BtnDelete: Löschen
|
||||||
|
BtnRegister: Anmelden
|
||||||
|
BtnDeregister: Abmelden
|
||||||
|
|
||||||
|
RegisterFrom: Anmeldungen von
|
||||||
|
RegisterTo: Anmeldungen bis
|
||||||
|
|
||||||
SummerTerm year@Integer: Sommersemester #{tshow year}
|
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
|
||||||
@ -5,13 +14,23 @@ 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.
|
||||||
|
Term: Semester
|
||||||
|
TermPlaceholder: W/S + vierstellige Jahreszahl
|
||||||
|
TermEditHeading: Semester editieren/anlegen
|
||||||
|
|
||||||
|
Course: Kurs
|
||||||
|
CourseSecret: Zugangspasswort
|
||||||
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
|
||||||
|
TermCourseListHeading tid@TermIdentifier: Kursübersicht #{termToText tid}
|
||||||
|
TermCourseListTitle tid@TermIdentifier: Kurse #{termToText tid}
|
||||||
|
CourseEditHeading: Kurs editieren/anlegen
|
||||||
|
CourseEditTitle: Kurs editieren/anlegen
|
||||||
|
|
||||||
|
Sheet: Blatt
|
||||||
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
|
||||||
@ -21,9 +40,12 @@ SheetDelTitle tid@TermIdentifier courseShortHand@Text sheetName@Text: Übun
|
|||||||
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.
|
||||||
|
|
||||||
|
Deadline: Abgabe
|
||||||
|
Done: Eingereicht
|
||||||
|
|
||||||
Unauthorized: Sie haben hierfür keine explizite Berechtigung.
|
Unauthorized: Sie haben hierfür keine explizite Berechtigung.
|
||||||
UnauthorizedAnd l@Text r@Text: "#{l}" und "#{r}"
|
UnauthorizedAnd l@Text r@Text: #{l} UND #{r}
|
||||||
UnauthorizedOr l@Text r@Text: "#{l}" oder "#{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.
|
||||||
@ -31,12 +53,15 @@ UnauthorizedCorrector: Sie sind nicht als Korrektor für diese Veranstaltung ein
|
|||||||
UnauthorizedSheetCorrector: Sie sind nicht als Korrektor für dieses Übungsblatt eingetragen.
|
UnauthorizedSheetCorrector: Sie sind nicht als Korrektor für dieses Übungsblatt eingetragen.
|
||||||
UnauthorizedCorrectorAny: Sie sind nicht als Korrektor für eine Veranstaltung 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.
|
||||||
|
UnauthorizedCourseTime: Dieses Kurs erlaubt momentan keine Anmeldungen.
|
||||||
UnauthorizedSheetTime: Dieses Übungsblatt ist momentan nicht freigegeben.
|
UnauthorizedSheetTime: Dieses Übungsblatt ist momentan nicht freigegeben.
|
||||||
UnauthorizedSubmissionOwner: Sie sind an dieser Abgabe nicht beteiligt.
|
UnauthorizedSubmissionOwner: Sie sind an dieser Abgabe nicht beteiligt.
|
||||||
UnauthorizedSubmissionCorrector: Sie sind nicht der Korrektor für diese Abgabe.
|
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.
|
DeprecatedRoute: Diese Ansicht ist obsolet und könnte in Zukunft entfallen.
|
||||||
UnfreeMaterials: Die Materialien für diese Veranstaltung sind nicht allgemein freigegeben.
|
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.
|
SubmissionWrongSheet: Abgabenummer gehört nicht zum angegebenen Übungsblatt.
|
||||||
SubmissionAlreadyExists: Sie haben bereits eine Abgabe zu diesem Übungsblatt.
|
SubmissionAlreadyExists: Sie haben bereits eine Abgabe zu diesem Übungsblatt.
|
||||||
@ -44,8 +69,9 @@ SubmissionTitle tid@TermIdentifier courseShortHand@Text sheetName@Text: #{termTo
|
|||||||
SubmissionMember g@Int: Mitabgebende(r) ##{tshow g}
|
SubmissionMember g@Int: Mitabgebende(r) ##{tshow g}
|
||||||
SubmissionArchive: Zip-Archiv der Abgabedatei(en)
|
SubmissionArchive: Zip-Archiv der Abgabedatei(en)
|
||||||
SubmissionFile: Datei zur Abgabe
|
SubmissionFile: Datei zur Abgabe
|
||||||
SubmissionAlreadyExistsFor user@Text: #{user} hat bereits eine Abgabe zu diesem Übungsblatt.
|
SubmissionAlreadyExistsFor user@Text: #{user} hat bereits eine Abgabe zu diesem bÜbungsblatt.
|
||||||
|
|
||||||
|
EMail: E-Mail
|
||||||
EMailUnknown email@Text: E-Mail #{email} gehört zu keinem bekannten Benutzer.
|
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.
|
NotAParticipant user@Text tid@TermIdentifier csh@Text: #{user} ist nicht im Kurs #{termToText tid}-#{csh} angemeldet.
|
||||||
|
|
||||||
@ -61,3 +87,28 @@ DeleteRow: Zeile entfernen
|
|||||||
ProportionNegative: Anteile dürfen nicht negativ sein
|
ProportionNegative: Anteile dürfen nicht negativ sein
|
||||||
CorrectorsUpdated: Korrektoren erfolgreich aktualisiert
|
CorrectorsUpdated: Korrektoren erfolgreich aktualisiert
|
||||||
CorrectorsPlaceholder: Korrektoren...
|
CorrectorsPlaceholder: Korrektoren...
|
||||||
|
|
||||||
|
HomeHeading: Aktuelle Termine
|
||||||
|
ProfileHeading: Benutzerprofil und Einstellungen
|
||||||
|
ProfileDataHeading: Gespeicherte Benutzerdaten
|
||||||
|
TermsHeading: Semesterübersicht
|
||||||
|
|
||||||
|
NumCourses n@Int64: #{tshow n} Kurse
|
||||||
|
CloseAlert: Schliessen
|
||||||
|
|
||||||
|
Name: Name
|
||||||
|
MatrikelNr: Matrikelnummer
|
||||||
|
Theme: Oberflächen Design
|
||||||
|
Favoriten: Anzahl gespeicherter Favoriten
|
||||||
|
Plugin: Plugin
|
||||||
|
Ident: Identifizierung
|
||||||
|
Settings: Individuelle Benutzereinstellungen
|
||||||
|
SettingsUpdate: Einstellungen wurden gespeichert.
|
||||||
|
|
||||||
|
SheetExercise: Aufgabenstellung
|
||||||
|
SheetHint: Hinweise
|
||||||
|
SheetSolution: Lösung
|
||||||
|
SheetMarking: Korrekturhinweise
|
||||||
|
|
||||||
|
MultiFileUploadInfo: (Mehrere Dateien mit Shift oder Strg auswählen)
|
||||||
|
NatField xyz@Text: #{xyz} muss eine natürliche Zahl sein!
|
||||||
|
|||||||
5
models
5
models
@ -4,7 +4,8 @@ User
|
|||||||
matrikelnummer Text Maybe
|
matrikelnummer Text Maybe
|
||||||
email Text
|
email Text
|
||||||
displayName Text
|
displayName Text
|
||||||
maxFavourites Int default=12
|
maxFavourites Int default=12
|
||||||
|
theme Theme default='default'
|
||||||
UniqueAuthentication plugin ident
|
UniqueAuthentication plugin ident
|
||||||
UniqueEmail email
|
UniqueEmail email
|
||||||
UserAdmin
|
UserAdmin
|
||||||
@ -59,7 +60,7 @@ Course
|
|||||||
term TermId
|
term TermId
|
||||||
school SchoolId
|
school SchoolId
|
||||||
capacity Int Maybe
|
capacity Int Maybe
|
||||||
hasRegistration Bool -- canRegisterNow = hasRegistration && maybe False (<= currentTime) registerFrom && maybe True (>= currentTime) registerTo
|
-- canRegisterNow = maybe False (<= currentTime) registerFrom && maybe True (>= currentTime) registerTo
|
||||||
registerFrom UTCTime Maybe
|
registerFrom UTCTime Maybe
|
||||||
registerTo UTCTime Maybe
|
registerTo UTCTime Maybe
|
||||||
deregisterUntil UTCTime Maybe
|
deregisterUntil UTCTime Maybe
|
||||||
|
|||||||
41
routes
41
routes
@ -18,8 +18,10 @@
|
|||||||
--
|
--
|
||||||
-- !materials -- only if course allows all materials to be free (no meaning outside of courses)
|
-- !materials -- only if course allows all materials to be free (no meaning outside of courses)
|
||||||
-- !time -- access depends on time somehow
|
-- !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
|
-- !deprecated -- like free, but logs and gives a warning; entirely disabled in production
|
||||||
--
|
--
|
||||||
|
|
||||||
/static StaticR Static appStatic !free
|
/static StaticR Static appStatic !free
|
||||||
@ -28,38 +30,49 @@
|
|||||||
/favicon.ico FaviconR GET !free
|
/favicon.ico FaviconR GET !free
|
||||||
/robots.txt RobotsR GET !free
|
/robots.txt RobotsR GET !free
|
||||||
|
|
||||||
/ HomeR GET POST !free
|
/ HomeR GET !free
|
||||||
/profile ProfileR GET !free
|
/users UsersR GET -- no tags, i.e. admins only
|
||||||
/users UsersR GET -- no tags, i.e. admins only
|
/admin/test AdminTestR GET POST
|
||||||
|
/admin/user/#CryptoUUIDUser AdminUserR GET
|
||||||
|
|
||||||
/term TermShowR GET !free
|
/profile ProfileR GET POST !free !free
|
||||||
/term/edit TermEditR GET POST
|
/profile/data ProfileDataR GET !free !free
|
||||||
/term/#TermId/edit TermEditExistR GET
|
|
||||||
!/term/#TermId TermCourseListR GET !free
|
/terms TermShowR GET !free
|
||||||
|
/terms/current TermCurrentR GET !free
|
||||||
|
/terms/edit TermEditR GET POST
|
||||||
|
/terms/#TermId/edit TermEditExistR GET
|
||||||
|
!/terms/#TermId TermCourseListR GET !free
|
||||||
|
|
||||||
-- For Pattern Synonyms see Foundation
|
-- For Pattern Synonyms see Foundation
|
||||||
/course/ CourseListR GET !free
|
/course/ CourseListR GET !free
|
||||||
!/course/new CourseNewR GET POST !lecturer
|
!/course/new CourseNewR GET POST !lecturer
|
||||||
/course/#TermId/#Text CourseR !lecturer:
|
/course/#TermId/#Text CourseR !lecturer:
|
||||||
/show CShowR GET POST !free
|
/show CShowR GET !free
|
||||||
|
/register CRegisterR POST !time
|
||||||
/edit CEditR GET POST
|
/edit CEditR GET POST
|
||||||
/ex SheetListR GET !registered !materials
|
/ex SheetListR GET !registered !materials
|
||||||
!/ex/new SheetNewR GET POST
|
!/ex/new SheetNewR GET POST
|
||||||
/ex/#Text SheetR:
|
/ex/#Text SheetR:
|
||||||
/show SShowR GET !timeANDregistered !timeANDmaterials !corrector
|
/show SShowR GET !timeANDregistered !timeANDmaterials !corrector
|
||||||
/#SheetFileType/#FilePath SFileR GET !timeANDregistered !timeANDmaterials !corrector
|
!/#SheetFileType/*FilePath SFileR GET !timeANDregistered !timeANDmaterials !corrector
|
||||||
/edit SEditR GET POST
|
/edit SEditR GET POST
|
||||||
/delete SDelR GET POST
|
/delete SDelR GET POST
|
||||||
!/submission/#SubmissionMode SubmissionR GET POST !timeANDregistered !owner
|
!/sub/new SubmissionNewR GET POST !timeANDregistered
|
||||||
|
!/sub/own SubmissionOwnR GET !free
|
||||||
|
!/sub/#CryptoFileNameSubmission SubmissionR GET POST !owner !corrector
|
||||||
|
|
||||||
/correctors SCorrR GET POST
|
/correctors SCorrR GET POST
|
||||||
|
|
||||||
|
|
||||||
!/#UUID CryptoUUIDDispatchR GET !free -- just redirect
|
|
||||||
|
|
||||||
-- TODO below
|
-- TODO below
|
||||||
!/#{ZIPArchiveName SubmissionId} SubmissionDownloadArchiveR GET !deprecated
|
!/#{ZIPArchiveName SubmissionId} SubmissionDownloadArchiveR GET !deprecated
|
||||||
!/#CryptoUUIDSubmission/#FilePath SubmissionDownloadSingleR GET !deprecated
|
!/#CryptoFileNameSubmission/*FilePath SubmissionDownloadSingleR GET !deprecated
|
||||||
|
|
||||||
/submission SubmissionListR GET !deprecated
|
/submission SubmissionListR GET !deprecated
|
||||||
/submission/#CryptoUUIDSubmission SubmissionDemoR GET POST !deprecated
|
/submission/#CryptoUUIDSubmission SubmissionDemoR GET POST !deprecated
|
||||||
/submissions.zip SubmissionDownloadMultiArchiveR POST !deprecated
|
/submissions.zip SubmissionDownloadMultiArchiveR POST !deprecated
|
||||||
|
-- TODO above
|
||||||
|
|
||||||
|
!/#UUID CryptoUUIDDispatchR GET !free -- just redirect
|
||||||
|
!/*{CI FilePath} CryptoFileNameDispatchR GET !free
|
||||||
|
|||||||
@ -43,6 +43,7 @@ import Handler.Common
|
|||||||
import Handler.Home
|
import Handler.Home
|
||||||
import Handler.Profile
|
import Handler.Profile
|
||||||
import Handler.Users
|
import Handler.Users
|
||||||
|
import Handler.Admin
|
||||||
import Handler.Term
|
import Handler.Term
|
||||||
import Handler.Course
|
import Handler.Course
|
||||||
import Handler.Sheet
|
import Handler.Sheet
|
||||||
|
|||||||
@ -24,6 +24,8 @@ import Data.CryptoID.Poly.ImplicitNamespace
|
|||||||
import Data.UUID.Cryptographic.ImplicitNamespace
|
import Data.UUID.Cryptographic.ImplicitNamespace
|
||||||
import System.FilePath.Cryptographic.ImplicitNamespace
|
import System.FilePath.Cryptographic.ImplicitNamespace
|
||||||
|
|
||||||
|
import qualified Data.Text as Text
|
||||||
|
|
||||||
import Data.UUID.Types
|
import Data.UUID.Types
|
||||||
import Web.PathPieces
|
import Web.PathPieces
|
||||||
|
|
||||||
@ -35,11 +37,21 @@ instance PathPiece UUID where
|
|||||||
fromPathPiece = fromString . unpack
|
fromPathPiece = fromString . unpack
|
||||||
toPathPiece = pack . toString
|
toPathPiece = pack . toString
|
||||||
|
|
||||||
|
instance (CI.FoldCase s, PathPiece s) => PathPiece (CI s) where
|
||||||
|
fromPathPiece = fmap CI.mk . fromPathPiece
|
||||||
|
toPathPiece = toPathPiece . CI.foldedCase
|
||||||
|
|
||||||
-- Generates CryptoUUID... Datatypes
|
instance {-# OVERLAPS #-} PathMultiPiece FilePath where
|
||||||
|
fromPathMultiPiece = Just . unpack . intercalate "/"
|
||||||
|
toPathMultiPiece = Text.splitOn "/" . pack
|
||||||
|
|
||||||
|
instance (CI.FoldCase s, PathMultiPiece s) => PathMultiPiece (CI s) where
|
||||||
|
fromPathMultiPiece = fmap CI.mk . fromPathMultiPiece
|
||||||
|
toPathMultiPiece = toPathMultiPiece . CI.foldedCase
|
||||||
|
|
||||||
|
|
||||||
|
-- Generates CryptoUUID... and CryptoFileName... Datatypes
|
||||||
decCryptoIDs [ ''SubmissionId
|
decCryptoIDs [ ''SubmissionId
|
||||||
, ''CourseId
|
|
||||||
, ''SheetId
|
|
||||||
, ''FileId
|
, ''FileId
|
||||||
, ''UserId
|
, ''UserId
|
||||||
]
|
]
|
||||||
@ -47,12 +59,12 @@ decCryptoIDs [ ''SubmissionId
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
newtype SubmissionMode = SubmissionMode (Maybe CryptoUUIDSubmission)
|
newtype SubmissionMode = SubmissionMode (Maybe CryptoFileNameSubmission)
|
||||||
deriving (Show, Read, Eq)
|
deriving (Show, Read, Eq)
|
||||||
|
|
||||||
pattern NewSubmission :: SubmissionMode
|
pattern NewSubmission :: SubmissionMode
|
||||||
pattern NewSubmission = SubmissionMode Nothing
|
pattern NewSubmission = SubmissionMode Nothing
|
||||||
pattern ExistingSubmission :: CryptoUUIDSubmission -> SubmissionMode
|
pattern ExistingSubmission :: CryptoFileNameSubmission -> SubmissionMode
|
||||||
pattern ExistingSubmission cID = SubmissionMode (Just cID)
|
pattern ExistingSubmission cID = SubmissionMode (Just cID)
|
||||||
|
|
||||||
instance PathPiece SubmissionMode where
|
instance PathPiece SubmissionMode where
|
||||||
@ -62,6 +74,7 @@ instance PathPiece SubmissionMode where
|
|||||||
toPathPiece (SubmissionMode Nothing) = "new"
|
toPathPiece (SubmissionMode Nothing) = "new"
|
||||||
toPathPiece (SubmissionMode (Just x)) = toPathPiece x
|
toPathPiece (SubmissionMode (Just x)) = toPathPiece x
|
||||||
|
|
||||||
|
|
||||||
newtype ZIPArchiveName objID = ZIPArchiveName (CryptoID (CI FilePath) objID)
|
newtype ZIPArchiveName objID = ZIPArchiveName (CryptoID (CI FilePath) objID)
|
||||||
deriving (Show, Read, Eq)
|
deriving (Show, Read, Eq)
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
{-# LANGUAGE NoImplicitPrelude #-}
|
{-# LANGUAGE NoImplicitPrelude #-}
|
||||||
{-# LANGUAGE OverloadedStrings #-}
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
{-# LANGUAGE OverloadedLists #-}
|
{-# LANGUAGE TemplateHaskell, QuasiQuotes #-}
|
||||||
{-# LANGUAGE TemplateHaskell #-}
|
|
||||||
{-# LANGUAGE MultiParamTypeClasses #-}
|
{-# LANGUAGE MultiParamTypeClasses #-}
|
||||||
{-# LANGUAGE TypeFamilies #-}
|
{-# LANGUAGE TypeFamilies #-}
|
||||||
{-# LANGUAGE ViewPatterns #-}
|
{-# LANGUAGE ViewPatterns #-}
|
||||||
@ -49,6 +48,7 @@ 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.List (foldr1)
|
||||||
|
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 Data.Map (Map, (!?))
|
||||||
@ -68,9 +68,17 @@ import System.FilePath
|
|||||||
|
|
||||||
import Handler.Utils.Templates
|
import Handler.Utils.Templates
|
||||||
import Handler.Utils.StudyFeatures
|
import Handler.Utils.StudyFeatures
|
||||||
|
import Handler.Utils.DateTime
|
||||||
import Control.Lens
|
import Control.Lens
|
||||||
import Utils.Lens
|
import Utils.Lens
|
||||||
|
|
||||||
|
-- -- TODO: Move me to appropriate Place
|
||||||
|
instance DisplayAble TermId where
|
||||||
|
display = termToText . unTermKey
|
||||||
|
|
||||||
|
instance DisplayAble UTCTime where
|
||||||
|
display = pack . formatTimeGerDT2 -- default Time Format to be used: 00.00.00 00:00
|
||||||
|
|
||||||
-- infixl 9 :$:
|
-- infixl 9 :$:
|
||||||
-- pattern a :$: b = a b
|
-- pattern a :$: b = a b
|
||||||
|
|
||||||
@ -110,7 +118,6 @@ type MsgRenderer = MsgRendererS UniWorX -- see Utils
|
|||||||
pattern CSheetR tid csh shn ptn
|
pattern CSheetR tid csh shn ptn
|
||||||
= CourseR tid csh (SheetR shn ptn)
|
= CourseR tid csh (SheetR shn ptn)
|
||||||
|
|
||||||
|
|
||||||
-- Menus and Favourites
|
-- Menus and Favourites
|
||||||
data MenuItem = MenuItem
|
data MenuItem = MenuItem
|
||||||
{ menuItemLabel :: Text
|
{ menuItemLabel :: Text
|
||||||
@ -146,6 +153,13 @@ instance RenderMessage UniWorX TermIdentifier where
|
|||||||
Winter -> renderMessage' $ MsgWinterTerm year
|
Winter -> renderMessage' $ MsgWinterTerm year
|
||||||
where renderMessage' = renderMessage foundation ls
|
where renderMessage' = renderMessage foundation ls
|
||||||
|
|
||||||
|
instance RenderMessage UniWorX SheetFileType where
|
||||||
|
renderMessage foundation ls = \case
|
||||||
|
SheetExercise -> renderMessage' MsgSheetExercise
|
||||||
|
SheetHint -> renderMessage' MsgSheetHint
|
||||||
|
SheetSolution -> renderMessage' MsgSheetSolution
|
||||||
|
SheetMarking -> renderMessage' MsgSheetMarking
|
||||||
|
where renderMessage' = renderMessage foundation ls
|
||||||
|
|
||||||
-- Access Control
|
-- Access Control
|
||||||
data AccessPredicate
|
data AccessPredicate
|
||||||
@ -159,14 +173,12 @@ orAR _ _ Authorized = Authorized
|
|||||||
orAR _ AuthenticationRequired _ = AuthenticationRequired
|
orAR _ AuthenticationRequired _ = AuthenticationRequired
|
||||||
orAR _ _ AuthenticationRequired = AuthenticationRequired
|
orAR _ _ AuthenticationRequired = AuthenticationRequired
|
||||||
orAR mr (Unauthorized x) (Unauthorized y) = Unauthorized . render mr $ MsgUnauthorizedOr x y
|
orAR mr (Unauthorized x) (Unauthorized y) = Unauthorized . render mr $ MsgUnauthorizedOr x y
|
||||||
andAR _ Authorized Authorized = Authorized
|
-- and
|
||||||
andAR _ Authorized other = other
|
|
||||||
andAR _ other Authorized = other
|
|
||||||
andAR _ AuthenticationRequired other = other
|
|
||||||
andAR _ other AuthenticationRequired = other
|
|
||||||
andAR mr (Unauthorized x) (Unauthorized y) = Unauthorized . render mr $ MsgUnauthorizedAnd x y
|
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,andAP :: AccessPredicate -> AccessPredicate -> AccessPredicate
|
||||||
orAP = liftAR orAR (== Authorized)
|
orAP = liftAR orAR (== Authorized)
|
||||||
@ -176,22 +188,23 @@ liftAR :: (MsgRenderer -> AuthResult -> AuthResult -> AuthResult)
|
|||||||
-> (AuthResult -> Bool) -- ^ Predicate to Short-Circuit on first argument
|
-> (AuthResult -> Bool) -- ^ Predicate to Short-Circuit on first argument
|
||||||
-> AccessPredicate -> AccessPredicate -> AccessPredicate
|
-> AccessPredicate -> AccessPredicate -> AccessPredicate
|
||||||
-- Ensure to first evaluate Pure conditions, then Handler before DB
|
-- 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 ops sc (APPure f) (APPure g) = APPure $ \r -> shortCircuitM sc (f r) (g r) . ops =<< ask
|
||||||
liftAR op sc (APHandler f) (APHandler g) = APHandler $ \r -> shortCircuitM sc (f r) (g r) . op =<< getMsgRenderer
|
liftAR ops sc (APHandler f) (APHandler g) = APHandler $ \r -> shortCircuitM sc (f r) (g r) . ops =<< getMsgRenderer
|
||||||
liftAR op sc (APDB f) (APDB g) = APDB $ \r -> shortCircuitM sc (f r) (g r) . op =<< getMsgRenderer
|
liftAR ops sc (APDB f) (APDB g) = APDB $ \r -> shortCircuitM sc (f r) (g r) . ops =<< getMsgRenderer
|
||||||
liftAR op sc (APPure f) apg = liftAR op sc (APHandler $ \r -> runReader (f r) <$> getMsgRenderer) apg
|
liftAR ops sc (APPure f) apg = liftAR ops sc (APHandler $ \r -> runReader (f r) <$> getMsgRenderer) apg
|
||||||
liftAR op sc apf apg@(APPure _) = liftAR op sc apg apf
|
liftAR ops sc apf apg@(APPure _) = liftAR ops sc apg apf
|
||||||
liftAR op sc (APHandler f) apdb = liftAR op sc (APDB $ lift . f) apdb
|
liftAR ops sc (APHandler f) apdb = liftAR ops sc (APDB $ lift . f) apdb
|
||||||
liftAR op sc apdb apg@(APHandler _) = liftAR op sc apg apdb
|
liftAR ops sc apdb apg@(APHandler _) = liftAR ops sc apg apdb
|
||||||
|
|
||||||
|
|
||||||
trueAP,falseAP :: AccessPredicate
|
trueAP,falseAP :: AccessPredicate
|
||||||
trueAP = APPure . const $ return Authorized
|
trueAP = APPure . const $ return Authorized
|
||||||
falseAP = APPure . const $ Unauthorized . ($ MsgUnauthorized) . render <$> ask
|
falseAP = APPure . const $ Unauthorized . ($ MsgUnauthorized) . render <$> ask -- always use adminAP instead
|
||||||
-- TODO: I believe falseAP := adminAP
|
|
||||||
|
|
||||||
adminAP :: AccessPredicate
|
|
||||||
|
adminAP :: AccessPredicate -- access for admins (of appropriate school in case of course-routes)
|
||||||
adminAP = APDB $ \case
|
adminAP = APDB $ \case
|
||||||
|
-- Courses: access only to school admins
|
||||||
CourseR tid csh _ -> exceptT return return $ do
|
CourseR tid csh _ -> exceptT return return $ do
|
||||||
authId <- maybeExceptT AuthenticationRequired $ lift maybeAuthId
|
authId <- maybeExceptT AuthenticationRequired $ lift maybeAuthId
|
||||||
[E.Value c] <- lift . E.select . E.from $ \(course `E.InnerJoin` userAdmin) -> do
|
[E.Value c] <- lift . E.select . E.from $ \(course `E.InnerJoin` userAdmin) -> do
|
||||||
@ -200,23 +213,24 @@ adminAP = APDB $ \case
|
|||||||
E.&&. course E.^. CourseTerm E.==. E.val tid
|
E.&&. course E.^. CourseTerm E.==. E.val tid
|
||||||
E.&&. course E.^. CourseShorthand E.==. E.val csh
|
E.&&. course E.^. CourseShorthand E.==. E.val csh
|
||||||
return (E.countRows :: E.SqlExpr (E.Value Int64))
|
return (E.countRows :: E.SqlExpr (E.Value Int64))
|
||||||
guardMExceptT (unauthorizedI MsgUnauthorizedSchoolAdmin) (c > 0)
|
guardMExceptT (c > 0) (unauthorizedI MsgUnauthorizedSchoolAdmin)
|
||||||
return Authorized
|
return Authorized
|
||||||
|
-- other routes: access to any admin is granted here
|
||||||
_other -> exceptT return return $ do
|
_other -> exceptT return return $ do
|
||||||
authId <- maybeExceptT AuthenticationRequired $ lift maybeAuthId
|
authId <- maybeExceptT AuthenticationRequired $ lift maybeAuthId
|
||||||
adrights <- lift $ selectFirst [UserAdminUser ==. authId] []
|
adrights <- lift $ selectFirst [UserAdminUser ==. authId] []
|
||||||
case adrights of
|
guardMExceptT (isJust adrights) (unauthorizedI $ MsgUnauthorized)
|
||||||
(Just _) -> return Authorized
|
return Authorized
|
||||||
Nothing -> lift $ unauthorizedI $ MsgUnauthorized
|
|
||||||
|
|
||||||
knownTags :: Map (CI Text) AccessPredicate
|
knownTags :: Map (CI Text) AccessPredicate
|
||||||
knownTags = -- should not throw exceptions, i.e. no getBy404 or requireAuthId
|
knownTags = Map.fromList -- should not throw exceptions, i.e. no getBy404 or requireAuthId
|
||||||
[("free", trueAP)
|
[("free", trueAP)
|
||||||
,("deprecated", APHandler $ \r -> do
|
,("deprecated", APHandler $ \r -> do
|
||||||
$logWarnS "AccessControl" ("deprecated route: " <> tshow r)
|
$logWarnS "AccessControl" ("deprecated route: " <> tshow r)
|
||||||
addMessageI "error" MsgDeprecatedRoute
|
addMessageI "error" MsgDeprecatedRoute
|
||||||
return Authorized
|
allow <- appAllowDeprecated . appSettings <$> getYesod
|
||||||
|
return $ bool (Unauthorized "Deprecated Route") Authorized allow
|
||||||
)
|
)
|
||||||
,("lecturer", APDB $ \case
|
,("lecturer", APDB $ \case
|
||||||
CourseR tid csh _ -> exceptT return return $ do
|
CourseR tid csh _ -> exceptT return return $ do
|
||||||
@ -227,7 +241,7 @@ knownTags = -- should not throw exceptions, i.e. no getBy404 or requireAuthId
|
|||||||
E.&&. course E.^. CourseTerm E.==. E.val tid
|
E.&&. course E.^. CourseTerm E.==. E.val tid
|
||||||
E.&&. course E.^. CourseShorthand E.==. E.val csh
|
E.&&. course E.^. CourseShorthand E.==. E.val csh
|
||||||
return (E.countRows :: E.SqlExpr (E.Value Int64))
|
return (E.countRows :: E.SqlExpr (E.Value Int64))
|
||||||
guardMExceptT (unauthorizedI MsgUnauthorizedLecturer) (c > 0)
|
guardMExceptT (c>0) (unauthorizedI MsgUnauthorizedLecturer)
|
||||||
return Authorized
|
return Authorized
|
||||||
_ -> exceptT return return $ do
|
_ -> exceptT return return $ do
|
||||||
authId <- maybeExceptT AuthenticationRequired $ lift maybeAuthId
|
authId <- maybeExceptT AuthenticationRequired $ lift maybeAuthId
|
||||||
@ -245,7 +259,7 @@ knownTags = -- should not throw exceptions, i.e. no getBy404 or requireAuthId
|
|||||||
resMap :: Map CourseId (Set SheetId)
|
resMap :: Map CourseId (Set SheetId)
|
||||||
resMap = Map.fromListWith Set.union [ (cid, Set.singleton sid) | (E.Value cid, E.Value sid) <- resList ]
|
resMap = Map.fromListWith Set.union [ (cid, Set.singleton sid) | (E.Value cid, E.Value sid) <- resList ]
|
||||||
case route of
|
case route of
|
||||||
CSheetR _ _ _ (SubmissionR (ExistingSubmission cID)) -> maybeT (unauthorizedI MsgUnauthorizedSubmissionCorrector) $ do
|
CSheetR _ _ _ (SubmissionR cID) -> maybeT (unauthorizedI MsgUnauthorizedSubmissionCorrector) $ do
|
||||||
sid <- catchIfMaybeT (const True :: CryptoIDError -> Bool) $ decrypt cID
|
sid <- catchIfMaybeT (const True :: CryptoIDError -> Bool) $ decrypt cID
|
||||||
Submission{..} <- MaybeT . lift $ get sid
|
Submission{..} <- MaybeT . lift $ get sid
|
||||||
guard $ maybe False (== authId) submissionRatingBy
|
guard $ maybe False (== authId) submissionRatingBy
|
||||||
@ -260,21 +274,32 @@ knownTags = -- should not throw exceptions, i.e. no getBy404 or requireAuthId
|
|||||||
guard $ cid `Set.member` Map.keysSet resMap
|
guard $ cid `Set.member` Map.keysSet resMap
|
||||||
return Authorized
|
return Authorized
|
||||||
_ -> do
|
_ -> do
|
||||||
guardMExceptT (unauthorizedI MsgUnauthorizedCorrectorAny) . not $ Map.null resMap
|
guardMExceptT (not $ Map.null resMap) (unauthorizedI MsgUnauthorizedCorrectorAny)
|
||||||
return Authorized
|
return Authorized
|
||||||
)
|
)
|
||||||
,("time", APDB $ \case
|
,("time", APDB $ \case
|
||||||
CSheetR tid csh shn subRoute -> maybeT (unauthorizedI MsgUnauthorizedSheetTime) $ do
|
CSheetR tid csh shn subRoute -> maybeT (unauthorizedI MsgUnauthorizedSheetTime) $ do
|
||||||
Entity cid _ <- MaybeT . getBy $ CourseTermShort tid csh
|
Entity cid _ <- MaybeT . getBy $ CourseTermShort tid csh
|
||||||
Entity sid Sheet{..} <- MaybeT . getBy $ CourseSheet cid shn
|
Entity sid Sheet{..} <- MaybeT . getBy $ CourseSheet cid shn
|
||||||
cTime <- liftIO getCurrentTime
|
cTime <- liftIO getCurrentTime
|
||||||
case subRoute of
|
let started = sheetActiveFrom <= cTime || NTop sheetVisibleFrom <= (NTop $ Just cTime)
|
||||||
SFileR SheetHint _ -> guard $ maybe False (<= cTime) sheetHintFrom
|
case subRoute of
|
||||||
SFileR SheetSolution _ -> guard $ maybe False (<= cTime) sheetSolutionFrom
|
SFileR SheetExercise _ -> guard started
|
||||||
SubmissionR NewSubmission -> guard $ sheetActiveFrom <= cTime && cTime <= sheetActiveTo
|
SFileR SheetHint _ -> guard $ maybe False (<= cTime) sheetHintFrom
|
||||||
_ -> guard $ maybe False (<= cTime) sheetVisibleFrom
|
SFileR SheetSolution _ -> guard $ maybe False (<= cTime) sheetSolutionFrom
|
||||||
return Authorized
|
SFileR SheetMarking _ -> mzero -- only for correctors and lecturers
|
||||||
r -> do
|
SubmissionNewR -> guard $ sheetActiveFrom <= cTime && cTime <= sheetActiveTo
|
||||||
|
_ -> guard started
|
||||||
|
return Authorized
|
||||||
|
|
||||||
|
CourseR tid csh CRegisterR -> maybeT (unauthorizedI MsgUnauthorizedCourseTime) $ do
|
||||||
|
Entity cid Course{..} <- MaybeT . getBy $ CourseTermShort tid csh
|
||||||
|
cTime <- (NTop . Just) <$> liftIO getCurrentTime
|
||||||
|
guard $ NTop courseRegisterFrom <= cTime
|
||||||
|
&& NTop courseRegisterTo >= cTime
|
||||||
|
return Authorized
|
||||||
|
|
||||||
|
r -> do
|
||||||
$logErrorS "AccessControl" $ "'!time' used on route that doesn't support it: " <> tshow r
|
$logErrorS "AccessControl" $ "'!time' used on route that doesn't support it: " <> tshow r
|
||||||
unauthorizedI MsgUnauthorized
|
unauthorizedI MsgUnauthorized
|
||||||
)
|
)
|
||||||
@ -287,7 +312,7 @@ knownTags = -- should not throw exceptions, i.e. no getBy404 or requireAuthId
|
|||||||
E.&&. course E.^. CourseTerm E.==. E.val tid
|
E.&&. course E.^. CourseTerm E.==. E.val tid
|
||||||
E.&&. course E.^. CourseShorthand E.==. E.val csh
|
E.&&. course E.^. CourseShorthand E.==. E.val csh
|
||||||
return (E.countRows :: E.SqlExpr (E.Value Int64))
|
return (E.countRows :: E.SqlExpr (E.Value Int64))
|
||||||
guardMExceptT (unauthorizedI MsgUnauthorizedParticipant) (c > 0)
|
guardMExceptT (c > 0) (unauthorizedI MsgUnauthorizedParticipant)
|
||||||
return Authorized
|
return Authorized
|
||||||
r -> do
|
r -> do
|
||||||
$logErrorS "AccessControl" $ "'!registered' used on route that doesn't support it: " <> tshow r
|
$logErrorS "AccessControl" $ "'!registered' used on route that doesn't support it: " <> tshow r
|
||||||
@ -303,16 +328,27 @@ knownTags = -- should not throw exceptions, i.e. no getBy404 or requireAuthId
|
|||||||
unauthorizedI MsgUnauthorized
|
unauthorizedI MsgUnauthorized
|
||||||
)
|
)
|
||||||
,("owner", APDB $ \case
|
,("owner", APDB $ \case
|
||||||
CSheetR _ _ _ (SubmissionR (ExistingSubmission cID)) -> exceptT return return $ do
|
CSheetR _ _ _ (SubmissionR cID) -> exceptT return return $ do
|
||||||
sid <- catchIfMExceptT (const $ unauthorizedI MsgUnauthorizedSubmissionOwner) (const True :: CryptoIDError -> Bool) $ decrypt cID
|
sid <- catchIfMExceptT (const $ unauthorizedI MsgUnauthorizedSubmissionOwner) (const True :: CryptoIDError -> Bool) $ decrypt cID
|
||||||
authId <- maybeExceptT AuthenticationRequired $ lift maybeAuthId
|
authId <- maybeExceptT AuthenticationRequired $ lift maybeAuthId
|
||||||
void . maybeMExceptT (unauthorizedI MsgUnauthorizedSubmissionOwner) . getBy $ UniqueSubmissionUser authId sid
|
void . maybeMExceptT (unauthorizedI MsgUnauthorizedSubmissionOwner) . getBy $ UniqueSubmissionUser authId sid
|
||||||
return Authorized
|
return Authorized
|
||||||
CSheetR _ _ _ (SubmissionR NewSubmission) -> unauthorizedI MsgUnauthorizedSubmissionOwner
|
CSheetR _ _ _ SubmissionNewR -> unauthorizedI MsgUnauthorizedSubmissionOwner
|
||||||
r -> do
|
r -> do
|
||||||
$logErrorS "AccessControl" $ "'!owner' used on route that doesn't support it: " <> tshow r
|
$logErrorS "AccessControl" $ "'!owner' used on route that doesn't support it: " <> tshow r
|
||||||
unauthorizedI MsgUnauthorized
|
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
|
||||||
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -341,9 +377,6 @@ evalAccess r = case route2ap r of
|
|||||||
(APHandler p) -> p r
|
(APHandler p) -> p r
|
||||||
(APDB p) -> runDB $ 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
|
||||||
@ -370,9 +403,9 @@ instance Yesod UniWorX where
|
|||||||
-- To add it, chain it together with the defaultMiddleware: yesodMiddleware = defaultYesodMiddleware . defaultCsrfMiddleware
|
-- To add it, chain it together with the defaultMiddleware: yesodMiddleware = defaultYesodMiddleware . defaultCsrfMiddleware
|
||||||
-- For details, see the CSRF documentation in the Yesod.Core.Handler module of the yesod-core package.
|
-- For details, see the CSRF documentation in the Yesod.Core.Handler module of the yesod-core package.
|
||||||
yesodMiddleware handler = do
|
yesodMiddleware handler = do
|
||||||
res <- defaultYesodMiddleware handler
|
|
||||||
void . runMaybeT $ do
|
void . runMaybeT $ do
|
||||||
route <- MaybeT getCurrentRoute
|
route <- MaybeT getCurrentRoute
|
||||||
|
guardM . lift $ (== Authorized) <$> isAuthorized route False
|
||||||
case route of -- update Course Favourites here
|
case route of -- update Course Favourites here
|
||||||
CourseR tid csh _ -> do
|
CourseR tid csh _ -> do
|
||||||
uid <- MaybeT maybeAuthId
|
uid <- MaybeT maybeAuthId
|
||||||
@ -392,52 +425,67 @@ instance Yesod UniWorX where
|
|||||||
[ Desc CourseFavouriteTime
|
[ Desc CourseFavouriteTime
|
||||||
, OffsetBy $ userMaxFavourites user
|
, OffsetBy $ userMaxFavourites user
|
||||||
]
|
]
|
||||||
lift $ mapM delete oldFavs
|
lift $ mapM_ delete oldFavs
|
||||||
|
|
||||||
_other -> return ()
|
_other -> return ()
|
||||||
return res
|
defaultYesodMiddleware handler -- handler is executed afterwards, so Favourites are updated immediately
|
||||||
|
|
||||||
defaultLayout widget = do
|
defaultLayout widget = do
|
||||||
master <- getYesod
|
master <- getYesod
|
||||||
mmsgs <- getMessages
|
mmsgs <- getMessages
|
||||||
|
messageRender <- getMessageRender -- needed, since there is no i18n interpolation in Julius
|
||||||
|
|
||||||
mcurrentRoute <- getCurrentRoute
|
mcurrentRoute <- getCurrentRoute
|
||||||
|
|
||||||
-- Get the breadcrumbs, as defined in the YesodBreadcrumbs instance.
|
-- Get the breadcrumbs, as defined in the YesodBreadcrumbs instance.
|
||||||
(title, parents) <- breadcrumbs
|
(title, parents) <- breadcrumbs
|
||||||
|
|
||||||
|
-- let isParent :: Route UniWorX -> Bool
|
||||||
|
-- isParent r = r == (fst parents)
|
||||||
|
|
||||||
|
|
||||||
let
|
let
|
||||||
menu = defaultLinks ++ maybe [] pageActions mcurrentRoute
|
menu = defaultLinks ++ maybe [] pageActions mcurrentRoute
|
||||||
|
|
||||||
menuTypes <- filterM (menuItemAccessCallback . menuItem) menu
|
menuTypes <- filterM (menuItemAccessCallback . menuItem) menu
|
||||||
|
|
||||||
-- Lookup Favourites if possible
|
-- Lookup Favourites & Theme if possible -- TODO: cache this info in a cookie?!
|
||||||
favourites' <- do
|
(favourites',show -> currentTheme) <- do
|
||||||
muid <- maybeAuthId
|
muid <- maybeAuthPair
|
||||||
case muid of
|
case muid of
|
||||||
Nothing -> return []
|
Nothing -> return ([],Default)
|
||||||
(Just uid) -> runDB . E.select . E.from $ \(course `E.InnerJoin` courseFavourite) -> do
|
(Just (uid,user)) -> do
|
||||||
E.on (course E.^. CourseId E.==. courseFavourite E.^. CourseFavouriteCourse)
|
favs <- runDB $ E.select . E.from $ \(course `E.InnerJoin` courseFavourite) -> do
|
||||||
E.where_ (courseFavourite E.^. CourseFavouriteUser E.==. E.val uid)
|
E.on (course E.^. CourseId E.==. courseFavourite E.^. CourseFavouriteCourse)
|
||||||
E.orderBy [ E.asc $ course E.^. CourseShorthand ]
|
E.where_ (courseFavourite E.^. CourseFavouriteUser E.==. E.val uid)
|
||||||
return course
|
E.orderBy [ E.asc $ course E.^. CourseShorthand ]
|
||||||
|
return course
|
||||||
|
return (favs, userTheme user)
|
||||||
favourites <- forM favourites' $ \(Entity _ c@Course{..})
|
favourites <- forM favourites' $ \(Entity _ c@Course{..})
|
||||||
-> let
|
-> let
|
||||||
courseRoute = CourseR courseTerm courseShorthand CShowR
|
courseRoute = CourseR courseTerm courseShorthand CShowR
|
||||||
in (c, courseRoute, ) <$> filterM (menuItemAccessCallback . menuItem) (pageActions courseRoute)
|
in (c, courseRoute, ) <$> filterM (menuItemAccessCallback . menuItem) (pageActions courseRoute)
|
||||||
|
|
||||||
|
let highlight :: Route UniWorX -> Bool -- highlight last route in breadcrumbs, favorites taking priority
|
||||||
|
highlight = let crumbs = mcons mcurrentRoute $ fst <$> parents
|
||||||
|
actFav = List.intersect (snd3 <$> favourites) crumbs
|
||||||
|
highRs = if null actFav then crumbs else actFav
|
||||||
|
in \r -> r `elem` highRs
|
||||||
|
|
||||||
-- We break up the default layout into two components:
|
-- We break up the default layout into two components:
|
||||||
-- default-layout is the contents of the body tag, and
|
-- default-layout is the contents of the body tag, and
|
||||||
-- default-layout-wrapper is the entire page. Since the final
|
-- default-layout-wrapper is the entire page. Since the final
|
||||||
-- value passed to hamletToRepHtml cannot be a widget, this allows
|
-- value passed to hamletToRepHtml cannot be a widget, this allows
|
||||||
-- you to use normal widget features in default-layout.
|
-- you to use normal widget features in default-layout.
|
||||||
|
|
||||||
|
|
||||||
let
|
let
|
||||||
navbar :: Widget
|
navbar :: Widget
|
||||||
navbar = $(widgetFile "widgets/navbar")
|
navbar = $(widgetFile "widgets/navbar")
|
||||||
asidenav :: Widget
|
asidenav :: Widget
|
||||||
asidenav = $(widgetFile "widgets/asidenav")
|
asidenav = $(widgetFile "widgets/asidenav")
|
||||||
|
contentHeadline :: Maybe Widget
|
||||||
|
contentHeadline = pageHeading =<< mcurrentRoute
|
||||||
breadcrumbs :: Widget
|
breadcrumbs :: Widget
|
||||||
breadcrumbs = $(widgetFile "widgets/breadcrumbs")
|
breadcrumbs = $(widgetFile "widgets/breadcrumbs")
|
||||||
pageactionprime :: Widget
|
pageactionprime :: Widget
|
||||||
@ -450,19 +498,26 @@ instance Yesod UniWorX where
|
|||||||
hasPageActions = any isPageActionPrime menuTypes
|
hasPageActions = any isPageActionPrime menuTypes
|
||||||
|
|
||||||
pc <- widgetToPageContent $ do
|
pc <- widgetToPageContent $ do
|
||||||
addStylesheetRemote "https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,600,800,900"
|
addStylesheetRemote "https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,600,800,900|Roboto:300,400,600"
|
||||||
addScript $ StaticR js_zepto_js
|
addScript $ StaticR js_zepto_js
|
||||||
addScript $ StaticR js_fetchPolyfill_js
|
addScript $ StaticR js_fetchPolyfill_js
|
||||||
addScript $ StaticR js_urlPolyfill_js
|
addScript $ StaticR js_urlPolyfill_js
|
||||||
addScript $ StaticR js_featureChecker_js
|
addScript $ StaticR js_featureChecker_js
|
||||||
|
addScript $ StaticR js_flatpickr_js
|
||||||
addScript $ StaticR js_tabber_js
|
addScript $ StaticR js_tabber_js
|
||||||
|
addStylesheet $ StaticR css_flatpickr_css
|
||||||
addStylesheet $ StaticR css_tabber_css
|
addStylesheet $ StaticR css_tabber_css
|
||||||
addStylesheet $ StaticR css_fonts_css
|
addStylesheet $ StaticR css_fonts_css
|
||||||
addStylesheet $ StaticR css_icons_css
|
addStylesheet $ StaticR css_icons_css
|
||||||
|
addStylesheet $ StaticR css_fontawesome_css
|
||||||
$(widgetFile "default-layout")
|
$(widgetFile "default-layout")
|
||||||
$(widgetFile "standalone/modal")
|
$(widgetFile "standalone/modal")
|
||||||
$(widgetFile "standalone/showHide")
|
$(widgetFile "standalone/showHide")
|
||||||
$(widgetFile "standalone/inputs")
|
$(widgetFile "standalone/inputs")
|
||||||
|
$(widgetFile "standalone/tooltip")
|
||||||
|
$(widgetFile "standalone/tabber")
|
||||||
|
$(widgetFile "standalone/alerts")
|
||||||
|
$(widgetFile "standalone/datepicker")
|
||||||
withUrlRenderer $(hamletFile "templates/default-layout-wrapper.hamlet")
|
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.
|
||||||
@ -510,97 +565,6 @@ instance Yesod UniWorX where
|
|||||||
makeLogger = return . appLogger
|
makeLogger = return . appLogger
|
||||||
|
|
||||||
|
|
||||||
{- ALL DEPRECATED and will be deleted, once knownTags is completed
|
|
||||||
|
|
||||||
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 (SubmissionDemoR cID) _ = return Authorized -- submissionAccess $ Right cID
|
|
||||||
isAuthorizedDB (SubmissionDownloadSingleR cID _) _ = submissionAccess $ Right cID
|
|
||||||
isAuthorizedDB (SubmissionDownloadArchiveR (ZIPArchiveName cID)) _ = submissionAccess $ Left cID
|
|
||||||
isAuthorizedDB TermEditR _ = adminAccess Nothing
|
|
||||||
isAuthorizedDB (TermEditExistR _) _ = adminAccess Nothing
|
|
||||||
isAuthorizedDB CourseNewR _ = lecturerAccess Nothing
|
|
||||||
isAuthorizedDB (CourseR t c CEditR) _ = 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 (SShowR 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 (SEditR s))) _ = courseLecturerAccess . entityKey =<< getBy404 (CourseTermShort t c)
|
|
||||||
isAuthorizedDB (CourseR t c (SheetR (SDelR s))) _ = courseLecturerAccess . entityKey =<< getBy404 (CourseTermShort t c)
|
|
||||||
isAuthorizedDB (CourseR t c (SheetR (SubmissionR s m))) _ = return Authorized -- TODO -- submissionAccess $ Right cID
|
|
||||||
isAuthorizedDB (CourseR t c (SheetR (SheetCorrectorsR 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 (submissionUserUser . entityVal) <$> selectList [SubmissionUserSubmission ==. 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
|
|
||||||
-- TODO: Correctors are no longer unit, could be ByTutorial and also by ByProportion
|
|
||||||
|
|
||||||
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)
|
||||||
@ -611,7 +575,7 @@ instance YesodBreadcrumbs UniWorX where
|
|||||||
breadcrumb (TermCourseListR term) = return (toPathPiece term, Just TermShowR)
|
breadcrumb (TermCourseListR term) = return (toPathPiece term, Just TermShowR)
|
||||||
breadcrumb (CourseR term course CShowR) = return (course, Just $ TermCourseListR 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 _ _ CEditR) = return ("Editieren", Just CourseListR)
|
breadcrumb (CourseR tid csh CEditR) = return ("Editieren", Just $ CourseR tid csh CShowR)
|
||||||
|
|
||||||
breadcrumb (CourseR tid csh SheetListR) = return ("Übungen",Just $ CourseR tid csh CShowR)
|
breadcrumb (CourseR tid csh SheetListR) = return ("Übungen",Just $ CourseR tid csh CShowR)
|
||||||
breadcrumb (CourseR tid csh SheetNewR ) = return ("Neu", Just $ CourseR tid csh SheetListR)
|
breadcrumb (CourseR tid csh SheetNewR ) = return ("Neu", Just $ CourseR tid csh SheetListR)
|
||||||
@ -624,29 +588,46 @@ instance YesodBreadcrumbs UniWorX where
|
|||||||
breadcrumb SubmissionListR = return ("Abgaben", Just HomeR)
|
breadcrumb SubmissionListR = return ("Abgaben", Just HomeR)
|
||||||
|
|
||||||
|
|
||||||
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 ProfileDataR = return ("Data", Just ProfileR)
|
||||||
breadcrumb _ = return ("home", Nothing)
|
breadcrumb _ = return ("home", Nothing)
|
||||||
|
|
||||||
pageActions :: Route UniWorX -> [MenuTypes]
|
pageActions :: Route UniWorX -> [MenuTypes]
|
||||||
pageActions (CourseR tid csh CShowR) =
|
pageActions (CourseR tid csh CShowR) =
|
||||||
[ PageActionPrime $ MenuItem
|
[ PageActionPrime $ MenuItem
|
||||||
{ menuItemLabel = "Übungsblätter"
|
|
||||||
, menuItemIcon = Nothing
|
|
||||||
, menuItemRoute = CourseR tid csh SheetListR
|
|
||||||
, menuItemAccessCallback' = return True
|
|
||||||
}
|
|
||||||
, PageActionPrime $ MenuItem
|
|
||||||
{ menuItemLabel = "Kurs Editieren"
|
{ menuItemLabel = "Kurs Editieren"
|
||||||
, menuItemIcon = Nothing
|
, menuItemIcon = Nothing
|
||||||
, menuItemRoute = CourseR tid csh CEditR
|
, menuItemRoute = CourseR tid csh CEditR
|
||||||
, menuItemAccessCallback' = return True
|
, menuItemAccessCallback' = return True
|
||||||
}
|
}
|
||||||
|
, PageActionPrime $ MenuItem
|
||||||
|
{ menuItemLabel = "Übungsblätter"
|
||||||
|
, menuItemIcon = Nothing
|
||||||
|
, menuItemRoute = CourseR tid csh SheetListR
|
||||||
|
, menuItemAccessCallback' = do --TODO always show for lecturer
|
||||||
|
let sheetRouteAccess shn = (== Authorized) <$> (isAuthorized (CSheetR tid csh shn SShowR) False)
|
||||||
|
muid <- maybeAuthId
|
||||||
|
(sheets,lecturer) <- runDB $ do
|
||||||
|
cid <- getKeyBy404 $ CourseTermShort tid csh
|
||||||
|
sheets <- map (sheetName.entityVal) <$> selectList [SheetCourse ==. cid] [Desc SheetActiveFrom]
|
||||||
|
lecturer <- case muid of
|
||||||
|
Nothing -> return False
|
||||||
|
(Just uid) -> existsBy $ UniqueLecturer uid cid
|
||||||
|
return (sheets,lecturer)
|
||||||
|
or2M (return lecturer) $ anyM sheets sheetRouteAccess
|
||||||
|
}
|
||||||
|
, PageActionSecondary $ MenuItem
|
||||||
|
{ menuItemLabel = "Neues Übungsblatt anlegen"
|
||||||
|
, menuItemIcon = Nothing
|
||||||
|
, menuItemRoute = CourseR tid csh SheetNewR
|
||||||
|
, menuItemAccessCallback' = return True
|
||||||
|
}
|
||||||
]
|
]
|
||||||
pageActions (CourseR tid csh SheetListR) =
|
pageActions (CourseR tid csh SheetListR) =
|
||||||
[ PageActionPrime $ MenuItem
|
[ PageActionPrime $ MenuItem
|
||||||
{ menuItemLabel = "Neues Übungsblatt"
|
{ menuItemLabel = "Neues Übungsblatt anlegen"
|
||||||
, menuItemIcon = Nothing
|
, menuItemIcon = Nothing
|
||||||
, menuItemRoute = CourseR tid csh SheetNewR
|
, menuItemRoute = CourseR tid csh SheetNewR
|
||||||
, menuItemAccessCallback' = return True
|
, menuItemAccessCallback' = return True
|
||||||
@ -654,10 +635,16 @@ pageActions (CourseR tid csh SheetListR) =
|
|||||||
]
|
]
|
||||||
pageActions (CSheetR tid csh shn SShowR) =
|
pageActions (CSheetR tid csh shn SShowR) =
|
||||||
[ PageActionPrime $ MenuItem
|
[ PageActionPrime $ MenuItem
|
||||||
{ menuItemLabel = "Abgabe"
|
{ menuItemLabel = "Abgabe anlegen"
|
||||||
, menuItemIcon = Nothing
|
, menuItemIcon = Nothing
|
||||||
, menuItemRoute = CSheetR tid csh shn (SubmissionR NewSubmission)
|
, menuItemRoute = CSheetR tid csh shn SubmissionNewR
|
||||||
, menuItemAccessCallback' = return True
|
, menuItemAccessCallback' = return True -- TODO: check that no submission already exists
|
||||||
|
}
|
||||||
|
, PageActionPrime $ MenuItem
|
||||||
|
{ menuItemLabel = "Abgabe ansehen"
|
||||||
|
, menuItemIcon = Nothing
|
||||||
|
, menuItemRoute = CSheetR tid csh shn SubmissionOwnR
|
||||||
|
, menuItemAccessCallback' = return True -- TODO: check that a submission already exists
|
||||||
}
|
}
|
||||||
, PageActionPrime $ MenuItem
|
, PageActionPrime $ MenuItem
|
||||||
{ menuItemLabel = "Korrektoren"
|
{ menuItemLabel = "Korrektoren"
|
||||||
@ -668,7 +655,7 @@ pageActions (CSheetR tid csh shn SShowR) =
|
|||||||
]
|
]
|
||||||
pageActions TermShowR =
|
pageActions TermShowR =
|
||||||
[ PageActionPrime $ MenuItem
|
[ PageActionPrime $ MenuItem
|
||||||
{ menuItemLabel = "Neues Semester"
|
{ menuItemLabel = "Neues Semester anlegen"
|
||||||
, menuItemIcon = Nothing
|
, menuItemIcon = Nothing
|
||||||
, menuItemRoute = TermEditR
|
, menuItemRoute = TermEditR
|
||||||
, menuItemAccessCallback' = return True
|
, menuItemAccessCallback' = return True
|
||||||
@ -676,15 +663,67 @@ pageActions TermShowR =
|
|||||||
]
|
]
|
||||||
pageActions (TermCourseListR _) =
|
pageActions (TermCourseListR _) =
|
||||||
[ PageActionPrime $ MenuItem
|
[ PageActionPrime $ MenuItem
|
||||||
{ menuItemLabel = "Neuer Kurs"
|
{ menuItemLabel = "Neuen Kurs anlegen"
|
||||||
, menuItemIcon = Just "book"
|
, menuItemIcon = Just "book"
|
||||||
, menuItemRoute = CourseNewR
|
, menuItemRoute = CourseNewR
|
||||||
, menuItemAccessCallback' = return True
|
, menuItemAccessCallback' = return True
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
pageActions (ProfileR) =
|
||||||
|
[ PageActionPrime $ MenuItem
|
||||||
|
{ menuItemLabel = "Gespeicherte Daten anzeigen"
|
||||||
|
, menuItemIcon = Just "book"
|
||||||
|
, menuItemRoute = ProfileDataR
|
||||||
|
, menuItemAccessCallback' = return True
|
||||||
|
}
|
||||||
|
]
|
||||||
|
pageActions (HomeR) =
|
||||||
|
[
|
||||||
|
-- NavbarAside $ MenuItem
|
||||||
|
-- { menuItemLabel = "Benutzer"
|
||||||
|
-- , menuItemIcon = Just "users"
|
||||||
|
-- , menuItemRoute = UsersR
|
||||||
|
-- , menuItemAccessCallback' = return True
|
||||||
|
-- }
|
||||||
|
-- ,
|
||||||
|
NavbarAside $ MenuItem
|
||||||
|
{ menuItemLabel = "AdminDemo"
|
||||||
|
, menuItemIcon = Nothing
|
||||||
|
, menuItemRoute = AdminTestR
|
||||||
|
, menuItemAccessCallback' = return True
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
pageActions _ = []
|
pageActions _ = []
|
||||||
|
|
||||||
|
i18nHeading :: (MonadWidget m, RenderMessage site msg, HandlerSite m ~ site) => msg -> m ()
|
||||||
|
i18nHeading msg = liftWidgetT $ toWidget =<< getMessageRender <*> pure msg
|
||||||
|
|
||||||
|
pageHeading :: Route UniWorX -> Maybe Widget
|
||||||
|
pageHeading HomeR
|
||||||
|
= Just $ i18nHeading MsgHomeHeading
|
||||||
|
pageHeading (AdminTestR)
|
||||||
|
= Just $ [whamlet|Internal Code Demonstration Page|]
|
||||||
|
pageHeading ProfileR
|
||||||
|
= Just $ i18nHeading MsgProfileHeading
|
||||||
|
pageHeading ProfileDataR
|
||||||
|
= Just $ i18nHeading MsgProfileDataHeading
|
||||||
|
pageHeading TermShowR
|
||||||
|
= Just $ i18nHeading MsgTermsHeading
|
||||||
|
pageHeading TermEditR
|
||||||
|
= Just $ i18nHeading MsgTermEditHeading
|
||||||
|
pageHeading (TermCourseListR tid)
|
||||||
|
= Just . i18nHeading . MsgTermCourseListHeading $ unTermKey tid
|
||||||
|
pageHeading CourseNewR
|
||||||
|
= Just $ i18nHeading MsgCourseEditHeading
|
||||||
|
pageHeading (CourseR tid csh CShowR)
|
||||||
|
= Just $ do
|
||||||
|
Entity _ Course{..} <- handlerToWidget . runDB . getBy404 $ CourseTermShort tid csh
|
||||||
|
toWidget courseName
|
||||||
|
-- TODO: add headings for more single course- and single term-pages
|
||||||
|
pageHeading _
|
||||||
|
= Nothing
|
||||||
|
|
||||||
defaultLinks :: [MenuTypes]
|
defaultLinks :: [MenuTypes]
|
||||||
defaultLinks = -- Define the menu items of the header.
|
defaultLinks = -- Define the menu items of the header.
|
||||||
[ NavbarRight $ MenuItem
|
[ NavbarRight $ MenuItem
|
||||||
@ -712,26 +751,20 @@ defaultLinks = -- Define the menu items of the header.
|
|||||||
, menuItemAccessCallback' = isJust <$> maybeAuthPair
|
, menuItemAccessCallback' = isJust <$> maybeAuthPair
|
||||||
}
|
}
|
||||||
, NavbarAside $ MenuItem
|
, NavbarAside $ MenuItem
|
||||||
{ menuItemLabel = "Aktuelle Veranstaltungen"
|
{ menuItemLabel = "Kurse"
|
||||||
, menuItemIcon = Just "book"
|
, menuItemIcon = Just "calendar-alt"
|
||||||
, menuItemRoute = CourseListR -- should be CourseListActiveR or similar in the future
|
, menuItemRoute = CourseListR -- should be CourseListActiveR or similar in the future
|
||||||
, menuItemAccessCallback' = return True
|
, menuItemAccessCallback' = return True
|
||||||
}
|
}
|
||||||
, NavbarAside $ MenuItem
|
, NavbarAside $ MenuItem
|
||||||
{ menuItemLabel = "Alte Veranstaltungen"
|
{ menuItemLabel = "Semester"
|
||||||
, menuItemIcon = Just "book"
|
, menuItemIcon = Just "graduation-cap"
|
||||||
, menuItemRoute = CourseListR -- should be CourseListInactiveR or similar in the future
|
, menuItemRoute = TermShowR
|
||||||
, menuItemAccessCallback' = return True
|
|
||||||
}
|
|
||||||
, NavbarAside $ MenuItem
|
|
||||||
{ menuItemLabel = "Veranstaltungen"
|
|
||||||
, menuItemIcon = Just "book"
|
|
||||||
, menuItemRoute = CourseListR
|
|
||||||
, menuItemAccessCallback' = return True
|
, menuItemAccessCallback' = return True
|
||||||
}
|
}
|
||||||
, NavbarAside $ MenuItem
|
, NavbarAside $ MenuItem
|
||||||
{ menuItemLabel = "Benutzer"
|
{ menuItemLabel = "Benutzer"
|
||||||
, menuItemIcon = Just "user"
|
, menuItemIcon = Just "users"
|
||||||
, menuItemRoute = UsersR
|
, menuItemRoute = UsersR
|
||||||
, menuItemAccessCallback' = return True -- Creates a LOOP: (Authorized ==) <$> isAuthorized UsersR False
|
, menuItemAccessCallback' = return True -- Creates a LOOP: (Authorized ==) <$> isAuthorized UsersR False
|
||||||
}
|
}
|
||||||
@ -779,6 +812,7 @@ instance YesodAuth UniWorX where
|
|||||||
|
|
||||||
let
|
let
|
||||||
userMaxFavourites = 12 -- TODO: appDefaultFavourites appSettings
|
userMaxFavourites = 12 -- TODO: appDefaultFavourites appSettings
|
||||||
|
userTheme = Default -- TODO: appDefaultFavourites appSettings
|
||||||
newUser = User{..}
|
newUser = User{..}
|
||||||
userUpdate = [ UserMatrikelnummer =. userMatrikelnummer
|
userUpdate = [ UserMatrikelnummer =. userMatrikelnummer
|
||||||
, UserDisplayName =. userDisplayName
|
, UserDisplayName =. userDisplayName
|
||||||
|
|||||||
71
src/Handler/Admin.hs
Normal file
71
src/Handler/Admin.hs
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
{-# LANGUAGE RecordWildCards #-}
|
||||||
|
{-# LANGUAGE NoImplicitPrelude #-}
|
||||||
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
{-# LANGUAGE ViewPatterns #-}
|
||||||
|
{-# LANGUAGE TemplateHaskell #-}
|
||||||
|
{-# LANGUAGE QuasiQuotes #-}
|
||||||
|
{-# LANGUAGE MultiParamTypeClasses #-}
|
||||||
|
{-# LANGUAGE TypeFamilies #-}
|
||||||
|
{-# LANGUAGE FlexibleContexts #-}
|
||||||
|
|
||||||
|
module Handler.Admin where
|
||||||
|
|
||||||
|
import Import
|
||||||
|
import Handler.Utils
|
||||||
|
|
||||||
|
-- import Data.Time
|
||||||
|
-- import qualified Data.Text as T
|
||||||
|
-- import Data.Function ((&))
|
||||||
|
-- import Yesod.Form.Bootstrap3
|
||||||
|
|
||||||
|
import Web.PathPieces (showToPathPiece, readFromPathPiece)
|
||||||
|
|
||||||
|
-- import Colonnade hiding (fromMaybe)
|
||||||
|
-- import Yesod.Colonnade
|
||||||
|
|
||||||
|
-- import qualified Data.UUID.Cryptographic as UUID
|
||||||
|
|
||||||
|
-- BEGIN - Buttons needed only here
|
||||||
|
data CreateButton = CreateMath | CreateInf -- Dummy for Example
|
||||||
|
deriving (Enum, Eq, Ord, Bounded, Read, Show)
|
||||||
|
|
||||||
|
instance PathPiece CreateButton where -- for displaying the button only, not really for paths
|
||||||
|
toPathPiece = showToPathPiece
|
||||||
|
fromPathPiece = readFromPathPiece
|
||||||
|
|
||||||
|
instance Button CreateButton where
|
||||||
|
label CreateMath = [whamlet|Ma<i>thema</i>tik|]
|
||||||
|
label CreateInf = "Informatik"
|
||||||
|
|
||||||
|
cssClass CreateMath = BCInfo
|
||||||
|
cssClass CreateInf = BCPrimary
|
||||||
|
-- END Button needed here
|
||||||
|
|
||||||
|
|
||||||
|
getAdminTestR :: Handler Html -- Demo Page. Referenzimplementierungen sollte hier gezeigt werden!
|
||||||
|
getAdminTestR = do
|
||||||
|
(btnWdgt, btnEnctype) <- generateFormPost (buttonForm :: Form CreateButton)
|
||||||
|
defaultLayout $ do
|
||||||
|
-- setTitle "UniWorkY Admin Testpage"
|
||||||
|
$(widgetFile "adminTest")
|
||||||
|
|
||||||
|
postAdminTestR :: Handler Html
|
||||||
|
postAdminTestR = do
|
||||||
|
((btnResult,_), _) <- runFormPost $ buttonForm
|
||||||
|
case btnResult of
|
||||||
|
(FormSuccess CreateInf) -> setMessage "Informatik-Knopf gedrückt"
|
||||||
|
(FormSuccess CreateMath) -> addMessage "warning" "Knopf Mathematik erkannt"
|
||||||
|
_other -> return ()
|
||||||
|
getAdminTestR
|
||||||
|
|
||||||
|
|
||||||
|
getAdminUserR :: CryptoUUIDUser -> Handler Html
|
||||||
|
getAdminUserR uuid = do
|
||||||
|
uid <- decrypt uuid
|
||||||
|
User{..} <- runDB $ get404 uid
|
||||||
|
defaultLayout $
|
||||||
|
[whamlet|
|
||||||
|
<h1>TODO
|
||||||
|
<h2>Admin Page for User #{display userDisplayName}
|
||||||
|
|]
|
||||||
|
|
||||||
@ -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 #-}
|
||||||
@ -15,9 +16,9 @@ import Handler.Utils
|
|||||||
-- import Data.Time
|
-- import Data.Time
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
import Data.Function ((&))
|
import Data.Function ((&))
|
||||||
import Yesod.Form.Bootstrap3
|
-- import Yesod.Form.Bootstrap3
|
||||||
|
|
||||||
import Colonnade hiding (fromMaybe)
|
import Colonnade hiding (fromMaybe,bool)
|
||||||
import Yesod.Colonnade
|
import Yesod.Colonnade
|
||||||
|
|
||||||
import qualified Data.UUID.Cryptographic as UUID
|
import qualified Data.UUID.Cryptographic as UUID
|
||||||
@ -26,6 +27,13 @@ import qualified Data.UUID.Cryptographic as UUID
|
|||||||
getCourseListR :: Handler TypedContent
|
getCourseListR :: Handler TypedContent
|
||||||
getCourseListR = redirect TermShowR
|
getCourseListR = redirect TermShowR
|
||||||
|
|
||||||
|
getTermCurrentR :: Handler Html
|
||||||
|
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 :: TermId -> Handler Html
|
||||||
getTermCourseListR tidini = do
|
getTermCourseListR tidini = do
|
||||||
(term,courses) <- runDB $ (,)
|
(term,courses) <- runDB $ (,)
|
||||||
@ -65,13 +73,13 @@ getTermCourseListR tidini = do
|
|||||||
]
|
]
|
||||||
let coursesTable = encodeWidgetTable tableSortable colonnadeTerms courses
|
let coursesTable = encodeWidgetTable tableSortable colonnadeTerms courses
|
||||||
defaultLayout $ do
|
defaultLayout $ do
|
||||||
setTitle "Semesterkurse"
|
setTitleI . MsgTermCourseListTitle $ unTermKey tidini
|
||||||
$(widgetFile "courses")
|
$(widgetFile "courses")
|
||||||
|
|
||||||
getCShowR :: TermId -> Text -> Handler Html
|
getCShowR :: TermId -> Text -> Handler Html
|
||||||
getCShowR tid csh = do
|
getCShowR tid csh = do
|
||||||
mbAid <- maybeAuthId
|
mbAid <- maybeAuthId
|
||||||
(courseEnt,(schoolMB,participants,mbRegistered)) <- runDB $ do
|
(courseEnt,(schoolMB,participants,registered)) <- runDB $ do
|
||||||
courseEnt@(Entity cid course) <- getBy404 $ CourseTermShort tid csh
|
courseEnt@(Entity cid course) <- getBy404 $ CourseTermShort tid csh
|
||||||
dependent <- (,,)
|
dependent <- (,,)
|
||||||
<$> get (courseSchool course) -- join
|
<$> get (courseSchool course) -- join
|
||||||
@ -83,60 +91,63 @@ getCShowR tid csh = do
|
|||||||
return $ isJust regL)
|
return $ isJust regL)
|
||||||
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" $ registerForm registered $ courseRegisterSecret course
|
||||||
|
registrationOpen <- (==Authorized) <$> isAuthorized (CourseR tid csh CRegisterR) True
|
||||||
defaultLayout $ do
|
defaultLayout $ do
|
||||||
setTitle $ [shamlet| #{toPathPiece tid} - #{csh}|]
|
setTitle $ [shamlet| #{toPathPiece tid} - #{csh}|]
|
||||||
$(widgetFile "course")
|
$(widgetFile "course")
|
||||||
|
|
||||||
registerButton :: Bool -> Form ()
|
|
||||||
registerButton registered = renderAForm FormStandard $
|
|
||||||
pure () <* bootstrapSubmit regMsg
|
|
||||||
where
|
|
||||||
msg = if registered then "Abmelden" else "Anmelden"
|
|
||||||
regMsg = msg :: BootstrapSubmit Text
|
|
||||||
|
|
||||||
postCShowR :: TermId -> Text -> Handler Html
|
registerForm :: Bool -> Maybe Text -> Form Bool
|
||||||
postCShowR tid csh = do
|
registerForm registered msecret extra = do
|
||||||
|
(msecretRes', msecretView) <- case msecret of
|
||||||
|
(Just _) | not registered -> bimap Just Just <$> (mreq textField (fslpI MsgCourseSecret "Code") Nothing)
|
||||||
|
_ -> return (Nothing,Nothing)
|
||||||
|
(btnRes, btnView) <- mreq (buttonField $ bool BtnRegister BtnDeregister registered) "buttonField ignores settings anyway" Nothing
|
||||||
|
let widget = $(widgetFile "widgets/registerForm")
|
||||||
|
let msecretRes | Just res <- msecretRes' = Just <$> res
|
||||||
|
| otherwise = FormSuccess Nothing
|
||||||
|
return (btnRes *> ((==msecret) <$> msecretRes), widget) -- checks that correct button was pressed, and ignores result of btnRes
|
||||||
|
|
||||||
|
|
||||||
|
postCRegisterR :: TermId -> Text -> Handler Html
|
||||||
|
postCRegisterR tid csh = do
|
||||||
aid <- requireAuthId
|
aid <- requireAuthId
|
||||||
(cid, registered) <- runDB $ do
|
(cid, course, registered) <- runDB $ do
|
||||||
(Entity cid _) <- getBy404 $ CourseTermShort tid csh
|
(Entity cid course) <- getBy404 $ CourseTermShort tid csh
|
||||||
registered <- isJust <$> (getBy $ UniqueParticipant aid cid)
|
registered <- isJust <$> (getBy $ UniqueParticipant aid cid)
|
||||||
return (cid, registered)
|
return (cid, course, registered)
|
||||||
((regResult,_), _) <- runFormPost $ identifyForm "registerBtn" $ registerButton registered
|
((regResult,_), _) <- runFormPost $ identifyForm "registerBtn" $ registerForm registered $ courseRegisterSecret course
|
||||||
case regResult of
|
case regResult of
|
||||||
(FormSuccess _)
|
(FormSuccess codeOk)
|
||||||
| registered -> do
|
| registered -> do
|
||||||
runDB $ deleteBy $ UniqueParticipant aid cid
|
runDB $ deleteBy $ UniqueParticipant aid cid
|
||||||
addMessage "info" "Sie wurden abgemeldet."
|
addMessage "info" "Sie wurden abgemeldet."
|
||||||
| otherwise -> do
|
| codeOk -> do
|
||||||
actTime <- liftIO $ getCurrentTime
|
actTime <- liftIO $ getCurrentTime
|
||||||
regOk <- runDB $ insertUnique $ CourseParticipant cid aid actTime
|
regOk <- runDB $ insertUnique $ CourseParticipant cid aid actTime
|
||||||
when (isJust regOk) $ addMessage "success" "Erfolgreich angemeldet!"
|
when (isJust regOk) $ addMessage "success" "Erfolgreich angemeldet!"
|
||||||
|
| otherwise -> addMessage "danger" "Falsches Kennwort!"
|
||||||
(_other) -> return () -- TODO check this!
|
(_other) -> return () -- TODO check this!
|
||||||
-- redirect or not?! I guess not, since we want GET now
|
redirect $ CourseR tid csh CShowR
|
||||||
getCShowR tid csh
|
|
||||||
|
|
||||||
getCourseNewR :: Handler Html
|
getCourseNewR :: Handler Html
|
||||||
getCourseNewR = do
|
getCourseNewR = do
|
||||||
-- TODO: Defaults für Semester hier ermitteln und übergeben
|
-- TODO: Defaults für Semester hier ermitteln und übergeben
|
||||||
courseEditHandler Nothing
|
courseEditHandler True Nothing
|
||||||
|
|
||||||
postCourseNewR :: Handler Html
|
postCourseNewR :: Handler Html
|
||||||
postCourseNewR = courseEditHandler Nothing
|
postCourseNewR = courseEditHandler False Nothing
|
||||||
|
|
||||||
getCEditR :: TermId -> Text -> Handler Html
|
getCEditR :: TermId -> Text -> Handler Html
|
||||||
getCEditR tid csh = do
|
getCEditR tid csh = do
|
||||||
course <- runDB $ getBy $ CourseTermShort tid csh
|
course <- runDB $ getBy $ CourseTermShort tid csh
|
||||||
courseEditHandler course
|
courseEditHandler True course
|
||||||
|
|
||||||
postCEditR :: TermId -> Text -> Handler Html
|
postCEditR :: TermId -> Text -> Handler Html
|
||||||
postCEditR = getCEditR
|
postCEditR tid csh = do
|
||||||
|
course <- runDB $ getBy $ CourseTermShort tid csh
|
||||||
getCourseEditIDR :: CryptoUUIDCourse -> Handler Html
|
courseEditHandler False course
|
||||||
getCourseEditIDR cID = do
|
|
||||||
cIDKey <- getsYesod appCryptoIDKey
|
|
||||||
courseID <- UUID.decrypt cIDKey cID
|
|
||||||
courseEditHandler =<< runDB (getEntity courseID)
|
|
||||||
|
|
||||||
|
|
||||||
courseDeleteHandler :: Handler Html -- not called anywhere yet
|
courseDeleteHandler :: Handler Html -- not called anywhere yet
|
||||||
@ -150,8 +161,8 @@ courseDeleteHandler = undefined
|
|||||||
redirect $ TermCourseListR $ cfTerm res
|
redirect $ TermCourseListR $ cfTerm res
|
||||||
-}
|
-}
|
||||||
|
|
||||||
courseEditHandler :: Maybe (Entity Course) -> Handler Html
|
courseEditHandler :: Bool -> Maybe (Entity Course) -> Handler Html
|
||||||
courseEditHandler course = do
|
courseEditHandler isGet course = do
|
||||||
aid <- requireAuthId -- TODO: Verify that Editor is owner of the Course to be Edited!!!
|
aid <- requireAuthId -- TODO: Verify that Editor is owner of the Course to be Edited!!!
|
||||||
((result, formWidget), formEnctype) <- runFormPost $ newCourseForm $ courseToForm <$> course
|
((result, formWidget), formEnctype) <- runFormPost $ newCourseForm $ courseToForm <$> course
|
||||||
case result of
|
case result of
|
||||||
@ -170,11 +181,10 @@ courseEditHandler course = do
|
|||||||
, courseTerm = cfTerm res
|
, courseTerm = cfTerm res
|
||||||
, courseSchool = cfSchool res
|
, courseSchool = cfSchool res
|
||||||
, courseCapacity = cfCapacity res
|
, courseCapacity = cfCapacity res
|
||||||
, courseHasRegistration = cfHasReg res
|
, courseRegisterSecret = cfSecret res
|
||||||
, courseRegisterFrom = cfRegFrom res
|
, courseRegisterFrom = cfRegFrom res
|
||||||
, courseRegisterTo = cfRegTo res
|
, courseRegisterTo = cfRegTo res
|
||||||
, courseDeregisterUntil = Nothing -- TODO
|
, courseDeregisterUntil = Nothing -- TODO
|
||||||
, courseRegisterSecret = Nothing -- TODO
|
|
||||||
, courseMaterialFree = True -- TODO
|
, courseMaterialFree = True -- TODO
|
||||||
}
|
}
|
||||||
case insertOkay of
|
case insertOkay of
|
||||||
@ -226,27 +236,26 @@ courseEditHandler course = do
|
|||||||
, courseTerm = cfTerm res
|
, courseTerm = cfTerm res
|
||||||
, courseSchool = cfSchool res
|
, courseSchool = cfSchool res
|
||||||
, courseCapacity = cfCapacity res
|
, courseCapacity = cfCapacity res
|
||||||
, courseHasRegistration = cfHasReg res
|
, courseRegisterSecret = cfSecret res
|
||||||
, courseRegisterFrom = cfRegFrom res
|
, courseRegisterFrom = cfRegFrom res
|
||||||
, courseRegisterTo = cfRegTo res
|
, courseRegisterTo = cfRegTo res
|
||||||
, courseDeregisterUntil = Nothing -- TODO
|
, courseDeregisterUntil = Nothing -- TODO
|
||||||
, courseRegisterSecret = Nothing -- TODO
|
|
||||||
, courseMaterialFree = True -- TODO
|
, courseMaterialFree = True -- TODO
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
insert_ $ CourseEdit aid now cid
|
insert_ $ CourseEdit aid now cid
|
||||||
-- if (isNothing updOkay)
|
-- if (isNothing updOkay)
|
||||||
-- then do
|
-- then do
|
||||||
addMessageI "info" $ MsgCourseEditOk tident csh
|
addMessageI "success" $ MsgCourseEditOk tident csh
|
||||||
-- redirect $ TermCourseListR tid
|
-- redirect $ TermCourseListR tid
|
||||||
-- else addMessageI "danger" $ MsgCourseEditDupShort tident csh
|
-- else addMessageI "danger" $ MsgCourseEditDupShort tident csh
|
||||||
|
|
||||||
(FormFailure _) -> addMessageI "warning" MsgInvalidInput
|
(FormFailure _) -> addMessageI "warning" MsgInvalidInput
|
||||||
|
(FormMissing) | isGet -> return ()
|
||||||
other -> addMessage "error" $ [shamlet| Error: #{show other}|]
|
other -> addMessage "error" $ [shamlet| Error: #{show other}|]
|
||||||
let formTitle = "Kurs editieren/anlegen" :: Text
|
|
||||||
actionUrl <- fromMaybe CourseNewR <$> getCurrentRoute
|
actionUrl <- fromMaybe CourseNewR <$> getCurrentRoute
|
||||||
defaultLayout $ do
|
defaultLayout $ do
|
||||||
setTitle [shamlet| #{formTitle} |]
|
setTitleI MsgCourseEditTitle
|
||||||
$(widgetFile "formPage")
|
$(widgetFile "formPage")
|
||||||
|
|
||||||
|
|
||||||
@ -259,7 +268,7 @@ data CourseForm = CourseForm
|
|||||||
, cfTerm :: TermId
|
, cfTerm :: TermId
|
||||||
, cfSchool :: SchoolId
|
, cfSchool :: SchoolId
|
||||||
, cfCapacity :: Maybe Int
|
, cfCapacity :: Maybe Int
|
||||||
, cfHasReg :: Bool
|
, cfSecret :: Maybe Text
|
||||||
, cfRegFrom :: Maybe UTCTime
|
, cfRegFrom :: Maybe UTCTime
|
||||||
, cfRegTo :: Maybe UTCTime
|
, cfRegTo :: Maybe UTCTime
|
||||||
}
|
}
|
||||||
@ -278,7 +287,7 @@ courseToForm cEntity = CourseForm
|
|||||||
, cfTerm = courseTerm course
|
, cfTerm = courseTerm course
|
||||||
, cfSchool = courseSchool course
|
, cfSchool = courseSchool course
|
||||||
, cfCapacity = courseCapacity course
|
, cfCapacity = courseCapacity course
|
||||||
, cfHasReg = courseHasRegistration course
|
, cfSecret = courseRegisterSecret course
|
||||||
, cfRegFrom = courseRegisterFrom course
|
, cfRegFrom = courseRegisterFrom course
|
||||||
, cfRegTo = courseRegisterTo course
|
, cfRegTo = courseRegisterTo course
|
||||||
}
|
}
|
||||||
@ -305,9 +314,15 @@ newCourseForm template = identForm FIDcourse $ \html -> do
|
|||||||
<*> areq termActiveField (fsb "Semester") (cfTerm <$> template)
|
<*> areq termActiveField (fsb "Semester") (cfTerm <$> template)
|
||||||
<*> areq schoolField (fsb "Institut") (cfSchool <$> template)
|
<*> areq schoolField (fsb "Institut") (cfSchool <$> template)
|
||||||
<*> aopt (natField "Kapazität") (fsb "Kapazität") (cfCapacity <$> template)
|
<*> aopt (natField "Kapazität") (fsb "Kapazität") (cfCapacity <$> template)
|
||||||
<*> areq checkBoxField (fsb "Anmeldung") (cfHasReg <$> template)
|
<*> aopt textField (fslpI MsgCourseSecret "beliebige Zeichenkette"
|
||||||
<*> aopt utcTimeField (fsb "Anmeldung von:") (cfRegFrom <$> template)
|
& setTooltip "Optional: Anmeldung zum Kurs benötigt ein Passwort")
|
||||||
<*> aopt utcTimeField (fsb "Anmeldung bis:") (cfRegTo <$> template)
|
(cfSecret <$> template)
|
||||||
|
<*> aopt utcTimeField (fslpI MsgRegisterFrom "(ohne Datum keine Anmeldung möglich)"
|
||||||
|
& setTooltip "Ohne Datum ist keine Anmeldung zu diesem Kurs möglich!")
|
||||||
|
(cfRegFrom <$> template)
|
||||||
|
<*> aopt utcTimeField (fslpI MsgRegisterTo "(ohne Datum unbegrenzte Anmeldung möglich)"
|
||||||
|
& setTooltip "Die Anmeldung darf ohne Begrenzung sein")
|
||||||
|
(cfRegTo <$> template)
|
||||||
<* submitButton
|
<* submitButton
|
||||||
return $ case result of
|
return $ case result of
|
||||||
FormSuccess courseResult
|
FormSuccess courseResult
|
||||||
@ -316,10 +331,11 @@ newCourseForm template = identForm FIDcourse $ \html -> do
|
|||||||
(FormFailure errorMsgs,
|
(FormFailure errorMsgs,
|
||||||
[whamlet|
|
[whamlet|
|
||||||
<div class="alert alert-danger">
|
<div class="alert alert-danger">
|
||||||
<h4> Fehler:
|
<div class="alert__content">
|
||||||
<ul>
|
<h4> Fehler:
|
||||||
$forall errmsg <- errorMsgs
|
<ul>
|
||||||
<li> #{errmsg}
|
$forall errmsg <- errorMsgs
|
||||||
|
<li> #{errmsg}
|
||||||
^{widget}
|
^{widget}
|
||||||
|]
|
|]
|
||||||
)
|
)
|
||||||
@ -333,20 +349,12 @@ validateCourse :: CourseForm -> [Text]
|
|||||||
validateCourse (CourseForm{..}) =
|
validateCourse (CourseForm{..}) =
|
||||||
[ msg | (False, msg) <-
|
[ msg | (False, msg) <-
|
||||||
[
|
[
|
||||||
( cfRegFrom <= cfRegTo
|
( NTop cfRegFrom <= NTop cfRegTo
|
||||||
, "Ende des Anmeldezeitraums muss nach dem Anfang liegen"
|
, "Ende des Anmeldezeitraums muss nach dem Anfang liegen"
|
||||||
)
|
)
|
||||||
,
|
|
||||||
-- No starting date is okay: effective immediately
|
-- No starting date is okay: effective immediately
|
||||||
-- ( cfHasReg <= (isNothing cfRegFrom)
|
-- ( cfHasReg <= (isNothing cfRegFrom)
|
||||||
-- , "Beginn der Anmeldung angeben oder Anmeldungen deaktivieren"
|
-- , "Beginn der Anmeldung angeben oder Anmeldungen deaktivieren"
|
||||||
-- )
|
-- )
|
||||||
-- ,
|
-- ,
|
||||||
( cfHasReg == (isJust cfRegTo)
|
|
||||||
, "Ende des Anmeldezeitraums angeben oder Anmeldungen deaktivieren"
|
|
||||||
)
|
|
||||||
,
|
|
||||||
( isJust cfRegFrom <= cfHasReg
|
|
||||||
, "Anmeldungen aktivieren oder Anmeldezeitraum löschen"
|
|
||||||
)
|
|
||||||
] ]
|
] ]
|
||||||
|
|||||||
@ -14,6 +14,7 @@
|
|||||||
|
|
||||||
module Handler.CryptoIDDispatch
|
module Handler.CryptoIDDispatch
|
||||||
( getCryptoUUIDDispatchR
|
( getCryptoUUIDDispatchR
|
||||||
|
, getCryptoFileNameDispatchR
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import Import hiding (Proxy)
|
import Import hiding (Proxy)
|
||||||
@ -26,6 +27,9 @@ import Yesod.Core.Types (HandlerContents(..), ErrorResponse(..))
|
|||||||
|
|
||||||
import qualified Control.Monad.Catch as E (Handler(..))
|
import qualified Control.Monad.Catch as E (Handler(..))
|
||||||
|
|
||||||
|
import Data.CaseInsensitive (CI)
|
||||||
|
import qualified Data.CaseInsensitive as CI
|
||||||
|
|
||||||
|
|
||||||
class CryptoRoute ciphertext plaintext where
|
class CryptoRoute ciphertext plaintext where
|
||||||
cryptoIDRoute :: p plaintext -> ciphertext -> Handler (Route UniWorX)
|
cryptoIDRoute :: p plaintext -> ciphertext -> Handler (Route UniWorX)
|
||||||
@ -33,13 +37,28 @@ class CryptoRoute ciphertext plaintext where
|
|||||||
instance CryptoRoute UUID SubmissionId where
|
instance CryptoRoute UUID SubmissionId where
|
||||||
cryptoIDRoute _ (CryptoID -> cID) = do
|
cryptoIDRoute _ (CryptoID -> cID) = do
|
||||||
(smid :: SubmissionId) <- decrypt cID
|
(smid :: SubmissionId) <- decrypt cID
|
||||||
|
cID' <- encrypt smid
|
||||||
(tid,csh,shn) <- runDB $ do
|
(tid,csh,shn) <- runDB $ do
|
||||||
shid <- submissionSheet <$> get404 smid
|
shid <- submissionSheet <$> get404 smid
|
||||||
Sheet{..} <- get404 shid
|
Sheet{..} <- get404 shid
|
||||||
Course{..} <- get404 sheetCourse
|
Course{..} <- get404 sheetCourse
|
||||||
return (courseTerm, courseShorthand, sheetName)
|
return (courseTerm, courseShorthand, sheetName)
|
||||||
return $ CSheetR tid csh shn $ SubmissionR $ SubmissionMode $ Just cID
|
return $ CSheetR tid csh shn $ SubmissionR cID'
|
||||||
|
|
||||||
|
instance CryptoRoute (CI FilePath) SubmissionId where
|
||||||
|
cryptoIDRoute _ (CryptoID -> cID) = do
|
||||||
|
(smid :: SubmissionId) <- decrypt cID
|
||||||
|
(tid,csh,shn) <- runDB $ do
|
||||||
|
shid <- submissionSheet <$> get404 smid
|
||||||
|
Sheet{..} <- get404 shid
|
||||||
|
Course{..} <- get404 sheetCourse
|
||||||
|
return (courseTerm, courseShorthand, sheetName)
|
||||||
|
return $ CSheetR tid csh shn $ SubmissionR cID
|
||||||
|
|
||||||
|
instance CryptoRoute UUID UserId where
|
||||||
|
cryptoIDRoute _ (CryptoID -> cID) = do
|
||||||
|
(_ :: UserId) <- decrypt cID
|
||||||
|
return $ AdminUserR cID
|
||||||
|
|
||||||
class Dispatch ciphertext (x :: [*]) where
|
class Dispatch ciphertext (x :: [*]) where
|
||||||
dispatchID :: p x -> ciphertext -> Handler (Maybe (Route UniWorX))
|
dispatchID :: p x -> ciphertext -> Handler (Maybe (Route UniWorX))
|
||||||
@ -64,5 +83,12 @@ getCryptoUUIDDispatchR :: UUID -> Handler ()
|
|||||||
getCryptoUUIDDispatchR uuid = dispatchID p uuid >>= maybe notFound (redirectWith found302)
|
getCryptoUUIDDispatchR uuid = dispatchID p uuid >>= maybe notFound (redirectWith found302)
|
||||||
where
|
where
|
||||||
p :: Proxy '[ SubmissionId
|
p :: Proxy '[ SubmissionId
|
||||||
|
, UserId
|
||||||
]
|
]
|
||||||
p = Proxy
|
p = Proxy
|
||||||
|
|
||||||
|
getCryptoFileNameDispatchR :: CI FilePath -> Handler ()
|
||||||
|
getCryptoFileNameDispatchR path = dispatchID p path >>= maybe notFound (redirectWith found302)
|
||||||
|
where
|
||||||
|
p :: Proxy '[ SubmissionId ]
|
||||||
|
p = Proxy
|
||||||
|
|||||||
@ -4,54 +4,171 @@
|
|||||||
{-# LANGUAGE TemplateHaskell #-}
|
{-# LANGUAGE TemplateHaskell #-}
|
||||||
{-# LANGUAGE QuasiQuotes #-}
|
{-# LANGUAGE QuasiQuotes #-}
|
||||||
{-# LANGUAGE MultiParamTypeClasses #-}
|
{-# LANGUAGE MultiParamTypeClasses #-}
|
||||||
|
{-# LANGUAGE IncoherentInstances #-} -- why is this needed? Instance for "display deadline" ought to be clear
|
||||||
{-# LANGUAGE TypeFamilies #-}
|
{-# LANGUAGE TypeFamilies #-}
|
||||||
{-# LANGUAGE FlexibleContexts #-}
|
{-# LANGUAGE FlexibleContexts #-}
|
||||||
|
{-# LANGUAGE PartialTypeSignatures #-}
|
||||||
|
|
||||||
module Handler.Home where
|
module Handler.Home where
|
||||||
|
|
||||||
import Import
|
import Import
|
||||||
import Handler.Utils
|
import Handler.Utils
|
||||||
|
|
||||||
-- import Data.Time
|
import qualified Data.Map as Map
|
||||||
|
|
||||||
|
import Data.Time
|
||||||
-- import qualified Data.Text as T
|
-- import qualified Data.Text as T
|
||||||
-- import Yesod.Form.Bootstrap3
|
-- import Yesod.Form.Bootstrap3
|
||||||
|
|
||||||
import Web.PathPieces (showToPathPiece, readFromPathPiece)
|
-- import Web.PathPieces (showToPathPiece, readFromPathPiece)
|
||||||
|
|
||||||
-- import Colonnade
|
-- import Control.Lens
|
||||||
-- import Yesod.Colonnade
|
import Colonnade hiding (fromMaybe, singleton)
|
||||||
|
import Yesod.Colonnade
|
||||||
|
import qualified Database.Esqueleto as E
|
||||||
|
|
||||||
-- import qualified Data.UUID.Cryptographic as UUID
|
-- import qualified Data.UUID.Cryptographic as UUID
|
||||||
|
|
||||||
-- BEGIN - Buttons needed only here
|
|
||||||
data CreateButton = CreateMath | CreateInf -- Dummy for Example
|
|
||||||
deriving (Enum, Eq, Ord, Bounded, Read, Show)
|
|
||||||
|
|
||||||
instance PathPiece CreateButton where -- for displaying the button only, not really for paths
|
-- Some constants:
|
||||||
toPathPiece = showToPathPiece
|
nrSheetDeadlines :: Int64
|
||||||
fromPathPiece = readFromPathPiece
|
nrSheetDeadlines = 10
|
||||||
|
offSheetDeadlines :: NominalDiffTime
|
||||||
|
offSheetDeadlines = 15
|
||||||
|
--nrExamDeadlines = 10
|
||||||
|
--offExamDeadlines = 15
|
||||||
|
--nrCourseDeadlines = 10
|
||||||
|
--offCourseDeadlines = 15
|
||||||
|
|
||||||
instance Button CreateButton where
|
|
||||||
label CreateMath = [whamlet|Ma<i>thema</i>tik|]
|
|
||||||
label CreateInf = "Informatik"
|
|
||||||
|
|
||||||
cssClass CreateMath = BCInfo
|
|
||||||
cssClass CreateInf = BCPrimary
|
|
||||||
-- END Button needed here
|
|
||||||
|
|
||||||
getHomeR :: Handler Html
|
getHomeR :: Handler Html
|
||||||
getHomeR = do
|
getHomeR = do
|
||||||
(btnWdgt, btnEnctype) <- generateFormPost (buttonForm :: Form CreateButton)
|
muid <- maybeAuthId
|
||||||
|
case muid of
|
||||||
|
Nothing -> homeAnonymous
|
||||||
|
Just uid -> homeUser uid
|
||||||
|
|
||||||
|
|
||||||
|
homeAnonymous :: Handler Html
|
||||||
|
homeAnonymous = do
|
||||||
|
cTime <- liftIO getCurrentTime
|
||||||
|
let fTime = addUTCTime (offSheetDeadlines * nominalDay) cTime
|
||||||
|
let tableData :: E.SqlExpr (Entity Course)
|
||||||
|
-> E.SqlQuery (E.SqlExpr (Entity Course))
|
||||||
|
tableData course = do
|
||||||
|
E.where_ $ (E.not_ $ E.isNothing $ course E.^. CourseRegisterFrom)
|
||||||
|
E.&&. (course E.^. CourseRegisterFrom E.<=. E.val (Just cTime))
|
||||||
|
E.&&. ((E.isNothing $ course E.^. CourseRegisterTo)
|
||||||
|
E.||. (course E.^. CourseRegisterTo E.>=. E.val (Just cTime)))
|
||||||
|
E.limit nrSheetDeadlines
|
||||||
|
E.orderBy [ E.asc $ course E.^. CourseRegisterTo
|
||||||
|
, E.desc $ course E.^. CourseShorthand
|
||||||
|
]
|
||||||
|
E.limit nrSheetDeadlines
|
||||||
|
return course
|
||||||
|
|
||||||
|
colonnade :: Colonnade Sortable (DBRow (Entity Course)) (Cell UniWorX)
|
||||||
|
colonnade = mconcat
|
||||||
|
[ -- dbRow
|
||||||
|
sortable (Just "course") (i18nCell MsgCourse) $ \DBRow{ dbrOutput=(Entity {entityVal = course}) } -> do
|
||||||
|
let tid = courseTerm course
|
||||||
|
csh = courseShorthand course
|
||||||
|
cell [whamlet|<a href=@{CourseR tid csh CShowR}>#{display csh}|]
|
||||||
|
, sortable (Just "deadline") (i18nCell MsgRegisterTo) $ \DBRow{ dbrOutput=(Entity {entityVal = course}) } ->
|
||||||
|
textCell $ display $ courseRegisterTo course
|
||||||
|
]
|
||||||
|
courseTable <- dbTable def $ DBTable
|
||||||
|
{ dbtSQLQuery = tableData
|
||||||
|
, dbtColonnade = colonnade
|
||||||
|
, dbtSorting = Map.fromList
|
||||||
|
[ ( "term"
|
||||||
|
, SortColumn $ \(course) -> course E.^. CourseTerm
|
||||||
|
)
|
||||||
|
, ( "course"
|
||||||
|
, SortColumn $ \(course) -> course E.^. CourseShorthand
|
||||||
|
)
|
||||||
|
-- TODO
|
||||||
|
]
|
||||||
|
, dbtFilter = mempty {- [ ( "term"
|
||||||
|
, FilterColumn $ \(course `E.InnerJoin` _ `E.InnerJoin` _ ) tids -> if
|
||||||
|
| Set.null tids -> E.val True :: E.SqlExpr (E.Value Bool)
|
||||||
|
| otherwise -> course E.^. CourseTerm `E.in_` E.valList (Set.toList tids)
|
||||||
|
)
|
||||||
|
] -}
|
||||||
|
, dbtAttrs = tableDefault
|
||||||
|
, dbtIdent = "upcomingdeadlines" :: Text
|
||||||
|
}
|
||||||
|
|
||||||
defaultLayout $ do
|
defaultLayout $ do
|
||||||
setTitle "Willkommen zum Uniworky Test!"
|
$(widgetFile "dsgvDisclaimer")
|
||||||
$(widgetFile "home")
|
$(widgetFile "home")
|
||||||
|
|
||||||
|
homeUser :: Key User -> Handler Html
|
||||||
|
homeUser uid = do
|
||||||
|
cTime <- liftIO getCurrentTime
|
||||||
|
let fTime = addUTCTime (offSheetDeadlines * nominalDay) cTime
|
||||||
|
|
||||||
postHomeR :: Handler Html
|
tableData :: E.InnerJoin (E.InnerJoin (E.SqlExpr (Entity CourseParticipant))
|
||||||
postHomeR = do
|
(E.SqlExpr (Entity Course )))
|
||||||
((btnResult,_), _) <- runFormPost $ buttonForm
|
(E.SqlExpr (Entity Sheet ))
|
||||||
case btnResult of
|
-> E.SqlQuery ( E.SqlExpr (E.Value (Key Term))
|
||||||
(FormSuccess CreateInf) -> setMessage "Informatik-Knopf gedrückt"
|
, E.SqlExpr (E.Value Text)
|
||||||
(FormSuccess CreateMath) -> addMessage "warning" "Knopf Mathematik erkannt"
|
, E.SqlExpr (E.Value Text)
|
||||||
_other -> return ()
|
, E.SqlExpr (E.Value UTCTime))
|
||||||
getHomeR
|
tableData (participant `E.InnerJoin` course `E.InnerJoin` sheet) = do
|
||||||
|
E.on $ course E.^. CourseId E.==. sheet E.^. SheetCourse
|
||||||
|
E.on $ course E.^. CourseId E.==. participant E.^. CourseParticipantCourse
|
||||||
|
E.where_ $ participant E.^. CourseParticipantUser E.==. E.val uid
|
||||||
|
E.&&. sheet E.^. SheetActiveTo E.<=. E.val fTime
|
||||||
|
E.&&. sheet E.^. SheetActiveTo E.>=. E.val cTime
|
||||||
|
E.orderBy [ E.asc $ sheet E.^. SheetActiveTo
|
||||||
|
, E.desc $ sheet E.^. SheetName
|
||||||
|
, E.desc $ course E.^. CourseShorthand
|
||||||
|
]
|
||||||
|
E.limit nrSheetDeadlines
|
||||||
|
return
|
||||||
|
( course E.^. CourseTerm
|
||||||
|
, course E.^. CourseShorthand
|
||||||
|
, sheet E.^. SheetName
|
||||||
|
, sheet E.^. SheetActiveTo
|
||||||
|
)
|
||||||
|
|
||||||
|
colonnade :: Colonnade Sortable (DBRow (E.Value (Key Term), E.Value Text, E.Value Text, E.Value UTCTime)) (Cell UniWorX)
|
||||||
|
colonnade = mconcat
|
||||||
|
[ -- dbRow
|
||||||
|
sortable (Just "course") (i18nCell MsgCourse) $ \DBRow{ dbrOutput=(E.Value tid, E.Value csh, _, _) } ->
|
||||||
|
cell [whamlet|<a href=@{CourseR tid csh CShowR}>#{display csh}|]
|
||||||
|
, sortable (Just "sheet") (i18nCell MsgSheet) $ \DBRow{ dbrOutput=(E.Value tid, E.Value csh, E.Value shn, _) } ->
|
||||||
|
cell [whamlet|<a href=@{CSheetR tid csh shn SShowR}>#{display shn}|]
|
||||||
|
, sortable (Just "deadline") (i18nCell MsgDeadline) $ \DBRow{ dbrOutput=(_, _, _, E.Value deadline) } ->
|
||||||
|
textCell $ display deadline
|
||||||
|
, sortable (Just "done") (i18nCell MsgDone) $ \DBRow{ dbrOutput=(_, _, _, _) } ->
|
||||||
|
textCell $ "?"
|
||||||
|
]
|
||||||
|
sheetTable <- dbTable def $ DBTable
|
||||||
|
{ dbtSQLQuery = tableData
|
||||||
|
, dbtColonnade = colonnade
|
||||||
|
, dbtSorting = Map.fromList
|
||||||
|
[ ( "term"
|
||||||
|
, SortColumn $ \(_ `E.InnerJoin` course `E.InnerJoin` _ ) -> course E.^. CourseTerm
|
||||||
|
)
|
||||||
|
, ( "course"
|
||||||
|
, SortColumn $ \(_ `E.InnerJoin` course `E.InnerJoin` _ ) -> course E.^. CourseShorthand
|
||||||
|
)
|
||||||
|
-- TODO
|
||||||
|
]
|
||||||
|
, dbtFilter = mempty {- [ ( "term"
|
||||||
|
, FilterColumn $ \(course `E.InnerJoin` _ `E.InnerJoin` _ ) tids -> if
|
||||||
|
| Set.null tids -> E.val True :: E.SqlExpr (E.Value Bool)
|
||||||
|
| otherwise -> course E.^. CourseTerm `E.in_` E.valList (Set.toList tids)
|
||||||
|
)
|
||||||
|
] -}
|
||||||
|
, dbtAttrs = tableDefault
|
||||||
|
, dbtIdent = "upcomingdeadlines" :: Text
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultLayout $ do
|
||||||
|
-- setTitle "Willkommen zum Uniworky Test!"
|
||||||
|
$(widgetFile "homeUser")
|
||||||
|
$(widgetFile "dsgvDisclaimer")
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
{-# LANGUAGE NoImplicitPrelude #-}
|
{-# LANGUAGE NoImplicitPrelude #-}
|
||||||
{-# LANGUAGE OverloadedStrings #-}
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
{-# LANGUAGE RecordWildCards #-}
|
||||||
|
{-# LANGUAGE QuasiQuotes #-}
|
||||||
{-# LANGUAGE TemplateHaskell #-}
|
{-# LANGUAGE TemplateHaskell #-}
|
||||||
{-# LANGUAGE MultiParamTypeClasses #-}
|
{-# LANGUAGE MultiParamTypeClasses #-}
|
||||||
{-# LANGUAGE TypeFamilies #-}
|
{-# LANGUAGE TypeFamilies #-}
|
||||||
@ -7,24 +9,121 @@ module Handler.Profile where
|
|||||||
|
|
||||||
import Import
|
import Import
|
||||||
|
|
||||||
|
import Handler.Utils
|
||||||
|
|
||||||
|
-- import Colonnade hiding (fromMaybe, singleton)
|
||||||
|
-- import Yesod.Colonnade
|
||||||
|
|
||||||
import qualified Database.Esqueleto as E
|
import qualified Database.Esqueleto as E
|
||||||
import Database.Esqueleto ((^.))
|
import Database.Esqueleto ((^.))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
data SettingsForm = SettingsForm
|
||||||
|
{ stgMaxFavourties :: Int
|
||||||
|
, stgTheme :: Theme
|
||||||
|
}
|
||||||
|
|
||||||
|
makeSettingForm :: Maybe SettingsForm -> Form SettingsForm
|
||||||
|
makeSettingForm template = identForm FIDsettings $ \html -> do
|
||||||
|
let themeList = [(display t,t) | t <- allThemes]
|
||||||
|
(result, widget) <- flip (renderAForm FormStandard) html $ SettingsForm
|
||||||
|
<$> areq (natFieldI $ MsgNatField "Favoriten") -- TODO: natFieldI not working here
|
||||||
|
(fslpI MsgFavoriten "Anzahl Favoriten") (stgMaxFavourties <$> template)
|
||||||
|
<*> areq (selectFieldList themeList)
|
||||||
|
(fslI MsgTheme ) (stgTheme <$> template)
|
||||||
|
<* submitButton
|
||||||
|
return (result, widget) -- no validation required here
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
getProfileR :: Handler Html
|
getProfileR :: Handler Html
|
||||||
getProfileR = do
|
getProfileR = do
|
||||||
(uid, user) <- requireAuthPair
|
(uid, User{..}) <- requireAuthPair
|
||||||
(admin_rights,lecturer_rights) <- runDB $ (,) <$>
|
let settingsTemplate = Just $ SettingsForm
|
||||||
|
{ stgMaxFavourties = userMaxFavourites
|
||||||
|
, stgTheme = userTheme
|
||||||
|
}
|
||||||
|
((res,formWidget), formEnctype) <- runFormPost $ makeSettingForm settingsTemplate
|
||||||
|
case res of
|
||||||
|
(FormSuccess SettingsForm{..}) -> do
|
||||||
|
runDB $ do
|
||||||
|
update uid [ UserMaxFavourites =. stgMaxFavourties
|
||||||
|
, UserTheme =. stgTheme
|
||||||
|
]
|
||||||
|
when (stgMaxFavourties < userMaxFavourites) $ do
|
||||||
|
-- prune Favourites to user-defined size
|
||||||
|
oldFavs <- selectKeysList [ CourseFavouriteUser ==. uid]
|
||||||
|
[ Desc CourseFavouriteTime
|
||||||
|
, OffsetBy $ stgMaxFavourties
|
||||||
|
]
|
||||||
|
mapM_ delete oldFavs
|
||||||
|
addMessageI "info" $ MsgSettingsUpdate
|
||||||
|
redirect ProfileR -- TODO: them change does not happen without redirect
|
||||||
|
|
||||||
|
(FormFailure msgs) -> forM_ msgs $ (addMessage "warning") . toHtml
|
||||||
|
_ -> return ()
|
||||||
|
|
||||||
|
|
||||||
|
(admin_rights,lecturer_rights,lecture_owner,lecture_corrector,participant,studies) <- runDB $ (,,,,,) <$>
|
||||||
(E.select $ E.from $ \(adright `E.InnerJoin` school) -> do
|
(E.select $ E.from $ \(adright `E.InnerJoin` school) -> do
|
||||||
E.where_ $ adright ^. UserAdminUser E.==. E.val uid
|
E.where_ $ adright ^. UserAdminUser E.==. E.val uid
|
||||||
E.on $ adright ^. UserAdminSchool E.==. school ^. SchoolId
|
E.on $ adright ^. UserAdminSchool E.==. school ^. SchoolId
|
||||||
return (school ^. SchoolName)
|
return (school ^. SchoolShorthand)
|
||||||
)
|
)
|
||||||
<*>
|
<*>
|
||||||
(E.select $ E.from $ \(lecright `E.InnerJoin` school) -> do
|
(E.select $ E.from $ \(lecright `E.InnerJoin` school) -> do
|
||||||
E.where_ $ lecright ^. UserLecturerUser E.==. E.val uid
|
E.where_ $ lecright ^. UserLecturerUser E.==. E.val uid
|
||||||
E.on $ lecright ^. UserLecturerSchool E.==. school ^. SchoolId
|
E.on $ lecright ^. UserLecturerSchool E.==. school ^. SchoolId
|
||||||
return (school ^. SchoolName)
|
return (school ^. SchoolShorthand)
|
||||||
)
|
)
|
||||||
defaultLayout $ do
|
<*>
|
||||||
setTitle . toHtml $ userIdent user <> "'s User page"
|
(E.select $ E.from $ \(lecturer `E.InnerJoin` course) -> do
|
||||||
$(widgetFile "profile")
|
E.where_ $ lecturer ^. LecturerUser E.==. E.val uid
|
||||||
|
E.on $ lecturer ^. LecturerCourse E.==. course ^. CourseId
|
||||||
|
return (course ^. CourseShorthand, course ^. CourseTerm)
|
||||||
|
)
|
||||||
|
<*>
|
||||||
|
(E.select $ E.from $ \(corrector `E.InnerJoin` course) -> do
|
||||||
|
E.where_ $ corrector ^. CorrectorUser E.==. E.val uid
|
||||||
|
E.on $ corrector ^. CorrectorCourse E.==. course ^. CourseId
|
||||||
|
return (course ^. CourseShorthand, course ^. CourseTerm)
|
||||||
|
)
|
||||||
|
<*>
|
||||||
|
(E.select $ E.from $ \(participant `E.InnerJoin` course) -> do
|
||||||
|
E.where_ $ participant ^. CourseParticipantUser E.==. E.val uid
|
||||||
|
E.on $ participant ^. CourseParticipantCourse E.==. course ^. CourseId
|
||||||
|
return (course ^. CourseShorthand, course ^. CourseTerm, participant ^. CourseParticipantRegistration)
|
||||||
|
)
|
||||||
|
<*>
|
||||||
|
(E.select $ E.from $ \(studydegree `E.InnerJoin` studyfeat `E.InnerJoin` studyterms) -> do
|
||||||
|
E.where_ $ studyfeat ^. StudyFeaturesUser E.==. E.val uid
|
||||||
|
E.on $ studyfeat ^. StudyFeaturesField E.==. studyterms ^. StudyTermsId
|
||||||
|
E.on $ studyfeat ^. StudyFeaturesDegree E.==. studydegree ^. StudyDegreeId
|
||||||
|
return (studydegree ^. StudyDegreeName
|
||||||
|
,studyterms ^. StudyTermsName
|
||||||
|
,studyfeat ^. StudyFeaturesType
|
||||||
|
,studyfeat ^. StudyFeaturesSemester)
|
||||||
|
)
|
||||||
|
let formText = Just MsgSettings
|
||||||
|
actionUrl = ProfileR
|
||||||
|
settingsForm = $(widgetFile "formPageI18n")
|
||||||
|
defaultLayout $ do
|
||||||
|
setTitle . toHtml $ userIdent <> "'s User page"
|
||||||
|
$(widgetFile "profile")
|
||||||
|
$(widgetFile "dsgvDisclaimer")
|
||||||
|
|
||||||
|
postProfileR :: Handler Html
|
||||||
|
postProfileR = do
|
||||||
|
-- TODO
|
||||||
|
getProfileR
|
||||||
|
|
||||||
|
|
||||||
|
getProfileDataR :: Handler Html
|
||||||
|
getProfileDataR = do
|
||||||
|
(uid, User{..}) <- requireAuthPair
|
||||||
|
-- mr <- getMessageRender
|
||||||
|
|
||||||
|
defaultLayout $ do
|
||||||
|
$(widgetFile "profileData")
|
||||||
|
$(widgetFile "dsgvDisclaimer")
|
||||||
|
|||||||
@ -10,6 +10,7 @@
|
|||||||
{-# LANGUAGE FlexibleInstances #-}
|
{-# LANGUAGE FlexibleInstances #-}
|
||||||
{-# LANGUAGE ViewPatterns #-}
|
{-# LANGUAGE ViewPatterns #-}
|
||||||
{-# LANGUAGE RankNTypes #-}
|
{-# LANGUAGE RankNTypes #-}
|
||||||
|
{-# LANGUAGE TypeOperators #-}
|
||||||
|
|
||||||
module Handler.Sheet where
|
module Handler.Sheet where
|
||||||
|
|
||||||
@ -44,6 +45,7 @@ import Network.Mime
|
|||||||
|
|
||||||
import Data.Set (Set)
|
import Data.Set (Set)
|
||||||
import qualified Data.Set as Set
|
import qualified Data.Set as Set
|
||||||
|
import qualified Data.Map as Map
|
||||||
|
|
||||||
import Data.Map (Map, (!), (!?))
|
import Data.Map (Map, (!), (!?))
|
||||||
import qualified Data.Map as Map
|
import qualified Data.Map as Map
|
||||||
@ -104,6 +106,7 @@ 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")
|
||||||
|
<*> formToAForm (correctorForm msId (maybe [] sfCorrectors template))
|
||||||
<* submitButton
|
<* submitButton
|
||||||
return $ case result of
|
return $ case result of
|
||||||
FormSuccess sheetResult
|
FormSuccess sheetResult
|
||||||
@ -112,10 +115,11 @@ makeSheetForm msId template = identForm FIDsheet $ \html -> do
|
|||||||
(FormFailure errorMsgs,
|
(FormFailure errorMsgs,
|
||||||
[whamlet|
|
[whamlet|
|
||||||
<div class="alert alert-danger">
|
<div class="alert alert-danger">
|
||||||
<h4> Fehler:
|
<div class="alert__content">
|
||||||
<ul>
|
<h4> Fehler:
|
||||||
$forall errmsg <- errorMsgs
|
<ul>
|
||||||
<li> #{errmsg}
|
$forall errmsg <- errorMsgs
|
||||||
|
<li> #{errmsg}
|
||||||
^{widget}
|
^{widget}
|
||||||
|]
|
|]
|
||||||
)
|
)
|
||||||
@ -157,16 +161,16 @@ getSheetList courseEnt = do
|
|||||||
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 $ CSheetR tid csh (sheetName sheet) SShowR
|
[ 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 lbis" $ toWgt . formatTimeGerWD . sheetActiveTo . snd3
|
||||||
, headed "Bewertung" $ toWgt . show . sheetType . snd3
|
, headed "Bewertung" $ toWgt . display . sheetType . snd3
|
||||||
]
|
]
|
||||||
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 $ CSheetR tid csh (sheetName $ snd3 s) SEditR
|
, headed "" $ \s -> simpleLink "Edit" $ CSheetR tid csh (sheetName $ snd3 s) SEditR
|
||||||
, headed "" $ \s -> linkButton "Delete" BCLink $ CSheetR tid csh (sheetName $ snd3 s) SDelR
|
, headed "" $ \s -> simpleLink "Delete" $ CSheetR tid csh (sheetName $ snd3 s) SDelR
|
||||||
]
|
]
|
||||||
showAdmin <- case sheets of
|
showAdmin <- case sheets of
|
||||||
((_,firstSheet,_):_) -> do
|
((_,firstSheet,_):_) -> do
|
||||||
@ -177,7 +181,7 @@ getSheetList courseEnt = do
|
|||||||
then colBase `mappend` colAdmin
|
then colBase `mappend` colAdmin
|
||||||
else colBase
|
else colBase
|
||||||
defaultLayout $ do
|
defaultLayout $ do
|
||||||
setTitle $ toHtml $ T.append "Übungsblätter " csh
|
setTitle $ toHtml $ csh <> " Übungsblätter"
|
||||||
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
|
||||||
@ -212,16 +216,18 @@ getSShowR tid csh shn = do
|
|||||||
-- return desired columns
|
-- return desired columns
|
||||||
return $ (file E.^. FileTitle, file E.^. FileModified, sheetFile E.^. SheetFileType)
|
return $ (file E.^. FileTitle, file E.^. FileModified, sheetFile E.^. SheetFileType)
|
||||||
let colonnadeFiles = mconcat
|
let colonnadeFiles = mconcat
|
||||||
[ sortable (Just "type") "Typ" $ \(_,_, E.Value ftype) -> textCell $ toPathPiece ftype
|
[ sortable (Just "type") "Typ" $ \(_,_, E.Value ftype) -> cell $ [whamlet| _{ftype}|]
|
||||||
, sortable (Just "path") "Dateiname" $ anchorCell (\(E.Value fName,_,E.Value fType) -> CSheetR tid csh shn (SFileR fType fName))
|
, sortable (Just "path") "Dateiname" $ anchorCell (\(E.Value fName,_,E.Value fType) -> CSheetR tid csh shn (SFileR fType fName))
|
||||||
(\(E.Value fName,_,_) -> str2widget fName)
|
(\(E.Value fName,_,_) -> str2widget fName)
|
||||||
, sortable (Just "time") "Modifikation" $ \(_,E.Value modified,_) -> stringCell $ formatTimeGerWDT modified
|
, sortable (Just "time") "Modifikation" $ \(_,E.Value modified,_) -> stringCell $ formatTimeGerWDT (modified :: UTCTime)
|
||||||
]
|
]
|
||||||
fileTable <- dbTable def $ DBTable
|
fileTable <- dbTable def $ DBTable
|
||||||
{ dbtSQLQuery = fileData
|
{ dbtSQLQuery = fileData
|
||||||
, dbtColonnade = colonnadeFiles
|
, dbtColonnade = colonnadeFiles
|
||||||
, dbtAttrs = tableDefault
|
, dbtAttrs = tableDefault
|
||||||
|
, dbtFilter = Map.empty
|
||||||
, dbtIdent = "files" :: Text
|
, dbtIdent = "files" :: Text
|
||||||
|
-- TODO: Add column for and visibility date
|
||||||
, dbtSorting = [ ( "type"
|
, dbtSorting = [ ( "type"
|
||||||
, SortColumn $ \(sheet `E.InnerJoin` sheetFile `E.InnerJoin` file) -> sheetFile E.^. SheetFileType
|
, SortColumn $ \(sheet `E.InnerJoin` sheetFile `E.InnerJoin` file) -> sheetFile E.^. SheetFileType
|
||||||
)
|
)
|
||||||
|
|||||||
@ -61,7 +61,7 @@ makeSubmissionForm :: Maybe SubmissionId -> Bool -> SheetGroup -> [Text] -> Form
|
|||||||
makeSubmissionForm msmid unpackZips grouping buddies = identForm FIDsubmission $ \html -> do
|
makeSubmissionForm msmid unpackZips grouping buddies = identForm FIDsubmission $ \html -> do
|
||||||
flip (renderAForm FormStandard) html $ (,)
|
flip (renderAForm FormStandard) html $ (,)
|
||||||
<$> (bool (\f fs _ -> Just <$> areq f fs Nothing) aopt $ isJust msmid) (zipFileField unpackZips) (fsm $ bool MsgSubmissionFile MsgSubmissionArchive unpackZips) Nothing
|
<$> (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
|
<*> (catMaybes <$> sequenceA [bool aforced' aopt editableBuddies textField (fslpI (MsgSubmissionMember g) "user@campus.lmu.de" ) buddy
|
||||||
| g <- [1..(max groupNr $ length buddies)] -- groupNr might have decreased meanwhile
|
| g <- [1..(max groupNr $ length buddies)] -- groupNr might have decreased meanwhile
|
||||||
| buddy <- map (Just . Just) buddies ++ repeat Nothing -- show current buddies
|
| buddy <- map (Just . Just) buddies ++ repeat Nothing -- show current buddies
|
||||||
])
|
])
|
||||||
@ -74,10 +74,33 @@ makeSubmissionForm msmid unpackZips grouping buddies = identForm FIDsubmission $
|
|||||||
aforced' f fs (Just (Just v)) = Just <$> aforced f fs v
|
aforced' f fs (Just (Just v)) = Just <$> aforced f fs v
|
||||||
aforced' _ _ _ = error "Cannot happen since groupNr==0 if grouping/=Arbitrary"
|
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 -> SubmissionMode -> Handler Html
|
|
||||||
|
getSubmissionR, postSubmissionR :: TermId -> Text -> Text -> CryptoFileNameSubmission -> Handler Html
|
||||||
getSubmissionR = postSubmissionR
|
getSubmissionR = postSubmissionR
|
||||||
postSubmissionR tid csh shn (SubmissionMode mcid) = do
|
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
|
uid <- requireAuthId
|
||||||
msmid <- traverse decrypt mcid
|
msmid <- traverse decrypt mcid
|
||||||
(Entity shid Sheet{..}, buddies, lastEdits) <- runDB $ do
|
(Entity shid Sheet{..}, buddies, lastEdits) <- runDB $ do
|
||||||
@ -112,7 +135,7 @@ postSubmissionR tid csh shn (SubmissionMode mcid) = do
|
|||||||
(E.Value smid:_) -> do
|
(E.Value smid:_) -> do
|
||||||
cID <- encrypt smid
|
cID <- encrypt smid
|
||||||
addMessageI "info" $ MsgSubmissionAlreadyExists
|
addMessageI "info" $ MsgSubmissionAlreadyExists
|
||||||
redirect $ CSheetR tid csh shn $ SubmissionR $ SubmissionMode $ Just cID
|
redirect $ CSheetR tid csh shn $ SubmissionR cID
|
||||||
(Just smid) -> do
|
(Just smid) -> do
|
||||||
shid' <- submissionSheet <$> get404 smid
|
shid' <- submissionSheet <$> get404 smid
|
||||||
when (shid /= shid') $ invalidArgsI [MsgSubmissionWrongSheet]
|
when (shid /= shid') $ invalidArgsI [MsgSubmissionWrongSheet]
|
||||||
@ -181,7 +204,7 @@ postSubmissionR tid csh shn (SubmissionMode mcid) = do
|
|||||||
(Nothing, Just smid) -- no new files, existing submission partners updated
|
(Nothing, Just smid) -- no new files, existing submission partners updated
|
||||||
-> return smid
|
-> return smid
|
||||||
(Just files, _) -- new files
|
(Just files, _) -- new files
|
||||||
-> runConduit $ transPipe lift files .| Conduit.map Left .| sinkSubmission shid uid ((,False) <$> msmid)
|
-> runConduit $ transPipe lift files .| extractRatings .| sinkSubmission shid uid ((,False) <$> msmid)
|
||||||
_ -> error "Impossible, because of definition of `makeSubmissionForm`"
|
_ -> error "Impossible, because of definition of `makeSubmissionForm`"
|
||||||
-- Determine members of pre-registered group
|
-- Determine members of pre-registered group
|
||||||
groupUids <- fmap (setFromList . map E.unValue) . E.select . E.from $ \(submissionGroupUser `E.InnerJoin` submissionGroup `E.InnerJoin` submissionGroupUser') -> do
|
groupUids <- fmap (setFromList . map E.unValue) . E.select . E.from $ \(submissionGroupUser `E.InnerJoin` submissionGroup `E.InnerJoin` submissionGroupUser') -> do
|
||||||
@ -203,7 +226,7 @@ postSubmissionR tid csh shn (SubmissionMode mcid) = do
|
|||||||
_other -> return Nothing
|
_other -> return Nothing
|
||||||
|
|
||||||
case mCID of
|
case mCID of
|
||||||
Just cID -> redirect $ CSheetR tid csh shn $ SubmissionR $ SubmissionMode $ Just cID
|
Just cID -> redirect $ CSheetR tid csh shn $ SubmissionR cID
|
||||||
Nothing -> return ()
|
Nothing -> return ()
|
||||||
|
|
||||||
mArCid <- fmap ZIPArchiveName <$> traverse encrypt msmid
|
mArCid <- fmap ZIPArchiveName <$> traverse encrypt msmid
|
||||||
@ -231,6 +254,7 @@ postSubmissionR tid csh shn (SubmissionMode mcid) = do
|
|||||||
, SortColumn $ \(sf `E.InnerJoin` f) -> f E.^. FileModified
|
, SortColumn $ \(sf `E.InnerJoin` f) -> f E.^. FileModified
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
, dbtFilter = []
|
||||||
}
|
}
|
||||||
mFileTable <- traverse (dbTable def) . fmap smid2ArchiveTable $ (,) <$> msmid <*> mcid
|
mFileTable <- traverse (dbTable def) . fmap smid2ArchiveTable $ (,) <$> msmid <*> mcid
|
||||||
|
|
||||||
@ -265,16 +289,15 @@ submissionFileQuery submissionID (sf `E.InnerJoin` f) = E.distinctOnOrderBy [E.a
|
|||||||
E.orderBy [E.desc $ sf E.^. SubmissionFileIsUpdate] -- E.desc returns corrector updated data first
|
E.orderBy [E.desc $ sf E.^. SubmissionFileIsUpdate] -- E.desc returns corrector updated data first
|
||||||
return f
|
return f
|
||||||
|
|
||||||
getSubmissionDownloadSingleR :: CryptoUUIDSubmission -> FilePath -> Handler TypedContent
|
getSubmissionDownloadSingleR :: CryptoFileNameSubmission -> FilePath -> Handler TypedContent
|
||||||
getSubmissionDownloadSingleR cID path = do
|
getSubmissionDownloadSingleR cID path = do
|
||||||
submissionID <- decrypt cID
|
submissionID <- decrypt cID
|
||||||
cID' <- encrypt submissionID
|
|
||||||
|
|
||||||
runDB $ do
|
runDB $ do
|
||||||
isRating <- maybe False (== submissionID) <$> isRatingFile path
|
isRating <- maybe False (== submissionID) <$> isRatingFile path
|
||||||
case isRating of
|
case isRating of
|
||||||
True -> do
|
True -> do
|
||||||
file <- runMaybeT $ lift . ratingFile cID' =<< MaybeT (getRating submissionID)
|
file <- runMaybeT $ lift . ratingFile cID =<< MaybeT (getRating submissionID)
|
||||||
maybe notFound (return . toTypedContent . Text.decodeUtf8) $ fileContent =<< file
|
maybe notFound (return . toTypedContent . Text.decodeUtf8) $ fileContent =<< file
|
||||||
False -> do
|
False -> do
|
||||||
results <- E.select . E.from $ \(sf `E.InnerJoin` f) -> E.distinctOnOrderBy [E.asc $ f E.^. FileTitle] $ do
|
results <- E.select . E.from $ \(sf `E.InnerJoin` f) -> E.distinctOnOrderBy [E.asc $ f E.^. FileTitle] $ do
|
||||||
|
|||||||
@ -31,10 +31,10 @@ getTermShowR = do
|
|||||||
-- return term
|
-- return term
|
||||||
--
|
--
|
||||||
let
|
let
|
||||||
|
termData :: E.SqlExpr (Entity Term) -> E.SqlQuery (E.SqlExpr (Entity Term), E.SqlExpr (E.Value Int64))
|
||||||
termData term = do
|
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.^. CourseTerm
|
||||||
return E.countRows
|
return E.countRows
|
||||||
return (term, courseCount)
|
return (term, courseCount)
|
||||||
@ -58,11 +58,9 @@ getTermShowR = do
|
|||||||
stringCell $ formatTimeGerWD termLectureEnd
|
stringCell $ formatTimeGerWD termLectureEnd
|
||||||
, sortable Nothing "Aktiv" $ \(Entity _ Term{..},_) ->
|
, sortable Nothing "Aktiv" $ \(Entity _ Term{..},_) ->
|
||||||
textCell $ bool "" tickmark termActive
|
textCell $ bool "" tickmark termActive
|
||||||
, sortable Nothing "Kursliste" $ \(Entity tid Term{..}, E.Value numCourses) ->
|
, sortable Nothing "Kursliste" $ anchorCell
|
||||||
cell [whamlet|
|
(\(Entity tid _, _) -> TermCourseListR tid)
|
||||||
<a href=@{TermCourseListR tid}>
|
(\(_, E.Value numCourses) -> [whamlet|_{MsgNumCourses numCourses}|])
|
||||||
#{show numCourses} Kurse
|
|
||||||
|]
|
|
||||||
, sortable (Just "start") "Semesteranfang" $ \(Entity _ Term{..},_) ->
|
, sortable (Just "start") "Semesteranfang" $ \(Entity _ Term{..},_) ->
|
||||||
stringCell $ formatTimeGerWD termStart
|
stringCell $ formatTimeGerWD termStart
|
||||||
, sortable (Just "end") "Semesterende" $ \(Entity _ Term{..},_) ->
|
, sortable (Just "end") "Semesterende" $ \(Entity _ Term{..},_) ->
|
||||||
@ -86,6 +84,17 @@ getTermShowR = do
|
|||||||
, SortColumn $ \term -> term E.^. TermLectureEnd
|
, SortColumn $ \term -> term E.^. TermLectureEnd
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
, dbtFilter = [ ( "active"
|
||||||
|
, FilterColumn $ \term -> (term E.^. TermActive :: E.SqlExpr (E.Value Bool))
|
||||||
|
)
|
||||||
|
, ( "course"
|
||||||
|
, FilterColumn $ \term csh -> case csh of -- FilterColumn-Lambdas are
|
||||||
|
[] -> E.val True :: E.SqlExpr (E.Value Bool)
|
||||||
|
cshs -> E.exists . E.from $ \course -> do
|
||||||
|
E.where_ $ course E.^. CourseTerm E.==. term E.^. TermId
|
||||||
|
E.&&. course E.^. CourseShorthand `E.in_` E.valList cshs
|
||||||
|
)
|
||||||
|
]
|
||||||
, dbtAttrs = tableDefault
|
, dbtAttrs = tableDefault
|
||||||
, dbtIdent = "terms" :: Text
|
, dbtIdent = "terms" :: Text
|
||||||
}
|
}
|
||||||
@ -123,21 +132,21 @@ termEditHandler term = do
|
|||||||
redirect TermShowR
|
redirect TermShowR
|
||||||
(FormMissing ) -> return ()
|
(FormMissing ) -> return ()
|
||||||
(FormFailure _) -> addMessageI "warning" MsgInvalidInput
|
(FormFailure _) -> addMessageI "warning" MsgInvalidInput
|
||||||
let formTitle = "Semester editieren/anlegen" :: Text
|
|
||||||
let actionUrl = TermEditR
|
let actionUrl = TermEditR
|
||||||
defaultLayout $ do
|
defaultLayout $ do
|
||||||
setTitle [shamlet| #{formTitle} |]
|
setTitleI MsgTermEditHeading
|
||||||
$(widgetFile "formPage")
|
$(widgetFile "formPage")
|
||||||
|
|
||||||
newTermForm :: Maybe Term -> Form Term
|
newTermForm :: Maybe Term -> Form Term
|
||||||
newTermForm template html = do
|
newTermForm template html = do
|
||||||
|
renderMessage <- getMessageRender
|
||||||
(result, widget) <- flip (renderAForm FormStandard) html $ Term
|
(result, widget) <- flip (renderAForm FormStandard) html $ Term
|
||||||
<$> areq termNewField (bfs ("Semester" :: Text)) (termName <$> template)
|
<$> areq termNewField (fslpI MsgTerm (renderMessage MsgTermPlaceholder)) (termName <$> template)
|
||||||
<*> areq dayField (bfs ("Erster Tag" :: Text)) (termStart <$> template)
|
<*> areq dayField (fsl ("Erster Tag" :: Text)) (termStart <$> template)
|
||||||
<*> areq dayField (bfs ("Letzer Tag" :: Text)) (termEnd <$> template)
|
<*> areq dayField (fsl ("Letzer Tag" :: Text)) (termEnd <$> template)
|
||||||
<*> pure [] -- TODO: List of Day field required, must probably be done as its own form and then combined
|
<*> pure [] -- TODO: List of Day field required, must probably be done as its own form and then combined
|
||||||
<*> areq dayField (bfs ("Beginn Vorlesungen" :: Text)) (termLectureStart <$> template)
|
<*> areq dayField (fsl "Beginn Vorlesungen") (termLectureStart <$> template)
|
||||||
<*> areq dayField (bfs ("Ende Vorlesungen" :: Text)) (termLectureEnd <$> template)
|
<*> areq dayField (fsl ("Ende Vorlesungen" :: Text)) (termLectureEnd <$> template)
|
||||||
<*> areq checkBoxField (bfs ("Aktiv" :: Text)) (termActive <$> template)
|
<*> areq checkBoxField (bfs ("Aktiv" :: Text)) (termActive <$> template)
|
||||||
<* submitButton
|
<* submitButton
|
||||||
return $ case result of
|
return $ case result of
|
||||||
@ -147,10 +156,11 @@ newTermForm template html = do
|
|||||||
(FormFailure errorMsgs,
|
(FormFailure errorMsgs,
|
||||||
[whamlet|
|
[whamlet|
|
||||||
<div class="alert alert-danger">
|
<div class="alert alert-danger">
|
||||||
<h4> Fehler:
|
<div class="alert__content">
|
||||||
<ul>
|
<h4> Fehler:
|
||||||
$forall errmsg <- errorMsgs
|
<ul>
|
||||||
<li> #{errmsg}
|
$forall errmsg <- errorMsgs
|
||||||
|
<li> #{errmsg}
|
||||||
^{widget}
|
^{widget}
|
||||||
|]
|
|]
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
{-# LANGUAGE NoImplicitPrelude #-}
|
{-# LANGUAGE NoImplicitPrelude #-}
|
||||||
{-# LANGUAGE OverloadedStrings #-}
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
{-# LANGUAGE TemplateHaskell #-}
|
{-# LANGUAGE TemplateHaskell #-}
|
||||||
|
{-# LANGUAGE QuasiQuotes #-}
|
||||||
{-# LANGUAGE MultiParamTypeClasses #-}
|
{-# LANGUAGE MultiParamTypeClasses #-}
|
||||||
{-# LANGUAGE TypeFamilies #-}
|
{-# LANGUAGE TypeFamilies #-}
|
||||||
|
|
||||||
@ -34,7 +35,10 @@ getUsersR = do
|
|||||||
Nothing -> "???"
|
Nothing -> "???"
|
||||||
(Just school) -> schoolShorthand school
|
(Just school) -> schoolShorthand school
|
||||||
let colonnadeUsers = mconcat $
|
let colonnadeUsers = mconcat $
|
||||||
[ headed "User" $ text2widget.userDisplayName.entityVal.fst3
|
[ headed "User" $ \u -> do
|
||||||
|
cID <- encrypt $ entityKey $ fst3 u
|
||||||
|
let name = display $ userDisplayName $ entityVal $ fst3 u
|
||||||
|
[whamlet|<a href=@{AdminUserR cID}>#{name}|]
|
||||||
, headed "Admin for Schools" $ (\u -> text2widget $ intercalate ", " $ map (getSchoolname.userAdminSchool .entityVal) $ snd3 u)
|
, headed "Admin for Schools" $ (\u -> text2widget $ intercalate ", " $ map (getSchoolname.userAdminSchool .entityVal) $ snd3 u)
|
||||||
, headed "Lecturer at Schools" $ (\u -> text2widget $ intercalate ", " $ map (getSchoolname.userLecturerSchool.entityVal) $ trd3 u)
|
, headed "Lecturer at Schools" $ (\u -> text2widget $ intercalate ", " $ map (getSchoolname.userLecturerSchool.entityVal) $ trd3 u)
|
||||||
]
|
]
|
||||||
|
|||||||
@ -43,8 +43,11 @@ formatTimeGerDTlong = formatTimeGer "%A, %e. %B %Y, %k:%M:%S"
|
|||||||
formatTimeGerWDT :: FormatTime t => t -> String
|
formatTimeGerWDT :: FormatTime t => t -> String
|
||||||
formatTimeGerWDT = formatTimeGer $ dateTimeFmt germanTimeLocale
|
formatTimeGerWDT = formatTimeGer $ dateTimeFmt germanTimeLocale
|
||||||
|
|
||||||
formatTimeGerDT :: FormatTime t => t -> String
|
formatTimeGerDT :: FormatTime t => t -> String -- 0.00.00 0:00
|
||||||
formatTimeGerDT = formatTimeGer "%e.%m.%y %k:%M"
|
formatTimeGerDT = formatTimeGer "%e.%m.%y %k:%M" -- leading spaces at start, otherwise 0 padding
|
||||||
|
|
||||||
|
formatTimeGerDT2 :: FormatTime t => t -> String -- 00.00.00 00:00
|
||||||
|
formatTimeGerDT2 = formatTimeGer "%d.%m.%y %H:%M" -- always padding with 0
|
||||||
|
|
||||||
formatTimeGerWD :: FormatTime t => t -> String
|
formatTimeGerWD :: FormatTime t => t -> String
|
||||||
formatTimeGerWD = formatTimeGer "%a %e.%m.%y"
|
formatTimeGerWD = formatTimeGer "%a %e.%m.%y"
|
||||||
|
|||||||
@ -14,7 +14,7 @@
|
|||||||
module Handler.Utils.Form where
|
module Handler.Utils.Form where
|
||||||
|
|
||||||
import Handler.Utils.Form.Types
|
import Handler.Utils.Form.Types
|
||||||
|
import Handler.Utils.Templates
|
||||||
|
|
||||||
import Handler.Utils.DateTime
|
import Handler.Utils.DateTime
|
||||||
|
|
||||||
@ -48,7 +48,7 @@ import Control.Monad.Writer.Class
|
|||||||
-- Unique Form Identifiers to avoid accidents --
|
-- Unique Form Identifiers to avoid accidents --
|
||||||
------------------------------------------------
|
------------------------------------------------
|
||||||
|
|
||||||
data FormIdentifier = FIDcourse | FIDsheet | FIDsubmission | FIDcorrectors
|
data FormIdentifier = FIDcourse | FIDsheet | FIDsubmission | FIDsettings | FIDcorrectors
|
||||||
deriving (Enum, Eq, Ord, Bounded, Read, Show)
|
deriving (Enum, Eq, Ord, Bounded, Read, Show)
|
||||||
|
|
||||||
|
|
||||||
@ -100,8 +100,8 @@ instance PathPiece BtnDelete where -- for displaying the button only, not rea
|
|||||||
fromPathPiece = readFromPathPiece
|
fromPathPiece = readFromPathPiece
|
||||||
|
|
||||||
instance Button BtnDelete where
|
instance Button BtnDelete where
|
||||||
label BtnDelete = "Löschen"
|
label BtnDelete = [whamlet|_{MsgBtnDelete}|]
|
||||||
label BtnAbort = "Abrechen"
|
label BtnAbort = [whamlet|_{MsgBtnAbort}|]
|
||||||
|
|
||||||
cssClass BtnDelete = BCDanger
|
cssClass BtnDelete = BCDanger
|
||||||
cssClass BtnAbort = BCDefault
|
cssClass BtnAbort = BCDefault
|
||||||
@ -115,10 +115,26 @@ instance PathPiece SubmitButton where
|
|||||||
fromPathPiece = readFromPathPiece
|
fromPathPiece = readFromPathPiece
|
||||||
|
|
||||||
instance Button SubmitButton where
|
instance Button SubmitButton where
|
||||||
label BtnSubmit = "Submit"
|
label BtnSubmit = [whamlet|_{MsgBtnSubmit}|]
|
||||||
|
|
||||||
cssClass BtnSubmit = BCPrimary
|
cssClass BtnSubmit = BCPrimary
|
||||||
|
|
||||||
|
|
||||||
|
data RegisterButton = BtnRegister | BtnDeregister
|
||||||
|
deriving (Enum, Eq, Ord, Bounded, Read, Show)
|
||||||
|
|
||||||
|
instance PathPiece RegisterButton where
|
||||||
|
toPathPiece = showToPathPiece
|
||||||
|
fromPathPiece = readFromPathPiece
|
||||||
|
|
||||||
|
instance Button RegisterButton where
|
||||||
|
label BtnRegister = [whamlet|_{MsgBtnRegister}|]
|
||||||
|
label BtnDeregister = [whamlet|_{MsgBtnDeregister}|]
|
||||||
|
|
||||||
|
cssClass BtnRegister = BCPrimary
|
||||||
|
cssClass BtnDeregister = BCDanger
|
||||||
|
|
||||||
|
|
||||||
-- -- Looks like a button, but is just a link (e.g. for create course, etc.)
|
-- -- Looks like a button, but is just a link (e.g. for create course, etc.)
|
||||||
-- data LinkButton = LinkButton (Route UniWorX)
|
-- data LinkButton = LinkButton (Route UniWorX)
|
||||||
-- deriving (Enum, Eq, Ord, Bounded, Read, Show)
|
-- deriving (Enum, Eq, Ord, Bounded, Read, Show)
|
||||||
@ -135,8 +151,10 @@ 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 -- already validates that the correct button press was received (result only neccessary for combinedButtonField)
|
||||||
buttonField btn = Field {fieldParse, fieldView, fieldEnctype}
|
buttonField btn = Field {fieldParse, fieldView, fieldEnctype}
|
||||||
where
|
where
|
||||||
fieldEnctype = UrlEncoded
|
fieldEnctype = UrlEncoded
|
||||||
@ -224,6 +242,10 @@ buttonForm csrf = do
|
|||||||
-- Fields --
|
-- Fields --
|
||||||
------------
|
------------
|
||||||
|
|
||||||
|
|
||||||
|
natFieldI :: (Monad m, Integral i, RenderMessage (HandlerSite m) msg, RenderMessage (HandlerSite m) FormMessage) => msg -> Field m i
|
||||||
|
natFieldI msg = checkBool (>= 0) msg intField
|
||||||
|
|
||||||
natField :: (Monad m, Integral i, RenderMessage (HandlerSite m) FormMessage) => Text -> Field m i
|
natField :: (Monad m, Integral i, RenderMessage (HandlerSite m) FormMessage) => Text -> Field m i
|
||||||
natField d = checkBool (>= 0) (T.append d " muss eine natürliche Zahl sein.") $ intField
|
natField d = checkBool (>= 0) (T.append d " muss eine natürliche Zahl sein.") $ intField
|
||||||
|
|
||||||
@ -364,7 +386,7 @@ utcTimeField = Field
|
|||||||
where
|
where
|
||||||
fieldTimeFormat :: String
|
fieldTimeFormat :: String
|
||||||
--fieldTimeFormat = "%e.%m.%y %k:%M"
|
--fieldTimeFormat = "%e.%m.%y %k:%M"
|
||||||
fieldTimeFormat = "%Y-%m-%eT%H:%M"
|
fieldTimeFormat = "%Y-%m-%dT%H:%M"
|
||||||
|
|
||||||
readTime :: Text -> Either FormMessage UTCTime
|
readTime :: Text -> Either FormMessage UTCTime
|
||||||
readTime t =
|
readTime t =
|
||||||
@ -376,12 +398,48 @@ utcTimeField = Field
|
|||||||
showTime = fromString . (formatTime germanTimeLocale fieldTimeFormat)
|
showTime = fromString . (formatTime germanTimeLocale fieldTimeFormat)
|
||||||
|
|
||||||
|
|
||||||
fsm :: RenderMessage UniWorX msg => msg -> FieldSettings UniWorX
|
fsm :: RenderMessage UniWorX msg => msg -> FieldSettings UniWorX -- DEPRECATED
|
||||||
fsm = bfs -- TODO: get rid of Bootstrap
|
fsm = bfs -- TODO: get rid of Bootstrap
|
||||||
|
|
||||||
fsb :: Text -> FieldSettings site
|
fsb :: Text -> FieldSettings site -- DEPRECATED
|
||||||
fsb = bfs -- Just to avoid annoying Ambiguous Type Errors
|
fsb = bfs -- Just to avoid annoying Ambiguous Type Errors
|
||||||
|
|
||||||
|
fsl :: Text -> FieldSettings UniWorX
|
||||||
|
fsl lbl =
|
||||||
|
FieldSettings { fsLabel = (SomeMessage lbl)
|
||||||
|
, fsTooltip = Nothing
|
||||||
|
, fsId = Nothing
|
||||||
|
, fsName = Nothing
|
||||||
|
, fsAttrs = []
|
||||||
|
}
|
||||||
|
|
||||||
|
fslI :: RenderMessage UniWorX msg => msg -> FieldSettings UniWorX
|
||||||
|
fslI lbl =
|
||||||
|
FieldSettings { fsLabel = (SomeMessage lbl)
|
||||||
|
, fsTooltip = Nothing
|
||||||
|
, fsId = Nothing
|
||||||
|
, fsName = Nothing
|
||||||
|
, fsAttrs = []
|
||||||
|
}
|
||||||
|
|
||||||
|
fslp :: Text -> Text -> FieldSettings UniWorX
|
||||||
|
fslp lbl placeholder =
|
||||||
|
FieldSettings { fsLabel = (SomeMessage lbl)
|
||||||
|
, fsTooltip = Nothing
|
||||||
|
, fsId = Nothing
|
||||||
|
, fsName = Nothing
|
||||||
|
, fsAttrs = [("placeholder", placeholder)]
|
||||||
|
}
|
||||||
|
|
||||||
|
fslpI :: RenderMessage UniWorX msg => msg -> Text -> FieldSettings UniWorX
|
||||||
|
fslpI lbl placeholder =
|
||||||
|
FieldSettings { fsLabel = (SomeMessage lbl)
|
||||||
|
, fsTooltip = Nothing
|
||||||
|
, fsId = Nothing
|
||||||
|
, fsName = Nothing
|
||||||
|
, fsAttrs = [("placeholder", placeholder)]
|
||||||
|
}
|
||||||
|
|
||||||
addAttr :: Text -> Text -> FieldSettings site -> FieldSettings site
|
addAttr :: Text -> Text -> FieldSettings site -> FieldSettings site
|
||||||
addAttr attr valu fs = fs { fsAttrs=newAttrs (fsAttrs fs) }
|
addAttr attr valu fs = fs { fsAttrs=newAttrs (fsAttrs fs) }
|
||||||
where
|
where
|
||||||
@ -425,7 +483,7 @@ setNameClass fs gName gClass = fs { fsName= Just gName, fsAttrs=("class",gClass)
|
|||||||
setTooltip :: String -> FieldSettings site -> FieldSettings site
|
setTooltip :: String -> FieldSettings site -> FieldSettings site
|
||||||
setTooltip tt fs
|
setTooltip tt fs
|
||||||
| null tt = fs { fsTooltip = Nothing }
|
| null tt = fs { fsTooltip = Nothing }
|
||||||
| otherwise = fs { fsTooltip = Just $ fromString tt }
|
| otherwise = fs { fsTooltip = Just $ fromString tt, fsAttrs=("info",fromString tt):(fsAttrs fs) }
|
||||||
|
|
||||||
optionsPersistCryptoId :: forall site backend a msg.
|
optionsPersistCryptoId :: forall site backend a msg.
|
||||||
( YesodPersist site
|
( YesodPersist site
|
||||||
|
|||||||
@ -35,6 +35,9 @@ numberColonnade = headed "Nr" (fromString.show)
|
|||||||
pairColonnade :: (Functor h) => Colonnade h a c -> Colonnade h b c -> Colonnade h (a,b) c
|
pairColonnade :: (Functor h) => Colonnade h a c -> Colonnade h b c -> Colonnade h (a,b) c
|
||||||
pairColonnade a b = mconcat [ lmap fst a, lmap snd b]
|
pairColonnade a b = mconcat [ lmap fst a, lmap snd b]
|
||||||
|
|
||||||
|
i18nCell :: RenderMessage site a => a -> Cell site
|
||||||
|
i18nCell msg = cell [whamlet|_{msg}|]
|
||||||
|
|
||||||
|
|
||||||
-- Table Modification
|
-- Table Modification
|
||||||
encodeHeadedWidgetTableNumbered :: Attribute -> Colonnade Headed a (WidgetT site IO ()) -> [a] -> WidgetT site IO ()
|
encodeHeadedWidgetTableNumbered :: Attribute -> Colonnade Headed a (WidgetT site IO ()) -> [a] -> WidgetT site IO ()
|
||||||
|
|||||||
@ -7,10 +7,15 @@
|
|||||||
, LambdaCase
|
, LambdaCase
|
||||||
, ViewPatterns
|
, ViewPatterns
|
||||||
, FlexibleContexts
|
, 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(..)
|
||||||
@ -22,7 +27,7 @@ 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 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
|
||||||
@ -36,11 +41,14 @@ 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 Colonnade.Encode
|
||||||
import Yesod.Colonnade
|
import Yesod.Colonnade
|
||||||
|
|
||||||
@ -65,21 +73,64 @@ sqlSortDirection :: t -> (SortColumn t, SortDirection) -> E.SqlExpr E.OrderBy
|
|||||||
sqlSortDirection t (SortColumn e, SortAsc ) = E.asc $ e t
|
sqlSortDirection t (SortColumn e, SortAsc ) = E.asc $ e t
|
||||||
sqlSortDirection t (SortColumn e, SortDesc) = E.desc $ e t
|
sqlSortDirection t (SortColumn e, SortDesc) = E.desc $ e t
|
||||||
|
|
||||||
data DBTable = forall a r h i t.
|
|
||||||
( ToSortable 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
|
, E.From E.SqlQuery E.SqlExpr E.SqlBackend t
|
||||||
) => DBTable
|
) => DBTable
|
||||||
{ dbtSQLQuery :: t -> E.SqlQuery a
|
{ dbtSQLQuery :: t -> E.SqlQuery a
|
||||||
, dbtColonnade :: Colonnade h r (Cell UniWorX)
|
, dbtColonnade :: Colonnade h r' (Cell UniWorX)
|
||||||
, dbtSorting :: Map Text (SortColumn t)
|
, 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
|
||||||
@ -88,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
|
||||||
@ -106,7 +158,7 @@ instance Default PSValidator where
|
|||||||
|
|
||||||
|
|
||||||
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'
|
||||||
@ -114,39 +166,53 @@ 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
|
||||||
<$> (fromMaybe [] <$> iopt (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' = E.from $ \t -> dbtSQLQuery t
|
sqlQuery' = E.from $ \t -> dbtSQLQuery t
|
||||||
<* E.orderBy (map (sqlSortDirection t) 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 <$ E.from 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
|
||||||
getParams <- handlerToWidget $ queryToQueryText . Wai.queryString . reqWaiRequest <$> getRequest
|
getParams <- handlerToWidget $ queryToQueryText . Wai.queryString . reqWaiRequest <$> getRequest
|
||||||
|
|||||||
@ -12,7 +12,7 @@ import Colonnade
|
|||||||
import Colonnade.Encode
|
import Colonnade.Encode
|
||||||
|
|
||||||
data Sortable a = Sortable
|
data Sortable a = Sortable
|
||||||
{ sortableKey :: (Maybe Text)
|
{ sortableKey :: Maybe Text
|
||||||
, sortableContent :: a
|
, sortableContent :: a
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -24,6 +24,9 @@ instance Headedness Sortable where
|
|||||||
headednessExtract = Just $ \(Sortable _ x) -> x
|
headednessExtract = Just $ \(Sortable _ x) -> x
|
||||||
headednessExtractForall = Just $ ExtractForall (\(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}
|
newtype SortableP s = SortableP { toSortable :: forall a. s a -> Sortable a}
|
||||||
|
|
||||||
class Headedness s => ToSortable s where
|
class Headedness s => ToSortable s where
|
||||||
|
|||||||
@ -12,6 +12,8 @@ module Model.Types where
|
|||||||
import ClassyPrelude
|
import ClassyPrelude
|
||||||
import Utils
|
import Utils
|
||||||
|
|
||||||
|
import Data.Map (Map)
|
||||||
|
import qualified Data.Map as Map
|
||||||
import Data.Fixed
|
import Data.Fixed
|
||||||
|
|
||||||
import Database.Persist.TH
|
import Database.Persist.TH
|
||||||
@ -23,7 +25,7 @@ import Web.HttpApiData
|
|||||||
import Data.Text (Text)
|
import Data.Text (Text)
|
||||||
import qualified Data.Text as Text
|
import qualified Data.Text as Text
|
||||||
|
|
||||||
import Text.Read (readMaybe)
|
import Text.Read (readMaybe,readsPrec)
|
||||||
|
|
||||||
-- import Data.CaseInsensitive (CI)
|
-- import Data.CaseInsensitive (CI)
|
||||||
import qualified Data.CaseInsensitive as CI
|
import qualified Data.CaseInsensitive as CI
|
||||||
@ -41,6 +43,9 @@ type Points = Centi
|
|||||||
toPoints :: Integral a => a -> Points
|
toPoints :: Integral a => a -> Points
|
||||||
toPoints = MkFixed . fromIntegral
|
toPoints = MkFixed . fromIntegral
|
||||||
|
|
||||||
|
pToI :: Points -> Integer
|
||||||
|
pToI = fromPoints -- TODO: do we want to multiply?
|
||||||
|
|
||||||
fromPoints :: Integral a => Points -> a
|
fromPoints :: Integral a => Points -> a
|
||||||
fromPoints (MkFixed c) = fromInteger c
|
fromPoints (MkFixed c) = fromInteger c
|
||||||
|
|
||||||
@ -50,6 +55,13 @@ data SheetType
|
|||||||
| Pass { maxPoints, passingPoints :: Points }
|
| Pass { maxPoints, passingPoints :: Points }
|
||||||
| NotGraded
|
| NotGraded
|
||||||
deriving (Show, Read, Eq)
|
deriving (Show, Read, Eq)
|
||||||
|
|
||||||
|
instance DisplayAble SheetType where
|
||||||
|
display (Bonus {..}) = tshow (pToI maxPoints) <> " Bonuspunkte"
|
||||||
|
display (Normal{..}) = tshow (pToI maxPoints) <> " Punkte"
|
||||||
|
display (Pass {..}) = "Bestanden ab " <> tshow (pToI passingPoints) <> " von " <> tshow (pToI maxPoints)
|
||||||
|
display (NotGraded) = "Unbewertet"
|
||||||
|
|
||||||
deriveJSON defaultOptions ''SheetType
|
deriveJSON defaultOptions ''SheetType
|
||||||
derivePersistFieldJSON "SheetType"
|
derivePersistFieldJSON "SheetType"
|
||||||
|
|
||||||
@ -73,6 +85,13 @@ instance PathPiece SheetFileType where
|
|||||||
fromPathPiece t =
|
fromPathPiece t =
|
||||||
lookup (CI.mk t) [(CI.mk $ toPathPiece ty,ty) | ty <- [minBound..maxBound]]
|
lookup (CI.mk t) [(CI.mk $ toPathPiece ty,ty) | ty <- [minBound..maxBound]]
|
||||||
|
|
||||||
|
-- $(deriveSimpleWith ''DisplayAble 'display (drop 17) ''SheetFileType)
|
||||||
|
instance DisplayAble SheetFileType where -- deprecated, see RenderMessage instance in Foundation
|
||||||
|
display SheetExercise = "Aufgabenstellung"
|
||||||
|
display SheetHint = "Hinweise"
|
||||||
|
display SheetSolution = "Musterlösung"
|
||||||
|
display SheetMarking = "Korrekturhinweise"
|
||||||
|
|
||||||
data ExamStatus = Attended | NoShow | Voided
|
data ExamStatus = Attended | NoShow | Voided
|
||||||
deriving (Show, Read, Eq, Ord, Enum, Bounded)
|
deriving (Show, Read, Eq, Ord, Enum, Bounded)
|
||||||
derivePersistField "ExamStatus"
|
derivePersistField "ExamStatus"
|
||||||
@ -128,6 +147,9 @@ data TermIdentifier = TermIdentifier
|
|||||||
-- from_TermId_to_TermIdentifier = unTermKey
|
-- from_TermId_to_TermIdentifier = unTermKey
|
||||||
-- from_TermIdentifier_to_TermId = TermKey
|
-- from_TermIdentifier_to_TermId = TermKey
|
||||||
|
|
||||||
|
instance DisplayAble TermIdentifier where
|
||||||
|
display = termToText
|
||||||
|
|
||||||
--TODO: Enforce the number of digits within year, with parsing filling in the current leading digits? Goal: short urls
|
--TODO: Enforce the number of digits within year, with parsing filling in the current leading digits? Goal: short urls
|
||||||
termToText :: TermIdentifier -> Text
|
termToText :: TermIdentifier -> Text
|
||||||
termToText TermIdentifier{..} = Text.pack $ seasonToChar season : show year
|
termToText TermIdentifier{..} = Text.pack $ seasonToChar season : show year
|
||||||
@ -183,4 +205,29 @@ data StudyFieldType = FieldPrimary | FieldSecondary
|
|||||||
derivePersistField "StudyFieldType"
|
derivePersistField "StudyFieldType"
|
||||||
|
|
||||||
|
|
||||||
|
-- Skins / Themes
|
||||||
|
data Theme --Simply add Themes to this type only. CamelCase will be converted to "-lower"
|
||||||
|
= Default
|
||||||
|
| NeutralBlue
|
||||||
|
| AberdeenReds
|
||||||
|
| MintGreen
|
||||||
|
| SkyLove
|
||||||
|
deriving (Eq,Ord,Bounded,Enum)
|
||||||
|
|
||||||
|
$(deriveShowWith uncamel ''Theme) -- show for internal use in css/js
|
||||||
|
$(deriveSimpleWith ''DisplayAble 'display camelSpace ''Theme) -- display to display at user
|
||||||
|
|
||||||
|
allThemes :: [Theme]
|
||||||
|
allThemes = [minBound..maxBound]
|
||||||
|
|
||||||
|
readTheme :: Map String Theme
|
||||||
|
readTheme = Map.fromList [ (show t,t) | t <- allThemes ]
|
||||||
|
|
||||||
|
instance Read Theme where -- generic Read-Instance for Show/Bounded
|
||||||
|
readsPrec _ s
|
||||||
|
| (Just t) <- (Map.lookup s readTheme) = [(t,"")]
|
||||||
|
| otherwise = [(Default,"")] -- read shall always succeed
|
||||||
|
|
||||||
|
derivePersistField "Theme"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -71,6 +71,8 @@ data AppSettings = AppSettings
|
|||||||
|
|
||||||
, appAuthDummyLogin :: Bool
|
, appAuthDummyLogin :: Bool
|
||||||
-- ^ Indicate if auth dummy login should be enabled.
|
-- ^ Indicate if auth dummy login should be enabled.
|
||||||
|
, appAllowDeprecated :: Bool
|
||||||
|
-- ^ Indicate if deprecated routes are accessible for everyone
|
||||||
}
|
}
|
||||||
|
|
||||||
instance FromJSON AppSettings where
|
instance FromJSON AppSettings where
|
||||||
@ -104,6 +106,7 @@ instance FromJSON AppSettings where
|
|||||||
appCryptoIDKeyFile <- o .: "cryptoid-keyfile"
|
appCryptoIDKeyFile <- o .: "cryptoid-keyfile"
|
||||||
|
|
||||||
appAuthDummyLogin <- o .:? "auth-dummy-login" .!= defaultDev
|
appAuthDummyLogin <- o .:? "auth-dummy-login" .!= defaultDev
|
||||||
|
appAllowDeprecated <- o .:? "allow-deprecated" .!= defaultDev
|
||||||
|
|
||||||
return AppSettings {..}
|
return AppSettings {..}
|
||||||
|
|
||||||
|
|||||||
135
src/Utils.hs
135
src/Utils.hs
@ -2,6 +2,7 @@
|
|||||||
{-# LANGUAGE OverloadedStrings #-}
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
{-# LANGUAGE ScopedTypeVariables #-}
|
{-# LANGUAGE ScopedTypeVariables #-}
|
||||||
{-# LANGUAGE RankNTypes #-}
|
{-# LANGUAGE RankNTypes #-}
|
||||||
|
{-# LANGUAGE TypeSynonymInstances, FlexibleInstances, UndecidableInstances #-}
|
||||||
{-# LANGUAGE TypeFamilies, FlexibleContexts, ConstraintKinds #-}
|
{-# LANGUAGE TypeFamilies, FlexibleContexts, ConstraintKinds #-}
|
||||||
{-# LANGUAGE QuasiQuotes #-}
|
{-# LANGUAGE QuasiQuotes #-}
|
||||||
|
|
||||||
@ -11,6 +12,10 @@ module Utils
|
|||||||
|
|
||||||
import ClassyPrelude.Yesod
|
import ClassyPrelude.Yesod
|
||||||
|
|
||||||
|
import Data.List (foldl)
|
||||||
|
import Data.Foldable as Fold
|
||||||
|
import qualified Data.Char as Char
|
||||||
|
|
||||||
import Utils.DB as Utils
|
import Utils.DB as Utils
|
||||||
import Utils.Common as Utils
|
import Utils.Common as Utils
|
||||||
|
|
||||||
@ -25,6 +30,8 @@ import Control.Monad.Trans.Maybe (MaybeT(..))
|
|||||||
import Control.Monad.Catch
|
import Control.Monad.Catch
|
||||||
|
|
||||||
|
|
||||||
|
import qualified Database.Esqueleto as E (Value, unValue)
|
||||||
|
|
||||||
-----------
|
-----------
|
||||||
-- Yesod --
|
-- Yesod --
|
||||||
-----------
|
-----------
|
||||||
@ -65,10 +72,70 @@ withFragment :: ( Monad m
|
|||||||
withFragment form html = (flip fmap) form $ \(x, widget) -> (x, toWidget html >> widget)
|
withFragment form html = (flip fmap) form $ \(x, widget) -> (x, toWidget html >> widget)
|
||||||
|
|
||||||
|
|
||||||
|
uncamel :: String -> String -- "Model.Theme.CamelCaseThing" -> "camel-case-thing"
|
||||||
|
uncamel = ("theme-" ++) . reverse . foldl helper []
|
||||||
|
where
|
||||||
|
helper _ '.' = []
|
||||||
|
helper acc c
|
||||||
|
| Char.isSpace c = acc
|
||||||
|
| Char.isUpper c = Char.toLower c : '-' : acc
|
||||||
|
| otherwise = c : acc
|
||||||
|
|
||||||
|
camelSpace :: String -> String -- "Model.Theme.CamelCaseThing" -> "Camel Case Thing"
|
||||||
|
camelSpace = reverse . foldl helper []
|
||||||
|
where
|
||||||
|
helper _ '.' = []
|
||||||
|
helper acc c
|
||||||
|
| Char.isSpace c = acc
|
||||||
|
| Char.isUpper c = c : ' ' : acc
|
||||||
|
| otherwise = c : acc
|
||||||
|
|
||||||
|
-- Convert anything to Text, and I don't care how
|
||||||
|
class DisplayAble a where
|
||||||
|
display :: a -> Text
|
||||||
|
|
||||||
|
instance DisplayAble Text where
|
||||||
|
display = id
|
||||||
|
|
||||||
|
instance DisplayAble String where
|
||||||
|
display = pack
|
||||||
|
|
||||||
|
instance DisplayAble a => DisplayAble (Maybe a) where
|
||||||
|
display Nothing = ""
|
||||||
|
display (Just x) = display x
|
||||||
|
|
||||||
|
instance DisplayAble a => DisplayAble (E.Value a) where
|
||||||
|
display = display . E.unValue
|
||||||
|
|
||||||
|
-- The easy way out of UndecidableInstances (TypeFamilies would have been proper, but are much more complicated)
|
||||||
|
instance {-# OVERLAPPABLE #-} Show a => DisplayAble a where
|
||||||
|
display = pack . show
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
------------
|
------------
|
||||||
-- Tuples --
|
-- Tuples --
|
||||||
------------
|
------------
|
||||||
|
|
||||||
|
fst3 :: (a,b,c) -> a
|
||||||
|
fst3 (x,_,_) = x
|
||||||
|
snd3 :: (a,b,c) -> b
|
||||||
|
snd3 (_,y,_) = y
|
||||||
|
trd3 :: (a,b,c) -> c
|
||||||
|
trd3 (_,_,z) = z
|
||||||
|
|
||||||
|
-- Further projections are available via TemplateHaskell, defined in Utils.Common:
|
||||||
|
-- $(projN n m) :: (t1,..,tn) -> tm (for m<=n)
|
||||||
|
-- snd3 = $(projNI 3 2)
|
||||||
|
|
||||||
|
|
||||||
|
-----------
|
||||||
|
-- Lists --
|
||||||
|
-----------
|
||||||
|
|
||||||
|
-- notNull = not . null
|
||||||
|
|
||||||
|
|
||||||
----------
|
----------
|
||||||
-- Maps --
|
-- Maps --
|
||||||
----------
|
----------
|
||||||
@ -85,7 +152,22 @@ maybeT :: Monad m => m a -> MaybeT m a -> m a
|
|||||||
maybeT x m = runMaybeT m >>= maybe x return
|
maybeT x m = runMaybeT m >>= maybe x return
|
||||||
|
|
||||||
catchIfMaybeT :: (MonadCatch m, Exception e) => (e -> Bool) -> m a -> MaybeT m a
|
catchIfMaybeT :: (MonadCatch m, Exception e) => (e -> Bool) -> m a -> MaybeT m a
|
||||||
catchIfMaybeT pred act = catchIf pred (lift act) (const mzero)
|
catchIfMaybeT p act = catchIf p (lift act) (const mzero)
|
||||||
|
|
||||||
|
mcons :: Maybe a -> [a] -> [a]
|
||||||
|
mcons Nothing xs = xs
|
||||||
|
mcons (Just x) xs = x:xs
|
||||||
|
|
||||||
|
newtype NTop a = NTop a -- treat Nothing as Top for Ord (Maybe a); default implementation treats Nothing as bottom
|
||||||
|
|
||||||
|
instance Eq a => Eq (NTop (Maybe a)) where
|
||||||
|
(NTop x) == (NTop y) = x == y
|
||||||
|
|
||||||
|
instance Ord a => Ord (NTop (Maybe a)) where
|
||||||
|
compare (NTop Nothing) (NTop Nothing) = EQ
|
||||||
|
compare (NTop Nothing) _ = GT
|
||||||
|
compare _ (NTop Nothing) = LT
|
||||||
|
compare (NTop (Just x)) (NTop (Just y)) = compare x y
|
||||||
|
|
||||||
---------------
|
---------------
|
||||||
-- Exception --
|
-- Exception --
|
||||||
@ -97,17 +179,23 @@ maybeExceptT err act = lift act >>= maybe (throwE err) return
|
|||||||
maybeMExceptT :: Monad m => (m e) -> m (Maybe b) -> ExceptT e m b
|
maybeMExceptT :: Monad m => (m e) -> m (Maybe b) -> ExceptT e m b
|
||||||
maybeMExceptT err act = lift act >>= maybe (lift err >>= throwE) return
|
maybeMExceptT err act = lift act >>= maybe (lift err >>= throwE) return
|
||||||
|
|
||||||
guardExceptT :: Monad m => e -> Bool -> ExceptT e m ()
|
whenExceptT :: Monad m => Bool -> e -> ExceptT e m ()
|
||||||
guardExceptT err b = unless b $ throwE err
|
whenExceptT b err = when b $ throwE err
|
||||||
|
|
||||||
guardMExceptT :: Monad m => (m e) -> Bool -> ExceptT e m ()
|
whenMExceptT :: Monad m => Bool -> (m e) -> ExceptT e m ()
|
||||||
guardMExceptT err b = unless b $ lift err >>= throwE
|
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 :: Monad m => (e -> m b) -> (a -> m b) -> ExceptT e m a -> m b
|
||||||
exceptT f g = either f g <=< runExceptT
|
exceptT f g = either f g <=< runExceptT
|
||||||
|
|
||||||
catchIfMExceptT :: (MonadCatch m, Exception e) => (e -> m e') -> (e -> Bool) -> m a -> ExceptT e' m a
|
catchIfMExceptT :: (MonadCatch m, Exception e) => (e -> m e') -> (e -> Bool) -> m a -> ExceptT e' m a
|
||||||
catchIfMExceptT err pred act = catchIf pred (lift act) (throwE <=< lift . err)
|
catchIfMExceptT err p act = catchIf p (lift act) (throwE <=< lift . err)
|
||||||
|
|
||||||
|
|
||||||
------------
|
------------
|
||||||
@ -120,3 +208,38 @@ shortCircuitM sc mx my op = do
|
|||||||
case sc x of
|
case sc x of
|
||||||
True -> return x
|
True -> return x
|
||||||
False -> op <$> pure x <*> my
|
False -> op <$> pure x <*> my
|
||||||
|
|
||||||
|
|
||||||
|
guardM :: MonadPlus m => m Bool -> m ()
|
||||||
|
guardM f = guard =<< f
|
||||||
|
|
||||||
|
-- Some Utility Functions from Agda.Utils.Monad
|
||||||
|
-- | Monadic if-then-else.
|
||||||
|
ifM :: Monad m => m Bool -> m a -> m a -> m a
|
||||||
|
ifM c m m' =
|
||||||
|
do b <- c
|
||||||
|
if b then m else m'
|
||||||
|
|
||||||
|
-- | @ifNotM mc = ifM (not <$> mc)@
|
||||||
|
ifNotM :: Monad m => m Bool -> m a -> m a -> m a
|
||||||
|
ifNotM c = flip $ ifM c
|
||||||
|
|
||||||
|
-- | Lazy monadic conjunction.
|
||||||
|
and2M :: Monad m => m Bool -> m Bool -> m Bool
|
||||||
|
and2M ma mb = ifM ma mb (return False)
|
||||||
|
|
||||||
|
andM :: (Foldable f, Monad m) => f (m Bool) -> m Bool
|
||||||
|
andM = Fold.foldr and2M (return True)
|
||||||
|
|
||||||
|
allM :: (Functor f, Foldable f, Monad m) => f a -> (a -> m Bool) -> m Bool
|
||||||
|
allM xs f = andM $ fmap f xs
|
||||||
|
|
||||||
|
-- | Lazy monadic disjunction.
|
||||||
|
or2M :: Monad m => m Bool -> m Bool -> m Bool
|
||||||
|
or2M ma mb = ifM ma (return True) mb
|
||||||
|
|
||||||
|
orM :: (Foldable f, Monad m) => f (m Bool) -> m Bool
|
||||||
|
orM = Fold.foldr or2M (return False)
|
||||||
|
|
||||||
|
anyM :: (Functor f, Foldable f, Monad m) => f a -> (a -> m Bool) -> m Bool
|
||||||
|
anyM xs f = orM $ fmap f xs
|
||||||
|
|||||||
@ -2,24 +2,20 @@
|
|||||||
{-# LANGUAGE QuasiQuotes #-}
|
{-# LANGUAGE QuasiQuotes #-}
|
||||||
|
|
||||||
module Utils.Common where
|
module Utils.Common where
|
||||||
-- Common Utility Functions
|
-- Common Utility Functions that require TemplateHaskell
|
||||||
|
|
||||||
|
-- import Data.Char
|
||||||
|
|
||||||
import Language.Haskell.TH
|
import Language.Haskell.TH
|
||||||
import Control.Monad
|
-- import Control.Monad
|
||||||
import Control.Monad.Trans.Class
|
-- import Control.Monad.Trans.Class
|
||||||
import Control.Monad.Trans.Maybe
|
-- import Control.Monad.Trans.Maybe
|
||||||
import Control.Monad.Trans.Except
|
-- import Control.Monad.Trans.Except
|
||||||
|
|
||||||
------------
|
------------
|
||||||
-- Tuples --
|
-- Tuples --
|
||||||
------------
|
------------
|
||||||
|
|
||||||
fst3 :: (a,b,c) -> a
|
|
||||||
fst3 (x,_,_) = x
|
|
||||||
snd3 :: (a,b,c) -> b
|
|
||||||
snd3 (_,y,_) = y
|
|
||||||
trd3 :: (a,b,c) -> c
|
|
||||||
trd3 (_,_,z) = z
|
|
||||||
|
|
||||||
projNI :: Int -> Int -> ExpQ -- generic projection gives I-th element of N-tuple, i.e. snd3 = $(projNI 3 2) --ghci -fth
|
projNI :: Int -> Int -> ExpQ -- generic projection gives I-th element of N-tuple, i.e. snd3 = $(projNI 3 2) --ghci -fth
|
||||||
-- $(projN n m) :: (t1,..,tn) -> tm (for m<=n)
|
-- $(projN n m) :: (t1,..,tn) -> tm (for m<=n)
|
||||||
@ -54,3 +50,30 @@ altFun perm = lamE pat rhs
|
|||||||
fn = mkName "fn"
|
fn = mkName "fn"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- Special Show-Instances for Themes
|
||||||
|
deriveShowWith :: (String -> String) -> Name -> Q [Dec]
|
||||||
|
deriveShowWith = deriveSimpleWith ''Show 'show
|
||||||
|
|
||||||
|
-- deriveDisplayWith :: (String -> String) -> Name -> Q [Dec]
|
||||||
|
-- deriveDisplayWith = deriveSimpleWith ''DisplayAble 'display
|
||||||
|
|
||||||
|
deriveSimpleWith :: Name -> Name -> (String -> String) -> Name -> Q [Dec]
|
||||||
|
deriveSimpleWith cls fun strOp ty = do
|
||||||
|
(TyConI tyCon) <- reify ty
|
||||||
|
(tyConName, cs) <- case tyCon of
|
||||||
|
DataD [] nm [] _ cs _ -> return (nm, cs)
|
||||||
|
_ -> fail "deriveShowTheme: tyCon must be a plain datatype enumeration"
|
||||||
|
let instanceT = conT cls `appT` conT tyConName
|
||||||
|
return <$> instanceD (return []) instanceT [genDecs cs]
|
||||||
|
where
|
||||||
|
genDecs :: [Con] -> Q Dec
|
||||||
|
genDecs cs = funD fun (map genClause cs)
|
||||||
|
|
||||||
|
genClause :: Con -> Q Clause
|
||||||
|
genClause (NormalC name []) =
|
||||||
|
let pats = [ConP name []]
|
||||||
|
body = NormalB $ LitE $ StringL $ strOp $ show $ name
|
||||||
|
in return $ Clause pats body []
|
||||||
|
genClause _ = fail "deriveShowTheme: constructors not allowed to have arguments"
|
||||||
|
|
||||||
|
|||||||
@ -11,10 +11,8 @@ import qualified Data.List as List
|
|||||||
import Data.Map (Map)
|
import Data.Map (Map)
|
||||||
import qualified Data.Map as Map
|
import qualified Data.Map as Map
|
||||||
|
|
||||||
import Database.Persist
|
-- import Database.Persist -- currently not needed here
|
||||||
|
|
||||||
-- getKeyBy :: PersistEntity val => Unique val -> ReaderT backend0 m0 (Maybe (Entity val))
|
|
||||||
-- getKeyBy :: Unique a -> YesodDB site (Key a)
|
|
||||||
|
|
||||||
entities2map :: PersistEntity record => [Entity record] -> Map (Key record) record
|
entities2map :: PersistEntity record => [Entity record] -> Map (Key record) record
|
||||||
entities2map = foldl' (\m entity -> Map.insert (entityKey entity) (entityVal entity) m) Map.empty
|
entities2map = foldl' (\m entity -> Map.insert (entityKey entity) (entityVal entity) m) Map.empty
|
||||||
@ -27,6 +25,10 @@ getKeyBy404 :: (PersistEntityBackend record ~ BaseBackend backend, PersistEntity
|
|||||||
=> Unique record -> ReaderT backend m (Key record)
|
=> Unique record -> ReaderT backend m (Key record)
|
||||||
getKeyBy404 = (fmap entityKey) . getBy404 -- TODO optimize this, so that DB does not deliver entire record!
|
getKeyBy404 = (fmap entityKey) . getBy404 -- TODO optimize this, so that DB does not deliver entire record!
|
||||||
|
|
||||||
|
existsBy :: (PersistEntityBackend record ~ BaseBackend backend, PersistEntity record, PersistUniqueRead backend, MonadIO m)
|
||||||
|
=> Unique record -> ReaderT backend m Bool
|
||||||
|
existsBy = fmap isJust . getBy
|
||||||
|
|
||||||
|
|
||||||
myReplaceUnique
|
myReplaceUnique
|
||||||
:: (MonadIO m
|
:: (MonadIO m
|
||||||
|
|||||||
1
start.sh
1
start.sh
@ -4,5 +4,6 @@ unset HOST
|
|||||||
export DETAILED_LOGGING=true
|
export DETAILED_LOGGING=true
|
||||||
export LOG_ALL=true
|
export LOG_ALL=true
|
||||||
export DUMMY_LOGIN=true
|
export DUMMY_LOGIN=true
|
||||||
|
export ALLOW_DEPRECATED=true
|
||||||
|
|
||||||
exec -- stack exec -- yesod devel
|
exec -- stack exec -- yesod devel
|
||||||
|
|||||||
740
static/css/flatpickr.css
Normal file
740
static/css/flatpickr.css
Normal file
@ -0,0 +1,740 @@
|
|||||||
|
.flatpickr-calendar {
|
||||||
|
background: transparent;
|
||||||
|
opacity: 0;
|
||||||
|
display: none;
|
||||||
|
text-align: center;
|
||||||
|
visibility: hidden;
|
||||||
|
padding: 0;
|
||||||
|
-webkit-animation: none;
|
||||||
|
animation: none;
|
||||||
|
direction: ltr;
|
||||||
|
border: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 24px;
|
||||||
|
border-radius: 5px;
|
||||||
|
position: absolute;
|
||||||
|
width: 307.875px;
|
||||||
|
-webkit-box-sizing: border-box;
|
||||||
|
box-sizing: border-box;
|
||||||
|
-ms-touch-action: manipulation;
|
||||||
|
touch-action: manipulation;
|
||||||
|
background: #fff;
|
||||||
|
-webkit-box-shadow: 1px 0 0 #e6e6e6, -1px 0 0 #e6e6e6, 0 1px 0 #e6e6e6, 0 -1px 0 #e6e6e6, 0 3px 13px rgba(0,0,0,0.08);
|
||||||
|
box-shadow: 1px 0 0 #e6e6e6, -1px 0 0 #e6e6e6, 0 1px 0 #e6e6e6, 0 -1px 0 #e6e6e6, 0 3px 13px rgba(0,0,0,0.08);
|
||||||
|
}
|
||||||
|
.flatpickr-calendar.open,
|
||||||
|
.flatpickr-calendar.inline {
|
||||||
|
opacity: 1;
|
||||||
|
max-height: 640px;
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
.flatpickr-calendar.open {
|
||||||
|
display: inline-block;
|
||||||
|
z-index: 99999;
|
||||||
|
}
|
||||||
|
.flatpickr-calendar.animate.open {
|
||||||
|
-webkit-animation: fpFadeInDown 300ms cubic-bezier(0.23, 1, 0.32, 1);
|
||||||
|
animation: fpFadeInDown 300ms cubic-bezier(0.23, 1, 0.32, 1);
|
||||||
|
}
|
||||||
|
.flatpickr-calendar.inline {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
top: 2px;
|
||||||
|
}
|
||||||
|
.flatpickr-calendar.static {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% + 2px);
|
||||||
|
}
|
||||||
|
.flatpickr-calendar.static.open {
|
||||||
|
z-index: 999;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.flatpickr-calendar.multiMonth .flatpickr-days .dayContainer:nth-child(n+1) .flatpickr-day.inRange:nth-child(7n+7) {
|
||||||
|
-webkit-box-shadow: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
.flatpickr-calendar.multiMonth .flatpickr-days .dayContainer:nth-child(n+2) .flatpickr-day.inRange:nth-child(7n+1) {
|
||||||
|
-webkit-box-shadow: -2px 0 0 #e6e6e6, 5px 0 0 #e6e6e6;
|
||||||
|
box-shadow: -2px 0 0 #e6e6e6, 5px 0 0 #e6e6e6;
|
||||||
|
}
|
||||||
|
.flatpickr-calendar .hasWeeks .dayContainer,
|
||||||
|
.flatpickr-calendar .hasTime .dayContainer {
|
||||||
|
border-bottom: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
}
|
||||||
|
.flatpickr-calendar .hasWeeks .dayContainer {
|
||||||
|
border-left: 0;
|
||||||
|
}
|
||||||
|
.flatpickr-calendar.showTimeInput.hasTime .flatpickr-time {
|
||||||
|
height: 40px;
|
||||||
|
border-top: 1px solid #e6e6e6;
|
||||||
|
}
|
||||||
|
.flatpickr-calendar.noCalendar.hasTime .flatpickr-time {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
.flatpickr-calendar:before,
|
||||||
|
.flatpickr-calendar:after {
|
||||||
|
position: absolute;
|
||||||
|
display: block;
|
||||||
|
pointer-events: none;
|
||||||
|
border: solid transparent;
|
||||||
|
content: '';
|
||||||
|
height: 0;
|
||||||
|
width: 0;
|
||||||
|
left: 22px;
|
||||||
|
}
|
||||||
|
.flatpickr-calendar.rightMost:before,
|
||||||
|
.flatpickr-calendar.rightMost:after {
|
||||||
|
left: auto;
|
||||||
|
right: 22px;
|
||||||
|
}
|
||||||
|
.flatpickr-calendar:before {
|
||||||
|
border-width: 5px;
|
||||||
|
margin: 0 -5px;
|
||||||
|
}
|
||||||
|
.flatpickr-calendar:after {
|
||||||
|
border-width: 4px;
|
||||||
|
margin: 0 -4px;
|
||||||
|
}
|
||||||
|
.flatpickr-calendar.arrowTop:before,
|
||||||
|
.flatpickr-calendar.arrowTop:after {
|
||||||
|
bottom: 100%;
|
||||||
|
}
|
||||||
|
.flatpickr-calendar.arrowTop:before {
|
||||||
|
border-bottom-color: #e6e6e6;
|
||||||
|
}
|
||||||
|
.flatpickr-calendar.arrowTop:after {
|
||||||
|
border-bottom-color: #fff;
|
||||||
|
}
|
||||||
|
.flatpickr-calendar.arrowBottom:before,
|
||||||
|
.flatpickr-calendar.arrowBottom:after {
|
||||||
|
top: 100%;
|
||||||
|
}
|
||||||
|
.flatpickr-calendar.arrowBottom:before {
|
||||||
|
border-top-color: #e6e6e6;
|
||||||
|
}
|
||||||
|
.flatpickr-calendar.arrowBottom:after {
|
||||||
|
border-top-color: #fff;
|
||||||
|
}
|
||||||
|
.flatpickr-calendar:focus {
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
.flatpickr-wrapper {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.flatpickr-months {
|
||||||
|
display: -webkit-box;
|
||||||
|
display: -webkit-flex;
|
||||||
|
display: -ms-flexbox;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.flatpickr-months .flatpickr-month {
|
||||||
|
background: transparent;
|
||||||
|
color: rgba(0,0,0,0.9);
|
||||||
|
fill: rgba(0,0,0,0.9);
|
||||||
|
height: 28px;
|
||||||
|
line-height: 1;
|
||||||
|
text-align: center;
|
||||||
|
position: relative;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
overflow: hidden;
|
||||||
|
-webkit-box-flex: 1;
|
||||||
|
-webkit-flex: 1;
|
||||||
|
-ms-flex: 1;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.flatpickr-months .flatpickr-prev-month,
|
||||||
|
.flatpickr-months .flatpickr-next-month {
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
position: absolute;
|
||||||
|
top: 0px;
|
||||||
|
line-height: 16px;
|
||||||
|
height: 28px;
|
||||||
|
padding: 10px;
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
.flatpickr-months .flatpickr-prev-month.disabled,
|
||||||
|
.flatpickr-months .flatpickr-next-month.disabled {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.flatpickr-months .flatpickr-prev-month i,
|
||||||
|
.flatpickr-months .flatpickr-next-month i {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.flatpickr-months .flatpickr-prev-month.flatpickr-prev-month,
|
||||||
|
.flatpickr-months .flatpickr-next-month.flatpickr-prev-month {
|
||||||
|
/*
|
||||||
|
/*rtl:begin:ignore*/
|
||||||
|
/*
|
||||||
|
*/
|
||||||
|
left: 0;
|
||||||
|
/*
|
||||||
|
/*rtl:end:ignore*/
|
||||||
|
/*
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
/*rtl:begin:ignore*/
|
||||||
|
/*
|
||||||
|
/*rtl:end:ignore*/
|
||||||
|
.flatpickr-months .flatpickr-prev-month.flatpickr-next-month,
|
||||||
|
.flatpickr-months .flatpickr-next-month.flatpickr-next-month {
|
||||||
|
/*
|
||||||
|
/*rtl:begin:ignore*/
|
||||||
|
/*
|
||||||
|
*/
|
||||||
|
right: 0;
|
||||||
|
/*
|
||||||
|
/*rtl:end:ignore*/
|
||||||
|
/*
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
/*rtl:begin:ignore*/
|
||||||
|
/*
|
||||||
|
/*rtl:end:ignore*/
|
||||||
|
.flatpickr-months .flatpickr-prev-month:hover,
|
||||||
|
.flatpickr-months .flatpickr-next-month:hover {
|
||||||
|
color: #959ea9;
|
||||||
|
}
|
||||||
|
.flatpickr-months .flatpickr-prev-month:hover svg,
|
||||||
|
.flatpickr-months .flatpickr-next-month:hover svg {
|
||||||
|
fill: #f64747;
|
||||||
|
}
|
||||||
|
.flatpickr-months .flatpickr-prev-month svg,
|
||||||
|
.flatpickr-months .flatpickr-next-month svg {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
.flatpickr-months .flatpickr-prev-month svg path,
|
||||||
|
.flatpickr-months .flatpickr-next-month svg path {
|
||||||
|
-webkit-transition: fill 0.1s;
|
||||||
|
transition: fill 0.1s;
|
||||||
|
fill: inherit;
|
||||||
|
}
|
||||||
|
.numInputWrapper {
|
||||||
|
position: relative;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
.numInputWrapper input,
|
||||||
|
.numInputWrapper span {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.numInputWrapper input {
|
||||||
|
width: 100%;
|
||||||
|
min-width: auto !important;
|
||||||
|
}
|
||||||
|
.numInputWrapper input::-ms-clear {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.numInputWrapper span {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
width: 14px;
|
||||||
|
padding: 0 4px 0 2px;
|
||||||
|
height: 50%;
|
||||||
|
line-height: 50%;
|
||||||
|
opacity: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 1px solid rgba(57,57,57,0.15);
|
||||||
|
-webkit-box-sizing: border-box;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.numInputWrapper span:hover {
|
||||||
|
background: rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
.numInputWrapper span:active {
|
||||||
|
background: rgba(0,0,0,0.2);
|
||||||
|
}
|
||||||
|
.numInputWrapper span:after {
|
||||||
|
display: block;
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
.numInputWrapper span.arrowUp {
|
||||||
|
top: 0;
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
.numInputWrapper span.arrowUp:after {
|
||||||
|
border-left: 4px solid transparent;
|
||||||
|
border-right: 4px solid transparent;
|
||||||
|
border-bottom: 4px solid rgba(57,57,57,0.6);
|
||||||
|
top: 26%;
|
||||||
|
}
|
||||||
|
.numInputWrapper span.arrowDown {
|
||||||
|
top: 50%;
|
||||||
|
}
|
||||||
|
.numInputWrapper span.arrowDown:after {
|
||||||
|
border-left: 4px solid transparent;
|
||||||
|
border-right: 4px solid transparent;
|
||||||
|
border-top: 4px solid rgba(57,57,57,0.6);
|
||||||
|
top: 40%;
|
||||||
|
}
|
||||||
|
.numInputWrapper span svg {
|
||||||
|
width: inherit;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
.numInputWrapper span svg path {
|
||||||
|
fill: rgba(0,0,0,0.5);
|
||||||
|
}
|
||||||
|
.numInputWrapper:hover {
|
||||||
|
background: rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
.numInputWrapper:hover span {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.flatpickr-current-month {
|
||||||
|
font-size: 135%;
|
||||||
|
line-height: inherit;
|
||||||
|
font-weight: 300;
|
||||||
|
color: inherit;
|
||||||
|
position: absolute;
|
||||||
|
width: 75%;
|
||||||
|
left: 12.5%;
|
||||||
|
padding: 6.16px 0 0 0;
|
||||||
|
line-height: 1;
|
||||||
|
height: 28px;
|
||||||
|
display: inline-block;
|
||||||
|
text-align: center;
|
||||||
|
-webkit-transform: translate3d(0px, 0px, 0px);
|
||||||
|
transform: translate3d(0px, 0px, 0px);
|
||||||
|
}
|
||||||
|
.flatpickr-current-month span.cur-month {
|
||||||
|
font-family: inherit;
|
||||||
|
font-weight: 700;
|
||||||
|
color: inherit;
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 0.5ch;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.flatpickr-current-month span.cur-month:hover {
|
||||||
|
background: rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
.flatpickr-current-month .numInputWrapper {
|
||||||
|
width: 6ch;
|
||||||
|
width: 7ch\0;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.flatpickr-current-month .numInputWrapper span.arrowUp:after {
|
||||||
|
border-bottom-color: rgba(0,0,0,0.9);
|
||||||
|
}
|
||||||
|
.flatpickr-current-month .numInputWrapper span.arrowDown:after {
|
||||||
|
border-top-color: rgba(0,0,0,0.9);
|
||||||
|
}
|
||||||
|
.flatpickr-current-month input.cur-year {
|
||||||
|
background: transparent;
|
||||||
|
-webkit-box-sizing: border-box;
|
||||||
|
box-sizing: border-box;
|
||||||
|
color: inherit;
|
||||||
|
cursor: text;
|
||||||
|
padding: 0 0 0 0.5ch;
|
||||||
|
margin: 0;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: inherit;
|
||||||
|
font-family: inherit;
|
||||||
|
font-weight: 300;
|
||||||
|
line-height: inherit;
|
||||||
|
height: auto;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
vertical-align: initial;
|
||||||
|
}
|
||||||
|
.flatpickr-current-month input.cur-year:focus {
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
.flatpickr-current-month input.cur-year[disabled],
|
||||||
|
.flatpickr-current-month input.cur-year[disabled]:hover {
|
||||||
|
font-size: 100%;
|
||||||
|
color: rgba(0,0,0,0.5);
|
||||||
|
background: transparent;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.flatpickr-weekdays {
|
||||||
|
background: transparent;
|
||||||
|
text-align: center;
|
||||||
|
overflow: hidden;
|
||||||
|
width: 100%;
|
||||||
|
display: -webkit-box;
|
||||||
|
display: -webkit-flex;
|
||||||
|
display: -ms-flexbox;
|
||||||
|
display: flex;
|
||||||
|
-webkit-box-align: center;
|
||||||
|
-webkit-align-items: center;
|
||||||
|
-ms-flex-align: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 28px;
|
||||||
|
}
|
||||||
|
.flatpickr-weekdays .flatpickr-weekdaycontainer {
|
||||||
|
display: -webkit-box;
|
||||||
|
display: -webkit-flex;
|
||||||
|
display: -ms-flexbox;
|
||||||
|
display: flex;
|
||||||
|
-webkit-box-flex: 1;
|
||||||
|
-webkit-flex: 1;
|
||||||
|
-ms-flex: 1;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
span.flatpickr-weekday {
|
||||||
|
cursor: default;
|
||||||
|
font-size: 90%;
|
||||||
|
background: transparent;
|
||||||
|
color: rgba(0,0,0,0.54);
|
||||||
|
line-height: 1;
|
||||||
|
margin: 0;
|
||||||
|
text-align: center;
|
||||||
|
display: block;
|
||||||
|
-webkit-box-flex: 1;
|
||||||
|
-webkit-flex: 1;
|
||||||
|
-ms-flex: 1;
|
||||||
|
flex: 1;
|
||||||
|
font-weight: bolder;
|
||||||
|
}
|
||||||
|
.dayContainer,
|
||||||
|
.flatpickr-weeks {
|
||||||
|
padding: 1px 0 0 0;
|
||||||
|
}
|
||||||
|
.flatpickr-days {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
display: -webkit-box;
|
||||||
|
display: -webkit-flex;
|
||||||
|
display: -ms-flexbox;
|
||||||
|
display: flex;
|
||||||
|
-webkit-box-align: start;
|
||||||
|
-webkit-align-items: flex-start;
|
||||||
|
-ms-flex-align: start;
|
||||||
|
align-items: flex-start;
|
||||||
|
width: 307.875px;
|
||||||
|
}
|
||||||
|
.flatpickr-days:focus {
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
.dayContainer {
|
||||||
|
padding: 0;
|
||||||
|
outline: 0;
|
||||||
|
text-align: left;
|
||||||
|
width: 307.875px;
|
||||||
|
min-width: 307.875px;
|
||||||
|
max-width: 307.875px;
|
||||||
|
-webkit-box-sizing: border-box;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: inline-block;
|
||||||
|
display: -ms-flexbox;
|
||||||
|
display: -webkit-box;
|
||||||
|
display: -webkit-flex;
|
||||||
|
display: flex;
|
||||||
|
-webkit-flex-wrap: wrap;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
-ms-flex-wrap: wrap;
|
||||||
|
-ms-flex-pack: justify;
|
||||||
|
-webkit-justify-content: space-around;
|
||||||
|
justify-content: space-around;
|
||||||
|
-webkit-transform: translate3d(0px, 0px, 0px);
|
||||||
|
transform: translate3d(0px, 0px, 0px);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.dayContainer + .dayContainer {
|
||||||
|
-webkit-box-shadow: -1px 0 0 #e6e6e6;
|
||||||
|
box-shadow: -1px 0 0 #e6e6e6;
|
||||||
|
}
|
||||||
|
.flatpickr-day {
|
||||||
|
background: none;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 150px;
|
||||||
|
-webkit-box-sizing: border-box;
|
||||||
|
box-sizing: border-box;
|
||||||
|
color: #393939;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 400;
|
||||||
|
width: 14.2857143%;
|
||||||
|
-webkit-flex-basis: 14.2857143%;
|
||||||
|
-ms-flex-preferred-size: 14.2857143%;
|
||||||
|
flex-basis: 14.2857143%;
|
||||||
|
max-width: 39px;
|
||||||
|
height: 39px;
|
||||||
|
line-height: 39px;
|
||||||
|
margin: 0;
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
-webkit-box-pack: center;
|
||||||
|
-webkit-justify-content: center;
|
||||||
|
-ms-flex-pack: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.flatpickr-day.inRange,
|
||||||
|
.flatpickr-day.prevMonthDay.inRange,
|
||||||
|
.flatpickr-day.nextMonthDay.inRange,
|
||||||
|
.flatpickr-day.today.inRange,
|
||||||
|
.flatpickr-day.prevMonthDay.today.inRange,
|
||||||
|
.flatpickr-day.nextMonthDay.today.inRange,
|
||||||
|
.flatpickr-day:hover,
|
||||||
|
.flatpickr-day.prevMonthDay:hover,
|
||||||
|
.flatpickr-day.nextMonthDay:hover,
|
||||||
|
.flatpickr-day:focus,
|
||||||
|
.flatpickr-day.prevMonthDay:focus,
|
||||||
|
.flatpickr-day.nextMonthDay:focus {
|
||||||
|
cursor: pointer;
|
||||||
|
outline: 0;
|
||||||
|
background: #e6e6e6;
|
||||||
|
border-color: #e6e6e6;
|
||||||
|
}
|
||||||
|
.flatpickr-day.today {
|
||||||
|
border-color: #959ea9;
|
||||||
|
}
|
||||||
|
.flatpickr-day.today:hover,
|
||||||
|
.flatpickr-day.today:focus {
|
||||||
|
border-color: #959ea9;
|
||||||
|
background: #959ea9;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.flatpickr-day.selected,
|
||||||
|
.flatpickr-day.startRange,
|
||||||
|
.flatpickr-day.endRange,
|
||||||
|
.flatpickr-day.selected.inRange,
|
||||||
|
.flatpickr-day.startRange.inRange,
|
||||||
|
.flatpickr-day.endRange.inRange,
|
||||||
|
.flatpickr-day.selected:focus,
|
||||||
|
.flatpickr-day.startRange:focus,
|
||||||
|
.flatpickr-day.endRange:focus,
|
||||||
|
.flatpickr-day.selected:hover,
|
||||||
|
.flatpickr-day.startRange:hover,
|
||||||
|
.flatpickr-day.endRange:hover,
|
||||||
|
.flatpickr-day.selected.prevMonthDay,
|
||||||
|
.flatpickr-day.startRange.prevMonthDay,
|
||||||
|
.flatpickr-day.endRange.prevMonthDay,
|
||||||
|
.flatpickr-day.selected.nextMonthDay,
|
||||||
|
.flatpickr-day.startRange.nextMonthDay,
|
||||||
|
.flatpickr-day.endRange.nextMonthDay {
|
||||||
|
background: #569ff7;
|
||||||
|
-webkit-box-shadow: none;
|
||||||
|
box-shadow: none;
|
||||||
|
color: #fff;
|
||||||
|
border-color: #569ff7;
|
||||||
|
}
|
||||||
|
.flatpickr-day.selected.startRange,
|
||||||
|
.flatpickr-day.startRange.startRange,
|
||||||
|
.flatpickr-day.endRange.startRange {
|
||||||
|
border-radius: 50px 0 0 50px;
|
||||||
|
}
|
||||||
|
.flatpickr-day.selected.endRange,
|
||||||
|
.flatpickr-day.startRange.endRange,
|
||||||
|
.flatpickr-day.endRange.endRange {
|
||||||
|
border-radius: 0 50px 50px 0;
|
||||||
|
}
|
||||||
|
.flatpickr-day.selected.startRange + .endRange,
|
||||||
|
.flatpickr-day.startRange.startRange + .endRange,
|
||||||
|
.flatpickr-day.endRange.startRange + .endRange {
|
||||||
|
-webkit-box-shadow: -10px 0 0 #569ff7;
|
||||||
|
box-shadow: -10px 0 0 #569ff7;
|
||||||
|
}
|
||||||
|
.flatpickr-day.selected.startRange.endRange,
|
||||||
|
.flatpickr-day.startRange.startRange.endRange,
|
||||||
|
.flatpickr-day.endRange.startRange.endRange {
|
||||||
|
border-radius: 50px;
|
||||||
|
}
|
||||||
|
.flatpickr-day.inRange {
|
||||||
|
border-radius: 0;
|
||||||
|
-webkit-box-shadow: -5px 0 0 #e6e6e6, 5px 0 0 #e6e6e6;
|
||||||
|
box-shadow: -5px 0 0 #e6e6e6, 5px 0 0 #e6e6e6;
|
||||||
|
}
|
||||||
|
.flatpickr-day.disabled,
|
||||||
|
.flatpickr-day.disabled:hover,
|
||||||
|
.flatpickr-day.prevMonthDay,
|
||||||
|
.flatpickr-day.nextMonthDay,
|
||||||
|
.flatpickr-day.notAllowed,
|
||||||
|
.flatpickr-day.notAllowed.prevMonthDay,
|
||||||
|
.flatpickr-day.notAllowed.nextMonthDay {
|
||||||
|
color: rgba(57,57,57,0.3);
|
||||||
|
background: transparent;
|
||||||
|
border-color: transparent;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
.flatpickr-day.disabled,
|
||||||
|
.flatpickr-day.disabled:hover {
|
||||||
|
cursor: not-allowed;
|
||||||
|
color: rgba(57,57,57,0.1);
|
||||||
|
}
|
||||||
|
.flatpickr-day.week.selected {
|
||||||
|
border-radius: 0;
|
||||||
|
-webkit-box-shadow: -5px 0 0 #569ff7, 5px 0 0 #569ff7;
|
||||||
|
box-shadow: -5px 0 0 #569ff7, 5px 0 0 #569ff7;
|
||||||
|
}
|
||||||
|
.flatpickr-day.hidden {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
.rangeMode .flatpickr-day {
|
||||||
|
margin-top: 1px;
|
||||||
|
}
|
||||||
|
.flatpickr-weekwrapper {
|
||||||
|
display: inline-block;
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
.flatpickr-weekwrapper .flatpickr-weeks {
|
||||||
|
padding: 0 12px;
|
||||||
|
-webkit-box-shadow: 1px 0 0 #e6e6e6;
|
||||||
|
box-shadow: 1px 0 0 #e6e6e6;
|
||||||
|
}
|
||||||
|
.flatpickr-weekwrapper .flatpickr-weekday {
|
||||||
|
float: none;
|
||||||
|
width: 100%;
|
||||||
|
line-height: 28px;
|
||||||
|
}
|
||||||
|
.flatpickr-weekwrapper span.flatpickr-day,
|
||||||
|
.flatpickr-weekwrapper span.flatpickr-day:hover {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
max-width: none;
|
||||||
|
color: rgba(57,57,57,0.3);
|
||||||
|
background: transparent;
|
||||||
|
cursor: default;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
.flatpickr-innerContainer {
|
||||||
|
display: block;
|
||||||
|
display: -webkit-box;
|
||||||
|
display: -webkit-flex;
|
||||||
|
display: -ms-flexbox;
|
||||||
|
display: flex;
|
||||||
|
-webkit-box-sizing: border-box;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.flatpickr-rContainer {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0;
|
||||||
|
-webkit-box-sizing: border-box;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.flatpickr-time {
|
||||||
|
text-align: center;
|
||||||
|
outline: 0;
|
||||||
|
display: block;
|
||||||
|
height: 0;
|
||||||
|
line-height: 40px;
|
||||||
|
max-height: 40px;
|
||||||
|
-webkit-box-sizing: border-box;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow: hidden;
|
||||||
|
display: -webkit-box;
|
||||||
|
display: -webkit-flex;
|
||||||
|
display: -ms-flexbox;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.flatpickr-time:after {
|
||||||
|
content: "";
|
||||||
|
display: table;
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
.flatpickr-time .numInputWrapper {
|
||||||
|
-webkit-box-flex: 1;
|
||||||
|
-webkit-flex: 1;
|
||||||
|
-ms-flex: 1;
|
||||||
|
flex: 1;
|
||||||
|
width: 40%;
|
||||||
|
height: 40px;
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
.flatpickr-time .numInputWrapper span.arrowUp:after {
|
||||||
|
border-bottom-color: #393939;
|
||||||
|
}
|
||||||
|
.flatpickr-time .numInputWrapper span.arrowDown:after {
|
||||||
|
border-top-color: #393939;
|
||||||
|
}
|
||||||
|
.flatpickr-time.hasSeconds .numInputWrapper {
|
||||||
|
width: 26%;
|
||||||
|
}
|
||||||
|
.flatpickr-time.time24hr .numInputWrapper {
|
||||||
|
width: 49%;
|
||||||
|
}
|
||||||
|
.flatpickr-time input {
|
||||||
|
background: transparent;
|
||||||
|
-webkit-box-shadow: none;
|
||||||
|
box-shadow: none;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
text-align: center;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
height: inherit;
|
||||||
|
line-height: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #393939;
|
||||||
|
font-size: 14px;
|
||||||
|
position: relative;
|
||||||
|
-webkit-box-sizing: border-box;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.flatpickr-time input.flatpickr-hour {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.flatpickr-time input.flatpickr-minute,
|
||||||
|
.flatpickr-time input.flatpickr-second {
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
.flatpickr-time input:focus {
|
||||||
|
outline: 0;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
.flatpickr-time .flatpickr-time-separator,
|
||||||
|
.flatpickr-time .flatpickr-am-pm {
|
||||||
|
height: inherit;
|
||||||
|
display: inline-block;
|
||||||
|
float: left;
|
||||||
|
line-height: inherit;
|
||||||
|
color: #393939;
|
||||||
|
font-weight: bold;
|
||||||
|
width: 2%;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-align-self: center;
|
||||||
|
-ms-flex-item-align: center;
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
.flatpickr-time .flatpickr-am-pm {
|
||||||
|
outline: 0;
|
||||||
|
width: 18%;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
.flatpickr-time .flatpickr-am-pm:hover,
|
||||||
|
.flatpickr-time .flatpickr-am-pm:focus {
|
||||||
|
background: #f0f0f0;
|
||||||
|
}
|
||||||
|
.flatpickr-input[readonly] {
|
||||||
|
cursor: pointer;
|
||||||
|
min-width: auto;
|
||||||
|
}
|
||||||
|
@-webkit-keyframes fpFadeInDown {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
-webkit-transform: translate3d(0, -20px, 0);
|
||||||
|
transform: translate3d(0, -20px, 0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
-webkit-transform: translate3d(0, 0, 0);
|
||||||
|
transform: translate3d(0, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes fpFadeInDown {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
-webkit-transform: translate3d(0, -20px, 0);
|
||||||
|
transform: translate3d(0, -20px, 0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
-webkit-transform: translate3d(0, 0, 0);
|
||||||
|
transform: translate3d(0, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
5
static/css/fontawesome.css
vendored
Normal file
5
static/css/fontawesome.css
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -1,9 +1,29 @@
|
|||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Glyphicons Halflings';
|
font-family: 'Glyphicons Halflings';
|
||||||
src: url('../fonts/glyphicons-halflings-regular.eot');
|
src: url('../fonts/glyphicons/glyphicons-halflings-regular.eot');
|
||||||
src: url('../fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'),
|
src: url('../fonts/glyphicons/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'),
|
||||||
url('../fonts/glyphicons-halflings-regular.woff2') format('woff2'),
|
url('../fonts/glyphicons/glyphicons-halflings-regular.woff2') format('woff2'),
|
||||||
url('../fonts/glyphicons-halflings-regular.woff') format('woff'),
|
url('../fonts/glyphicons/glyphicons-halflings-regular.woff') format('woff'),
|
||||||
url('../fonts/glyphicons-halflings-regular.ttf') format('truetype'),
|
url('../fonts/glyphicons/glyphicons-halflings-regular.ttf') format('truetype'),
|
||||||
url('../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg');
|
url('../fonts/glyphicons/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* Font Awesome Free 5.1.0 by @fontawesome - https://fontawesome.com
|
||||||
|
* License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
|
||||||
|
*/
|
||||||
|
@font-face{
|
||||||
|
font-family:"Font Awesome 5 Free";
|
||||||
|
font-style:normal;
|
||||||
|
font-weight:900;
|
||||||
|
src:url(../fonts/fontawesome/fa-solid-900.eot);
|
||||||
|
src:url(../fonts/fontawesome/fa-solid-900.eot?#iefix) format("embedded-opentype"),
|
||||||
|
url(../fonts/fontawesome/fa-solid-900.woff2) format("woff2"),
|
||||||
|
url(../fonts/fontawesome/fa-solid-900.woff) format("woff"),
|
||||||
|
url(../fonts/fontawesome/fa-solid-900.ttf) format("truetype"),
|
||||||
|
url(../fonts/fontawesome/fa-solid-900.svg#fontawesome) format("svg");
|
||||||
|
}
|
||||||
|
.fa,.fas{
|
||||||
|
font-family:"Font Awesome 5 Free";
|
||||||
|
font-weight:900;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,9 +26,19 @@
|
|||||||
.glyphicon--user::before {
|
.glyphicon--user::before {
|
||||||
content: '\e008';
|
content: '\e008';
|
||||||
}
|
}
|
||||||
|
.glyphicon--group::before {
|
||||||
|
/* TODO: get updated glyphicons for group-icon */
|
||||||
|
content: '\e284';
|
||||||
|
}
|
||||||
|
.glyphicon--education::before {
|
||||||
|
content: '\e233';
|
||||||
|
}
|
||||||
.glyphicon--login::before {
|
.glyphicon--login::before {
|
||||||
content: '\e161';
|
content: '\e161';
|
||||||
}
|
}
|
||||||
.glyphicon--logout::before {
|
.glyphicon--logout::before {
|
||||||
content: '\e163';
|
content: '\e163';
|
||||||
}
|
}
|
||||||
|
.glyphicon--none::before {
|
||||||
|
content: '';
|
||||||
|
}
|
||||||
|
|||||||
@ -1,8 +1,6 @@
|
|||||||
.tab-group {
|
.tab-group {
|
||||||
/* box-shadow: 0 0 0 18px white, 0 0 0 20px #b3b7c1; */
|
|
||||||
border-top: 2px solid #dcdcdc;
|
border-top: 2px solid #dcdcdc;
|
||||||
padding-top: 30px;
|
padding-top: 30px;
|
||||||
margin-top: 40px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-group-openers {
|
.tab-group-openers {
|
||||||
|
|||||||
BIN
static/fonts/fontawesome/fa-solid-900.eot
Normal file
BIN
static/fonts/fontawesome/fa-solid-900.eot
Normal file
Binary file not shown.
2231
static/fonts/fontawesome/fa-solid-900.svg
Normal file
2231
static/fonts/fontawesome/fa-solid-900.svg
Normal file
File diff suppressed because it is too large
Load Diff
|
After Width: | Height: | Size: 579 KiB |
BIN
static/fonts/fontawesome/fa-solid-900.ttf
Normal file
BIN
static/fonts/fontawesome/fa-solid-900.ttf
Normal file
Binary file not shown.
BIN
static/fonts/fontawesome/fa-solid-900.woff
Normal file
BIN
static/fonts/fontawesome/fa-solid-900.woff
Normal file
Binary file not shown.
BIN
static/fonts/fontawesome/fa-solid-900.woff2
Normal file
BIN
static/fonts/fontawesome/fa-solid-900.woff2
Normal file
Binary file not shown.
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
2
static/js/flatpickr.js
Normal file
2
static/js/flatpickr.js
Normal file
File diff suppressed because one or more lines are too long
@ -80,9 +80,11 @@
|
|||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// apply plugin to all available tab-groups
|
// apply plugin to all available tab-groups if on wide screen
|
||||||
$('.tab-group').each(function(i, t) {
|
if (window.innerWidth > 768) {
|
||||||
$(t).tabgroup();
|
$('.tab-group').each(function(i, t) {
|
||||||
})
|
$(t).tabgroup();
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
})($);
|
})($);
|
||||||
|
|||||||
44
templates/adminTest.hamlet
Normal file
44
templates/adminTest.hamlet
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
<div .container>
|
||||||
|
<h1>Uniworky - Admin Demopage
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Diese interne Seite dient lediglich zum Testen diverser Funktionalitäten
|
||||||
|
und zur Demonstration der verschiedenen Hilfsfunktionen/Module.
|
||||||
|
|
||||||
|
Der Handler sollte jeweils aktuelle Beispiele für alle möglichen Funktionalitäten enthalten, so dass man immer weiß, wo man nachschlagen kann.
|
||||||
|
|
||||||
|
|
||||||
|
<div .container>
|
||||||
|
<h2 .js-show-hide__toggle>Teilweise funktionierende Abschnitte
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li .list-group-item>
|
||||||
|
<a href=@{UsersR}>Benutzer Verwaltung
|
||||||
|
|
||||||
|
<li .list-group-item>
|
||||||
|
<a href=@{TermShowR}>Semester Verwaltung
|
||||||
|
<a href=@{TermEditR}>Neues Semester anlegen
|
||||||
|
|
||||||
|
<li .list-group-item>
|
||||||
|
<a href=@{CourseNewR}>Kurse anlegen
|
||||||
|
|
||||||
|
<li .list-group-item>
|
||||||
|
<a href=@{SubmissionListR}>Dateien hochladen und abrufen
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
<div .container>
|
||||||
|
<h2>Funktionen zum Testen
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
Knopf-Test:
|
||||||
|
<form .form-inline method=post action=@{AdminTestR} enctype=#{btnEnctype}>
|
||||||
|
^{btnWdgt}
|
||||||
|
<li><br>
|
||||||
|
Modals:
|
||||||
|
^{modal ".toggler1" Nothing}
|
||||||
|
<a href="/" .btn.toggler1>Klick mich für Ajax-Test
|
||||||
|
<noscript>(Für Modals bitte JS aktivieren)</noscript>
|
||||||
|
^{modal ".toggler2" (Just "Test Inhalt für Modal")}
|
||||||
|
<div .btn.toggler2>Klick mich für Content-Test
|
||||||
|
<noscript>(Für Modals bitte JS aktivieren)</noscript>
|
||||||
@ -1,102 +1,53 @@
|
|||||||
<div .container>
|
<div .container>
|
||||||
<h2>#{courseName course}
|
<div .scrolltable>
|
||||||
<table>
|
<table .table.table--striped.table--hover.table--vertical>
|
||||||
$maybe school <- schoolMB
|
$maybe school <- schoolMB
|
||||||
<tr>
|
<tr .table__row>
|
||||||
<th #school>Fakultät/Institut
|
<th #school>Fakultät/Institut
|
||||||
|
<td>
|
||||||
|
#{schoolName school}
|
||||||
|
$maybe descr <- courseDescription course
|
||||||
|
<tr .table__row>
|
||||||
|
<th #description>Beschreibung
|
||||||
|
<td>
|
||||||
|
#{descr}
|
||||||
|
$maybe link <- courseLinkExternal course
|
||||||
|
<tr .table__row>
|
||||||
|
<th #website>Website
|
||||||
|
<td>
|
||||||
|
<a href=#{link} target="_blank" rel="noopener" title="Website des Kurses">#{link}
|
||||||
|
<tr .table__row>
|
||||||
|
<th #participants>Teilnehmer
|
||||||
<td>
|
<td>
|
||||||
#{schoolName school}
|
#{participants}
|
||||||
$maybe descr <- courseDescription course
|
$maybe capacity <- courseCapacity course
|
||||||
<tr>
|
\ von #{capacity}
|
||||||
<th #description>Beschreibung
|
<tr .table__row>
|
||||||
|
<th #registration>Anmeldezeitraum
|
||||||
<td>
|
<td>
|
||||||
<p>#{descr}
|
$maybe regFrom <- courseRegisterFrom course
|
||||||
$maybe link <- courseLinkExternal course
|
#{formatTimeGerWD regFrom}
|
||||||
<tr>
|
$maybe regTo <- courseRegisterTo course
|
||||||
<th #website>Website
|
\ bis #{formatTimeGerWD regTo}
|
||||||
|
|
||||||
|
<tr .table__row>
|
||||||
|
<th>
|
||||||
<td>
|
<td>
|
||||||
<a href=#{link}>#{link}
|
$if registrationOpen
|
||||||
<tr>
|
<div .course__registration.container>
|
||||||
<th #participants>Teilnehmer
|
<form method=post action=@{CourseR tid csh CRegisterR} enctype=#{regEnctype}>
|
||||||
<td>
|
$# regWidget is defined through templates/widgets/registerForm
|
||||||
#{participants}
|
^{regWidget}
|
||||||
$maybe capacity <- courseCapacity course
|
|
||||||
\ von #{capacity}
|
|
||||||
<tr>
|
|
||||||
<th #registration>Anmeldezeitraum
|
|
||||||
<td>
|
|
||||||
$maybe regFrom <- courseRegisterFrom course
|
|
||||||
#{formatTimeGerWD regFrom}
|
|
||||||
$maybe regTo <- courseRegisterTo course
|
|
||||||
\ bis #{formatTimeGerWD regTo}
|
|
||||||
|
|
||||||
$# if allowed to register
|
$# <div .container>
|
||||||
<div .course__registration>
|
$# <div .tab-group>
|
||||||
<a href="#">Anmelden
|
$# <div .tab data-tab-name="Übungsblätter">
|
||||||
|
$# ^{modal "#modal-toggler__new-sheet" Nothing}
|
||||||
$# <form method=post action=@{CourseR tid csh CShow} enctype=#{regEnctype}>
|
$# <h3 .tab-title>Übungsblätter
|
||||||
$# ^{regWidget}
|
$# <h1>TODO: Sortierbare Tabelle der bisherigen Übungsblätter
|
||||||
|
$# <div .tab data-tab-name="Übungsgruppen">
|
||||||
<div .container>
|
$# <h3 .tab-title>Übungsgruppen
|
||||||
<div .tab-group>
|
$# <h1>TODO: Sortierbare Tabelle der Übungsgruppen
|
||||||
<div .tab data-tab-name="Übungsblätter">
|
$# <div .tab data-tab-name="Klausuren">
|
||||||
^{modal "#modal-toggler__new-sheet" Nothing}
|
$# <h3 .tab-title>Klausuren
|
||||||
<h3 .tab-title>Übungsblätter
|
$# <div>...
|
||||||
<table .table.table-striped.table-hover>
|
|
||||||
<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>
|
|
||||||
<td>
|
|
||||||
<td>
|
|
||||||
<td>
|
|
||||||
<td>
|
|
||||||
<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>
|
|
||||||
<a href="#">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>
|
|
||||||
<a href="#">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>...
|
|
||||||
|
|||||||
@ -0,0 +1,8 @@
|
|||||||
|
th {
|
||||||
|
vertical-align: top;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
th, td {
|
||||||
|
padding-bottom: 7px;
|
||||||
|
}
|
||||||
@ -1,4 +1,3 @@
|
|||||||
<div .container>
|
<div .container>
|
||||||
<h1>Kursübersicht für Semester #{termToText $ unTermKey tidini}
|
|
||||||
<div .scrolltable>
|
<div .scrolltable>
|
||||||
^{coursesTable}
|
^{coursesTable}
|
||||||
|
|||||||
@ -39,7 +39,7 @@ $newline never
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
<body .no-js>
|
<body .no-js .#{currentTheme}>
|
||||||
<!-- removes no-js class from body if client supports javascript -->
|
<!-- removes no-js class from body if client supports javascript -->
|
||||||
<script>
|
<script>
|
||||||
document.body.classList.remove('no-js');
|
document.body.classList.remove('no-js');
|
||||||
|
|||||||
@ -8,13 +8,28 @@
|
|||||||
|
|
||||||
<div .main__content>
|
<div .main__content>
|
||||||
|
|
||||||
<!-- alerts -->
|
<!-- breadcrumbs -->
|
||||||
$forall (status, msg) <- mmsgs
|
$if not $ Just HomeR == mcurrentRoute
|
||||||
$with status2 <- bool status "info" (status == "")
|
^{breadcrumbs}
|
||||||
<div class="alert alert-#{status2}">#{msg}
|
|
||||||
|
|
||||||
<!-- prime page actions -->
|
<div .main__content-body>
|
||||||
^{pageactionprime}
|
|
||||||
|
|
||||||
<!-- actual content -->
|
<h1>
|
||||||
^{widget}
|
$maybe headline <- contentHeadline
|
||||||
|
^{headline}
|
||||||
|
$nothing
|
||||||
|
HEADLINE MISSING!
|
||||||
|
|
||||||
|
<!-- prime page actions -->
|
||||||
|
^{pageactionprime}
|
||||||
|
|
||||||
|
<!-- alerts -->
|
||||||
|
$forall (status, msg) <- mmsgs
|
||||||
|
$with status2 <- bool status "info" (status == "")
|
||||||
|
<div class="alert alert-#{status2}">
|
||||||
|
<div .alert__content>
|
||||||
|
#{msg}
|
||||||
|
|
||||||
|
<!-- actual content -->
|
||||||
|
|
||||||
|
^{widget}
|
||||||
|
|||||||
@ -1,47 +1,19 @@
|
|||||||
: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 */
|
|
||||||
--darkerbase: #274a65;
|
|
||||||
--darkbase: #425d79;
|
|
||||||
--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: #ff3860;
|
||||||
--warningbase: #fe7700;
|
--color-warning: #ffdd57;
|
||||||
--validbase: #2dcc35;
|
--color-success: #23d160;
|
||||||
--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;
|
||||||
|
--font-logo: "Roboto", var(--font-base);
|
||||||
|
|
||||||
/* DIMENSIONS */
|
/* DIMENSIONS */
|
||||||
--header-height: 80px;
|
--header-height: 80px;
|
||||||
@ -56,13 +28,63 @@
|
|||||||
|
|
||||||
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: rgb(42, 74, 88);
|
||||||
|
--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;
|
||||||
@ -70,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;
|
||||||
}
|
}
|
||||||
@ -87,68 +118,47 @@ h1 {
|
|||||||
}
|
}
|
||||||
h2 {
|
h2 {
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
margin: 10px 0 5px;
|
margin: 10px 0;
|
||||||
}
|
}
|
||||||
h3 {
|
h3 {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
margin: 5px 0;
|
margin: 10px 0;
|
||||||
}
|
}
|
||||||
h4 {
|
h4 {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
table {
|
|
||||||
margin: 21px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scrolltable {
|
@media (max-width: 768px) {
|
||||||
width: 100%;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-striped {
|
h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
tbody {
|
h2 {
|
||||||
tr:not(.no-stripe):nth-child(even) {
|
font-size: 20px;
|
||||||
background-color: #e8e8e8;
|
}
|
||||||
}
|
|
||||||
|
h3 {
|
||||||
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-hover {
|
|
||||||
|
|
||||||
tbody {
|
|
||||||
tr:not(.no-hover):hover {
|
|
||||||
background-color: #d8d8d8;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
th, td {
|
|
||||||
text-align: left;
|
|
||||||
padding: 0 13px 0 7px;
|
|
||||||
vertical-align: baseline;
|
|
||||||
}
|
|
||||||
th:first-child,
|
|
||||||
td:first-child {
|
|
||||||
padding-left: 0;
|
|
||||||
border-left: 0;
|
|
||||||
}
|
|
||||||
th {
|
|
||||||
border-left: 2px solid var(--greybase);
|
|
||||||
}
|
|
||||||
/* LAYOUT */
|
/* LAYOUT */
|
||||||
.main {
|
.main {
|
||||||
display: flex;
|
|
||||||
min-height: calc(100vh - var(--header-height));
|
min-height: calc(100vh - var(--header-height));
|
||||||
overflow: hidden;
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
|
||||||
|
.main {
|
||||||
|
min-height: calc(100vh - var(--header-height-collapsed));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.main__content {
|
.main__content {
|
||||||
position: relative;
|
position: relative;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
padding: 0 40px;
|
|
||||||
flex: 1;
|
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
@ -159,18 +169,21 @@ th {
|
|||||||
p {
|
p {
|
||||||
margin: 10px 0;
|
margin: 10px 0;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
a {
|
.main__content-body {
|
||||||
color: var(--darkbase);
|
padding: 0 40px 60px;
|
||||||
}
|
}
|
||||||
|
|
||||||
a:hover {
|
@media (max-width: 768px) {
|
||||||
color: var(--lightbase);
|
|
||||||
|
.main__content-body {
|
||||||
|
padding: 10px 20px 60px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -182,28 +195,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],
|
||||||
@ -212,7 +219,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -221,20 +228,127 @@ 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 {
|
/* GENERAL TABLE STYLES */
|
||||||
background-color: rgb(240, 30, 240);
|
.table {
|
||||||
|
margin: 21px 0;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.table--striped {
|
||||||
|
|
||||||
|
.table__row:not(.no-stripe):nth-child(even) {
|
||||||
|
background-color: rgba(0, 0, 0, 0.03);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.table--hover {
|
||||||
|
|
||||||
|
.table__row:not(.no-hover):not(.table__row--head):hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.07);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* SCROLLTABLE */
|
||||||
|
.scrolltable {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* TABLE DESIGN */
|
||||||
|
.table__row {
|
||||||
|
|
||||||
|
/* TODO: move outside of table__row as soon as tds and ths get their own class */
|
||||||
|
/* .table__td, .table__th { */
|
||||||
|
td, th {
|
||||||
|
padding-top: 14px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
padding-left: 10px;
|
||||||
|
padding-right: 10px;
|
||||||
|
max-width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* .table__td { */
|
||||||
|
td {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #808080;
|
||||||
|
line-height: 1.4;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* .table__th { */
|
||||||
|
th {
|
||||||
|
background-color: var(--color-dark);
|
||||||
|
position: relative;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #fff;
|
||||||
|
line-height: 1.4;
|
||||||
|
padding-top: 10px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
|
||||||
|
.table th {
|
||||||
|
padding: 4px 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.table__td-content {
|
||||||
|
max-height: 100px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table__th-link {
|
||||||
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.table--vertical {
|
||||||
|
|
||||||
|
th {
|
||||||
|
background-color: transparent;
|
||||||
|
color: var(--color-font);
|
||||||
|
width: 170px;
|
||||||
|
text-align: right;
|
||||||
|
padding-right: 15px;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-font);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
15
templates/dsgvDisclaimer.hamlet
Normal file
15
templates/dsgvDisclaimer.hamlet
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<div .notification .notification-danger>
|
||||||
|
<div .notification__content>
|
||||||
|
<h1>
|
||||||
|
Hinweis zum Datenschutz
|
||||||
|
<p>
|
||||||
|
Dieses experimentelle Programm wurde noch nicht
|
||||||
|
hinsichtlich des Datenschutzes überprüft.
|
||||||
|
<em>
|
||||||
|
Die Benutzung erfolgt derzeit freiwillig und auf eigene Gefahr!
|
||||||
|
|
||||||
|
Wir sind natürlich bemüht, alle Datenschutzrechtlichen Vorgaben
|
||||||
|
zu erfüllen, doch eine Überprüfung kann erst stattfinden,
|
||||||
|
sobald die Software weitestgehend fertiggestellt wurde und
|
||||||
|
sich nicht mehr verändert. Um dies zu Erreichen sind jedoch Test
|
||||||
|
unter realen Bedingungen erforderlich. Wir bitten um Ihr Verständnis.
|
||||||
50
templates/dsgvDisclaimer.lucius
Normal file
50
templates/dsgvDisclaimer.lucius
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
.notification {
|
||||||
|
position: relative;
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 10px 20px 20px;
|
||||||
|
margin: 40px 0;
|
||||||
|
color: var(--color-lighter);
|
||||||
|
box-shadow: 0 0 4px 2px inset currentColor;
|
||||||
|
padding-left: 20%;
|
||||||
|
color: #318dc5 ;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: 'i';
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
height: 100%;
|
||||||
|
width: 20%;
|
||||||
|
font-size: 100px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
|
||||||
|
.notification {
|
||||||
|
|
||||||
|
padding-left: 40px;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
height: auto;
|
||||||
|
width: 45px;
|
||||||
|
font-size: 40px;
|
||||||
|
top: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-danger {
|
||||||
|
color: #c51919 ;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '!';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification__content {
|
||||||
|
color: var(--color-font);
|
||||||
|
}
|
||||||
@ -1,12 +1,2 @@
|
|||||||
<div .container>
|
<form .form-horizontal method=post action=@{actionUrl}#forms enctype=#{formEnctype}>
|
||||||
<div .bs-docs-section>
|
^{formWidget}
|
||||||
<div .row>
|
|
||||||
<div .col-lg-12>
|
|
||||||
<div .page-header>
|
|
||||||
<h1 #forms>
|
|
||||||
#{formTitle}
|
|
||||||
<div .row>
|
|
||||||
<div .col-md-10 .col-lg-9>
|
|
||||||
<div .bs-callout bs-callout-info well>
|
|
||||||
<form .form-horizontal method=post action=@{actionUrl}#forms enctype=#{formEnctype}>
|
|
||||||
^{formWidget}
|
|
||||||
|
|||||||
@ -1,15 +1,5 @@
|
|||||||
<div .container>
|
$maybe text <- formText
|
||||||
<div .bs-docs-section>
|
<h3>
|
||||||
<div .row>
|
_{text}
|
||||||
<div .col-lg-12>
|
<form .form-horizontal method=post action=@{actionUrl}#forms enctype=#{formEnctype}>
|
||||||
<div .page-header>
|
^{formWidget}
|
||||||
<h1 #forms>
|
|
||||||
_{formTitle}
|
|
||||||
$maybe text <- formText
|
|
||||||
_{text}
|
|
||||||
|
|
||||||
<div .row>
|
|
||||||
<div .col-md-10 .col-lg-9>
|
|
||||||
<div .bs-callout bs-callout-info well>
|
|
||||||
<form .form-horizontal method=post action=@{actionUrl}#forms enctype=#{formEnctype}>
|
|
||||||
^{formWidget}
|
|
||||||
|
|||||||
@ -1,63 +1,14 @@
|
|||||||
<div .container>
|
<div .container>
|
||||||
<h1>Uniworky - Demo
|
|
||||||
<h3>
|
<h3>
|
||||||
Testumgebung für die Re-Implementierung von <a href="https://uniworx.ifi.lmu.de/">UniWorX</a>
|
Re-Implementierung von <a href="https://uniworx.ifi.lmu.de/">UniWorX</a>
|
||||||
<p>
|
|
||||||
Die Reimplementierung von
|
|
||||||
UniWorX ist noch nicht abgeschlossen.
|
|
||||||
|
|
||||||
<p .alert .alert-danger>Das System ist noch nicht produktiv einsetzbar
|
<div .alert .alert-danger>
|
||||||
|
<div .alert__content>
|
||||||
|
Vorabversion!
|
||||||
|
Die Implementierung von
|
||||||
|
UniWorkY ist noch nicht abgeschlossen.
|
||||||
|
|
||||||
<hr>
|
<h1>Kurse mit offener Registrierung
|
||||||
<div .container>
|
<div .container>
|
||||||
<h2 .js-show-hide__toggle>Teilweise funktionierende Abschnitte
|
^{courseTable}
|
||||||
|
|
||||||
<ul>
|
|
||||||
<li .list-group-item>
|
|
||||||
<a href=@{UsersR}>Benutzer Verwaltung
|
|
||||||
|
|
||||||
<li .list-group-item>
|
|
||||||
<a href=@{TermShowR}>Semester Verwaltung
|
|
||||||
<a href=@{TermEditR}>Neues Semester anlegen
|
|
||||||
|
|
||||||
<li .list-group-item>
|
|
||||||
<a href=@{CourseNewR}>Kurse anlegen
|
|
||||||
|
|
||||||
<li .list-group-item>
|
|
||||||
<a href=@{SubmissionListR}>Dateien hochladen und abrufen
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
<div .container>
|
|
||||||
<h2>Funktionen zum Testen
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
Knopf-Test:
|
|
||||||
<form .form-inline method=post action=@{HomeR} enctype=#{btnEnctype}>
|
|
||||||
^{btnWdgt}
|
|
||||||
<li><br>
|
|
||||||
Modals:
|
|
||||||
^{modal ".toggler1" Nothing}
|
|
||||||
<a href="/" .btn.toggler1>Klick mich für Ajax-Test
|
|
||||||
<noscript>(Für Modals bitte JS aktivieren)</noscript>
|
|
||||||
^{modal ".toggler2" (Just "Test Inhalt für Modal")}
|
|
||||||
<div .btn.toggler2>Klick mich für Content-Test
|
|
||||||
<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">
|
|
||||||
|
|||||||
23
templates/homeUser.hamlet
Normal file
23
templates/homeUser.hamlet
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<div .container>
|
||||||
|
<h3>
|
||||||
|
Re-Implementierung von <a href="https://uniworx.ifi.lmu.de/">UniWorX</a>
|
||||||
|
|
||||||
|
<div .alert .alert-danger>
|
||||||
|
<div .alert__content>
|
||||||
|
Vorabversion!
|
||||||
|
Die Implementierung von
|
||||||
|
UniWorkY ist noch nicht abgeschlossen.
|
||||||
|
|
||||||
|
<h1>Anstehende Übungsblätter
|
||||||
|
<div .container>
|
||||||
|
^{sheetTable}
|
||||||
|
|
||||||
|
|
||||||
|
<h1>
|
||||||
|
Anstehende Klausuren
|
||||||
|
TODO
|
||||||
|
|
||||||
|
<h1>
|
||||||
|
Anstehende Kursanmeldungen
|
||||||
|
TODO
|
||||||
|
|
||||||
@ -6,16 +6,15 @@ $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} id=#{fieldId} multiple :req:required="required">
|
||||||
|
|
||||||
|
<div .file-input__multi-info>
|
||||||
|
_{MsgMultiFileUploadInfo}
|
||||||
|
|
||||||
<div .file-input__unpack>
|
<div .file-input__unpack>
|
||||||
<label for=#{fieldId}_zip>ZIPs entpacken
|
<label for=#{fieldId}_zip>ZIPs automatisch entpacken
|
||||||
<input type=checkbox id=#{fieldId}_zip name=#{fieldName} value=#{unpackZips} :req:required>
|
<input type=checkbox id=#{fieldId}_zip name=#{fieldName} value=#{unpackZips}>
|
||||||
|
<div class="js-tooltip">
|
||||||
|
<div class="tooltip__handle">?
|
||||||
|
<div class="tooltip__content hidden">Entpackt hochgeladene Zip-Dateien (*.zip) automatisch und fügt den Inhalt dem Stamm-Verzeichnis der Abgabe hinzu.
|
||||||
|
|||||||
24
templates/multiFileField.lucius
Normal file
24
templates/multiFileField.lucius
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
.file-input__unpack {
|
||||||
|
font-size: .9rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 10px;
|
||||||
|
|
||||||
|
.checkbox {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-input__multi-info {
|
||||||
|
font-size: .9rem;
|
||||||
|
font-style: italic;
|
||||||
|
margin-top: 10px;
|
||||||
|
color: var(--color-fontsec);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-input__list {
|
||||||
|
margin-left: 15px;
|
||||||
|
margin-top: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
@ -1,23 +1,77 @@
|
|||||||
<div .ui.container>
|
<div .profile>
|
||||||
|
|
||||||
<h1>
|
<div .scrolltable>
|
||||||
Access granted!
|
<table .table.table--striped.table--hover.table--vertical>
|
||||||
|
<tr.table__row>
|
||||||
|
<th> _{MsgName}
|
||||||
|
<td> #{display userDisplayName}
|
||||||
|
<tr.table__row>
|
||||||
|
<th> _{MsgMatrikelNr}
|
||||||
|
<td> #{display userMatrikelnummer}
|
||||||
|
<tr.table__row>
|
||||||
|
<th> _{MsgEMail}
|
||||||
|
<td> #{display userEmail}
|
||||||
|
<tr.table__row>
|
||||||
|
<th> _{MsgIdent}
|
||||||
|
<td> #{display userIdent}
|
||||||
|
<tr.table__row>
|
||||||
|
<th> _{MsgPlugin}
|
||||||
|
<td> #{display userPlugin}
|
||||||
|
$if not $ null admin_rights
|
||||||
|
<tr.table__row>
|
||||||
|
<th> Administrator
|
||||||
|
<td>
|
||||||
|
<ul>
|
||||||
|
$forall institute <- admin_rights
|
||||||
|
<li>#{display institute}
|
||||||
|
$if not $ null lecturer_rights
|
||||||
|
<tr.table__row>
|
||||||
|
<th> Lehrberechtigt
|
||||||
|
<td>
|
||||||
|
<ul>
|
||||||
|
$forall institute <- lecturer_rights
|
||||||
|
<li>#{display institute}
|
||||||
|
$if not $ null lecture_owner
|
||||||
|
<tr.table__row>
|
||||||
|
<th> Eigene Kurse
|
||||||
|
<td>
|
||||||
|
<ul>
|
||||||
|
$forall (E.Value csh, E.Value tid) <- lecture_owner
|
||||||
|
<li>
|
||||||
|
<a href=@{CourseR tid csh CShowR}>#{display tid} - #{csh}
|
||||||
|
$if not $ null lecture_corrector
|
||||||
|
<tr.table__row>
|
||||||
|
<th> Korrektor
|
||||||
|
<td>
|
||||||
|
<ul>
|
||||||
|
$forall (E.Value csh, E.Value tid) <- lecture_corrector
|
||||||
|
<li>
|
||||||
|
<a href=@{CourseR tid csh CShowR}>#{display tid} - #{csh}
|
||||||
|
$if not $ null studies
|
||||||
|
<tr.table__row>
|
||||||
|
<th> Studiengänge
|
||||||
|
<td>
|
||||||
|
<table .table .table-striped .table-hover>
|
||||||
|
<tr.table__row>
|
||||||
|
<th> Abschluss
|
||||||
|
<th> Studiengang
|
||||||
|
<th> Studienart
|
||||||
|
<th> Semester
|
||||||
|
|
||||||
<p>
|
$forall (degree,field,fieldtype,semester) <- studies
|
||||||
This page is protected and access is allowed only for authenticated users.
|
<tr.table__row>
|
||||||
|
<td> #{display degree}
|
||||||
|
<td> #{display field}
|
||||||
|
<td> #{display fieldtype}
|
||||||
|
<td> #{display semester}
|
||||||
|
$if not $ null participant
|
||||||
|
<tr.table__row>
|
||||||
|
<th> Teilnehmer
|
||||||
|
<td>
|
||||||
|
<ul>
|
||||||
|
$forall (E.Value csh, E.Value tid, regSince) <- participant
|
||||||
|
<li>
|
||||||
|
<a href=@{CourseR tid csh CShowR}>#{display tid} - #{csh}
|
||||||
|
seit #{display regSince}
|
||||||
|
|
||||||
<p>
|
^{settingsForm}
|
||||||
Your data is protected with us <strong><span class="username">#{userIdent user}</span></strong>!
|
|
||||||
|
|
||||||
$if not $ null admin_rights
|
|
||||||
<h1>
|
|
||||||
Administrator für die Institute
|
|
||||||
<ul>
|
|
||||||
$forall institute <- admin_rights
|
|
||||||
<li>#{show institute}
|
|
||||||
$if not $ null lecturer_rights
|
|
||||||
<h1>
|
|
||||||
Lehrberechtigung für die Institute
|
|
||||||
<ul>
|
|
||||||
$forall institute <- lecturer_rights
|
|
||||||
<li>#{show institute}
|
|
||||||
|
|||||||
24
templates/profileData.hamlet
Normal file
24
templates/profileData.hamlet
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<div .container>
|
||||||
|
<div .alert .alert-danger>
|
||||||
|
<div .alert__content>
|
||||||
|
TODO: Alle Benutzerbezogenen Daten sollen hier angezeigt
|
||||||
|
und verlinkt werden
|
||||||
|
(alle Abgaben, Klausurnoten, etc.)
|
||||||
|
|
||||||
|
<em> TODO: Hier mehr Daten in Tabellen anzeigen!
|
||||||
|
|
||||||
|
<h2>
|
||||||
|
<em> TODO: Knopf zum Löschen aller Daten erstellen
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<h4>Hinweise:
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
Nicht aufgeführt sind Zeitstempel mit Benutzerinformationen, z.B. bei der Editierung und Korrekturen von Übungen, Übungsgruppenleiterschaft, Raumbuchungen, etc.
|
||||||
|
<li>
|
||||||
|
Benutzerdaten bleiben so lange gespeichert, bis ein Institutsadministrator über die Exmatrikulation informiert wurde. Dann wird der Account gelöscht.
|
||||||
|
Abgaben/Bonuspunkte werden unwiderruflich gelöscht.
|
||||||
|
Klausurnoten verbleiben aus statistischen Gründen anonymisiert im System.
|
||||||
|
<li>
|
||||||
|
Bei gemeinsamen Gruppenabgaben wird nur die Zuordnung zu diesem Benutzer gelöscht.
|
||||||
|
Die Abgabe selbst wird erst gelöscht, wenn alle Benutzer einer Abgabe deren Löschung veranlasst haben.
|
||||||
@ -13,7 +13,7 @@
|
|||||||
<h2 #description>Hinweise
|
<h2 #description>Hinweise
|
||||||
<p> #{descr}
|
<p> #{descr}
|
||||||
<h3>Bewertung
|
<h3>Bewertung
|
||||||
<p> #{show $ sheetType sheet}
|
<p> #{display $ sheetType sheet}
|
||||||
$maybe marking <- sheetMarkingText sheet
|
$maybe marking <- sheetMarkingText sheet
|
||||||
<p> #{marking}
|
<p> #{marking}
|
||||||
<br>
|
<br>
|
||||||
|
|||||||
1
templates/standalone/alerts.hamlet
Normal file
1
templates/standalone/alerts.hamlet
Normal file
@ -0,0 +1 @@
|
|||||||
|
<!-- only here to be able to include alerts using `toWidget` -->
|
||||||
24
templates/standalone/alerts.julius
Normal file
24
templates/standalone/alerts.julius
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
(function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
window.utils = window.utils || {};
|
||||||
|
|
||||||
|
window.utils.alert = function(alertEl) {
|
||||||
|
var closeEl = document.createElement('DIV');
|
||||||
|
closeEl.classList.add('alert__close');
|
||||||
|
closeEl.innerText = #{String (messageRender MsgCloseAlert)};
|
||||||
|
closeEl.addEventListener('click', function(event) {
|
||||||
|
alertEl.classList.add('alert--invisible');
|
||||||
|
});
|
||||||
|
alertEl.appendChild(closeEl);
|
||||||
|
}
|
||||||
|
|
||||||
|
})();
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
|
||||||
|
// setup alerts
|
||||||
|
Array.from(document.querySelectorAll('.alert')).forEach(function(alertEl) {
|
||||||
|
window.utils.alert(alertEl);
|
||||||
|
});
|
||||||
|
});
|
||||||
72
templates/standalone/alerts.lucius
Normal file
72
templates/standalone/alerts.lucius
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
/* ALERTS */
|
||||||
|
.alert {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
font-size: 1rem;
|
||||||
|
border-color: #dbdbdb;
|
||||||
|
border-radius: 4px;
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 0 0 0 4px;
|
||||||
|
color: #4a4a4a;
|
||||||
|
z-index: 0;
|
||||||
|
max-height: 200px;
|
||||||
|
transition: all .2s ease-in-out;
|
||||||
|
transform-origin: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert__content {
|
||||||
|
padding: 1.25em 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert__close {
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: right;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0 10px;
|
||||||
|
background-color: var(--color-light);
|
||||||
|
color: var(--color-lightwhite);
|
||||||
|
transition: all .2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--color-primary);
|
||||||
|
transform: scale(1.05, 1.05);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-success {
|
||||||
|
background-color: #f6fef9;
|
||||||
|
border-color: var(--color-success);
|
||||||
|
|
||||||
|
.alert__close {
|
||||||
|
background-color: var(--color-success);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-warning {
|
||||||
|
background-color: #fffdf5;
|
||||||
|
border-color: var(--color-warning);
|
||||||
|
|
||||||
|
.alert__close {
|
||||||
|
background-color: var(--color-warning);
|
||||||
|
color: var(--color-dark);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-danger,
|
||||||
|
.alert-error {
|
||||||
|
border-color: var(--color-error);
|
||||||
|
background-color: #fff5f7;
|
||||||
|
|
||||||
|
.alert__close {
|
||||||
|
background-color: var(--color-error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert--invisible {
|
||||||
|
max-height: 0;
|
||||||
|
transform: scaleY(0);
|
||||||
|
}
|
||||||
1
templates/standalone/datepicker.hamlet
Normal file
1
templates/standalone/datepicker.hamlet
Normal file
@ -0,0 +1 @@
|
|||||||
|
<!-- only here to be able to include datepicker using `toWidget` -->
|
||||||
36
templates/standalone/datepicker.julius
Normal file
36
templates/standalone/datepicker.julius
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
var config = {
|
||||||
|
dtLocal: {
|
||||||
|
enableTime: true,
|
||||||
|
altInput: true,
|
||||||
|
altFormat: "j. F Y, H:i",
|
||||||
|
dateFormat: "Y-m-dTH:i",
|
||||||
|
time_24hr: true
|
||||||
|
},
|
||||||
|
d: {
|
||||||
|
altFormat: "j. F Y",
|
||||||
|
dateFormat: "Y-m-d",
|
||||||
|
altInput: true
|
||||||
|
},
|
||||||
|
t: {
|
||||||
|
enableTime: true,
|
||||||
|
noCalendar: true,
|
||||||
|
altFormat: "H:i",
|
||||||
|
dateFormat: "H:i",
|
||||||
|
altInput: true,
|
||||||
|
time_24hr: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Array.from(document.querySelectorAll('input[type="date"]')).forEach(function(el) {
|
||||||
|
flatpickr(el, config.d);
|
||||||
|
});
|
||||||
|
Array.from(document.querySelectorAll('input[type="time"]')).forEach(function(el) {
|
||||||
|
flatpickr(el, config.t);
|
||||||
|
});
|
||||||
|
Array.from(document.querySelectorAll('input[type="datetime-local"]')).forEach(function(el) {
|
||||||
|
flatpickr(el, config.dtLocal);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -3,153 +3,68 @@
|
|||||||
|
|
||||||
window.utils = window.utils || {};
|
window.utils = window.utils || {};
|
||||||
|
|
||||||
// makes <label> smaller if <input> is focussed
|
|
||||||
window.utils.reactiveInputLabel = function(input, label) {
|
|
||||||
// updates to dom
|
|
||||||
if (input.value.length > 0) {
|
|
||||||
label.classList.add('reactive-label--small');
|
|
||||||
}
|
|
||||||
// add event listeners
|
|
||||||
input.addEventListener('focus', function() {
|
|
||||||
label.classList.add('reactive-label--small');
|
|
||||||
});
|
|
||||||
label.addEventListener('click', function() {
|
|
||||||
label.classList.add('reactive-label--small');
|
|
||||||
input.focus();
|
|
||||||
});
|
|
||||||
input.addEventListener('blur', function() {
|
|
||||||
if (input.value.length < 1) {
|
|
||||||
label.classList.remove('reactive-label--small');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// allows for multiple file uploads with separate inputs
|
// allows for multiple file uploads with separate inputs
|
||||||
window.utils.reactiveFileUpload = function(input, formGroup) {
|
window.utils.initializeFileUpload = function(input) {
|
||||||
var currValidInputCount = 0;
|
var isMulti = input.hasAttribute('multiple');
|
||||||
var addMore = false;
|
var fileList = isMulti ? addFileList() : null;
|
||||||
var inputName = input.getAttribute('name');
|
var label = addFileLabel();
|
||||||
var isMulti = input.hasAttribute('multiple') ? true : false;
|
|
||||||
var wrapper = formGroup;
|
|
||||||
// FileInput PseudoClass
|
|
||||||
function FileInput(container, input, label, remover) {
|
|
||||||
this.container = container;
|
|
||||||
this.input = input;
|
|
||||||
this.label = label;
|
|
||||||
this.remover = remover;
|
|
||||||
addListener(this);
|
|
||||||
|
|
||||||
this.addTo = function(parentElement) {
|
function renderFileList(files) {
|
||||||
parentElement.appendChild(this.container);
|
fileList.innerHTML = '';
|
||||||
}
|
Array.from(files).forEach(function(file, index) {
|
||||||
this.remove = function() {
|
var fileDisplayEl = document.createElement('li');
|
||||||
this.container.remove();
|
fileDisplayEl.innerHTML = file.name;
|
||||||
}
|
fileList.appendChild(fileDisplayEl);
|
||||||
this.wasValid = function() {
|
});
|
||||||
return this.container.classList.contains('file-input__container--valid');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
function addNextInput() {
|
|
||||||
var inputs = wrapper.querySelectorAll('.file-input__container');
|
function updateLabel(files) {
|
||||||
if (inputs[inputs.length - 1].classList.contains('file-input__container--valid')) {
|
if (files.length) {
|
||||||
makeInput(inputName).addTo(wrapper);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// updates submitbutton and form-group-stripe
|
|
||||||
function updateForm() {
|
|
||||||
var submitBtn = formGroup.parentElement.querySelector('[type=submit]');
|
|
||||||
formGroup.classList.remove('form-group--has-error');
|
|
||||||
if (currValidInputCount > 0) {
|
|
||||||
if (formGroup.classList.contains('form-group')) {
|
|
||||||
formGroup.classList.add('form-group--valid')
|
|
||||||
}
|
|
||||||
if (isMulti) {
|
if (isMulti) {
|
||||||
addNextInput();
|
label.innerText = files.length + ' Dateien ausgwählt';
|
||||||
|
} else {
|
||||||
|
label.innerHTML = files[0].name;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (formGroup.classList.contains('form-group')) {
|
resetFileLabel();
|
||||||
formGroup.classList.remove('form-group--valid')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// addseventlistener destInput
|
|
||||||
function addListener(fileInput) {
|
|
||||||
fileInput.input.addEventListener('change', function(event) {
|
|
||||||
if (fileInput.input.value.length > 0) {
|
|
||||||
// update label
|
|
||||||
var filePath = fileInput.input.value.replace(/\\/g, '/').split('/');
|
|
||||||
var fileName = filePath[filePath.length - 1];
|
|
||||||
fileInput.label.innerHTML = fileName;
|
|
||||||
// increase count if this field was empty previously
|
|
||||||
if (!fileInput.wasValid()) {
|
|
||||||
currValidInputCount++;
|
|
||||||
}
|
|
||||||
fileInput.container.classList.add('file-input__container--valid')
|
|
||||||
// show next input
|
|
||||||
} else {
|
|
||||||
if (isMulti) {
|
|
||||||
currValidInputCount--;
|
|
||||||
}
|
|
||||||
clearInput(fileInput);
|
|
||||||
}
|
|
||||||
updateForm();
|
|
||||||
});
|
|
||||||
fileInput.input.addEventListener('focus', function() {
|
|
||||||
fileInput.container.classList.add('pseudo-focus');
|
|
||||||
});
|
|
||||||
fileInput.input.addEventListener('blur', function() {
|
|
||||||
fileInput.container.classList.remove('pseudo-focus');
|
|
||||||
});
|
|
||||||
fileInput.remover.addEventListener('click', function() {
|
|
||||||
if (fileInput.wasValid()) {
|
|
||||||
currValidInputCount--;
|
|
||||||
}
|
|
||||||
clearInput(fileInput);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// clears or removes fileinput based on multi-file or not
|
function addFileList() {
|
||||||
function clearInput(fileInput) {
|
var list = document.createElement('ol');
|
||||||
if (isMulti) {
|
list.classList.add('file-input__list');
|
||||||
fileInput.remove();
|
var unpackEl = input.parentElement.querySelector('.file-input__unpack');
|
||||||
|
if (unpackEl) {
|
||||||
|
input.parentElement.insertBefore(list, unpackEl);
|
||||||
} else {
|
} else {
|
||||||
fileInput.container.classList.remove('file-input__container--valid')
|
input.parentElement.appendChild(list);
|
||||||
fileInput.label.innerHTML = '';
|
|
||||||
}
|
}
|
||||||
updateForm();
|
return list;
|
||||||
}
|
}
|
||||||
// create new wrapped input element with name name
|
|
||||||
function makeInput(name) {
|
function addFileLabel() {
|
||||||
var cont = document.createElement('div');
|
var label = document.createElement('label');
|
||||||
var desc = document.createElement('label');
|
label.classList.add('file-input__label');
|
||||||
var nextInput = document.createElement('input');
|
label.setAttribute('for', input.id);
|
||||||
var remover = document.createElement('div');
|
input.parentElement.insertBefore(label, input);
|
||||||
cont.classList.add('file-input__container');
|
return label;
|
||||||
desc.classList.add('file-input__label', 'btn');
|
|
||||||
nextInput.classList.add('js-file-input');
|
|
||||||
desc.setAttribute('for', name + '-' + currValidInputCount);
|
|
||||||
remover.classList.add('file-input__remover');
|
|
||||||
nextInput.setAttribute('id', name + '-' + currValidInputCount);
|
|
||||||
nextInput.setAttribute('name', name);
|
|
||||||
nextInput.setAttribute('type', 'file');
|
|
||||||
cont.appendChild(nextInput);
|
|
||||||
cont.appendChild(desc);
|
|
||||||
cont.appendChild(remover);
|
|
||||||
return new FileInput(cont, nextInput, desc, remover);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resetFileLabel() {
|
||||||
|
// interpolate translated String here
|
||||||
|
label.innerText = 'Datei' + (isMulti ? 'en' : '') + ' auswählen';
|
||||||
|
}
|
||||||
|
|
||||||
// initial setup
|
// initial setup
|
||||||
function setup() {
|
resetFileLabel();
|
||||||
var newInput = makeInput(inputName);
|
input.classList.add('file-input__input--hidden');
|
||||||
|
input.addEventListener('change', function() {
|
||||||
if (isMulti) {
|
if (isMulti) {
|
||||||
wrapper = document.createElement('div');
|
renderFileList(input.files);
|
||||||
wrapper.classList.add('file-input__wrapper');
|
|
||||||
formGroup.insertBefore(wrapper, input);
|
|
||||||
}
|
}
|
||||||
input.remove();
|
|
||||||
newInput.addTo(wrapper);
|
updateLabel(input.files);
|
||||||
updateForm();
|
});
|
||||||
}
|
|
||||||
setup();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// to remove previously uploaded files
|
// to remove previously uploaded files
|
||||||
@ -182,63 +97,47 @@
|
|||||||
setup();
|
setup();
|
||||||
}
|
}
|
||||||
|
|
||||||
window.utils.reactiveFormGroup = function(formGroup, input) {
|
window.utils.initializeCheckboxRadio = function(input, type) {
|
||||||
// updates to dom
|
|
||||||
if (input.value.length > 0) {
|
if (!input.parentElement.classList.contains(type)) {
|
||||||
formGroup.classList.add('form-group--valid');
|
var parentEl = input.parentElement;
|
||||||
} else {
|
var siblingEl = input.nextElementSibling;
|
||||||
formGroup.classList.remove('form-group--valid');
|
var wrapperEl = document.createElement('div');
|
||||||
}
|
var labelEl = document.createElement('label');
|
||||||
input.addEventListener('input', function() {
|
wrapperEl.classList.add(type);
|
||||||
formGroup.classList.remove('form-group--has-error');
|
labelEl.setAttribute('for', input.id);
|
||||||
if (input.value.length > 0) {
|
wrapperEl.appendChild(input);
|
||||||
formGroup.classList.add('form-group--valid');
|
wrapperEl.appendChild(labelEl);
|
||||||
|
|
||||||
|
if (siblingEl) {
|
||||||
|
parentEl.insertBefore(wrapperEl, siblingEl);
|
||||||
} else {
|
} else {
|
||||||
formGroup.classList.remove('form-group--valid');
|
parentEl.appendChild(wrapperEl);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
})();
|
})();
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
|
||||||
// setup reactive labels
|
// initialize checkboxes
|
||||||
Array.from(document.querySelectorAll('.reactive-label')).forEach(function(label) {
|
Array.from(document.querySelectorAll('input[type="checkbox"]')).forEach(function(inp) {
|
||||||
var input = document.querySelector('#' + label.getAttribute('for'));
|
window.utils.initializeCheckboxRadio(inp, 'checkbox');
|
||||||
if (!input) {
|
});
|
||||||
console.error('No input found for ReactiveLabel! Targeted input: \'#%s\'', label.getAttribute('for'));
|
|
||||||
label.classList.remove('reactive-label');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var parent = label.parentElement;
|
// initialize radios
|
||||||
var type = input.getAttribute('type');
|
Array.from(document.querySelectorAll('input[type="radio"]')).forEach(function(inp) {
|
||||||
var isListening = !RegExp(['date', 'checkbox', 'radio', 'hidden', 'file'].join('|')).test(type);
|
window.utils.initializeCheckboxRadio(inp, 'radio');
|
||||||
var isInFormGroup = parent.classList.contains('form-group') && parent.classList.contains('form-group--required');
|
|
||||||
|
|
||||||
|
|
||||||
if (isInFormGroup) {
|
|
||||||
window.utils.reactiveFormGroup(parent, input);
|
|
||||||
}
|
|
||||||
if (isListening) {
|
|
||||||
window.utils.reactiveInputLabel(input, label);
|
|
||||||
} else {
|
|
||||||
label.classList.remove('reactive-label');
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// initialize file-upload-fields
|
// initialize file-upload-fields
|
||||||
Array.from(document.querySelectorAll('input[type="file"]')).map(function(inp) {
|
Array.from(document.querySelectorAll('input[type="file"]')).forEach(function(inp) {
|
||||||
var formGroup = inp.parentNode;
|
window.utils.initializeFileUpload(inp);
|
||||||
while (!formGroup.classList.contains('form-group') && formGroup !== document.body) {
|
|
||||||
formGroup = formGroup.parentNode;
|
|
||||||
}
|
|
||||||
window.utils.reactiveFileUpload(inp, formGroup);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// initialize file-checkbox-fields
|
// initialize file-checkbox-fields
|
||||||
Array.from(document.querySelectorAll('.js-file-checkbox')).map(function(inp) {
|
Array.from(document.querySelectorAll('.js-file-checkbox')).forEach(function(inp) {
|
||||||
window.utils.reactiveFileCheckbox(inp);
|
window.utils.reactiveFileCheckbox(inp);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,40 +1,49 @@
|
|||||||
/* GENERAL STYLES FOR FORMS */
|
/* GENERAL STYLES FOR FORMS */
|
||||||
form {
|
|
||||||
margin: 20px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* FORM GROUPS */
|
/* FORM GROUPS */
|
||||||
.form-group {
|
.form-group {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 25% max-content;
|
grid-template-columns: 1fr 3fr;
|
||||||
grid-auto-columns: 25%;
|
|
||||||
grid-gap: 5px;
|
grid-gap: 5px;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: center;
|
align-items: flex-start;
|
||||||
margin: 10px 0;
|
padding: 4px 0;
|
||||||
padding-left: 10px;
|
|
||||||
border-left: 2px solid transparent;
|
border-left: 2px solid transparent;
|
||||||
|
|
||||||
|
+ .form-group {
|
||||||
|
margin-top: 13px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group__label {
|
.form-group__label {
|
||||||
width: 25%;
|
|
||||||
white-space: nowrap;
|
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
padding-top: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 999px) {
|
.form-group--required {
|
||||||
|
|
||||||
|
.form-group__label::after {
|
||||||
|
content: ' *';
|
||||||
|
color: var(--color-error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group--has-error {
|
||||||
|
background-color: rgba(255, 0, 0, 0.1);
|
||||||
|
|
||||||
|
input, textarea {
|
||||||
|
border-color: var(--color-error) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
.form-group {
|
.form-group {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
grid-template-rows: 30px;
|
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
margin-top: 17px;
|
margin-top: 17px;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
> * {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -43,38 +52,36 @@ input[type="text"],
|
|||||||
input[type="password"],
|
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);
|
input[type*="date"],
|
||||||
padding: 7px 3px 7px;
|
input[type*="time"] {
|
||||||
outline: 0;
|
/* from bulma.css */
|
||||||
border: 0;
|
color: #363636;
|
||||||
border-bottom: 2px solid var(--darkbase);
|
border-color: #dbdbdb;
|
||||||
box-shadow: 0 2px 13px rgba(0, 0, 0, 0.05);
|
background-color: #f3f3f3;
|
||||||
color: var(--fontbase);
|
box-shadow: inset 0 1px 2px 1px rgba(50,50,50,.05);
|
||||||
transition: all .1s;
|
|
||||||
font-size: 16px;
|
width: 100%;
|
||||||
min-width: 400px;
|
max-width: 600px;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
align-items: center;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
padding: 4px 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group--required {
|
input[type="number"] {
|
||||||
|
width: 100px;
|
||||||
input, textarea {
|
text-align: right;
|
||||||
border-bottom-color: var(--lighterbase);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group--valid {
|
input[type*="date"],
|
||||||
|
input[type*="time"],
|
||||||
input, textarea {
|
.flatpickr-input[type="text"] {
|
||||||
border-bottom-color: var(--validbase);
|
width: 50%;
|
||||||
}
|
width: 250px;
|
||||||
}
|
|
||||||
|
|
||||||
.form-group--has-error {
|
|
||||||
|
|
||||||
input, textarea {
|
|
||||||
border-bottom-color: var(--errorbase);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="text"]:focus,
|
input[type="text"]:focus,
|
||||||
@ -82,32 +89,53 @@ 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(--lighterbase);
|
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;
|
width: 100%;
|
||||||
border: 0;
|
height: 170px;
|
||||||
padding: 7px 4px;
|
max-width: 600px;
|
||||||
min-width: 400px;
|
font-size: 1rem;
|
||||||
min-height: 100px;
|
line-height: 1.5;
|
||||||
font-family: var(--fontfamilybase);
|
color: #363636;
|
||||||
font-size: 16px;
|
background-color: #f3f3f3;
|
||||||
color: var(--fontbase);
|
padding: 4px 13px;
|
||||||
background-color: rgba(0, 0, 0, 0.05);
|
-webkit-appearance: none;
|
||||||
box-shadow: 0 2px 13px rgba(0, 0, 0, 0.05);
|
appearance: none;
|
||||||
border-bottom: 2px solid var(--darkbase);
|
border: 1px solid #dbdbdb;
|
||||||
|
border-radius: 2px;
|
||||||
|
box-shadow: inset 0 1px 2px 1px rgba(50,50,50,.05);
|
||||||
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
box-shadow: 0 0 13px var(--lighterbase);
|
outline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* OPTIONS */
|
||||||
|
select,
|
||||||
|
option {
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
padding: 4px 13px;
|
||||||
|
border: 1px solid #dbdbdb;
|
||||||
|
border-radius: 2px;
|
||||||
|
outline: 0;
|
||||||
|
color: #363636;
|
||||||
|
min-width: 200px;
|
||||||
|
background-color: #f3f3f3;
|
||||||
|
box-shadow: inset 0 1px 2px 1px rgba(50,50,50,.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* CUSTOM LEGACY CHECKBOX AND RADIO BOXES */
|
/* CUSTOM LEGACY CHECKBOX AND RADIO BOXES */
|
||||||
@ -116,6 +144,7 @@ input[type="checkbox"] {
|
|||||||
height: 20px;
|
height: 20px;
|
||||||
width: 20px;
|
width: 20px;
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
input[type="checkbox"]::before {
|
input[type="checkbox"]::before {
|
||||||
@ -123,14 +152,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: '✓';
|
||||||
@ -150,80 +179,56 @@ input[type="checkbox"]:checked::after {
|
|||||||
.radio {
|
.radio {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
> [type="checkbox"],
|
[type="checkbox"],
|
||||||
> [type="radio"] {
|
[type="radio"] {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
> label {
|
label {
|
||||||
display: block;
|
display: block;
|
||||||
height: 30px;
|
height: 24px;
|
||||||
width: 30px;
|
width: 24px;
|
||||||
background-color: var(--greybase);
|
background-color: #f3f3f3;
|
||||||
|
box-shadow: inset 0 1px 2px 1px rgba(50,50,50,.05);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
color: white;
|
color: white;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
> label::before,
|
label::before,
|
||||||
> label::after {
|
label::after {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 14px;
|
top: 11px;
|
||||||
left: 5px;
|
left: 3px;
|
||||||
display: block;
|
display: block;
|
||||||
width: 20px;
|
width: 18px;
|
||||||
height: 20px;
|
height: 2px;
|
||||||
background-color: white;
|
background-color: var(--color-font);
|
||||||
transition: all .2s;
|
transition: all .2s;
|
||||||
|
transform: scale(0.5, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
> label::before {
|
:checked + label {
|
||||||
width: 20px;
|
background-color: var(--color-primary);
|
||||||
height: 2px;
|
|
||||||
transform: scale(0.1, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
> label::after {
|
|
||||||
width: 20px;
|
|
||||||
height: 2px;
|
|
||||||
transform: scale(0.1, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
> :checked + label {
|
|
||||||
background-color: var(--lightbase);
|
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover > label {
|
:checked + label::before {
|
||||||
background-color: var(--lighterbase);
|
background-color: white;
|
||||||
}
|
|
||||||
|
|
||||||
&:hover > label::before {
|
|
||||||
transform: scale(0.8, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
> :checked + label::before {
|
|
||||||
transform: scale(1, 1) rotate(45deg);
|
transform: scale(1, 1) rotate(45deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
> :checked + label:hover::after,
|
:checked + label::after {
|
||||||
> :checked + label:hover::before {
|
background-color: white;
|
||||||
transform: scale(1, 1) rotate(0deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover > label::after {
|
|
||||||
transform: scale(0.8, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
> :checked + label::after {
|
|
||||||
transform: scale(1, 1) rotate(-45deg);
|
transform: scale(1, 1) rotate(-45deg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.radio > label::before {
|
|
||||||
|
.radio label::before {
|
||||||
transform: scale(0.01, 0.01) rotate(45deg);
|
transform: scale(0.01, 0.01) rotate(45deg);
|
||||||
}
|
}
|
||||||
.radio > label::after {
|
.radio label::after {
|
||||||
transform: scale(0.01, 0.01) rotate(-45deg);
|
transform: scale(0.01, 0.01) rotate(-45deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -239,140 +244,16 @@ input[type="checkbox"]:checked::after {
|
|||||||
z-index: -1;
|
z-index: -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* REACTIVE LABELS */
|
|
||||||
.reactive-label {
|
|
||||||
cursor: text;
|
|
||||||
color: var(--fontsec);
|
|
||||||
transform: translate(0, 0);
|
|
||||||
transition: all .1s;
|
|
||||||
}
|
|
||||||
.reactive-label--small {
|
|
||||||
cursor: default;
|
|
||||||
color: var(--fontbase);
|
|
||||||
}
|
|
||||||
@media (max-width: 999px) {
|
|
||||||
.reactive-label {
|
|
||||||
position: relative;
|
|
||||||
transform: translate(2px, 30px);
|
|
||||||
}
|
|
||||||
.reactive-label--small {
|
|
||||||
transform: translate(2px, 0px);
|
|
||||||
color: var(--fontsec);
|
|
||||||
/*font-size: 14px;*/
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* CUSTOM FILE INPUT */
|
/* CUSTOM FILE INPUT */
|
||||||
input[type="file"].js-file-input {
|
.file-input__label {
|
||||||
color: white;
|
|
||||||
width: 0.1px;
|
|
||||||
height: 0.1px;
|
|
||||||
opacity: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
position: absolute;
|
|
||||||
z-index: -1;
|
|
||||||
outline: 0;
|
|
||||||
border: 0;
|
|
||||||
}
|
|
||||||
.file-input__wrapper {
|
|
||||||
grid-column-start: 2;
|
|
||||||
}
|
|
||||||
.file-input__container,
|
|
||||||
.file-checkbox__container,
|
|
||||||
.file-input__unpack {
|
|
||||||
grid-column-start: 2;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin: 4px 0;
|
|
||||||
}
|
|
||||||
.file-input__label,
|
|
||||||
.file-input__remover,
|
|
||||||
.file-checkbox__label,
|
|
||||||
.file-checkbox__remover {
|
|
||||||
display: block;
|
|
||||||
border-radius: 2px;
|
|
||||||
padding: 5px 13px;
|
|
||||||
color: var(--whitebase);
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
display: inline-block;
|
||||||
.file-input__label,
|
background-color: var(--color-primary);
|
||||||
.file-checkbox__label {
|
color: white;
|
||||||
text-align: left;
|
padding: 10px 17px;
|
||||||
position: relative;
|
border-radius: 3px;
|
||||||
height: 30px;
|
|
||||||
}
|
|
||||||
.file-checkbox__label {
|
|
||||||
background-color: var(--greybase);
|
|
||||||
text-decoration: line-through;
|
|
||||||
}
|
|
||||||
.file-input__label.btn,
|
|
||||||
.file-checkbox__label.btn {
|
|
||||||
padding: 5px 13px;
|
|
||||||
}
|
|
||||||
.file-input__label::after,
|
|
||||||
.file-input__label::before {
|
|
||||||
position: absolute;
|
|
||||||
content: '';
|
|
||||||
background-color: white;
|
|
||||||
width: 16px;
|
|
||||||
height: 2px;
|
|
||||||
top: 14px;
|
|
||||||
top: 50%;
|
|
||||||
left: 12px;
|
|
||||||
left: 50%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-input__label::after {
|
.file-input__input--hidden {
|
||||||
transform: translate(-50%, -50%) rotate(90deg);
|
|
||||||
}
|
|
||||||
.file-input__label::before {
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
}
|
|
||||||
.file-checkbox__checkbox {
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
.file-input__remover {
|
|
||||||
display: none;
|
display: none;
|
||||||
width: 40px;
|
|
||||||
height: 30px;
|
|
||||||
text-align: center;
|
|
||||||
background-color: var(--warningbase);
|
|
||||||
position: relative;
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
.file-input__remover::before {
|
|
||||||
position: absolute;
|
|
||||||
content: '';
|
|
||||||
width: 16px;
|
|
||||||
height: 2px;
|
|
||||||
top: 14px;
|
|
||||||
left: 12px;
|
|
||||||
background-color: white;
|
|
||||||
}
|
|
||||||
.file-input__container--valid > .file-input__label {
|
|
||||||
background-color: var(--lightbase);
|
|
||||||
}
|
|
||||||
.file-checkbox__container--checked > .file-checkbox__label {
|
|
||||||
text-decoration: none;
|
|
||||||
background-color: var(--lighterbase);
|
|
||||||
|
|
||||||
&.btn:hover {
|
|
||||||
background-color: var(--lighterbase);
|
|
||||||
text-decoration: line-through;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.file-input__container--valid > .file-input__label::before,
|
|
||||||
.file-input__container--valid > .file-input__label::after {
|
|
||||||
content: none;
|
|
||||||
}
|
|
||||||
.file-input__container--valid > .file-input__remover {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
@media (max-width: 999px) {
|
|
||||||
.file-input__wrapper,
|
|
||||||
.file-input__container,
|
|
||||||
.file-checkbox__container,
|
|
||||||
.file-input__unpack {
|
|
||||||
grid-column-start: 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
@ -23,17 +23,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 999px) {
|
@media (max-width: 1024px) {
|
||||||
.modal {
|
.modal {
|
||||||
min-width: 80vw;
|
min-width: 80vw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@media (max-width: 666px) {
|
@media (max-width: 768px) {
|
||||||
.modal {
|
.modal {
|
||||||
min-width: 90vw;
|
min-width: 90vw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@media (max-width: 444px) {
|
@media (max-width: 425px) {
|
||||||
.modal {
|
.modal {
|
||||||
min-width: calc(100vw - 20px);
|
min-width: calc(100vw - 20px);
|
||||||
}
|
}
|
||||||
@ -70,7 +70,7 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 30px;
|
width: 30px;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
background-color: var(--darkerbase);
|
background-color: var(--color-darker);
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
z-index: 20;
|
z-index: 20;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
1
templates/standalone/tabber.hamlet
Normal file
1
templates/standalone/tabber.hamlet
Normal file
@ -0,0 +1 @@
|
|||||||
|
<!-- only here to be able to include tabber using `toWidget` -->
|
||||||
7
templates/standalone/tabber.lucius
Normal file
7
templates/standalone/tabber.lucius
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
.tab-opener {
|
||||||
|
background-color: var(--color-dark);
|
||||||
|
|
||||||
|
&.tab-visible {
|
||||||
|
border-bottom-color: var(--color-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
1
templates/standalone/tooltip.hamlet
Normal file
1
templates/standalone/tooltip.hamlet
Normal file
@ -0,0 +1 @@
|
|||||||
|
<!-- only here to be able to include tooltips using `toWidget` -->
|
||||||
37
templates/standalone/tooltip.julius
Normal file
37
templates/standalone/tooltip.julius
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
(function() {;
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
window.utils = window.utils || {};
|
||||||
|
|
||||||
|
// allows for multiple file uploads with separate inputs
|
||||||
|
window.utils.tooltip = function(tt) {
|
||||||
|
var handle = tt.querySelector('.tooltip__handle');
|
||||||
|
var content = tt.querySelector('.tooltip__content');
|
||||||
|
|
||||||
|
var left = false;
|
||||||
|
|
||||||
|
handle.addEventListener('mouseenter', function() {
|
||||||
|
left = false;
|
||||||
|
content.classList.toggle('to-left', handle.getBoundingClientRect().left + 300 > window.innerWidth);
|
||||||
|
content.classList.remove('hidden');
|
||||||
|
});
|
||||||
|
|
||||||
|
handle.addEventListener('mouseleave', function() {
|
||||||
|
left = true;
|
||||||
|
window.setTimeout(function() {
|
||||||
|
if (left) {
|
||||||
|
content.classList.add('hidden');
|
||||||
|
}
|
||||||
|
}, 250);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
})();
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
|
||||||
|
// initialize tooltips
|
||||||
|
Array.from(document.querySelectorAll('.js-tooltip')).forEach(function(tt) {
|
||||||
|
window.utils.tooltip(tt);
|
||||||
|
});
|
||||||
|
});
|
||||||
58
templates/standalone/tooltip.lucius
Normal file
58
templates/standalone/tooltip.lucius
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
.js-tooltip {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.tooltip__handle {
|
||||||
|
background-color: var(--color-dark);
|
||||||
|
border-radius: 50%;
|
||||||
|
height: 1.5rem;
|
||||||
|
width: 1.5rem;
|
||||||
|
line-height: 1.5rem;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
color: white;
|
||||||
|
display: inline-block;
|
||||||
|
text-align: center;
|
||||||
|
cursor: default;
|
||||||
|
margin: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip__content {
|
||||||
|
position: absolute;
|
||||||
|
top: -10px;
|
||||||
|
transform: translateY(-100%);
|
||||||
|
left: 3px;
|
||||||
|
width: 275px;
|
||||||
|
z-index: 10;
|
||||||
|
background-color: #fafafa;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 13px 17px;
|
||||||
|
box-shadow: 0 0 20px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
// display: none;
|
||||||
|
|
||||||
|
&.to-left {
|
||||||
|
left: auto;
|
||||||
|
right: 3px;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
left: auto;
|
||||||
|
right: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
background-color: #fafafa;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
position: absolute;
|
||||||
|
left: 10px;
|
||||||
|
// box-shadow: 0 0 4px black;
|
||||||
|
// outline: 1px solid red;
|
||||||
|
bottom: -8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -46,7 +46,7 @@
|
|||||||
#{fileTitle file}
|
#{fileTitle file}
|
||||||
<span .label .label-warning>Gelöscht
|
<span .label .label-warning>Gelöscht
|
||||||
$else
|
$else
|
||||||
<a href=@{SubmissionDownloadSingleR cID $ fileTitle file} download .list-group-item>
|
<a href=@{SubmissionDownloadSingleR cID' $ fileTitle file} download .list-group-item>
|
||||||
#{fileTitle file}
|
#{fileTitle file}
|
||||||
$if submissionFileIsUpdate sFile
|
$if submissionFileIsUpdate sFile
|
||||||
|
|
||||||
|
|||||||
@ -1,12 +1,18 @@
|
|||||||
<table id="#{dbtIdent}">
|
$newline never
|
||||||
|
<table id="#{dbtIdent}" .table.table--striped.table--hover>
|
||||||
$maybe sortableP <- pSortable
|
$maybe sortableP <- pSortable
|
||||||
$with toSortable <- toSortable sortableP
|
$with toSortable <- toSortable sortableP
|
||||||
<thead>
|
<thead>
|
||||||
$forall OneColonnade{..} <- getColonnade dbtColonnade
|
<tr .table__row>
|
||||||
^{widgetFromCell th $ withSortLinks $ toSortable oneColonnadeHead}
|
$forall OneColonnade{..} <- getColonnade dbtColonnade
|
||||||
|
<!-- TODO: give ths a class 'table__th' -->
|
||||||
|
<!-- TODO: wrap content of th in 'div.table__th-content' -->
|
||||||
|
^{widgetFromCell th $ withSortLinks $ toSortable oneColonnadeHead}
|
||||||
$nothing
|
$nothing
|
||||||
<tbody>
|
<tbody>
|
||||||
$forall row <- rows
|
$forall row <- rows
|
||||||
<tr>
|
<tr .table__row>
|
||||||
$forall OneColonnade{..} <- getColonnade dbtColonnade
|
$forall OneColonnade{..} <- getColonnade dbtColonnade
|
||||||
|
<!-- TODO: give tds a class 'table__td' -->
|
||||||
|
<!-- TODO: wrap content of td in 'div.table__td-content' -->
|
||||||
^{widgetFromCell td $ oneColonnadeEncode row}
|
^{widgetFromCell td $ oneColonnadeEncode row}
|
||||||
|
|||||||
@ -1,45 +1,44 @@
|
|||||||
table th {
|
/* SORTABLE TABLE */
|
||||||
position: relative;
|
.table {
|
||||||
padding-right: 20px;
|
|
||||||
|
|
||||||
&.sortable {
|
/* TODO: move outside of table as soon as tds and ths get their own class */
|
||||||
|
th.sortable {
|
||||||
|
position: relative;
|
||||||
|
padding-right: 24px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
th.sortable::after,
|
||||||
font-weight: 800;
|
th.sortable::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
right: 4px;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-left: 8px solid transparent;
|
||||||
|
border-right: 8px solid transparent;
|
||||||
|
border-bottom: 8px solid rgba(255, 255, 255, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
th.sortable::before {
|
||||||
|
/* magic numbers to move arrow back in the right position after flipping it.
|
||||||
|
this allows us to use the same border for the up and the down arrow */
|
||||||
|
transform: translateY(150%) scale(1, -1);
|
||||||
|
transform-origin: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
th.sortable::after {
|
||||||
|
transform: translateY(-150%);
|
||||||
|
}
|
||||||
|
|
||||||
|
th.sortable:hover::before,
|
||||||
|
th.sortable:hover::after {
|
||||||
|
border-bottom-color: rgba(255, 255, 255, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
th.sorted-asc::before,
|
||||||
|
th.sorted-desc::after {
|
||||||
|
border-bottom-color: white !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
table th.sorted-asc,
|
|
||||||
table th.sorted-desc {
|
|
||||||
color: var(--lightbase);
|
|
||||||
}
|
|
||||||
|
|
||||||
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(--lightbase);
|
|
||||||
}
|
|
||||||
|
|
||||||
table th.sorted-desc::after {
|
|
||||||
border-bottom: 8px solid var(--lightbase);
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
/* PAGINATION */
|
||||||
.pagination {
|
.pagination {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@ -5,25 +6,25 @@
|
|||||||
.pagination-link {
|
.pagination-link {
|
||||||
margin: 0 7px;
|
margin: 0 7px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
background-color: var(--greybase);
|
background-color: var(--color-grey);
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: var(--whitebase);
|
color: var(--color-lightwhite);
|
||||||
padding: 7px 13px;
|
padding: 7px 13px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:not(.current):hover {
|
&:not(.current):hover {
|
||||||
background-color: var(--lighterbase);
|
background-color: var(--color-lighter);
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: var(--whitebase);
|
color: var(--color-lightwhite);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.current {
|
&.current {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
background-color: var(--lightbase);
|
background-color: var(--color-light);
|
||||||
|
|
||||||
a {
|
a {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
$maybe flag <- sortableKey
|
$maybe flag <- sortableKey
|
||||||
$case directions
|
$case directions
|
||||||
$of [SortAsc]
|
$of [SortAsc]
|
||||||
<a href=#{tblLink $ setParam (wIdent "sorting") (Just $ flag <> "-desc")}>
|
<a .table__th-link href=#{tblLink $ setParam (wIdent "sorting") (Just $ flag <> "-desc")}>
|
||||||
^{cellContents}
|
^{cellContents}
|
||||||
$of _
|
$of _
|
||||||
<a href=#{tblLink $ setParam (wIdent "sorting") (Just $ flag <> "-asc")}>
|
<a .table__th-link href=#{tblLink $ setParam (wIdent "sorting") (Just $ flag <> "-asc")}>
|
||||||
^{cellContents}
|
^{cellContents}
|
||||||
$nothing
|
$nothing
|
||||||
^{cellContents}
|
^{cellContents}
|
||||||
|
|||||||
@ -1,4 +1,2 @@
|
|||||||
<div .container>
|
<div .container>
|
||||||
<h1>Semesterübersicht
|
|
||||||
|
|
||||||
^{table}
|
^{table}
|
||||||
|
|||||||
@ -1,24 +1,13 @@
|
|||||||
$newline never
|
$newline never
|
||||||
<aside .main__aside>
|
<aside .main__aside>
|
||||||
<div .asidenav>
|
<div .asidenav>
|
||||||
<div .asidenav__box>
|
|
||||||
<ul .asidenav__list>
|
|
||||||
$forall menuType <- menuTypes
|
|
||||||
$case menuType
|
|
||||||
$of NavbarAside (MenuItem label mIcon route _)
|
|
||||||
<li .asidenav__list-item :Just route == mcurrentRoute:.asidenav__list-item--active>
|
|
||||||
<a .asidenav__link-wrapper href=@{route}>
|
|
||||||
$if isJust mIcon
|
|
||||||
<div .glyphicon.glyphicon--#{fromMaybe "" mIcon}>
|
|
||||||
<div .asidenav__link-label>#{label}
|
|
||||||
$of _
|
|
||||||
|
|
||||||
<div .asidenav__box>
|
<div .asidenav__box>
|
||||||
<h3 .asidenav__box-title>
|
<h3 .asidenav__box-title>
|
||||||
|
$# TODO: this has to come from favourites somehow. Show favourites from older terms?
|
||||||
WiSe 17/18
|
WiSe 17/18
|
||||||
<ul .asidenav__list>
|
<ul .asidenav__list>
|
||||||
$forall (Course{..}, courseRoute, pageActions) <- favourites
|
$forall (Course{..}, courseRoute, pageActions) <- favourites
|
||||||
<li .asidenav__list-item>
|
<li .asidenav__list-item :highlight courseRoute:.asidenav__list-item--active>
|
||||||
<a .asidenav__link-wrapper href=@{courseRoute}>
|
<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}
|
||||||
@ -26,7 +15,7 @@ $newline never
|
|||||||
$forall action <- pageActions
|
$forall action <- pageActions
|
||||||
$case action
|
$case action
|
||||||
$of PageActionPrime (MenuItem{..})
|
$of PageActionPrime (MenuItem{..})
|
||||||
<li .asidenav__list-item>
|
<li .asidenav__nested-list-item>
|
||||||
<a .asidenav__link-wrapper href=@{menuItemRoute}>#{menuItemLabel}
|
<a .asidenav__link-wrapper href=@{menuItemRoute}>#{menuItemLabel}
|
||||||
$of _
|
$of _
|
||||||
|
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -80,6 +84,18 @@
|
|||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
|
||||||
utils.aside(document.querySelector('.main__aside'));
|
var asidenavEl = document.querySelector('.main__aside');
|
||||||
|
var mainEl = document.querySelector('.main__content');
|
||||||
|
|
||||||
|
asidenavEl.style.height = `${mainEl.clientHeight + 75}px`;
|
||||||
|
|
||||||
|
window.addEventListener('resize', function() {
|
||||||
|
window.requestAnimationFrame(function() {
|
||||||
|
asidenavEl.style.height = `${mainEl.clientHeight + 75}px`;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: make it swipeable on mobile and narrower
|
||||||
|
// utils.aside(asidenavEl);
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,40 +1,185 @@
|
|||||||
.main__aside {
|
.main__aside {
|
||||||
position: relative;
|
position: absolute;
|
||||||
background-color: #425d79;
|
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;
|
||||||
|
min-height: calc(100% - 80px);
|
||||||
|
transition: all .2s ease-out;
|
||||||
|
width: 24%;
|
||||||
|
|
||||||
|
~ .main__content {
|
||||||
|
padding-left: 24%;
|
||||||
|
transition: padding-left .2s ease-out;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.main__aside--transitioning {
|
|
||||||
transition: flex-basis .2s ease;
|
/* maximum width of 300px for wide screens */
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
.main__aside {
|
||||||
|
width: 300px;
|
||||||
|
|
||||||
|
~ .main__content {
|
||||||
|
padding-left: 300px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.main__aside--transitioning .asidenav__box{
|
|
||||||
|
.asidenav {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asidenav__box {
|
||||||
transition: opacity .2s ease;
|
transition: opacity .2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main__aside--collapsed.pseudo-hover {
|
.asidenav__box-title {
|
||||||
overflow: visible;
|
padding: 7px 13px;
|
||||||
|
margin-top: 30px;
|
||||||
|
background-color: transparent;
|
||||||
|
transition: all .2s ease;
|
||||||
}
|
}
|
||||||
.main__aside--collapsed {
|
|
||||||
width: 50px;
|
|
||||||
flex-basis: 50px;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
.asidenav__box-title {
|
.asidenav__list-item {
|
||||||
width: 50px;
|
position: relative;
|
||||||
padding: 1px;
|
color: var(--color-lightwhite);
|
||||||
font-size: 18px;
|
|
||||||
text-align: center;
|
&:hover {
|
||||||
margin-bottom: 0;
|
color: var(--color-link);
|
||||||
background-color: var(--fontsec);
|
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 {
|
||||||
|
transform: translateX(100%);
|
||||||
|
opacity: 1;
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
+ .asidenav__list-item {
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* small list-item-padding for medium to large screens */
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
|
||||||
|
.asidenav__list-item {
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.asidenav__list-item--active {
|
||||||
|
background-color: var(--color-lightwhite);
|
||||||
|
|
||||||
.asidenav__link-wrapper {
|
.asidenav__link-wrapper {
|
||||||
|
color: var(--color-link);
|
||||||
|
}
|
||||||
|
|
||||||
|
.asidenav__link-shorthand {
|
||||||
|
transform: scale(1.05, 1.0);
|
||||||
|
transform-origin: right;
|
||||||
|
text-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.asidenav__link-wrapper {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 7px 10px;
|
||||||
|
justify-content: flex-start;
|
||||||
|
color: var(--color-lightwhite);
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
.glyphicon {
|
||||||
|
width: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.glyphicon + .asidenav__link-label {
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.asidenav__link-shorthand {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asidenav__link-label {
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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__link-wrapper {
|
||||||
|
padding-left: 13px;
|
||||||
|
padding-right: 13px;
|
||||||
|
border-left: 20px solid white;
|
||||||
|
transition: all .2s ease;
|
||||||
|
color: var(--color-lightwhite);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* STATE COLLAPSED */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
|
||||||
|
.main__aside {
|
||||||
|
width: 50px;
|
||||||
|
flex-basis: 50px;
|
||||||
|
overflow: hidden;
|
||||||
|
min-height: calc(100% - 50px);
|
||||||
|
|
||||||
|
~ .main__content {
|
||||||
|
padding-left: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asidenav__box-title {
|
||||||
|
width: 50px;
|
||||||
|
padding: 1px;
|
||||||
|
font-size: 18px;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.asidenav__link-shorthand {
|
.asidenav__link-shorthand {
|
||||||
display: flex;
|
display: flex;
|
||||||
position: static;
|
position: static;
|
||||||
background-color: var(--darkbase);
|
|
||||||
color: var(--whitebase);
|
|
||||||
height: 50px;
|
height: 50px;
|
||||||
width: 50px;
|
width: 50px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@ -50,186 +195,32 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
.asidenav__link-label {
|
|
||||||
|
.asidenav__list-item {
|
||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.asidenav {
|
+ .asidenav__list-item {
|
||||||
width: 300px;
|
margin: 0;
|
||||||
margin-top: 20px;
|
}
|
||||||
color: white;
|
|
||||||
|
|
||||||
.js-show-hide__target {
|
|
||||||
overflow: visible;
|
|
||||||
}
|
|
||||||
.js-show-hide__toggle::before {
|
|
||||||
top: 14px;
|
|
||||||
right: 12px;
|
|
||||||
left: auto !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.js-show-hide__toggle:hover::before,
|
|
||||||
.js-show-hide--collapsed .js-show-hide__toggle::before {
|
|
||||||
top: 10px !important;
|
|
||||||
right: 8px !important;
|
|
||||||
}
|
|
||||||
.js-show-hide--collapsed .js-show-hide__toggle:hover::before {
|
|
||||||
top: 14px !important;
|
|
||||||
right: 12px !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.asidenav__box-title {
|
|
||||||
padding: 7px 13px;
|
|
||||||
margin-top: 13px;
|
|
||||||
background-color: transparent;
|
|
||||||
transition: all .2s ease;
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 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 {
|
|
||||||
position: relative;
|
|
||||||
background-color: white;
|
|
||||||
color: var(--darkbase);
|
|
||||||
margin: 4px 0;
|
|
||||||
|
|
||||||
&:not(.asidenav__list-item--active):hover {
|
|
||||||
color: white;
|
|
||||||
background-color: var(--darkbase);
|
|
||||||
|
|
||||||
.asidenav__nested-list {
|
|
||||||
transform: translateX(100%);
|
|
||||||
opacity: 1;
|
|
||||||
width: 200px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.asidenav__link-wrapper,
|
.asidenav__link-wrapper {
|
||||||
|
color: var(--color-lightwhite);
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asidenav__nested-list,
|
||||||
.asidenav__link-label {
|
.asidenav__link-label {
|
||||||
color: white;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.asidenav__link-shorthand {
|
.asidenav__list-item--active {
|
||||||
transform: scale(1.05, 1.0);
|
|
||||||
transform-origin: right;
|
.asidenav__link-wrapper {
|
||||||
|
background-color: var(--color-lightwhite);
|
||||||
|
color: var(--color-dark);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.asidenav__list-item--active {
|
|
||||||
background-color: var(--darkbase);
|
|
||||||
color: white;
|
|
||||||
|
|
||||||
.asidenav__link-wrapper {
|
|
||||||
pointer-events: none;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.asidenav__link-shorthand {
|
|
||||||
transform: scale(1.05, 1.0);
|
|
||||||
transform-origin: right;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.asidenav__link-wrapper {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
height: 50px;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: flex-start;
|
|
||||||
color: var(--darkbase);
|
|
||||||
z-index: 1;
|
|
||||||
|
|
||||||
.glyphicon {
|
|
||||||
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 {
|
|
||||||
padding-left: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.asidenav__toggler {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 20px;
|
|
||||||
height: 50px;
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
transition: background-color .2s ease;
|
|
||||||
border-top: 1px solid var(--whitebase);
|
|
||||||
border-bottom: 1px solid var(--whitebase);
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
content: '\e079';
|
|
||||||
display: block;
|
|
||||||
font-family: 'Glyphicons Halflings';
|
|
||||||
color: var(--whitebase);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--lightbase);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.main__aside--collapsed .asidenav__toggler::before {
|
|
||||||
content: '\e080';
|
|
||||||
}
|
|
||||||
|
|||||||
@ -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}
|
||||||
>
|
<li .breadcrumbs__item>#{title}
|
||||||
<li .breadcrumbs__item--active>#{title}
|
|
||||||
|
|||||||
@ -1,14 +1,60 @@
|
|||||||
.breadcrumbs__container {
|
.breadcrumbs__container {
|
||||||
position: relative;
|
position: relative;
|
||||||
color: white;
|
color: var(--color-font);
|
||||||
z-index: 10;
|
margin-left: 40px;
|
||||||
align-self: flex-end;
|
margin-top: 25px;
|
||||||
margin-bottom: 20px;
|
|
||||||
transition: margin-bottom .2s ease;
|
|
||||||
}
|
}
|
||||||
.breadcrumbs__container--animated {
|
|
||||||
transition: left .2s ease;
|
@media (max-width: 768px) {
|
||||||
|
|
||||||
|
.breadcrumbs__container {
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.breadcrumbs__container .breadcrumbs__link {
|
|
||||||
color: white;
|
.breadcrumbs__link {
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--color-fontsec);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumbs__item {
|
||||||
|
padding-right: 14px;
|
||||||
|
position: relative;
|
||||||
|
line-height: 28px;
|
||||||
|
opacity: 0.8;
|
||||||
|
z-index: 1;
|
||||||
|
margin-right: 10px;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-right: 0;
|
||||||
|
font-weight: 800;
|
||||||
|
color: var(--color-dark);
|
||||||
|
top: 1px;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
--color-separator: var(--color-primary);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 11px;
|
||||||
|
right: 0;
|
||||||
|
width: 7px;
|
||||||
|
height: 7px;
|
||||||
|
border-bottom: 1px solid var(--color-separator);
|
||||||
|
border-right: 1px solid var(--color-separator);
|
||||||
|
transform: rotate(-45deg);
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,5 +5,7 @@ $case formLayout
|
|||||||
$forall view <- views
|
$forall view <- views
|
||||||
<div .form-group :fvRequired view:.form-group--required :not $ fvRequired view:.form-group--optional :isJust $ fvErrors view:.form-group--has-error>
|
<div .form-group :fvRequired view:.form-group--required :not $ fvRequired view:.form-group--optional :isJust $ fvErrors view:.form-group--has-error>
|
||||||
$if not (Blaze.null $ fvLabel view)
|
$if not (Blaze.null $ fvLabel view)
|
||||||
<label .form-group__label .reactive-label for=#{fvId view}>#{fvLabel view}
|
<label .form-group__label for=#{fvId view}>#{fvLabel view}
|
||||||
^{fvInput view}
|
<div .form-group__input>
|
||||||
|
$# FIXME: file-input does not have `required` attribute, although set on form-group
|
||||||
|
^{fvInput view}
|
||||||
|
|||||||
@ -3,23 +3,25 @@
|
|||||||
|
|
||||||
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 <inputs> (array) and
|
||||||
// enables <button> if <validation> for these elements returns true
|
// enables <button> if <validation> for these inputs returns true
|
||||||
window.utils.reactiveButton = function(elements, button, validation) {
|
window.utils.reactiveButton = function(form, button, validation) {
|
||||||
if (elements.length == 0) {
|
var requireds = Array.from(form.querySelectorAll('[required]'));
|
||||||
|
if (requireds.length == 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
var checkboxes = elements[0].getAttribute('type') === 'checkbox';
|
|
||||||
var eventType = checkboxes ? 'change' : 'input';
|
|
||||||
updateButtonState();
|
updateButtonState();
|
||||||
elements.forEach(function(el) {
|
|
||||||
|
requireds.forEach(function(el) {
|
||||||
|
var checkbox = el.getAttribute('type') === 'checkbox';
|
||||||
|
var eventType = checkbox ? 'change' : 'input';
|
||||||
el.addEventListener(eventType, function() {
|
el.addEventListener(eventType, function() {
|
||||||
updateButtonState();
|
updateButtonState();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function updateButtonState() {
|
function updateButtonState() {
|
||||||
if (validation.call(null, elements) === true) {
|
if (validation.call(null, requireds) === true) {
|
||||||
button.removeAttribute('disabled');
|
button.removeAttribute('disabled');
|
||||||
} else {
|
} else {
|
||||||
button.setAttribute('disabled', 'true');
|
button.setAttribute('disabled', 'true');
|
||||||
@ -33,19 +35,21 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
// auto reactiveButton submit-buttons with required fields
|
// auto reactiveButton submit-buttons with required fields
|
||||||
var forms = document.querySelectorAll('form');
|
var forms = document.querySelectorAll('form');
|
||||||
Array.from(forms).forEach(function(form) {
|
Array.from(forms).forEach(function(form) {
|
||||||
var requireds = form.querySelectorAll('[required]');
|
|
||||||
var submitBtn = form.querySelector('[type=submit]');
|
var submitBtn = form.querySelector('[type=submit]');
|
||||||
if (submitBtn && requireds) {
|
if (submitBtn) {
|
||||||
window.utils.reactiveButton(Array.from(requireds), submitBtn, function validateForm(inputs) {
|
window.utils.reactiveButton(form, submitBtn, validateForm);
|
||||||
var done = true;
|
|
||||||
inputs.forEach(function(inp) {
|
|
||||||
var len = inp.value.trim().length;
|
|
||||||
if (done && len === 0) {
|
|
||||||
done = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return done;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function validateForm(inputs) {
|
||||||
|
var done = true;
|
||||||
|
inputs.forEach(function(inp) {
|
||||||
|
var len = inp.value.trim().length;
|
||||||
|
if (done && len === 0) {
|
||||||
|
done = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return done;
|
||||||
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@ -2,9 +2,17 @@ $newline never
|
|||||||
<div .navbar-container>
|
<div .navbar-container>
|
||||||
<nav .navbar.js-sticky-navbar>
|
<nav .navbar.js-sticky-navbar>
|
||||||
|
|
||||||
<!-- breadcrumbs -->
|
<a href="/" .navbar__logo>
|
||||||
$if not $ Just HomeR == mcurrentRoute
|
|
||||||
^{breadcrumbs}
|
<ul .navbar__list.list--inline>
|
||||||
|
$forall menuType <- menuTypes
|
||||||
|
$case menuType
|
||||||
|
$of NavbarAside (MenuItem label mIcon route _)
|
||||||
|
<li .navbar__list-item :highlight route:.navbar__list-item--active>
|
||||||
|
<a .navbar__link-wrapper href=@{route}>
|
||||||
|
<i .fas.fa-#{fromMaybe "none" mIcon}>
|
||||||
|
<div .navbar__link-label>#{label}
|
||||||
|
$of _
|
||||||
|
|
||||||
<ul .navbar__list.list--inline>
|
<ul .navbar__list.list--inline>
|
||||||
$forall menuType <- menuTypes
|
$forall menuType <- menuTypes
|
||||||
|
|||||||
@ -1,48 +1,31 @@
|
|||||||
(function() {
|
(function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
window.utils = window.utils || {};
|
window.utils = window.utils || {};
|
||||||
|
|
||||||
window.utils.stickynav = function(nav) {
|
window.utils.stickynav = function (nav) {
|
||||||
var ticking = false;
|
var ticking = false;
|
||||||
|
|
||||||
init();
|
|
||||||
function init() {
|
|
||||||
nav.style.paddingLeft = document.body.getBoundingClientRect().width < 999 ? '90px' : '';
|
|
||||||
window.setTimeout(function() {
|
|
||||||
nav.classList.add('navbar--animated');
|
|
||||||
}, 200);
|
|
||||||
checkScroll();
|
|
||||||
addListener();
|
|
||||||
}
|
|
||||||
|
|
||||||
// checks scroll direction and shows/hides navbar accordingly
|
|
||||||
function checkScroll() {
|
|
||||||
var sticky = window.scrollY > 0;
|
|
||||||
nav.classList.toggle('navbar--sticky', sticky);
|
|
||||||
ticking = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function addListener() {
|
|
||||||
window.addEventListener('scroll', function(e) {
|
|
||||||
if (!ticking) {
|
|
||||||
window.requestAnimationFrame(checkScroll);
|
|
||||||
ticking = true;
|
|
||||||
}
|
|
||||||
}, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener('resize', function() {
|
|
||||||
nav.style.paddingLeft = document.body.getBoundingClientRect().width < 999 ? '90px' : '';
|
|
||||||
}, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
init();
|
||||||
|
function init() {
|
||||||
|
window.addEventListener('scroll', function (e) {
|
||||||
|
if (!ticking) {
|
||||||
|
window.requestAnimationFrame(update);
|
||||||
|
ticking = true;
|
||||||
|
}
|
||||||
|
}, false);
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
function update() {
|
||||||
|
var sticky = window.scrollY > 30;
|
||||||
|
nav.classList.toggle('navbar--sticky', sticky);
|
||||||
|
ticking = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
})();
|
})();
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
// utils.stickynav(document.querySelector('.js-sticky-navbar'));
|
||||||
utils.stickynav(document.querySelector('.js-sticky-navbar'));
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@ -6,121 +6,221 @@
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: var(--header-height);
|
height: var(--header-height);
|
||||||
padding-right: 5vw;
|
padding-right: 2vw;
|
||||||
padding-left: 90px;
|
padding-left: calc(24% + 40px);
|
||||||
background: var(--darkerbase); /* Old browsers */
|
background: var(--color-darker); /* Old browsers */
|
||||||
background: -moz-linear-gradient(bottom, var(--darkerbase) 0%, #425d79 100%); /* FF3.6-15 */
|
background: -moz-linear-gradient(bottom, var(--color-dark) 0%, var(--color-darker) 100%); /* FF3.6-15 */
|
||||||
background: -webkit-linear-gradient(bottom, var(--darkerbase) 0%,#425d79 100%); /* Chrome10-25,Safari5.1-6 */
|
background: -webkit-linear-gradient(bottom, var(--color-dark) 0%,var(--color-darker) 100%); /* Chrome10-25,Safari5.1-6 */
|
||||||
background: linear-gradient(to top, var(--darkerbase) 0%,#425d79 100%); /* W3C, IE10+, FF16+, Chrome26+, Opera12+, Safari7+ */
|
background: linear-gradient(to top, var(--color-dark) 0%,var(--color-darker) 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;
|
box-shadow: 0 0 4px rgba(0, 0, 0, 0.2);
|
||||||
|
transition: all .2s cubic-bezier(0.03, 0.43, 0.58, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
|
||||||
|
.navbar {
|
||||||
|
padding-left: 340px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
|
||||||
|
.navbar {
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar__logo {
|
||||||
|
position: absolute;
|
||||||
|
top: 15px;
|
||||||
|
left: 20px;
|
||||||
|
transition: all .2s ease;
|
||||||
|
transform-origin: left;
|
||||||
|
width: 0px;
|
||||||
|
color: var(--color-lightwhite);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--color-lightwhite);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: 'UniWorkY';
|
||||||
|
font-family: var(--font-logo);
|
||||||
|
font-size: 42px;
|
||||||
|
font-weight: bold;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
|
||||||
|
.navbar__logo {
|
||||||
|
transform: scale(0.7);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
|
||||||
|
.navbar__logo {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* links */
|
||||||
|
.navbar__link-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: var(--header-height);
|
||||||
|
color: var(--color-lightwhite);
|
||||||
|
transition: height .2s cubic-bezier(0.03, 0.43, 0.58, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar__link-label {
|
||||||
|
transition: opacity .2s ease;
|
||||||
|
padding: 0 13px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
|
||||||
|
.navbar__link-label {
|
||||||
|
padding: 0 7px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 500px) {
|
||||||
|
|
||||||
|
.navbar__link-label {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* navbar list */
|
||||||
.navbar__list {
|
.navbar__list {
|
||||||
align-self: flex-end;
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* list item */
|
||||||
.navbar__list-item {
|
.navbar__list-item {
|
||||||
position: relative;
|
position: relative;
|
||||||
transition: background-color .1s ease;
|
transition: background-color .1s ease;
|
||||||
|
|
||||||
.glyphicon {
|
.glyphicon {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 20px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.glyphicon::before {
|
.glyphicon::before {
|
||||||
height: 20px;
|
height: 20px;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
.navbar :last-child {
|
.fas {
|
||||||
margin-left: auto;
|
height: 20px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar .navbar__link-wrapper {
|
@media (max-width: 500px) {
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
height: 80px;
|
|
||||||
color: var(--whitebase);
|
|
||||||
transition: height .2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar__link-label {
|
.navbar__list-item {
|
||||||
transition: opacity .2s ease;
|
width: 50px;
|
||||||
padding: 0 13px;
|
}
|
||||||
color: white;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar__list-item--secondary {
|
.navbar__list-item--secondary {
|
||||||
margin-left: 20px;
|
margin-left: 20px;
|
||||||
color: var(--greybase);
|
color: var(--color-grey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
|
||||||
|
.navbar__list-item--secondary {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.navbar__list-item--secondary + .navbar__list-item--secondary {
|
.navbar__list-item--secondary + .navbar__list-item--secondary {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
border-left: 0;
|
border-left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar__list-item--active {
|
.navbar__list-item--active {
|
||||||
background-color: white;
|
background-color: var(--color-lightwhite);
|
||||||
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;
|
color: var(--color-dark);
|
||||||
}
|
|
||||||
.navbar__list-item--active .navbar__link-label {
|
|
||||||
color: var(--darkbase);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar .navbar__list-item:not(.navbar__list-item--active):hover {
|
|
||||||
background-color: var(--darkerbase);
|
|
||||||
}
|
|
||||||
.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);
|
background-color: var(--color-darker);
|
||||||
}
|
color: var(--color-lightwhite);
|
||||||
.navbar .navbar__list-item:not(.navbar__list-item--active):hover .navbar__link-label {
|
|
||||||
color: var(--whitebase);
|
|
||||||
}
|
|
||||||
.navbar__list-item--secondary .navbar__link-wrapper,
|
|
||||||
.navbar__list-item--secondary .navbar__link-label {
|
|
||||||
color: var(--greybase);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.navbar__list-item--secondary .navbar__link-wrapper {
|
||||||
|
color: var(--color-grey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* sticky state */
|
||||||
.navbar--sticky {
|
.navbar--sticky {
|
||||||
height: var(--header-height-collapsed);
|
height: var(--header-height-collapsed);
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
|
|
||||||
.navbar__link-wrapper {
|
.navbar__link-wrapper {
|
||||||
height: 50px;
|
height: var(--header-height-collapsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
.breadcrumbs__container {
|
.navbar__logo {
|
||||||
margin-bottom: 7px;
|
top: 5px;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
|
||||||
.navbar--animated {
|
|
||||||
transition: all .2s ease;
|
|
||||||
}
|
|
||||||
.navbar__pushdown {
|
.navbar__pushdown {
|
||||||
/*display: none;*/
|
|
||||||
height: var(--header-height);
|
height: var(--header-height);
|
||||||
transition: height .2s ease;
|
transition: height .2s cubic-bezier(0.03, 0.43, 0.58, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar--sticky + .navbar__pushdown {
|
.navbar--sticky + .navbar__pushdown {
|
||||||
display: block;
|
display: block;
|
||||||
height: var(--header-height-collapsed);
|
height: var(--header-height-collapsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
|
||||||
|
.navbar,
|
||||||
|
.navbar__pushdown {
|
||||||
|
height: var(--header-height-collapsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar__link-wrapper {
|
||||||
|
height: var(--header-height-collapsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-height: 768px) {
|
||||||
|
|
||||||
|
.navbar,
|
||||||
|
.navbar__pushdown {
|
||||||
|
height: var(--header-height-collapsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar__link-wrapper {
|
||||||
|
height: var(--header-height-collapsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar__logo {
|
||||||
|
top: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1,13 +1,17 @@
|
|||||||
.page-nav-prime {
|
.page-nav-prime {
|
||||||
background-color: var(--lightgreybase);
|
margin: 13px 0;
|
||||||
padding: 13px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-nav-prime .pagenav__list {
|
.pagenav__list {
|
||||||
margin: 7px 0 0;
|
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
.page-nav-prime .pagenav__list-item {
|
|
||||||
|
.pagenav__list-item {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-right: 7px;
|
|
||||||
|
&:not(:last-child) {
|
||||||
|
margin-right: 7px;
|
||||||
|
padding-right: 7px;
|
||||||
|
border-right: 1px solid var(--color-primary);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
8
templates/widgets/registerForm.hamlet
Normal file
8
templates/widgets/registerForm.hamlet
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
$# extra protects us against CSRF
|
||||||
|
#{extra}
|
||||||
|
$# Maybe display textField for passcode
|
||||||
|
$maybe secretView <- msecretView
|
||||||
|
^{fvInput secretView}
|
||||||
|
$# Always display register/deregister button
|
||||||
|
^{fvInput btnView}
|
||||||
|
|
||||||
Reference in New Issue
Block a user