Merge branch 'master' of gitlab.cip.ifi.lmu.de:jost/UniWorX into feat/pagination

This commit is contained in:
Gregor Kleen 2018-04-03 16:20:44 +02:00
commit 54cd2bd266
73 changed files with 3451 additions and 7874 deletions

6
.directory Normal file
View File

@ -0,0 +1,6 @@
[Dolphin]
Timestamp=2018,3,14,10,57,55
Version=4
[Settings]
HiddenFilesShown=true

4
.kateproject Normal file
View File

@ -0,0 +1,4 @@
{
"name": "ReWorX"
, "files": [ { "git": 1, "filters": ["*.hs", "*.hamlet", "*.lucius", "*.cassius", "*.julius"], "recursive": 1 } ]
}

View File

@ -1,10 +1,11 @@
* Datensparsamkeit: nur Speichern was notwendig ist; Dokumentieren, warum was gespeichert wird!
* Verfügbarkeit: Backup / aktuelles System; nicht nur eine Person, Anfragen werden organisiert beantwortet
* Integrität: Konsistenzcheck bei Datenübertragen (z.B. LDAP), Sicherheit vor bösen Absichten, Änderungen protokolliert
* Vertraulichkeit: Jeder Benutzer sollte nur auf das zugreifen was unbedingt nötig ist; Backup Verschlüsselung
* Vertraulichkeit: Jeder Benutzer sollte nur auf das zugreifen was unbedingt nötig ist; Backup; Verschlüsselung jeglicher Übermittlung
* Nichtverkettbarkeit: (eher irrelevant für unseren Anwendungsfall)
* Transparenz: User weiß was über ihn gespeichert wird; Dokumentation; Vorfälle schnell melden?
* Intervenierbarkeit: Korrektur/Löschpflichten - auch im Backup; z.B. Korrekturen bei Einspielen des Backups einpflegen; Backup Verschlüsselung; Bei Löschanforderungen muss teilweise gelöscht werden (nur was Notenrelevant muss aufgehoben werden, Hausaufgaben werden gelöscht; Anzeige gelöschter Teilnehmer)
* Intervenierbarkeit: Korrektur/Löschpflichten - auch im Backup; z.B. Korrekturen bei Einspielen des Backups einpflegen; Backup Verschlüsselung; Bei Löschanforderungen muss teilweise gelöscht werden (nur was Notenrelevant muss aufgehoben werden, Hausaufgaben werden gelöscht; Anzeige gelöschter Teilnehmer).
Was sind die Aufbewahrungsfristen für Hausübungen? Wie bei Klausuren?
* Wer ist Datenschutzverantwortlicher? Steffen!?!
=> Sofort anzeigen, wenn etwas schiefläuft.
@ -20,7 +21,8 @@ Aktionen:
- Regularien für Prozess; Aufbewahrungsfristen, Verwaltungsrechtliche Fragen, Bayrisches E-Goverment Gesetz, Daten signierbar/verifizieren;
-> Aktuelle Archivierung von prüfungsrelevanten Daten (Klausur-Lagerung) ist nicht Gesetz-Konform; da Papier-Lagerung nicht konform ist.
Wo ist dokumentiert, welche Klausur wo gelagert wird?!

38
FragenSJ.txt Normal file
View File

@ -0,0 +1,38 @@
** Sicherheitsabfragen?
- Verschlüsselung des Zugriffs?
- SheetDelR tid csh sn : GET zeigt Sicherheitsabfrage
POST löscht.
Ist das so sinnvoll?
Sicherheitsabfrage als PopUpMessage?
- Utils.getKeyBy404 effiziente Variante, welche nur den Key liefert? Esq?
(Sheet.hs -> fetchSheet)
- Handler.Sheet.postSheetDelR: deleteCascade für Files? Klappt das?
Kann man abfragen, was bei deleteCascade alles gelöscht wird?
** i18n:
- i18n der
Links -> MenuItems verwenden wie bisher
Page Titles -> setTitleI
Buttons? -> Kann leicht geändert werden!
Was ist mit einfachen Text Feldern, z.B. die Beschriftung von Knöpfen wie in Handler.Course.getCourseListTermR, Zeile 66 "pageActions" für menuItemLabel?
** Page pageActions - Berechtigungen prüfen?
=> Eigener Constructor statt NavbarLeft/Right?!
** FORMS
3 - Sheets: Multiple Files -> wird später gemacht
- Versionen für Studenten/Korrektoren/Lecturers/Admins
-> ja über isAuthorizedDB siehe unten,
-> Lecturer kann gleich auf Edit-Seite gehen wie in UniWorX
Freischaltung von Teilen einer Webseite:
- Freigabe der Links über Authorisierung in der Foundation
- Anzeige der Links nach Authorisierung wie in menItemAccessCallback
- möglichst direkt isAuthorizedDB in einem runDB aufrufen!!!

View File

@ -1,21 +1,24 @@
# Quick Start Guide
Assuming Ubuntu or similar
The following Description applies to Ubuntu or similar.
## Clone repository
Clone this repository `git clone https://gitlab.cip.ifi.lmu.de/jost/UniWorX.git` and navigate into the new directory `cd UniWorX`.
## LDAP
install:<br>
`sudo apt-get install slapd ldap-utils`
install:
`sudo apt-get install slapd ldap-utils`
## PostgreSQL
install:<br>
`sudo apt-get install postgresql`
install:
`sudo apt-get install postgresql`
switch to user *postgres* (got created during installation):<br>
`sudo -i -u postgres`
switch to user *postgres* (got created during installation):
`sudo -i -u postgres`
add db user *uniworx*:<br>
`createuser --interactive`
add db user *uniworx*:
`createuser --interactive`
you'll get a prompt:
@ -24,49 +27,60 @@ Assuming Ubuntu or similar
Shall the new role be a superuser? (y/n)` - [not exactly sure. Guess not?]
```
create database *uniworx*:<br>
`createdb uniworx`
create database *uniworx*:
`createdb uniworx`
to access the database as user *uniworx* you now need to add a new linux-user called *uniworx*:<br>
`sudo adduser uniworx`
to access the database as user *uniworx* you now need to add a new linux-user called *uniworx*:
`sudo adduser uniworx`
log-in as new user *uniworx*:<br>
`sudo -i -u uniworx`
log-in as new user *uniworx*:
`sudo -i -u uniworx`
you can now use `psql uniworx` to execute SQL-commands and such.
you might for example want to add a test-account to be able to login on the page:<br>
`INSERT INTO user (plugin, ident, matrikelnummer, email, display_name) VALUES ('LDAP', '[YOUR_EMAIL_ADDRESS]', null, '[YOUR_EMAIL_ADDRESS]', '[YOUR_NAME]');`
## stack
Install with:<br>
`curl -sSL https://get.haskellstack.org/ | sh`
Install with:
`curl -sSL https://get.haskellstack.org/ | sh`
setup stack and install dependencies:<br>
`stack setup`
setup stack and install dependencies:
`stack setup`
There might be packages missing during setup. You most probably simply need to install them and try again.<br>
Instructions are easy to find via search engine of your choice and the specific error you got.<br>
Example from experience: For LDAP `ldab` and `lber` header files were missing.
During this step or the next you might get an error that says something about missing C libraries for `ldap` and `lber`. You can install these using
`sudo apt-get install libsasl2-dev libldap2-dev`
Build the app:<br>
`stack build`
If you get an error that says *You need to install postgresql-server-dev-X.Y for building a server-side extension or libpq-dev for building a client-side application.*
Go ahead an install `libpq-dev` with
`sudo apt-get install libpq-dev`
Run the app (with environment variable DUMMY_LUGIN set to true):<br>
`env DUMMY_LOGIN=true stack exec -- yesod devel`
Build the app:
`stack build`
`Devel application launched: http://localhost:3000`<br>
This might take a few minutes if not hours... be prepared.
install yesod:
`stack install yesod-bin --install-ghc`
## Add Dumy-Data and run the app
After building the app you can prepare the database and add some dummy data:
`./fill-db.hs`
Run the app:
`./start.sh`
`Devel application launched: http://localhost:3000`
means you are good to go.
If you followed the steps above you should now be able to login as user Gregor Kleen using `LDAP:g.kleen@ifi.lmu.de` as dummy login.
***
# Sources and more infos
PostgreSQl: <br>
PostgreSQl:
https://www.digitalocean.com/community/tutorials/how-to-install-and-use-postgresql-on-ubuntu-16-04
stack:<br> https://docs.haskellstack.org/en/stable/README/#how-to-install
stack: https://docs.haskellstack.org/en/stable/README/#how-to-install
ldap:<br>https://wiki.ubuntuusers.de/OpenLDAP_ab_Precise/
ldap: https://wiki.ubuntuusers.de/OpenLDAP_ab_Precise/
***
@ -86,5 +100,19 @@ psql -U uniworx -d uniworx -h 127.0.0.1 -w
--Zeige Tabellen
\dt
--Zeige Tabellen Inhalt:
TABEL "user";
-- Die Anführungszeichen können manchmal weggelassen werden, aber
-- bei user sind sie notwendig, da es auch Schlüsselwort in sql ist.
--Lösche Tabelle "course" und alle davon abhängigen:
DROP TABLE "course" CASCADE;
-- UserId 5 zum Lecturer in SchoolId 1 machen (27 ist laufende Nummer)
INSERT INTO "user_lecturer" (id,"user",school) VALUES (27,5,1);
-- Beenden:
\q
-- Hilfe:
\help

View File

@ -14,6 +14,7 @@ main :: IO ()
main = db $ do
now <- liftIO getCurrentTime
let
summer2017 = TermIdentifier 2017 Summer
winter2017 = TermIdentifier 2017 Winter
summer2018 = TermIdentifier 2018 Summer
gkleen <- insert User
@ -23,6 +24,29 @@ main = db $ do
, userEmail = "G.Kleen@campus.lmu.de"
, userDisplayName = "Gregor Kleen"
}
fhamann <- insert User
{ userPlugin = "LDAP"
, userIdent = "felix.hamann@campus.lmu.de"
, userMatrikelnummer = Nothing
, userEmail = "felix.hamann@campus.lmu.de"
, userDisplayName = "Felix Hamann"
}
jost <- insert User
{ userPlugin = "LDAP"
, userIdent = "jost@tcs.ifi.lmu.de"
, userMatrikelnummer = Nothing
, userEmail = "jost@tcs.ifi.lmu.de"
, userDisplayName = "Steffen Jost"
}
void . insert $ Term
{ termName = summer2017
, termStart = fromGregorian 2017 04 09
, termEnd = fromGregorian 2017 07 14
, termHolidays = []
, termLectureStart = fromGregorian 2017 04 09
, termLectureEnd = fromGregorian 2018 07 14
, termActive = False
}
void . insert $ Term
{ termName = winter2017
, termStart = fromGregorian 2017 10 16
@ -45,9 +69,16 @@ main = db $ do
mi <- insert $ School "Institut für Mathematik" "MI"
void . insert $ UserAdmin gkleen ifi
void . insert $ UserAdmin gkleen mi
void . insert $ UserAdmin fhamann ifi
void . insert $ UserAdmin jost ifi
void . insert $ UserAdmin jost mi
void . insert $ UserLecturer gkleen ifi
void . insert $ UserLecturer fhamann ifi
void . insert $ UserLecturer jost ifi
ifiBsc <- insert $ Degree "Bachelor Informatik" ifi
ifiMsc <- insert $ Degree "Master Informatik" ifi
miBsc <- insert $ Degree "Bachelor Mathematik" mi
-- FFP
ffp <- insert Course
{ courseName = "Fortgeschrittene Funktionale Programmierung"
, courseDescription = Nothing
@ -56,16 +87,96 @@ main = db $ do
, courseTermId = TermKey summer2018
, courseSchoolId = ifi
, courseCapacity = Just 20
, courseCreated = now
, courseChanged = now
, courseCreatedBy = gkleen
, courseChangedBy = gkleen
, courseHasRegistration = True
, courseRegisterFrom = Just now
, courseRegisterTo = Just ((3600 * 24 * 60) `addUTCTime` now )
}
insert_ $ CourseEdit jost now ffp
void . insert $ DegreeCourse ifiBsc ffp
void . insert $ DegreeCourse ifiMsc ffp
void . insert $ Lecturer gkleen ffp
void . insert $ Corrector gkleen ffp (ByProportion 1)
void . insert $ Sheet ffp "Blatt 1" Nothing NotGraded Nothing now now Nothing Nothing now now gkleen gkleen
insert_ $ Corrector gkleen ffp (ByProportion 1)
sheetkey <- insert $ Sheet ffp "Blatt 1" Nothing NotGraded NoGroups Nothing Nothing now now Nothing Nothing
insert_ $ SheetEdit gkleen now sheetkey
-- EIP
eip <- insert Course
{ courseName = "Einführung in die Programmierung"
, courseDescription = Nothing
, courseLinkExternal = Nothing
, courseShorthand = "eip"
, courseTermId = TermKey summer2017
, courseSchoolId = ifi
, courseCapacity = Just 20
, courseHasRegistration = False
, courseRegisterFrom = Nothing
, courseRegisterTo = Nothing
}
insert_ $ CourseEdit fhamann now eip
void . insert $ DegreeCourse ifiBsc eip
void . insert $ DegreeCourse ifiMsc eip
void . insert $ Lecturer fhamann eip
-- interaction design
ixd <- insert Course
{ courseName = "Interaction Design (User Experience Design I & II)"
, courseDescription = Nothing
, courseLinkExternal = Nothing
, courseShorthand = "ixd"
, courseTermId = TermKey summer2018
, courseSchoolId = ifi
, courseCapacity = Just 20
, courseHasRegistration = True
, courseRegisterFrom = Just now
, courseRegisterTo = Just ((3600 * 24 * 60) `addUTCTime` now )
}
insert_ $ CourseEdit fhamann now ixd
void . insert $ DegreeCourse ifiBsc ixd
void . insert $ Lecturer fhamann ixd
-- concept development
ux3 <- insert Course
{ courseName = "Concept Development (User Experience Design III)"
, courseDescription = Nothing
, courseLinkExternal = Nothing
, courseShorthand = "ux3"
, courseTermId = TermKey winter2017
, courseSchoolId = ifi
, courseCapacity = Just 30
, courseHasRegistration = False
, courseRegisterFrom = Nothing
, courseRegisterTo = Nothing
}
insert_ $ CourseEdit fhamann now ux3
void . insert $ DegreeCourse ifiBsc ux3
void . insert $ Lecturer fhamann ux3
-- promo
pmo <- insert Course
{ courseName = "Programmierung und Modellierung"
, courseDescription = Nothing
, courseLinkExternal = Nothing
, courseShorthand = "pmo"
, courseTermId = TermKey summer2017
, courseSchoolId = ifi
, courseCapacity = Just 50
, courseHasRegistration = False
, courseRegisterFrom = Nothing
, courseRegisterTo = Nothing
}
insert_ $ CourseEdit jost now pmo
void . insert $ DegreeCourse ifiBsc pmo
void . insert $ Lecturer jost pmo
-- datenbanksysteme
dbs <- insert Course
{ courseName = "Datenbanksysteme"
, courseDescription = Nothing
, courseLinkExternal = Nothing
, courseShorthand = "dbs"
, courseTermId = TermKey summer2018
, courseSchoolId = ifi
, courseCapacity = Just 50
, courseHasRegistration = False
, courseRegisterFrom = Nothing
, courseRegisterTo = Nothing
}
insert_ $ CourseEdit gkleen now dbs
void . insert $ DegreeCourse ifiBsc dbs
void . insert $ Lecturer gkleen dbs
void . insert $ Lecturer jost dbs

View File

@ -1,4 +1,24 @@
SummerTerm year@Integer: Sommersemester #{tshow year}
WinterTerm year@Integer: Wintersemester #{tshow year}/#{tshow $ succ year}
PSLimitNonPositive: “pagesize” muss größer als null sein
Page n@Int64 num@Int64: Seite #{tshow n} von #{tshow num}
Page n@Int64 num@Int64: Seite #{tshow n} von #{tshow num}
TermEdited tid@TermIdentifier: Semester #{termToText tid} erfolgreich editiert.
TermNewTitle: Semester editiere/anlegen.
InvalidInput: Eingaben bitte korrigieren.
CourseNewOk tid@TermIdentifier courseShortHand@Text: Kurs #{termToText tid}-#{courseShortHand} wurde erfolgreich erstellt.
CourseEditOk tid@TermIdentifier courseShortHand@Text: Kurs #{termToText tid}-#{courseShortHand} wurde erfolgreich geändert.
CourseNewDupShort tid@TermIdentifier courseShortHand@Text: Kurs #{termToText tid}-#{courseShortHand} konnte nicht erstellt werden: Es gibt bereits einen anderen Kurs mit dem Kürzel #{courseShortHand} in diesem Semester.
CourseEditDupShort tid@TermIdentifier courseShortHand@Text: Kurs #{termToText tid}-#{courseShortHand} konnte nicht geändert werden: Es gibt bereits einen anderen Kurs mit dem Kürzel #{courseShortHand} in diesem Semester.
SheetNewOk tid@TermIdentifier courseShortHand@Text sheetName@Text: Neues Übungsblatt #{sheetName} wurde im Kurs #{termToText tid}-#{courseShortHand} erfolgreich erstellt.
SheetTitle tid@TermIdentifier courseShortHand@Text sheetName@Text: #{termToText tid}-#{courseShortHand} #{sheetName}
SheetTitleNew tid@TermIdentifier courseShortHand@Text : #{termToText tid}-#{courseShortHand}: Neues Übungsblatt
SheetEditOk tid@TermIdentifier courseShortHand@Text sheetName@Text: Übungsblatt #{sheetName} aus Kurs #{termToText tid}-#{courseShortHand} wurde gespeichert.
SheetNameDup tid@TermIdentifier courseShortHand@Text sheetName@Text: Es gibt bereits ein Übungsblatt #{sheetName} in diesem Kurs #{termToText tid}-#{courseShortHand}.
SheetDelTitle tid@TermIdentifier courseShortHand@Text sheetName@Text: Übungsblatt #{sheetName} wirklich aus Kurs #{termToText tid}-#{courseShortHand} herauslöschen?
SheetDelText submissionNo@Int: Dies kann nicht mehr rückgängig gemacht werden! Alle Einreichungen gehen ebenfalls verloren! Es gibt #{show submissionNo} Abgaben.
SheetDelOk tid@TermIdentifier courseShortHand@Text sheetName@Text: #{termToText tid}-#{courseShortHand}: Übungsblatt #{sheetName} gelöscht.
UnauthorizedSchoolAdmin: Sie sind nicht als Administrator für dieses Institut eingetragen.
UnauthorizedSchoolLecturer: Sie sind nicht als Veranstalter für dieses Institut eingetragen.
UnauthorizedLecturer: Sie sind nicht als Veranstalter für diese Veranstaltung eingetragen.
UnauthorizedCorrector: Sie sind nicht als Korrektor für diese Veranstaltung eingetragen.
UnauthorizedParticipant: Sie sind nicht als Teilnehmer für diese Veranstaltung registriert.

61
models
View File

@ -11,7 +11,7 @@ UserAdmin
UserLecturer
user UserId
school SchoolId
UniqueSchoolLecturer school user
UniqueSchoolLecturer user school
StudyFeatures
user UserId
degree StudyDegreeId
@ -30,8 +30,8 @@ StudyTerms
name Text Maybe
Primary key
Term json
name TermIdentifier
start Day
name TermIdentifier -- unTermKey :: TermId -> TermIdentifier
start Day -- TermKey :: TermIdentifier -< TermId
end Day
holidays [Day]
lectureStart Day
@ -61,14 +61,14 @@ Course
termId TermId
schoolId SchoolId
capacity Int Maybe
created UTCTime
changed UTCTime
createdBy UserId
changedBy UserId
hasRegistration Bool -- canRegisterNow = hasRegistration && maybe False (<= currentTime) registerFrom && maybe True (>= currentTime) registerTo
registerFrom UTCTime Maybe
registerTo UTCTime Maybe
CourseTermShort termId shorthand
CourseTermShort termId shorthand
CourseEdit
user UserId
time UTCTime
course CourseId
Lecturer
userId UserId
courseId CourseId
@ -90,22 +90,24 @@ CourseParticipant
courseId CourseId
userId UserId
registration UTCTime
UniqueCourseParticipant courseId userId
UniqueParticipant userId courseId
Sheet
courseId CourseId
name Text
description Html Maybe
type SheetType
grouping SheetGroup
markingText Html Maybe
visibleFrom UTCTime Maybe
activeFrom UTCTime
activeTo UTCTime
hintFrom UTCTime Maybe
solutionFrom UTCTime Maybe
created UTCTime
changed UTCTime
createdBy UserId
changedBy UserId
CourseSheet courseId name
SheetEdit
user UserId
time UTCTime
sheet SheetId
SheetFile
sheetId SheetId
fileId FileId
@ -122,11 +124,11 @@ Submission
ratingComment Text Maybe
ratingBy UserId Maybe
ratingTime UTCTime Maybe
created UTCTime
changed UTCTime
createdBy UserId
changedBy UserId
deriving Show
SubmissionEdit
user UserId
time UTCTime
submission SubmissionId
SubmissionFile
submissionId SubmissionId
fileId FileId
@ -141,10 +143,10 @@ SubmissionUser
SubmissionGroup
courseId CourseId
name Text
created UTCTime
changed UTCTime
createdBy UserId
changedBy UserId
SubmissionGroupEdit
user UserId
time UTCTime
submissionGroup SubmissionGroupId
SubmissionGroupUser
submissionGroupId SubmissionGroupId
userId UserId
@ -163,13 +165,12 @@ Booking
end UTCTime
weekly Bool
exceptions [Day] -- only if weekly, begin in exception
created UTCTime
changed UTCTime
createdBy UserId
changedBy UserId
bookedFor RoomForId
room RoomId
BookingEdit
user UserId
time UTCTime
boooking BookingId
Room
name Text
capacity Int Maybe
@ -195,10 +196,10 @@ Exam
deregistrationEnd UTCTime
ratingVisible Bool
statisticsVisible Bool
created UTCTime
changed UTCTime
createdBy UserId
changedBy UserId
ExamEdit
user UserId
time UTCTime
exam ExamId
ExamUser
userId UserId
examId ExamId

34
routes
View File

@ -6,25 +6,29 @@
/ HomeR GET POST
/profile ProfileR GET
/users UsersR GET
/users UsersR GET !adminAny
/term TermShowR GET
/term/edit TermEditR GET POST
/term/#TermId/edit TermEditExistR GET
/term TermShowR GET
/term/edit TermEditR GET POST !adminAny
/term/#TermId/edit TermEditExistR GET !adminAny
/course/ CourseListR GET
!/course/new CourseEditR GET POST
!/course/#TermId CourseListTermR GET
/course/#TermId/#Text/edit CourseEditExistR GET
/course/#TermId/#Text/show CourseShowR GET POST
!/course/new CourseNewR GET POST !lecturerAny
!/course/#TermId CourseListTermR GET
/course/#TermId/#Text CourseR:
/show CourseShowR GET POST
/edit CourseEditR GET POST !lecturer
/course/#TermId/#Text/sheet/ SheetListR GET
/course/#TermId/#Text/sheet/#Text/show SheetShowR GET
/course/#TermId/#Text/sheet/#Text/#SheetFileType/#FilePath SheetFileR GET
/course/#TermId/#Text/sheet/new SheetNewR GET POST
/course/#TermId/#Text/sheet/#SheetId/edit SheetEditR GET POST
/course/#TermId/#Text/sheet/#SheetId/delete SheetDelR GET POST
/ex SheetR !registered:
/ SheetListR GET
/#Text/show SheetShowR GET !time
/#Text/#SheetFileType/#FilePath SheetFileR GET !time
/new SheetNewR GET POST !lecturer
/#Text/edit SheetEditR GET POST !lecturer
/#Text/delete SheetDelR GET POST !lecturer
-- TODO below
/submission SubmissionListR GET POST
/submission/#CryptoUUIDSubmission SubmissionR GET POST
/submissions.zip SubmissionDownloadMultiArchiveR POST
@ -34,4 +38,4 @@
!/#UUID CryptoUUIDDispatchR GET
-- For demonstration
/course/#CryptoUUIDCourse/edit CourseEditExistIDR GET
/course/#CryptoUUIDCourse/edit CourseEditIDR GET

View File

@ -34,5 +34,6 @@ instance PathPiece UUID where
decCryptoIDs [ ''SubmissionId
, ''CourseId
, ''SheetId
, ''FileId
]
{- TODO: Do we need/want CryptoUUIDs for Sheet numbers? -}

View File

@ -4,6 +4,7 @@
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE ViewPatterns #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE NamedFieldPuns #-}
@ -51,6 +52,11 @@ import Handler.Utils.StudyFeatures
import System.FilePath
import Handler.Utils.Templates
-- infixl 9 :$:
-- pattern a :$: b = a b
-- | The foundation datatype for your application. This can be a good place to
-- keep settings and values requiring initialization before your application
-- starts running, such as database connections. Every handler will have
@ -78,18 +84,27 @@ data UniWorX = UniWorX
-- type Widget = WidgetT UniWorX IO ()
mkYesodData "UniWorX" $(parseRoutesFile "routes")
-- Pattern Synonyms for convenience
pattern CSheetR tid csh ptn = CourseR tid csh (SheetR ptn)
data MenuItem = MenuItem
{ menuItemLabel :: Text
, menuItemIcon :: Maybe Text
, menuItemRoute :: Route UniWorX
, menuItemAccessCallback :: Handler Bool
}
data MenuTypes
= NavbarLeft { menuItem :: MenuItem }
| NavbarRight { menuItem :: MenuItem }
| NavbarExtra { menuItem :: MenuItem }
data MenuTypes -- Semantische Rolle:
= NavbarAside { menuItem :: MenuItem } -- TODO
| NavbarExtra { menuItem :: MenuItem } -- TODO
| NavbarRight { menuItem :: MenuItem } -- Generell, nahezu immer sichtbar
| NavbarSecondary { menuItem :: MenuItem } -- Generell, nahezu immer sichtbar
| PageActionPrime { menuItem :: MenuItem } -- Seitenspezifische Aktion, häufig
| PageActionSecondary { menuItem :: MenuItem } -- Seitenspezifische Aktion, selten
-- | A convenient synonym for creating forms.
-- | Convenient Type Synonyms:
type DB a = YesodDB UniWorX a
type Form x = Html -> MForm (HandlerT UniWorX IO) (FormResult x, Widget)
mkMessage "UniWorX" "messages" "de"
@ -145,7 +160,7 @@ instance Yesod UniWorX where
isAuthorized TermShowR _ = return Authorized
isAuthorized CourseListR _ = return Authorized
isAuthorized (CourseListTermR _) _ = return Authorized
isAuthorized (CourseShowR _ _) _ = return Authorized
isAuthorized (CourseR _ _ CourseShowR) _ = return Authorized
isAuthorized (CryptoUUIDDispatchR _) _ = return Authorized
isAuthorized SubmissionListR _ = isAuthenticated
isAuthorized SubmissionDownloadMultiArchiveR _ = isAuthenticated
@ -192,16 +207,28 @@ instance Yesod UniWorX where
makeLogger = return . appLogger
isAuthorizedDB :: Route UniWorX -> Bool -> YesodDB UniWorX AuthResult
isAuthorizedDB route@(routeAttrs -> attrs) writeable
| "adminAny" `member` attrs = adminAccess Nothing
| "lecturerAny" `member` attrs = lecturerAccess Nothing
isAuthorizedDB UsersR _ = adminAccess Nothing
isAuthorizedDB (SubmissionR cID) _ = submissionAccess $ Right cID
isAuthorizedDB (SubmissionR cID) _ = submissionAccess $ Right cID
isAuthorizedDB (SubmissionDownloadSingleR cID _) _ = submissionAccess $ Right cID
isAuthorizedDB (SubmissionDownloadArchiveR (splitExtension -> (baseName, _))) _ = submissionAccess . Left . CryptoID $ CI.mk baseName
isAuthorizedDB TermEditR _ = adminAccess Nothing
isAuthorizedDB (TermEditExistR _) _ = adminAccess Nothing
isAuthorizedDB CourseEditR _ = lecturerAccess Nothing
isAuthorizedDB (CourseEditExistR t c) _ = courseLecturerAccess . entityKey =<< getBy404 (CourseTermShort t c)
isAuthorizedDB (SheetNewR t c) _ = courseLecturerAccess . entityKey =<< getBy404 (CourseTermShort t c)
isAuthorizedDB (CourseEditExistIDR cID) _ = do
isAuthorizedDB TermEditR _ = adminAccess Nothing
isAuthorizedDB (TermEditExistR _) _ = adminAccess Nothing
isAuthorizedDB CourseNewR _ = lecturerAccess Nothing
isAuthorizedDB (CourseR t c CourseEditR) _ = courseLecturerAccess . entityKey =<< getBy404 (CourseTermShort t c)
isAuthorizedDB (CourseR t c (SheetR SheetListR)) False = return Authorized --
isAuthorizedDB (CourseR t c (SheetR SheetListR)) _ = courseLecturerAccess . entityKey =<< getBy404 (CourseTermShort t c)
isAuthorizedDB (CourseR t c (SheetR (SheetShowR s))) _ = return Authorized -- TODO: nur für angemeldete Kursteilnehmer falls sichtbar, sonst nur Lectrurer oder Korrektor
isAuthorizedDB (CourseR t c (SheetR (SheetFileR s _ _))) _ = return Authorized -- TODO: nur für angemeldete Kursteilnehmer falls sichtbar, sonst nur Lectrurer oder Korrektor
isAuthorizedDB (CourseR t c (SheetR SheetNewR)) _ = courseLecturerAccess . entityKey =<< getBy404 (CourseTermShort t c)
isAuthorizedDB (CourseR t c (SheetR (SheetEditR s))) _ = courseLecturerAccess . entityKey =<< getBy404 (CourseTermShort t c)
isAuthorizedDB (CourseR t c (SheetR (SheetDelR s))) _ = courseLecturerAccess . entityKey =<< getBy404 (CourseTermShort t c)
isAuthorizedDB (CourseEditIDR cID) _ = do
courseId <- decrypt cID
courseLecturerAccess courseId
isAuthorizedDB _route _isWrite = return $ Unauthorized "No access to this route." -- Calling isAuthorized here creates infinite loop!
@ -219,29 +246,45 @@ submissionAccess cID = do
adminAccess :: Maybe SchoolId -- ^ If @Just@, matched exactly against 'userAdminSchool'
-> YesodDB UniWorX AuthResult
adminAccess school = do
adminAccess school = do
authId <- lift requireAuthId
adrights <- selectList ((UserAdminUser ==. authId) : maybe [] (\s -> [UserAdminSchool ==. s]) school) []
return $ if (not $ null adrights)
adrights <- selectList ((UserAdminUser ==. authId) : maybe [] (\s -> [UserAdminSchool ==. s]) school) []
return $ if (not $ null adrights)
then Authorized
else Unauthorized "No admin access"
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) []
lecrights <- selectList ((UserLecturerUser ==. authId) : maybe [] (\s -> [UserLecturerSchool ==. s]) school) []
return $ if (not $ null lecrights)
then Authorized
else Unauthorized "No lecturer access"
else Unauthorized "No lecturer access" -- TODO internationalize
lecturerAccess' :: SchoolId -> YesodDB UniWorX AuthResult
lecturerAccess' = authorizedFor UniqueSchoolLecturer MsgUnauthorizedSchoolLecturer
courseLecturerAccess :: CourseId -> YesodDB UniWorX AuthResult
courseLecturerAccess courseId = do
courseLecturerAccess = authorizedFor UniqueLecturer MsgUnauthorizedLecturer
courseCorrectorAccess :: CourseId -> YesodDB UniWorX AuthResult
courseCorrectorAccess = authorizedFor UniqueCorrector MsgUnauthorizedCorrector
courseParticipantAccess :: CourseId -> YesodDB UniWorX AuthResult
courseParticipantAccess = authorizedFor UniqueParticipant MsgUnauthorizedParticipant
authorizedFor :: ( PersistEntityBackend record ~ BaseBackend backend
, PersistEntity record, PersistUniqueRead backend
, YesodAuth master, RenderMessage master msg
)
=> (AuthId master -> t -> Unique record) -> msg -> t -> ReaderT backend (HandlerT master IO) AuthResult
authorizedFor authType msg courseId = do
authId <- lift requireAuthId
users <- map (lecturerUserId . entityVal ) <$> selectList [ LecturerCourseId ==. courseId ] []
return $ case authId `elem` users of
True -> Authorized
False -> Unauthorized "No lecturer access for this course"
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
@ -250,61 +293,82 @@ isAuthorized' :: Route UniWorX -> Bool -> Handler Bool
isAuthorized' route isWrite = runDB $ isAuthorizedDB' route isWrite
-- Define breadcrumbs.
instance YesodBreadcrumbs UniWorX where
instance YesodBreadcrumbs UniWorX where
breadcrumb TermShowR = return ("Semester", Just HomeR)
breadcrumb TermEditR = return ("Neu", Just TermShowR)
breadcrumb (TermEditExistR _) = return ("Editieren", Just TermShowR)
breadcrumb CourseListR = return ("Kurs", Just HomeR)
breadcrumb (CourseListTermR term) = return (toPathPiece term, Just TermShowR)
breadcrumb (CourseShowR term course) = return (course, Just $ CourseListTermR term)
breadcrumb CourseEditR = return ("Neu", Just CourseListR)
breadcrumb (CourseEditExistR _ _) = return ("Editieren", Just CourseListR)
breadcrumb (SheetListR tid csh) = return ("Kurs", Just $ CourseShowR tid csh)
breadcrumb (SheetShowR tid csh _shn) = return ("Übungen", Just $ SheetListR tid csh)
breadcrumb CourseListR = return ("Kurs", Just HomeR)
breadcrumb (CourseListTermR term) = return (toPathPiece term, Just TermShowR)
breadcrumb (CourseR term course CourseShowR) = return (course, Just $ CourseListTermR term)
breadcrumb CourseNewR = return ("Neu", Just CourseListR)
breadcrumb (CourseR _ _ CourseEditR) = return ("Editieren", Just CourseListR)
breadcrumb (CourseR tid csh (SheetR SheetListR)) = return ("Übungen",Just $ CourseR tid csh CourseShowR)
breadcrumb (CourseR tid csh (SheetR SheetNewR )) = return ("Neu", Just $ CourseR tid csh $ SheetR SheetListR)
breadcrumb (CourseR tid csh (SheetR (SheetShowR shn))) = return (shn, Just $ CourseR tid csh $ SheetR SheetListR)
breadcrumb (CourseR tid csh (SheetR (SheetEditR shn))) = return ("Edit", Just $ CourseR tid csh $ SheetR $ SheetShowR shn)
breadcrumb (CourseR tid csh (SheetR (SheetDelR shn))) = return ("DELETE", Just $ CourseR tid csh $ SheetR $ SheetShowR shn)
breadcrumb SubmissionListR = return ("Abgaben", Just HomeR)
breadcrumb (SubmissionR _) = return ("Abgabe", Just SubmissionListR)
breadcrumb HomeR = return ("ReWorX", Nothing)
breadcrumb (AuthR _) = return ("Login", Just HomeR)
breadcrumb ProfileR = return ("Profile", Just HomeR)
breadcrumb _ = return ("home", Nothing)
defaultLinks :: [MenuTypes]
defaultLinks = -- Define the menu items of the header.
[ NavbarLeft $ MenuItem
[ NavbarRight $ MenuItem
{ menuItemLabel = "Home"
, menuItemIcon = Just "home"
, menuItemRoute = HomeR
, menuItemAccessCallback = return True
}
, NavbarLeft $ MenuItem
{ menuItemLabel = "Kurse"
, menuItemRoute = CourseListR
, menuItemAccessCallback = return True
}
, NavbarRight $ MenuItem
{ menuItemLabel = "Users"
, menuItemRoute = UsersR
, menuItemAccessCallback = return True -- Creates a LOOP: (Authorized ==) <$> isAuthorized UsersR False
}
, NavbarRight $ MenuItem
{ menuItemLabel = "Profile"
, menuItemIcon = Just "profile"
, menuItemRoute = ProfileR
, menuItemAccessCallback = isJust <$> maybeAuthPair
}
, NavbarRight $ MenuItem
, NavbarSecondary $ MenuItem
{ menuItemLabel = "Login"
, menuItemIcon = Just "login"
, menuItemRoute = AuthR LoginR
, menuItemAccessCallback = isNothing <$> maybeAuthPair
}
, NavbarRight $ MenuItem
, NavbarSecondary $ MenuItem
{ menuItemLabel = "Logout"
, menuItemIcon = Just "logout"
, menuItemRoute = AuthR LogoutR
, menuItemAccessCallback = isJust <$> maybeAuthPair
}
, NavbarAside $ MenuItem
{ menuItemLabel = "Aktuelle Veranstaltungen"
, menuItemIcon = Just "book"
, menuItemRoute = CourseListR -- should be CourseListActiveR or similar in the future
, menuItemAccessCallback = return True
}
, NavbarAside $ MenuItem
{ menuItemLabel = "Alte Veranstaltungen"
, menuItemIcon = Just "book"
, menuItemRoute = CourseListR -- should be CourseListInactiveR or similar in the future
, menuItemAccessCallback = return True
}
, NavbarAside $ MenuItem
{ menuItemLabel = "Veranstaltungen"
, menuItemIcon = Just "book"
, menuItemRoute = CourseListR
, menuItemAccessCallback = return True
}
, NavbarAside $ MenuItem
{ menuItemLabel = "Benutzer"
, menuItemIcon = Just "user"
, menuItemRoute = UsersR
, menuItemAccessCallback = return True -- Creates a LOOP: (Authorized ==) <$> isAuthorized UsersR False
}
]
defaultLinkLayout :: [MenuTypes] -> Widget -> Handler Html
@ -328,12 +392,34 @@ defaultMenuLayout menu widget = do
-- value passed to hamletToRepHtml cannot be a widget, this allows
-- you to use normal widget features in default-layout.
let
navbar :: Widget
navbar = $(widgetFile "widgets/navbar")
asidenav :: Widget
asidenav = $(widgetFile "widgets/asidenav")
breadcrumbs :: Widget
breadcrumbs = $(widgetFile "widgets/breadcrumbs")
pageactionprime :: Widget
pageactionprime = $(widgetFile "widgets/pageactionprime")
-- functions to determine if there are page-actions
isPageActionPrime :: MenuTypes -> Bool
isPageActionPrime (PageActionPrime _) = True
isPageActionPrime _ = False
hasPageActions :: Bool
hasPageActions = any isPageActionPrime menuTypes
pc <- widgetToPageContent $ do
addStylesheet $ StaticR css_bootstrap_css
addStylesheetRemote "https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,600,800,900"
addScript $ StaticR js_featureChecker_js
addStylesheet $ StaticR css_fonts_css
addStylesheet $ StaticR css_icons_css
$(widgetFile "default-layout")
$(widgetFile "standalone/modal")
$(widgetFile "standalone/showHide")
$(widgetFile "standalone/sortable")
$(widgetFile "standalone/inputs")
withUrlRenderer $(hamletFile "templates/default-layout-wrapper.hamlet")
-- How to run database actions.
instance YesodPersist UniWorX where
type YesodPersistBackend UniWorX = SqlBackend
@ -363,7 +449,7 @@ instance YesodAuth UniWorX where
$logDebugS "auth" $ tshow ((userPlugin, userIdent), creds)
when isDummy . (throwError =<<) . lift $
when isDummy . (throwError =<<) . lift $
maybe (UserError $ IdentifierNotFound credsIdent) (Authenticated . entityKey) <$> getBy uAuth
let
@ -373,7 +459,7 @@ instance YesodAuth UniWorX where
userEmail <- maybe (throwError $ ServerError "Could not retrieve user email") return userEmail'
userDisplayName <- maybe (throwError $ ServerError "Could not retrieve user name") return userDisplayName'
let
newUser = User{..}
userUpdate = [ UserMatrikelnummer =. userMatrikelnummer
@ -388,13 +474,13 @@ instance YesodAuth UniWorX where
userStudyFeatures' = [ v | (k, v) <- credsExtra, k == "dfnEduPersonFeaturesOfStudy" ]
fs <- either (\err -> throwError . ServerError $ "Could not parse features of study: " <> err) return userStudyFeatures
lift $ deleteWhere [StudyFeaturesUser ==. userId]
forM_ fs $ \StudyFeatures{..} -> do
lift . insertMaybe studyFeaturesDegree $ StudyDegree (unStudyDegreeKey studyFeaturesDegree) Nothing Nothing
lift . insertMaybe studyFeaturesField $ StudyTerms (unStudyTermsKey studyFeaturesField) Nothing Nothing
lift $ insertMany_ fs
return $ Authenticated userId
where
@ -419,7 +505,7 @@ ldapConfig _app@(appSettings -> settings) = LDAPConfig
}
where
principalName :: IsString a => a
principalName = "userPrincipalName"
principalName = "userPrincipalName"
identifierModifier _ entry = case lookup principalName $ leattrs entry of
Just [n] -> Text.pack n
_ -> error "Could not determine user principal name"

View File

@ -9,38 +9,38 @@
module Handler.Course where
import Import
import Import
import Handler.Utils
-- import Data.Time
import qualified Data.Text as T
import Data.Function ((&))
import Yesod.Form.Bootstrap3
import Yesod.Form.Bootstrap3
import Colonnade hiding (fromMaybe)
import Yesod.Colonnade
import Yesod.Colonnade
import qualified Data.UUID.Cryptographic as UUID
getCourseListR :: Handler TypedContent
getCourseListR = redirect TermShowR
getCourseListR = redirect TermShowR
getCourseListTermR :: TermId -> Handler Html
getCourseListTermR tidini = do
(term,courses) <- runDB $ (,)
(term,courses) <- runDB $ (,)
<$> get tidini
<*> selectList [CourseTermId ==. tidini] [Asc CourseShorthand]
when (isNothing term) $ do
addMessage "warning" [shamlet| Semester #{toPathPiece tidini} nicht gefunden. |]
redirect TermShowR
-- TODO: several runDBs per TableRow are probably too inefficient!
let colonnadeTerms = mconcat
[ headed "Kürzel" $ (\ckv ->
-- TODO: several runDBs per TableRow are probably too inefficient!
let colonnadeTerms = mconcat
[ headed "Kürzel" $ (\ckv ->
let c = entityVal ckv
shd = courseShorthand c
shd = courseShorthand c
tid = courseTermId c
in [whamlet| <a href=@{CourseShowR tid shd}>#{shd} |] )
in [whamlet| <a href=@{CourseR tid shd CourseShowR}>#{shd} |] )
-- , headed "Institut" $ [shamlet| #{course} |]
, headed "Beginn Anmeldung" $ fromString.(maybe "" formatTimeGerWD).courseRegisterFrom.entityVal
, headed "Ende Anmeldung" $ fromString.(maybe "" formatTimeGerWD).courseRegisterTo.entityVal
@ -49,201 +49,237 @@ getCourseListTermR tidini = do
partiNum <- handlerToWidget $ runDB $ count [CourseParticipantCourseId ==. cid]
[whamlet| #{show partiNum} |]
)
, headed " " $ (\ckv ->
, headed " " $ (\ckv ->
let c = entityVal ckv
shd = courseShorthand c
shd = courseShorthand c
tid = courseTermId c
in do
adminLink <- handlerToWidget $ isAuthorized (CourseEditExistR tid shd ) False
-- if (adminLink==Authorized) then linkButton "Ändern" BCWarning (CourseEditExistR tid shd) else ""
[whamlet|
adminLink <- handlerToWidget $ isAuthorized (CourseR tid shd CourseEditR) False
-- if (adminLink==Authorized) then linkButton "Ändern" BCWarning (CourseEditR tid shd) else ""
[whamlet|
$if adminLink == Authorized
<a href=@{CourseEditExistR tid shd}>
<a href=@{CourseR tid shd CourseEditR}>
editieren
|]
)
)
]
let pageLinks =
[ NavbarLeft $ MenuItem
let pageLinks =
[ PageActionPrime $ MenuItem
{ menuItemLabel = "Neuer Kurs"
, menuItemRoute = CourseEditR
, menuItemAccessCallback = (== Authorized) <$> isAuthorized CourseEditR False
, menuItemIcon = Just "book"
, menuItemRoute = CourseNewR
, menuItemAccessCallback = (== Authorized) <$> isAuthorized CourseNewR False
}
]
]
let coursesTable = encodeWidgetTable tableSortable colonnadeTerms courses
defaultLinkLayout pageLinks $ do
-- defaultLayout $ do
setTitle "Semesterkurse"
linkButton "Neuen Kurs anlegen" BCPrimary CourseEditR
encodeWidgetTable tableDefault colonnadeTerms courses -- (map entityVal courses)
setTitle "Semesterkurse"
$(widgetFile "courses")
getCourseShowR :: TermId -> Text -> Handler Html
getCourseShowR tid csh = do
mbAid <- maybeAuthId
(courseEnt,(schoolMB,participants,mbRegistered)) <- runDB $ do
courseEnt@(Entity cid course) <- getBy404 $ CourseTermShort tid csh
dependent <- (,,)
dependent <- (,,)
<$> get (courseSchoolId course) -- join
<*> count [CourseParticipantCourseId ==. cid] -- join
<*> count [CourseParticipantCourseId ==. cid] -- join
<*> (case mbAid of -- TODO: Someone please refactor this late-night mess here!
Nothing -> return False
(Just aid) -> do
regL <- getBy (UniqueCourseParticipant cid aid)
regL <- getBy (UniqueParticipant aid cid)
return $ isJust regL)
return $ (courseEnt,dependent)
let course = entityVal courseEnt
(regWidget, regEnctype) <- generateFormPost $ identifyForm "registerBtn" $ registerButton $ mbRegistered
defaultLayout $ do
let course = entityVal courseEnt
(regWidget, regEnctype) <- generateFormPost $ identifyForm "registerBtn" $ registerButton $ mbRegistered
let pageActions =
[ PageActionPrime $ MenuItem
{ menuItemLabel = "Übungsblätter"
, menuItemIcon = Nothing
, menuItemRoute = CSheetR tid csh SheetListR
, menuItemAccessCallback = (== Authorized) <$> isAuthorized (CSheetR tid csh SheetListR) False
}
]
defaultLinkLayout pageActions $ do
setTitle $ [shamlet| #{toPathPiece tid} - #{csh}|]
$(widgetFile "course")
registerButton :: Bool -> Form ()
registerButton registered = renderAForm FormStandard $
pure () <* bootstrapSubmit regMsg
where
registerButton registered = renderAForm FormStandard $
pure () <* bootstrapSubmit regMsg
where
msg = if registered then "Abmelden" else "Anmelden"
regMsg = msg :: BootstrapSubmit Text
postCourseShowR :: TermId -> Text -> Handler Html
postCourseShowR tid csh = do
aid <- requireAuthId
(cid, registered) <- runDB $ do
(Entity cid _) <- getBy404 $ CourseTermShort tid csh
registered <- isJust <$> (getBy $ UniqueCourseParticipant cid aid)
registered <- isJust <$> (getBy $ UniqueParticipant aid cid)
return (cid, registered)
((regResult,_), _) <- runFormPost $ identifyForm "registerBtn" $ registerButton registered
case regResult of
((regResult,_), _) <- runFormPost $ identifyForm "registerBtn" $ registerButton registered
case regResult of
(FormSuccess _)
| registered -> do
runDB $ deleteBy $ UniqueCourseParticipant cid aid
addMessage "info" "Sie wurden abgemeldet."
| registered -> do
runDB $ deleteBy $ UniqueParticipant aid cid
addMessage "info" "Sie wurden abgemeldet."
| otherwise -> do
actTime <- liftIO $ getCurrentTime
regOk <- runDB $ insertUnique $ CourseParticipant cid aid actTime
when (isJust regOk) $ addMessage "success" "Erfolgreich angemeldet!"
(_other) -> return () -- TODO check this!
-- redirect or not?! I guess not, since we want GET now
getCourseShowR tid csh
getCourseEditR :: Handler Html
getCourseEditR = do
getCourseShowR tid csh
getCourseNewR :: Handler Html
getCourseNewR = do
-- TODO: Defaults für Semester hier ermitteln und übergeben
courseEditHandler Nothing
postCourseEditR :: Handler Html
postCourseEditR = courseEditHandler Nothing
getCourseEditExistR :: TermId -> Text -> Handler Html
getCourseEditExistR tid csh = do
postCourseNewR :: Handler Html
postCourseNewR = courseEditHandler Nothing
getCourseEditR :: TermId -> Text -> Handler Html
getCourseEditR tid csh = do
course <- runDB $ getBy $ CourseTermShort tid csh
courseEditHandler course
getCourseEditExistIDR :: CryptoUUIDCourse -> Handler Html
getCourseEditExistIDR cID = do
postCourseEditR :: TermId -> Text -> Handler Html
postCourseEditR = getCourseEditR
getCourseEditIDR :: CryptoUUIDCourse -> Handler Html
getCourseEditIDR cID = do
cIDKey <- getsYesod appCryptoIDKey
courseID <- UUID.decrypt cIDKey cID
courseEditHandler =<< runDB (getEntity courseID)
courseEditHandler :: Maybe (Entity Course) -> Handler Html
courseEditHandler course = do
aid <- requireAuthId
((result, formWidget), formEnctype) <- runFormPost $ newCourseForm $ courseToForm <$> course
action <- lookupPostParam "formaction"
case (result,action) of
(FormSuccess res, fAct)
| fAct == formActionDelete
, Just cid <- cfCourseId res -> do
courseDeleteHandler :: Handler Html -- not called anywhere yet
courseDeleteHandler = undefined
{- TODO
| False -- DELETE -- TODO: This no longer works that way!!! See new way in Handler.Term.termEditHandler
, Just cid <- cfCourseId res -> do
runDB $ deleteCascade cid -- TODO Sicherheitsabfrage einbauen!
let cti = toPathPiece $ cfTerm res
addMessage "info" [shamlet| Kurs #{cti}/#{cfShort res} wurde gelöscht!|]
redirect $ CourseListTermR $ cfTerm res
| fAct == formActionSave
, Just cid <- cfCourseId res -> do
let tid = cfTerm res
actTime <- liftIO getCurrentTime
updateokay <- runDB $ do
exists <- getBy $ CourseTermShort tid $ cfShort res
let upokay = isNothing exists
when upokay $ update cid
[ CourseName =. cfName res
, CourseDescription =. cfDesc res
, CourseLinkExternal =. cfLink res
, CourseShorthand =. cfShort res -- TODO: change here should generate a warning, or only allowed for Admins?!
, CourseTermId =. tid -- TODO: change here should generate a warning, or only allowed for Admins?!
, CourseSchoolId =. cfSchool res
, CourseCapacity =. cfCapacity res
, CourseRegisterFrom =. cfRegFrom res
, CourseRegisterTo =. cfRegTo res
, CourseChangedBy =. aid
, CourseChanged =. actTime
]
return upokay
let cti = toPathPiece $ cfTerm res
if updateokay
then do
addMessage "info" [shamlet| Kurs #{cti}/#{cfShort res} wurde geändert. |]
redirect $ CourseListTermR $ cfTerm res
else do
addMessage "danger" [shamlet| Kurs #{cti}/#{cfShort res} konnte nicht geändert werden.
\ Es gibt bereits einen anderen Kurs mit diesem Kürzel in diesem Semester.|]
| fAct == formActionSave
, Nothing <- cfCourseId res -> do
actTime <- liftIO getCurrentTime
insertOkay <- runDB $ insertUnique $ Course
{ courseName = cfName res
, courseDescription = cfDesc res
, courseLinkExternal = cfLink res
, courseShorthand = cfShort res
, courseTermId = cfTerm res
, courseSchoolId = cfSchool res
, courseCapacity = cfCapacity res
, courseHasRegistration = cfHasReg res
, courseRegisterFrom = cfRegFrom res
, courseRegisterTo = cfRegTo res
, courseCreated = actTime
, courseChanged = actTime
, courseCreatedBy = aid
, courseChangedBy = aid
}
case insertOkay of
(Just cid) -> do
runDB $ insert_ $ Lecturer aid cid
let cti = toPathPiece $ cfTerm res
addMessage "info" [shamlet|Kurs #{cti}/#{cfShort res} wurde angelegt.|]
redirect $ CourseListTermR $ cfTerm res
Nothing -> do
let cti = toPathPiece $ cfTerm res
addMessage "danger" [shamlet|Es gibt bereits einen Kurs #{cfShort res} in Semester #{cti}.|]
(FormFailure _,_) -> addMessage "warning" "Bitte Eingabe korrigieren."
_other -> return ()
-}
courseEditHandler :: Maybe (Entity Course) -> Handler Html
courseEditHandler course = do
aid <- requireAuthId -- TODO: Verify that Editor is owner of the Course to be Edited!!!
((result, formWidget), formEnctype) <- runFormPost $ newCourseForm $ courseToForm <$> course
case result of
(FormSuccess res@(
CourseForm { cfCourseId = Nothing
, cfShort = csh
, cfTerm = tid
})) -> do -- create new course
let tident = unTermKey tid
now <- liftIO getCurrentTime
insertOkay <- runDB $ insertUnique $ Course
{ courseName = cfName res
, courseDescription = cfDesc res
, courseLinkExternal = cfLink res
, courseShorthand = cfShort res
, courseTermId = cfTerm res
, courseSchoolId = cfSchool res
, courseCapacity = cfCapacity res
, courseHasRegistration = cfHasReg res
, courseRegisterFrom = cfRegFrom res
, courseRegisterTo = cfRegTo res
}
case insertOkay of
(Just cid) -> do
runDB $ do
insert_ $ CourseEdit aid now cid
insert_ $ Lecturer aid cid
addMessageI "info" $ MsgCourseNewOk tident csh
redirect $ CourseListTermR tid
Nothing ->
addMessageI "danger" $ MsgCourseNewDupShort tident csh
(FormSuccess res@(
CourseForm { cfCourseId = Just cid
, cfShort = csh
, cfTerm = tid
})) -> do -- edit existing course
let tident = unTermKey tid
now <- liftIO getCurrentTime
-- addMessage "debug" [shamlet| #{show res}|]
runDB $ do
old <- get cid
case old of
Nothing -> addMessageI "error" $ MsgInvalidInput
(Just oldCourse) -> do
-- existing <- getBy $ CourseTermShort tid csh
-- if ((entityKey <$> existing) /= Just cid)
-- then addMessageI "danger" $ MsgCourseEditDupShort tident csh
-- else do
-- addMessage "debug" $ fromMaybe [shamlet|No description given.|] $ cfDesc res
-- update cid
-- [ CourseName =. cfName res
-- , CourseDescription =. cfDesc res
-- , CourseLinkExternal =. cfLink res
-- , CourseShorthand =. cfShort res -- TODO: change here should generate a warning, or only allowed for Admins?!
-- , CourseTermId =. tid -- TODO: change here should generate a warning, or only allowed for Admins?!
-- , CourseSchoolId =. cfSchool res
-- , CourseCapacity =. cfCapacity res
-- , CourseRegisterFrom =. cfRegFrom res
-- , CourseRegisterTo =. cfRegTo res
-- , CourseChangedBy =. aid
-- , CourseChanged =. now
-- ]
_updOkay <- replace cid ( -- TODO replaceUnique requires Eq?!
Course { courseName = cfName res
, courseDescription = cfDesc res
, courseLinkExternal = cfLink res
, courseShorthand = cfShort res
, courseTermId = cfTerm res
, courseSchoolId = cfSchool res
, courseCapacity = cfCapacity res
, courseHasRegistration = cfHasReg res
, courseRegisterFrom = cfRegFrom res
, courseRegisterTo = cfRegTo res
}
)
insert_ $ CourseEdit aid now cid
-- if (isNothing updOkay)
-- then do
addMessageI "info" $ MsgCourseEditOk tident csh
-- redirect $ CourseListTermR tid
-- else addMessageI "danger" $ MsgCourseEditDupShort tident csh
(FormFailure _) -> addMessageI "warning" MsgInvalidInput
other -> addMessage "error" $ [shamlet| Error: #{show other}|]
let formTitle = "Kurs editieren/anlegen" :: Text
let actionUrl = CourseEditR
let formActions = defaultFormActions
actionUrl <- fromMaybe CourseNewR <$> getCurrentRoute
defaultLayout $ do
setTitle [shamlet| #{formTitle} |]
$(widgetFile "formPage")
data CourseForm = CourseForm
data CourseForm = CourseForm
{ cfCourseId :: Maybe CourseId -- Maybe CryptoUUIDCourse
, cfName :: Text
, cfName :: Text
, cfDesc :: Maybe Html
, cfLink :: Maybe Text
, cfShort :: Text
, cfLink :: Maybe Text
, cfShort :: Text
, cfTerm :: TermId
, cfSchool :: SchoolId
, cfCapacity :: Maybe Int
, cfCapacity :: Maybe Int
, cfHasReg :: Bool
, cfRegFrom :: Maybe UTCTime
, cfRegTo :: Maybe UTCTime
}
, cfRegFrom :: Maybe UTCTime
, cfRegTo :: Maybe UTCTime
}
instance Show CourseForm where
show cf = T.unpack (cfShort cf) ++ ' ':(show $ cfCourseId cf)
courseToForm :: Entity Course -> CourseForm
courseToForm cEntity = CourseForm
courseToForm cEntity = CourseForm
{ cfCourseId = Just $ entityKey cEntity
, cfName = courseName course
, cfDesc = courseDescription course
@ -253,26 +289,26 @@ courseToForm cEntity = CourseForm
, cfSchool = courseSchoolId course
, cfCapacity = courseCapacity course
, cfHasReg = courseHasRegistration course
, cfRegFrom = courseRegisterFrom course
, cfRegTo = courseRegisterTo course
, cfRegFrom = courseRegisterFrom course
, cfRegTo = courseRegisterTo course
}
where
course = entityVal cEntity
newCourseForm :: Maybe CourseForm -> Form CourseForm
newCourseForm template = identForm FIDcourse $ \html -> do
-- mopt hiddenField
-- mopt hiddenField
-- cidKey <- getsYesod appCryptoIDKey
-- courseId <- runMaybeT $ do
-- cid <- cfCourseId template
-- UUID.encrypt cidKey cid
-- UUID.encrypt cidKey cid
(result, widget) <- flip (renderAForm FormStandard) html $ CourseForm
-- <$> pure cid -- $ join $ cfCourseId <$> template -- why doesnt this work?
<$> aopt hiddenField "KursId" (cfCourseId <$> template)
<*> areq textField (fsb "Name") (cfName <$> template)
<*> aopt htmlField (fsb "Beschreibung") (cfDesc <$> template)
<*> aopt urlField (fsb "Homepage") (cfLink <$> template)
<*> areq textField (fsb "Kürzel"
<*> areq textField (fsb "Kürzel"
-- & addAttr "disabled" "disabled"
& setTooltip "Muss innerhalb des Semesters eindeutig sein")
(cfShort <$> template)
@ -282,9 +318,9 @@ newCourseForm template = identForm FIDcourse $ \html -> do
<*> areq checkBoxField (fsb "Anmeldung") (cfHasReg <$> template)
<*> aopt utcTimeField (fsb "Anmeldung von:") (cfRegFrom <$> template)
<*> aopt utcTimeField (fsb "Anmeldung bis:") (cfRegTo <$> template)
-- <* bootstrapSubmit (bsSubmit (show cid))
return $ case result of
FormSuccess courseResult
<* submitButton
return $ case result of
FormSuccess courseResult
| errorMsgs <- validateCourse courseResult
, not $ null errorMsgs ->
(FormFailure errorMsgs,
@ -293,18 +329,18 @@ newCourseForm template = identForm FIDcourse $ \html -> do
<h4> Fehler:
<ul>
$forall errmsg <- errorMsgs
<li> #{errmsg}
<li> #{errmsg}
^{widget}
|]
)
)
_ -> (result, widget)
-- where
-- cid :: Maybe CourseId
-- cid :: Maybe CourseId
-- cid = join $ cfCourseId <$> template
validateCourse :: CourseForm -> [Text]
validateCourse (CourseForm{..}) =
validateCourse (CourseForm{..}) =
[ msg | (False, msg) <-
[
( cfRegFrom <= cfRegTo
@ -324,5 +360,3 @@ validateCourse (CourseForm{..}) =
, "Anmeldungen aktivieren oder Anmeldezeitraum löschen"
)
] ]

View File

@ -9,13 +9,13 @@
module Handler.Home where
import Import
import Import
import Handler.Utils
-- import Data.Time
-- import qualified Data.Text as T
-- import Yesod.Form.Bootstrap3
-- import Yesod.Form.Bootstrap3
import Web.PathPieces (showToPathPiece, readFromPathPiece)
-- import Colonnade
@ -30,29 +30,28 @@ data CreateButton = CreateMath | CreateInf -- Dummy for Example
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"
label CreateInf = "Informatik"
cssClass CreateMath = BCInfo
cssClass CreateInf = BCPrimary
-- END Button needed here
cssClass CreateInf = BCPrimary
-- END Button needed here
getHomeR :: Handler Html
getHomeR = do
getHomeR = do
(btnWdgt, btnEnctype) <- generateFormPost (buttonForm :: Form CreateButton)
defaultLayout $ do
setTitle "Willkommen zum ReWorX Test!"
$(widgetFile "home")
postHomeR :: Handler Html
postHomeR :: Handler Html
postHomeR = do
((btnResult,_), _) <- runFormPost $ buttonForm
case btnResult of
(FormSuccess CreateInf) -> setMessage "Informatik-Knopf gedrückt"
((btnResult,_), _) <- runFormPost $ buttonForm
case btnResult of
(FormSuccess CreateInf) -> setMessage "Informatik-Knopf gedrückt"
(FormSuccess CreateMath) -> addMessage "warning" "Knopf Mathematik erkannt"
_other -> return ()
getHomeR
_other -> return ()
getHomeR

View File

@ -6,28 +6,40 @@
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
module Handler.Sheet where
import Import
import System.FilePath (takeFileName)
import Handler.Utils
import Handler.Utils.Zip
-- import Data.Time
import qualified Data.Text as T
-- import Data.Function ((&))
--
import Colonnade -- hiding (fromMaybe)
import Colonnade hiding (fromMaybe, singleton)
import Yesod.Colonnade
--
import qualified Data.UUID.Cryptographic as UUID
import qualified Data.Conduit.List as C
import qualified Database.Esqueleto as E
import qualified Database.Esqueleto.Internal.Sql as E
import Control.Monad.Writer (MonadWriter(..), execWriterT)
import Network.Mime
import qualified Data.Set as Set
instance Eq (Unique Sheet) where
(CourseSheet cid1 name1) == (CourseSheet cid2 name2) =
cid1 == cid2 && name1 == name2
{-
* Implement Handlers
* Implement Breadcrumbs in Foundation
@ -36,36 +48,47 @@ import Network.Mime
data SheetForm = SheetForm
{ sfName :: Text
, sfComment :: Maybe Html
, sfDescription :: Maybe Html
, sfType :: SheetType
, sfGrouping :: SheetGroup
, sfMarkingText :: Maybe Html
, sfVisibleFrom :: Maybe UTCTime
, sfActiveFrom :: UTCTime
, sfActiveTo :: UTCTime
, sfSheetF :: Maybe FileInfo
, sfSheetF :: Maybe (Source Handler (Either FileId File))
, sfHintFrom :: Maybe UTCTime
, sfHintF :: Maybe FileInfo
, sfSolutionFrom :: Maybe UTCTime
, sfSolutionF :: Maybe FileInfo
-- Keine SheetId im Formular!
}
makeSheetForm :: CourseId -> Maybe SheetForm -> Form SheetForm
makeSheetForm cid template = identForm FIDsheet $ \html -> do
-- TODO: Yesod.Form.MassInput.inputList arbeitet Server-seitig :(
-- Erstmal nur mit ZIP arbeiten
makeSheetForm :: Maybe SheetId -> Maybe SheetForm -> Form SheetForm
makeSheetForm msId template = identForm FIDsheet $ \html -> do
let oldFileIds fType
| Just sId <- msId = fmap setFromList . fmap (map E.unValue) . runDB . E.select . E.from $ \(file `E.InnerJoin` sheetFile) -> do
E.on $ file E.^. FileId E.==. sheetFile E.^. SheetFileFileId
E.where_ $ sheetFile E.^. SheetFileSheetId E.==. E.val sId
E.&&. sheetFile E.^. SheetFileType E.==. E.val fType
return (file E.^. FileId)
| otherwise = return Set.empty
(result, widget) <- flip (renderAForm FormStandard) html $ SheetForm
<$> areq textField (fsb "Name") (sfName <$> template)
<*> aopt htmlField (fsb "Hinweise für Teilnehmer") (sfMarkingText <$> template)
<*> sheetTypeAFormReq (fsb "Bewertung") (sfType <$> template)
--TODO: SICHTBARKEIT hinzunehmen
<*> aopt htmlField (fsb "Hinweise für Korrektoren") (sfMarkingText <$> template)
<*> areq utcTimeField (fsb "Abgabe ab") (sfActiveFrom <$> template)
<*> areq utcTimeField (fsb "Abgabefrist") (sfActiveTo <$> template)
<*> fileAFormOpt (fsb "Aufgaben")
<*> aopt utcTimeField (fsb "Hinweis ab") (sfHintFrom <$> template)
<*> fileAFormOpt (fsb "Hinweis")
<*> aopt utcTimeField (fsb "Lösung ab") (sfSolutionFrom <$> template)
<*> fileAFormOpt (fsb "Lösung")
<$> areq textField (fsb "Name") (sfName <$> template)
<*> aopt htmlField (fsb "Hinweise für Teilnehmer") (sfDescription <$> template)
<*> sheetTypeAFormReq (fsb "Bewertung") (sfType <$> template)
<*> sheetGroupAFormReq (fsb "Abgabegruppengröße") (sfGrouping <$> template)
<*> aopt htmlField (fsb "Hinweise für Korrektoren") (sfMarkingText <$> template)
<*> aopt utcTimeField (fsb "Sichtbar ab") (sfVisibleFrom <$> template)
<*> areq utcTimeField (fsb "Abgabe ab") (sfActiveFrom <$> template)
<*> areq utcTimeField (fsb "Abgabefrist") (sfActiveTo <$> template)
<*> aopt (multiFileField $ oldFileIds SheetExercise) (fsb "Aufgabenstellung") (sfSheetF <$> template)
<*> aopt utcTimeField (fsb "Hinweis ab") (sfHintFrom <$> template)
<*> fileAFormOpt (fsb "Hinweis")
<*> aopt utcTimeField (fsb "Lösung ab") (sfSolutionFrom <$> template)
<*> fileAFormOpt (fsb "Lösung")
<* submitButton
return $ case result of
FormSuccess sheetResult
| errorMsgs <- validateSheet sheetResult
@ -82,14 +105,48 @@ makeSheetForm cid template = identForm FIDsheet $ \html -> do
)
_ -> (result, widget)
where
validateSheet _ = [] -- TODO
validateSheet :: SheetForm -> [Text]
validateSheet (SheetForm{..}) =
[ msg | (False, msg) <-
[ ( maybe True (sfActiveFrom >=) sfVisibleFrom
, "Sichtbarkeit muss vor Beginn der Abgabefrist liegen."
)
, ( sfActiveTo >= sfActiveFrom
, "Ende der Abgabefrist muss nach deren Beginn liegen."
)
-- TODO: continue validation here!!!
] ]
fetchSheetAux :: ( BaseBackend backend ~ SqlBackend
, E.SqlSelect b a
, Typeable a, MonadHandler m, IsPersistBackend backend
, PersistQueryRead backend, PersistUniqueRead backend
)
=> (E.SqlExpr (Entity Sheet) -> b)
-> Key Term -> Text -> Text -> ReaderT backend m a
fetchSheetAux prj tid csh shn =
let cachId = encodeUtf8 $ tshow (tid,csh,shn)
in cachedBy cachId $ do
-- Mit Yesod:
-- cid <- getKeyBy404 $ CourseTermShort tid csh
-- getBy404 $ CourseSheet cid shn
-- Mit Esqueleto:
sheetList <- E.select . E.from $ \(course `E.InnerJoin` sheet) -> do
E.on $ course E.^. CourseId E.==. sheet E.^. SheetCourseId
E.where_ $ course E.^. CourseTermId E.==. E.val tid
E.&&. course E.^. CourseShorthand E.==. E.val csh
E.&&. sheet E.^. SheetName E.==. E.val shn
return $ prj sheet
case sheetList of
[sheet] -> return sheet
_other -> notFound
fetchSheet :: TermId -> Text -> Text -> YesodDB UniWorX (Entity Sheet)
fetchSheet tid csh shn = do
-- TODO: More efficient with Esquleto?
(Entity cid _course) <- getBy404 $ CourseTermShort tid csh
getBy404 $ CourseSheet cid shn
fetchSheet = fetchSheetAux id
fetchSheetId :: TermId -> Text -> Text -> YesodDB UniWorX (Key Sheet)
fetchSheetId tid cid shn = E.unValue <$> fetchSheetAux (E.^. SheetId) tid cid shn
-- List Sheets
getSheetListCID :: CourseId -> Handler Html
@ -114,17 +171,35 @@ getSheetList courseEnt = do
submissions <- count sheetsub
rated <- count $ (SubmissionRatingTime !=. Nothing):sheetsub
return (sid, sheet, (submissions, rated))
let colSheets = mconcat
[ headed "Blatt" $ toWgt . sheetName . snd3
, headed "Abgabe ab" $ toWgt . formatTimeGerWD . sheetActiveFrom . snd3
, headed "Abgabe bis" $ toWgt . formatTimeGerWD . sheetActiveTo . snd3
, headed "Bewertung" $ toWgt . show . sheetType . snd3
, headed "Korrigiert" $ toWgt . snd . trd3
, headed "Eingereicht" $ toWgt . fst . trd3
-- TODO: only show edit button for allowed course assistants
, headed "" $ \s -> linkButton "Edit" BCLink $ SheetEditR tid csh $ fst3 s
]
defaultLayout $ do
let colBase = mconcat
[ headed "Blatt" $ \(sid,sheet,_) -> linkButton (toWgt $ sheetName sheet) BCLink $ CourseR tid csh $ SheetR $ SheetShowR $ sheetName sheet
, headed "Abgabe ab" $ toWgt . formatTimeGerWD . sheetActiveFrom . snd3
, headed "Abgabe bis" $ toWgt . formatTimeGerWD . sheetActiveTo . snd3
, headed "Bewertung" $ toWgt . show . sheetType . snd3
]
let colAdmin = mconcat -- only show edit button for allowed course assistants
[ headed "Korrigiert" $ toWgt . snd . trd3
, headed "Eingereicht" $ toWgt . fst . trd3
, headed "" $ \s -> linkButton "Edit" BCLink $ CourseR tid csh $ SheetR $ SheetEditR $ sheetName $ snd3 s
, headed "" $ \s -> linkButton "Delete" BCLink $ CourseR tid csh $ SheetR $ SheetDelR $ sheetName $ snd3 s
]
showAdmin <- case sheets of
((_,firstSheet,_):_) -> do
setUltDestCurrent
(Authorized ==) <$> isAuthorized (CourseR tid csh $ SheetR $ SheetEditR $ sheetName firstSheet) False
_otherwise -> return False
let colSheets = if showAdmin
then colBase `mappend` colAdmin
else colBase
let pageActions =
[ PageActionPrime $ MenuItem
{ menuItemLabel = "Neues Übungsblatt"
, menuItemIcon = Nothing
, menuItemRoute = CSheetR tid csh SheetNewR
, menuItemAccessCallback = (== Authorized) <$> isAuthorized CourseNewR False
}
]
defaultLinkLayout pageActions $ do
setTitle $ toHtml $ T.append "Übungsblätter " csh
if null sheets
then [whamlet|Es wurden noch keine Übungsblätter angelegt.|]
@ -132,29 +207,27 @@ getSheetList courseEnt = do
-- Show single sheet
getSheetShowR :: TermId -> Text -> Text -> Handler Html
getSheetShowR tid csh shn = getSheetShow =<<
(runDB $ fetchSheet tid csh shn)
{- Nur per UUID
getSheetIdShowR :: SheetId -> Handler Html
getSheetIdShowR sheetId = getSheetShow =<<
(Entity sheetId) <$> (runDB $ get404 sheetId)\
-}{-
getSheetUUIDShowR :: CryptoUUIDSheet -> Handler Html
getSheetUUIDShowR sUUID = do
cIDKey <- getsYesod appCryptoIDKey
sheetId <- UUID.decrypt cIDKey sUUID
sheetEnt <- runDB $ get404 sheetId
getSheetShow $ Entity sheetId sheetEnt
-}
getSheetShow :: (Entity Sheet) -> Handler Html
getSheetShow entSheet = do
getSheetShowR tid csh shn = do
entSheet <- runDB $ fetchSheet tid csh shn
let sheet = entityVal entSheet
sid = entityKey entSheet
--
fileNameTypes <- runDB $ E.select $ E.from $
\(sheet `E.InnerJoin` sheetFile `E.InnerJoin` file) -> do
-- Restrict to consistent rows that correspond to each other
E.on (file E.^. FileId E.==. sheetFile E.^. SheetFileFileId)
E.on (sheetFile E.^. SheetFileSheetId E.==. sheet E.^. SheetId)
-- filter to requested file
E.where_ (sheet E.^. SheetId E.==. E.val sid )
-- return desired columns
return $ (file E.^. FileTitle, sheetFile E.^. SheetFileType)
let fileLinks = map (\(E.Value fName, E.Value fType) -> CSheetR tid csh (SheetFileR shn fType fName)) fileNameTypes
defaultLayout $ do
setTitle $ toHtml $ T.append "Übung " $ sheetName sheet
$(widgetFile "sheetShow")
[whamlet| Under Construction !!! |] -- TODO
$(widgetFile "sheetAdmin")
getSheetFileR :: TermId -> Text -> Text -> SheetFileType -> FilePath -> Handler TypedContent
getSheetFileR tid csh shn typ title = do
@ -181,72 +254,149 @@ getSheetFileR tid csh shn typ title = do
[] -> notFound
_other -> error "Multiple matching files found."
getSheetNewR :: TermId -> Text -> Handler Html
getSheetNewR tid csh = do
(Entity cid course) <- runDB $ getBy404 $ CourseTermShort tid csh
let template = Nothing -- TODO: provide convenience by interpolating name/nr/dates+7days
((res,wdgt), enc) <- runFormPost $ makeSheetForm cid template
case res of
(FormSuccess SheetForm{..}) -> do
let sid = undefined -- TODO after first insert
let sname = undefined -- TODO after first insert
-- Prüfe, das FileTitle innerhalb des Sheets eindeutig ist für diesen SheetFileTpye
whenIsJust sfSheetF $ \sinfo -> do
let sheetInsert file = do
fid <- insert file
void . insert $ SheetFile sid fid SheetExercise
runDB . runConduit $ (sourceFiles sinfo) =$= C.mapM_ sheetInsert
addMessage "info" "Blatt angelegt"
redirect $ SheetShowR tid csh sname
(FormFailure msgs) -> forM_ msgs $ (addMessage "warning") . toHtml
_ -> return ()
defaultLayout $ do
$(widgetFile "newSheet")
let action newSheet = -- More specific error message for new sheet could go here, if insertUnique returns Nothing
insertUnique $ newSheet
handleSheetEdit tid csh Nothing template action
postSheetNewR :: TermId -> Text -> Handler Html
postSheetNewR = getSheetNewR
getSheetEditR :: TermId -> Text -> SheetId -> Handler Html
getSheetEditR _ _ _ = defaultLayout [whamlet| Under Construction !!! |] -- TODO
postSheetEditR :: TermId -> Text -> SheetId -> Handler Html
postSheetEditR _ _ _ = defaultLayout [whamlet| Under Construction !!! |] -- TODO
getSheetEditR :: TermId -> Text -> Text -> Handler Html
getSheetEditR tid csh shn = do
(sheetEnt, sheetFileIds) <- runDB $ do
ent <- fetchSheet tid csh shn
fIds <- fmap setFromList . fmap (map E.unValue) . E.select . E.from $ \(file `E.InnerJoin` sheetFile) -> do
E.on $ file E.^. FileId E.==. sheetFile E.^. SheetFileFileId
E.where_ $ sheetFile E.^. SheetFileSheetId E.==. E.val (entityKey ent)
E.&&. sheetFile E.^. SheetFileType E.==. E.val SheetExercise
return (file E.^. FileId)
return (ent, fIds)
let sid = entityKey sheetEnt
let oldSheet@(Sheet {..}) = entityVal sheetEnt
let template = Just $ SheetForm
{ sfName = sheetName
, sfDescription = sheetDescription
, sfType = sheetType
, sfGrouping = sheetGrouping
, sfMarkingText = sheetMarkingText
, sfVisibleFrom = sheetVisibleFrom
, sfActiveFrom = sheetActiveFrom
, sfActiveTo = sheetActiveTo
, sfSheetF = Just . yieldMany . map Left $ Set.toList sheetFileIds
, sfHintFrom = sheetHintFrom
, sfHintF = Nothing -- TODO
, sfSolutionFrom = sheetSolutionFrom
, sfSolutionF = Nothing -- TODO
}
let action newSheet = do
replaceRes <- myReplaceUnique sid $ newSheet
case replaceRes of
Nothing -> return $ Just sid
(Just _err) -> return $ Nothing -- More specific error message for edit old sheet could go here
handleSheetEdit tid csh (Just sid) template action
postSheetEditR :: TermId -> Text -> Text -> Handler Html
postSheetEditR = getSheetEditR
getSheetDelR :: TermId -> Text -> SheetId -> Handler Html
getSheetDelR _ _ _ = defaultLayout [whamlet| Under Construction !!! |] -- TODO
-- Sicherheitsabfrage
postSheetDelR :: TermId -> Text -> SheetId -> Handler Html
postSheetDelR _ _ _ = defaultLayout [whamlet| Under Construction !!! |] -- TODO
-- Tatsächlich löschen
{-
getCourseShowR :: TermId -> Text -> Handler Html
getCourseShowR tid csh = do
mbAid <- maybeAuthId
(courseEnt,(schoolMB,participants,mbRegistered)) <- runDB $ do
courseEnt@(Entity cid course) <- getBy404 $ CourseTermShort (TermKey tid) csh
dependent <- (,,)
<$> get (courseSchoolId course) -- join
<*> count [CourseParticipantCourseId ==. cid] -- join
<*> (case mbAid of -- TODO: Someone please refactor this late-night mess here!
Nothing -> return False
(Just aid) -> do
regL <- getBy (UniqueCourseParticipant cid aid)
return $ isJust regL)
return $ (courseEnt,dependent)
let course = entityVal courseEnt
(regWidget, regEnctype) <- generateFormPost $ identifyForm "registerBtn" $ registerButton $ mbRegistered
handleSheetEdit :: TermId -> Text -> Maybe SheetId -> Maybe SheetForm -> (Sheet -> YesodDB UniWorX (Maybe SheetId)) -> Handler Html
handleSheetEdit tid csh msId template dbAction = do
let tident = unTermKey tid
let mbshn = sfName <$> template
aid <- requireAuthId
((res,formWidget), formEnctype) <- runFormPost $ makeSheetForm msId template
case res of
(FormSuccess SheetForm{..}) -> do
saveOkay <- runDB $ do
actTime <- liftIO getCurrentTime
cid <- getKeyBy404 $ CourseTermShort tid csh
let newSheet = Sheet
{ sheetCourseId = cid
, sheetName = sfName
, sheetDescription = sfDescription
, sheetType = sfType
, sheetGrouping = sfGrouping
, sheetMarkingText = sfMarkingText
, sheetVisibleFrom = sfVisibleFrom
, sheetActiveFrom = sfActiveFrom
, sheetActiveTo = sfActiveTo
, sheetHintFrom = sfHintFrom
, sheetSolutionFrom = sfSolutionFrom
}
mbsid <- dbAction newSheet
case mbsid of
Nothing -> False <$ addMessageI "danger" (MsgSheetNameDup tident csh sfName)
(Just sid) -> do -- save files in DB:
whenIsJust sfSheetF $ insertSheetFile' sid SheetExercise
whenIsJust sfHintF $ insertSheetFile sid SheetHint
whenIsJust sfSolutionF $ insertSheetFile sid SheetSolution
insert_ $ SheetEdit aid actTime sid
addMessageI "info" $ MsgSheetEditOk tident csh sfName
return True
when saveOkay $ redirect $ CSheetR tid csh $ SheetShowR sfName -- redirect must happen outside of runDB
(FormFailure msgs) -> forM_ msgs $ (addMessage "warning") . toHtml
_ -> return ()
let pageTitle = maybe (MsgSheetTitleNew tident csh)
(MsgSheetTitle tident csh) mbshn
let formTitle = pageTitle
let formText = Nothing :: Maybe UniWorXMessage
actionUrl <- fromMaybe (CSheetR tid csh SheetNewR) <$> getCurrentRoute
defaultLayout $ do
setTitle $ [shamlet| #{termToText tid} - #{csh}|]
$(widgetFile "course")
-}
setTitleI pageTitle
$(widgetFile "formPageI18n")
getSheetDelR :: TermId -> Text -> Text -> Handler Html
getSheetDelR tid csh shn = do
let tident = unTermKey tid
((result,formWidget), formEnctype) <- runFormPost (buttonForm :: Form BtnDelete)
case result of
(FormSuccess BtnAbort) -> redirectUltDest $ CSheetR tid csh $ SheetShowR shn
(FormSuccess BtnDelete) -> do
runDB $ fetchSheetId tid csh shn >>= deleteCascade
-- TODO: deleteCascade löscht aber nicht die hochgeladenen Dateien!!!
setMessageI $ MsgSheetDelOk tident csh shn
redirect $ CSheetR tid csh SheetListR
_other -> do
submissionno <- runDB $ do
sid <- fetchSheetId tid csh shn
count [SubmissionSheetId ==. sid]
let formTitle = MsgSheetDelTitle tident csh shn
let formText = Just $ MsgSheetDelText submissionno
let actionUrl = CSheetR tid csh $ SheetDelR shn
defaultLayout $ do
setTitleI $ MsgSheetTitle tident csh shn
$(widgetFile "formPageI18n")
postSheetDelR :: TermId -> Text -> Text -> Handler Html
postSheetDelR = getSheetDelR
insertSheetFile :: SheetId -> SheetFileType -> FileInfo -> YesodDB UniWorX ()
insertSheetFile sid ftype finfo = do
runConduit $ (sourceFiles finfo) =$= C.mapM_ finsert
where
finsert file = do
fid <- insert file
void . insert $ SheetFile sid fid ftype -- cannot fail due to uniqueness, since we generated a fresh FileId in the previous step
insertSheetFile' :: SheetId -> SheetFileType -> Source Handler (Either FileId File) -> YesodDB UniWorX ()
insertSheetFile' sid ftype fs = do
oldFileIds <- fmap setFromList . fmap (map E.unValue) . E.select . E.from $ \(file `E.InnerJoin` sheetFile) -> do
E.on $ file E.^. FileId E.==. sheetFile E.^. SheetFileFileId
E.where_ $ sheetFile E.^. SheetFileSheetId E.==. E.val sid
E.&&. sheetFile E.^. SheetFileType E.==. E.val ftype
return (file E.^. FileId)
keep <- execWriterT . runConduit $ transPipe (lift . lift) fs =$= C.mapM_ finsert
mapM_ deleteCascade $ (oldFileIds \\ keep :: Set FileId)
where
finsert (Left fileId) = tell $ singleton fileId
finsert (Right file) = lift $ do
fid <- insert file
void . insert $ SheetFile sid fid ftype -- cannot fail due to uniqueness, since we generated a fresh FileId in the previous step

View File

@ -8,6 +8,7 @@
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE ViewPatterns #-}
module Handler.Submission where
@ -53,7 +54,7 @@ submissionTable = do
(,,) <$> encrypt submissionId <*> encrypt submissionId <*> pure s
let
anchorCourse (_, _, (_, _, Entity _ Course{..})) = CourseShowR courseTermId courseShorthand
anchorCourse (_, _, (_, _, Entity _ Course{..})) = CourseR courseTermId courseShorthand CourseShowR
courseText (_, _, (_, _, Entity _ Course{..})) = toWidget courseName
anchorSubmission (_, cUUID, _) = SubmissionR cUUID
submissionText (cID, _, _) = toWidget . toPathPiece . CI.foldedCase $ ciphertext cID
@ -177,8 +178,12 @@ postSubmissionDownloadMultiArchiveR = do
withinDirectory f@File{..} = f { fileTitle = directoryName </> fileTitle }
lastEditMb <- lift $ selectList [SubmissionEditSubmission ==. submissionID] [Desc SubmissionEditTime, LimitTo 1]
lastEditTime <- case lastEditMb of
[(submissionEditTime.entityVal -> time)] -> return time
_other -> liftIO getCurrentTime
yield $ File
{ fileModified = submissionChanged
{ fileModified = lastEditTime
, fileTitle = directoryName
, fileContent = Nothing
}

View File

@ -9,11 +9,11 @@
module Handler.Term where
import Import
import Import
import Handler.Utils
import qualified Data.Text as T
import Yesod.Form.Bootstrap3
-- import qualified Data.Text as T
import Yesod.Form.Bootstrap3
import Colonnade hiding (bool)
import Yesod.Colonnade
@ -27,47 +27,47 @@ getTermShowR = do
-- term <- runDB $ E.select . E.from $ \(term) -> do
-- E.orderBy [E.desc $ term E.^. TermStart ]
-- return term
--
--
let
termData = E.from $ \term -> do
-- E.orderBy [E.desc $ term E.^. TermStart ]
let courseCount :: E.SqlExpr (E.Value Int)
courseCount = E.sub_select . E.from $ \course -> do
E.where_ $ term E.^. TermId E.==. course E.^. CourseTermId
return E.countRows
return E.countRows
return (term, courseCount)
selectRep $ do
provideRep $ toJSON . map fst <$> runDB (E.select termData)
provideRep $ do
let colonnadeTerms = mconcat
provideRep $ do
let colonnadeTerms = mconcat
[ headed "Kürzel" $ \(Entity tid Term{..},_) -> cell $ do
-- Scrap this if to slow, create term edit page instead
adminLink <- handlerToWidget $ isAuthorized (TermEditExistR tid) False
[whamlet|
[whamlet|
$if adminLink == Authorized
<a href=@{TermEditExistR tid}>
#{termToText termName}
$else
$else
#{termToText termName}
|]
, headed "Beginn Vorlesungen" $ \(Entity _ Term{..},_) ->
|]
, headed "Beginn Vorlesungen" $ \(Entity _ Term{..},_) ->
stringCell $ formatTimeGerWD termLectureStart
, headed "Ende Vorlesungen" $ \(Entity _ Term{..},_) ->
, headed "Ende Vorlesungen" $ \(Entity _ Term{..},_) ->
stringCell $ formatTimeGerWD termLectureEnd
, headed "Aktiv" $ \(Entity _ Term{..},_) ->
, headed "Aktiv" $ \(Entity _ Term{..},_) ->
textCell $ bool "" tickmark termActive
, headed "Kursliste" $ \(Entity tid Term{..}, E.Value numCourses) ->
cell [whamlet|
<a href=@{CourseListTermR tid}>
#{show numCourses} Kurse
|]
|]
, headed "Semesteranfang" $ \(Entity _ Term{..},_) ->
stringCell $ formatTimeGerWD termStart
, headed "Semesterende" $ \(Entity _ Term{..},_) ->
stringCell $ formatTimeGerWD termEnd
, headed "Feiertage im Semester" $ \(Entity _ Term{..},_) ->
stringCell $ (intercalate ", ") $ map formatTimeGerWD termHolidays
]
]
table <- dbTable def $ DBTable
{ dbtSQLQuery = termData
, dbtColonnade = colonnadeTerms
@ -75,44 +75,54 @@ getTermShowR = do
, dbtAttrs = tableDefault
, dbtIdent = "terms" :: Text
}
defaultLayout $ do
let pageActions =
[ PageActionPrime $ MenuItem
{ menuItemLabel = "Neues Semester"
, menuItemIcon = Nothing
, menuItemRoute = TermEditR
, menuItemAccessCallback = (== Authorized) <$> isAuthorized TermEditR True
}
]
defaultLinkLayout pageActions $ do
setTitle "Freigeschaltete Semester"
table
$(widgetFile "terms")
getTermEditR :: Handler Html
getTermEditR = do
-- TODO: Defaults für Semester hier ermitteln und übergeben
termEditHandler Nothing
postTermEditR :: Handler Html
postTermEditR = termEditHandler Nothing
getTermEditExistR :: TermId -> Handler Html
getTermEditExistR tid = do
term <- runDB $ get tid
termEditHandler term
termEditHandler :: Maybe Term -> Handler Html
termEditHandler term = do
((result, formWidget), formEnctype) <- runFormPost $ newTermForm term
case result of
(FormSuccess res) -> do
-- term <- runDB $ get $ TermKey termName
-- term <- runDB $ get $ TermKey termName
runDB $ repsert (TermKey $ termName res) res
let tid = termToText $ termName res
let msg = "Semester " `T.append` tid `T.append` " erfolgreich editiert."
addMessage "success" [shamlet| #{msg} |]
-- VOR INTERNATIONALISIERUNG:
-- let tid = termToText $ termName res
-- let msg = "Semester " `T.append` tid `T.append` " erfolgreich editiert."
-- addMessage "success" [shamlet| #{msg} |]
-- MIT INTERNATIONALISIERUNG:
addMessageI "success" $ MsgTermEdited $ termName res
redirect TermShowR
(FormMissing ) -> return ()
(FormFailure _) -> addMessage "warning" "Bitte Eingabe korrigieren."
(FormFailure _) -> addMessageI "warning" MsgInvalidInput
let formTitle = "Semester editieren/anlegen" :: Text
let actionUrl = TermEditR
defaultLayout $ do
setTitle [shamlet| #{formTitle} |]
$(widgetFile "formPage")
newTermForm :: Maybe Term -> Form Term
newTermForm template html = do
(result, widget) <- flip (renderAForm FormStandard) html $ Term
@ -124,8 +134,8 @@ newTermForm template html = do
<*> areq dayField (bfs ("Ende Vorlesungen" :: Text)) (termLectureEnd <$> template)
<*> areq checkBoxField (bfs ("Aktiv" :: Text)) (termActive <$> template)
<* submitButton
return $ case result of
FormSuccess termResult
return $ case result of
FormSuccess termResult
| errorMsgs <- validateTerm termResult
, not $ null errorMsgs ->
(FormFailure errorMsgs,
@ -134,13 +144,13 @@ newTermForm template html = do
<h4> Fehler:
<ul>
$forall errmsg <- errorMsgs
<li> #{errmsg}
<li> #{errmsg}
^{widget}
|]
)
)
_ -> (result, widget)
{-
where
set :: Text -> FieldSettings site
set = bfs
-}
{-
where
set :: Text -> FieldSettings site
set = bfs
-}

View File

@ -41,5 +41,5 @@ getUsersR = do
-- ++ map (\school -> headed (text2widget $ schoolName $ entityVal school) (\u -> "xx")) schools
defaultLayout $ do
setTitle "Comprehensive User List"
let userList = encodeWidgetTable tableDefault colonnadeUsers users
let userList = encodeWidgetTable tableSortable colonnadeUsers users
$(widgetFile "users")

View File

@ -1,12 +1,15 @@
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE TypeFamilies, FlexibleContexts, ConstraintKinds #-}
{-# LANGUAGE QuasiQuotes #-}
module Handler.Utils
( module Handler.Utils
) where
import Import.NoFoundation
import Handler.Utils.DateTime as Handler.Utils
@ -18,11 +21,15 @@ import Handler.Utils.Table.Pagination as Handler.Utils
import Handler.Utils.Zip as Handler.Utils
import Handler.Utils.Rating as Handler.Utils
import Handler.Utils.Submission as Handler.Utils
import Handler.Utils.Templates as Handler.Utils
import Text.Blaze (Markup, ToMarkup)
import Data.Map (Map)
import qualified Data.Map as Map
import qualified Data.List as List
import Database.Persist.Class
tickmark :: IsString a => a
tickmark = fromString ""
@ -54,9 +61,69 @@ whenIsJust :: Monad m => Maybe a -> (a -> m ()) -> m ()
whenIsJust (Just x) f = f x
whenIsJust Nothing _ = return ()
----------
-- Maps --
----------
entities2map :: PersistEntity record => [Entity record] -> Map (Key record) record
entities2map = foldl' (\m entity -> Map.insert (entityKey entity) (entityVal entity) m) Map.empty
------------
-- Routes --
------------
-- -- redirectBack :: Handler Html
-- -- redirectBack :: HandlerT UniWorX IO Html
-- redirectBack = defaultLayout $ do
-- [whamlet| BACK |]
-- -- [julius| window.history.back(); |]
--------------
-- Database --
--------------
-- getKeyBy :: PersistEntity val => Unique val -> ReaderT backend0 m0 (Maybe (Entity val))
-- getKeyBy :: Unique a -> YesodDB site (Key a)
getKeyBy :: (PersistEntityBackend record ~ BaseBackend backend, PersistEntity record, PersistUniqueRead backend, MonadIO m)
=> Unique record -> ReaderT backend m (Maybe (Key record))
getKeyBy u = (fmap entityKey) <$> getBy u -- TODO optimize this, so that DB does not deliver entire record!
getKeyBy404 :: (PersistEntityBackend record ~ BaseBackend backend, PersistEntity record, PersistUniqueRead backend, MonadIO m)
=> Unique record -> ReaderT backend m (Key record)
getKeyBy404 = (fmap entityKey) . getBy404 -- TODO optimize this, so that DB does not deliver entire record!
myReplaceUnique
:: (MonadIO m
,Eq (Unique record)
,PersistRecordBackend record backend
,PersistUniqueWrite backend)
=> Key record -> record -> ReaderT backend m (Maybe (Unique record))
myReplaceUnique key datumNew = getJust key >>= replaceOriginal
where
uniqueKeysNew = persistUniqueKeys datumNew
replaceOriginal original = do
conflict <- checkUniqueKeys changedKeys
case conflict of
Nothing -> replace key datumNew >> return Nothing
(Just conflictingKey) -> return $ Just conflictingKey
where
changedKeys = uniqueKeysNew List.\\ uniqueKeysOriginal
uniqueKeysOriginal = persistUniqueKeys original
checkUniqueKeys
:: (MonadIO m
,PersistEntity record
,PersistUniqueRead backend
,PersistRecordBackend record backend)
=> [Unique record] -> ReaderT backend m (Maybe (Unique record))
checkUniqueKeys [] = return Nothing
checkUniqueKeys (x:xs) = do
y <- getBy x
case y of
Nothing -> checkUniqueKeys xs
Just _ -> return (Just x)

View File

@ -7,8 +7,13 @@
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE ViewPatterns #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE LambdaCase #-}
module Handler.Utils.Form where
module Handler.Utils.Form where
import Handler.Utils.Form.Types
import Import
import qualified Data.Char as Char
@ -21,12 +26,20 @@ import qualified Data.Foldable as Foldable
import qualified Data.Text as T
-- import Yesod.Form.Types
import Yesod.Form.Functions (parseHelper)
import Yesod.Form.Bootstrap3
import Yesod.Form.Bootstrap3
import qualified Text.Blaze.Internal as Blaze (null)
import Web.PathPieces (showToPathPiece, readFromPathPiece)
import Handler.Utils.Zip
import qualified Data.Conduit.List as C
import qualified Database.Esqueleto as E
import qualified Database.Esqueleto.Internal.Sql as E
import qualified Data.Set as Set
------------------------------------------------
-- Unique Form Identifiers to avoid accidents --
------------------------------------------------
@ -35,9 +48,14 @@ data FormIdentifier = FIDcourse | FIDsheet
deriving (Enum, Eq, Ord, Bounded, Read, Show)
identForm :: FormIdentifier -> Form a -> Form a
identForm :: FormIdentifier -> Form a -> Form a
identForm fid = identifyForm (T.pack $ show fid)
{- Hinweise zur Erinnerung:
- identForm primär, wenn es mehr als ein Formular pro Handler gibt
- nur einmal pro makeForm reicht
-}
-------------------
-- Form Renderer --
-------------------
@ -48,7 +66,7 @@ data FormLayout = FormStandard
renderAForm :: Monad m => FormLayout -> FormRender m a
renderAForm formLayout aform fragment = do
(res, (($ []) -> views)) <- aFormToForm aform
let widget = $(widgetFile "form")
let widget = $(widgetFile "widgets/form")
return (res, widget)
----------------------------
@ -67,26 +85,23 @@ class (Enum a, Bounded a, Ord a, PathPiece a) => Button a where
cssClass :: a -> ButtonCssClass
cssClass _ = BCDefault
{- Abort is not useful (press Back instead); Delete should be different:
data StandardButton = BtnDelete | BtnAbort | BtnSave
data BtnDelete = BtnDelete | BtnAbort
deriving (Enum, Eq, Ord, Bounded, Read, Show)
instance PathPiece StandardButton where -- for displaying the button only, not really for paths
instance PathPiece BtnDelete where -- for displaying the button only, not really for paths
toPathPiece = showToPathPiece
fromPathPiece = readFromPathPiece
instance Button StandardButton where
instance Button BtnDelete where
label BtnDelete = "Löschen"
label BtnAbort = "Abbrechen"
label BtnSave = "Speichern"
cssClass BtnDelete = BCWarning
label BtnAbort = "Abrechen"
cssClass BtnDelete = BCDanger
cssClass BtnAbort = BCDefault
cssClass BtnSave = BCPrimary
-}
data SubmitButton = BtnSubmit
deriving (Enum, Eq, Ord, Bounded, Read, Show)
@ -99,45 +114,45 @@ instance Button SubmitButton where
label BtnSubmit = "Submit"
cssClass BtnSubmit = BCPrimary
-- -- 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)
-- deriving (Enum, Eq, Ord, Bounded, Read, Show)
--
--
-- instance PathPiece LinkButton where
-- LinkButton route = ???
linkButton :: Widget -> ButtonCssClass -> Route UniWorX -> Widget
linkButton lbl cls url = [whamlet| <a href=@{url} .btn .#{bcc2txt cls} role=button>^{lbl} |]
-- [whamlet|
-- <form method=post action=@{url}>
-- <form method=post action=@{url}>
-- <input type="hidden" name="_formid" value="identify-linkButton">
-- <button .btn .#{bcc2txt cls} type=submit value="Link to @{url}">^{lbl}
-- |]
-- <input .btn .#{bcc2txt cls} type="submit" value=^{lbl}>
-- |]
-- <input .btn .#{bcc2txt cls} type="submit" value=^{lbl}>
buttonField :: Button a => a -> Field Handler a
buttonField btn = Field {fieldParse, fieldView, fieldEnctype}
where
where
fieldEnctype = UrlEncoded
fieldView fid name attrs _val _ =
fieldView fid name attrs _val _ =
[whamlet|
<button .btn .#{bcc2txt $ cssClass btn} type=submit name=#{name} value=#{toPathPiece btn} *{attrs} ##{fid}>^{label btn}
|]
fieldParse [] _ = return $ Right Nothing
fieldParse [str] _
fieldParse [str] _
| str == toPathPiece btn = return $ Right $ Just btn
| otherwise = return $ Left "Wrong button value"
fieldParse _ _ = return $ Left "Multiple button values"
combinedButtonField :: Button a => [a] -> AForm Handler [Maybe a]
combinedButtonField btns = traverse b2f btns
where
b2f b = aopt (buttonField b) "" Nothing
b2f b = aopt (buttonField b) "" Nothing
submitButton :: AForm Handler ()
submitButton = void $ combinedButtonField [BtnSubmit]
@ -146,10 +161,10 @@ submitButton = void $ combinedButtonField [BtnSubmit]
combinedButtonField :: Button a => [a] -> Form m -> Form (a,m)
combinedButtonField btns inner csrf = do
buttonIdent <- newFormIdent
let button b = mopt (buttonField b) ("n/a"{ fsName = Just buttonIdent }) Nothing
let button b = mopt (buttonField b) ("n/a"{ fsName = Just buttonIdent }) Nothing
(results, btnViews) <- unzip <$> mapM button [minBound..maxBound]
(innerRes,innerWdgt) <- inner
let widget = do
let widget = do
[whamlet|
#{csrf}
^{innerWdgt}
@ -171,14 +186,14 @@ combinedButtonField btns inner csrf = do
accResult' _ x@(FormSuccess _) = x --SJ: Is this safe? Shouldn't Failure override Success?
accResult' (FormSuccess Nothing) x = x
accResult' FormMissing _ = FormMissing
accResult' (FormFailure errs) _ = FormFailure errs
accResult' (FormFailure errs) _ = FormFailure errs
-}
-- buttonForm :: Button a => Markup -> MForm (HandlerT UniWorX IO) (FormResult a, (WidgetT UniWorX IO ()))
buttonForm :: (Button a) => Form a
buttonForm csrf = do
buttonIdent <- newFormIdent
let button b = mopt (buttonField b) ("n/a"{ fsName = Just buttonIdent }) Nothing
let button b = mopt (buttonField b) ("n/a"{ fsName = Just buttonIdent }) Nothing
(results, btnViews) <- unzip <$> mapM button [minBound..maxBound]
let widget = do
[whamlet|
@ -199,44 +214,8 @@ buttonForm csrf = do
accResult' FormMissing _ = FormMissing
accResult' (FormFailure errs) _ = FormFailure errs
---------------------------------------
-- Buttons (old version, deprecated) --
---------------------------------------
formBtnSave :: (Text,Text,Text)
formBtnSave = ("save" ,"Speichern" ,"btn-primary")
formBtnAbort :: (Text,Text,Text)
formBtnAbort = ("abort" ,"Abbrechen" ,"btn-default")
formBtnDelete ::(Text,Text,Text)
formBtnDelete = ("delete","Löschen" ,"btn-warning")
formActionSave :: Maybe Text
formActionSave = Just $ fst3 formBtnSave
formActionAbort :: Maybe Text
formActionAbort = Just $ fst3 formBtnAbort
formActionDelete :: Maybe Text
formActionDelete = Just $ fst3 formBtnDelete
defaultFormActions :: [(Text,Text,Text)]
defaultFormActions = [ formBtnDelete
, formBtnAbort
, formBtnSave
]
-- Post-Buttons
postButtonForm :: Text -> Form ()
postButtonForm lblId = identifyForm lblId buttonF
where
buttonF = renderAForm FormStandard $ pure () <* bootstrapSubmit bProps
bProps :: BootstrapSubmit Text
bProps = fromString $ unpack lblId
------------
-- Fields --
------------
@ -244,6 +223,9 @@ postButtonForm lblId = identifyForm lblId buttonF
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
natIntField ::(Monad m, RenderMessage (HandlerSite m) FormMessage) => Text -> Field m Integer
natIntField = natField
posIntField :: (Monad m, Integral i, RenderMessage (HandlerSite m) FormMessage) => Text -> Field m i
posIntField d = checkBool (>= 1) (T.append d " muss eine positive Zahl sein.") $ intField
@ -263,6 +245,55 @@ schoolEntField = selectField schools
where
schools = optionsPersist [] [Asc SchoolName] schoolName
multiFileField :: Handler (Set FileId) -> Field Handler (Source Handler (Either FileId File))
multiFileField permittedFiles' = Field{..}
where
fieldEnctype = Multipart
fieldParse vals files
| null files
, null vals = return $ Right Nothing
| otherwise = return . Right . Just $ do
pVals <- lift permittedFiles'
let
decrypt' :: CryptoUUIDFile -> Handler (Maybe FileId)
decrypt' = fmap (either (\(_ :: CryptoIDError) -> Nothing) Just) . try . decrypt
yieldMany vals
.| C.filter (/= unpackZips)
.| C.map fromPathPiece .| C.catMaybes
.| C.mapMaybeM decrypt'
.| C.filter (`elem` pVals)
.| C.map Left
let
handleFile :: FileInfo -> Source Handler File
handleFile
| doUnpack = sourceFiles
| otherwise = yieldM . acceptFile
mapM_ handleFile files .| C.map Right
where
doUnpack = unpackZips `elem` vals
fieldView fieldId fieldName attrs val req = do
pVals <- handlerToWidget permittedFiles'
sentVals <- for val $ \src -> handlerToWidget . sourceToList $ src .| takeLefts
let
toFUI (E.Value fuiId', E.Value fuiTitle) = do
fuiId <- encrypt fuiId'
fuiHtmlId <- newIdent
let fuiChecked
| Right sentVals' <- sentVals = fuiId' `elem` sentVals'
| otherwise = True
return FileUploadInfo{..}
fileInfos <- mapM toFUI <=< handlerToWidget . runDB . E.select . E.from $ \file -> do
E.where_ $ file E.^. FileId `E.in_` E.valList (setToList pVals)
E.orderBy [E.asc $ file E.^. FileTitle]
return (file E.^. FileId, file E.^. FileTitle)
$(widgetFile "multiFileField")
unpackZips :: Text
unpackZips = "unpack-zip"
takeLefts :: Monad m => ConduitM (Either b a) b m ()
takeLefts = awaitForever $ \case
Right _ -> return ()
Left r -> yield r
sheetTypeAFormReq :: FieldSettings UniWorX -> Maybe SheetType -> AForm Handler SheetType
sheetTypeAFormReq d Nothing =
-- TODO, offer options to choose between Normal/Bonus/Pass
@ -271,9 +302,18 @@ sheetTypeAFormReq d (Just (Normal p)) =
-- TODO, offer options to choose between Normal/Bonus/Pass
(Normal . toPoints) <$> areq (natField "Punkte") d (Just $ fromPoints p)
sheetGroupAFormReq :: FieldSettings UniWorX -> Maybe SheetGroup -> AForm Handler SheetGroup
sheetGroupAFormReq d (Just (Arbitrary n)) | n >= 1 =
-- TODO, offer options to choose between Arbitrary/Registered/NoGroups
Arbitrary <$> areq (natField "Abgabegruppengröße") d (Just n)
sheetGroupAFormReq d _other = -- TODO
-- TODO, offer options to choose between Arbitrary/Registered/NoGroups
Arbitrary <$> areq (natField "Abgabegruppengröße") d (Just 1)
utcTimeField :: (Monad m, RenderMessage (HandlerSite m) FormMessage) => Field m UTCTime
-- StackOverflow: dayToUTC <$> (areq (jqueryDayField def {...}) settings Nothing)
utcTimeField = Field
-- TODO: Verify whether this is UTC or local time from Browser
utcTimeField = Field
{ fieldParse = parseHelper $ readTime
, fieldView = \theId name attrs val isReq ->
[whamlet|
@ -282,42 +322,42 @@ utcTimeField = Field
|]
, fieldEnctype = UrlEncoded
}
where
where
fieldTimeFormat :: String
--fieldTimeFormat = "%e.%m.%y %k:%M"
fieldTimeFormat = "%Y-%m-%eT%H:%M"
readTime :: Text -> Either FormMessage UTCTime
readTime t =
readTime t =
case parseTimeM True germanTimeLocale fieldTimeFormat (T.unpack t) of
(Just time) -> Right time
Nothing -> Left $ MsgInvalidEntry $ "Datum/Zeit Format: tt.mm.yy hh:mm " ++ t
showTime :: UTCTime -> Text
showTime :: UTCTime -> Text
showTime = fromString . (formatTime germanTimeLocale fieldTimeFormat)
fsb :: Text -> FieldSettings site
fsb = bfs -- Just to avoid annoying Ambiguous Type Errors
fsb :: Text -> FieldSettings site
fsb = bfs -- Just to avoid annoying Ambiguous Type Errors
addAttr :: Text -> Text -> FieldSettings site -> FieldSettings site
addAttr attr valu fs = fs { fsAttrs=newAttrs (fsAttrs fs) }
where
where
newAttrs :: [(Text,Text)] -> [(Text,Text)]
newAttrs [] = [(attr,valu)]
newAttrs (p@(a,v):t)
newAttrs (p@(a,v):t)
| attr==a = (a,T.append valu $ cons ' ' v):t
| otherwise = p:(newAttrs t)
| otherwise = p:(newAttrs t)
addAttrs :: Text -> [Text] -> FieldSettings site -> FieldSettings site
addAttrs attr valus fs = fs { fsAttrs=newAttrs (fsAttrs fs) }
where
where
newAttrs :: [(Text,Text)] -> [(Text,Text)]
newAttrs [] = [(attr,T.intercalate " " valus)]
newAttrs (p@(a,v):t)
newAttrs (p@(a,v):t)
| attr==a = (a,T.intercalate " " (v:valus)):t
| otherwise = p:(newAttrs t)
| otherwise = p:(newAttrs t)
addClass :: Text -> FieldSettings site -> FieldSettings site
addClass = addAttr "class"
@ -334,7 +374,7 @@ addIdClass :: Text -> Text -> FieldSettings site -> FieldSettings site
addIdClass gId gClass fs = fs { fsId= Just gId, fsAttrs=("class",gClass):(fsAttrs fs) }
setClass :: FieldSettings site -> Text -> FieldSettings site -- deprecated
setClass :: FieldSettings site -> Text -> FieldSettings site -- deprecated
setClass fs c = fs { fsAttrs=("class",c):(fsAttrs fs) }
setNameClass :: FieldSettings site -> Text -> Text -> FieldSettings site -- deprecated
@ -345,3 +385,24 @@ setTooltip tt fs
| null tt = fs { fsTooltip = Nothing }
| otherwise = fs { fsTooltip = Just $ fromString tt }
optionsPersistCryptoId :: forall site backend a msg.
( YesodPersist site
, PersistQueryRead backend
, HasCryptoUUID (Key a) (HandlerT site IO)
, RenderMessage site msg
, YesodPersistBackend site ~ backend
, PersistRecordBackend a backend
)
=> [Filter a]
-> [SelectOpt a]
-> (a -> msg)
-> HandlerT site IO (OptionList (Key a))
optionsPersistCryptoId filts ords toDisplay = fmap mkOptionList $ do
mr <- getMessageRender
pairs <- runDB $ selectList filts ords
cPairs <- forM pairs $ \e@(Entity key _) -> (,) <$> encrypt key <*> pure e
return $ map (\(cId, Entity key value) -> Option
{ optionDisplay = mr (toDisplay value)
, optionInternalValue = key
, optionExternalValue = toPathPiece (cId :: CryptoID UUID (Key a))
}) cPairs

View File

@ -0,0 +1,12 @@
{-# LANGUAGE NoImplicitPrelude #-}
module Handler.Utils.Form.Types where
import Import
data FileUploadInfo = FileUploadInfo
{ fuiId :: CryptoUUIDFile
, fuiTitle :: FilePath
, fuiHtmlId :: Text
, fuiChecked :: Bool
}

View File

@ -70,12 +70,9 @@ sinkSubmission sheetId userId mExists = do
submissionRatingComment = Nothing
submissionRatingBy = Nothing
submissionRatingTime = Nothing
submissionCreated = now
submissionChanged = now
submissionCreatedBy = userId
submissionChangedBy = userId
(sId, isUpdate) <- lift $ maybe ((, False) <$> insert Submission{..}) return mExists
(sId, isUpdate) <- lift $ maybe ((, False) <$> (insert Submission{..} >>= (\sid -> sid <$ insert (SubmissionEdit userId now sid)))) return mExists
sId <$ sinkSubmission' sId isUpdate
where
@ -184,9 +181,9 @@ sinkSubmission sheetId userId mExists = do
alreadyTouched <- gets $ getAny . sinkSubmissionTouched
when (not alreadyTouched) $ do
now <- liftIO getCurrentTime
lift . update submissionId $ case isUpdate of
False -> [ SubmissionChangedBy =. userId, SubmissionChanged =. now ]
True -> [ SubmissionRatingBy =. Just userId, SubmissionRatingTime =. Just now ]
lift $ case isUpdate of
False -> insert_ $ SubmissionEdit userId now submissionId
True -> update submissionId [ SubmissionRatingBy =. Just userId, SubmissionRatingTime =. Just now ]
tell $ mempty{ sinkSubmissionTouched = Any True }
finalize :: SubmissionSinkState -> YesodDB UniWorX ()

View File

@ -23,12 +23,15 @@ import Data.Either
-- Table design
tableDefault :: Attribute
tableDefault = customAttribute "class" "table table-striped table-hover"
tableDefault = customAttribute "class" "table table-striped table-hover"
tableSortable :: Attribute
tableSortable = customAttribute "class" "js-sortable"
-- Colonnade Tools
numberColonnade :: (IsString c) => Colonnade Headed Int c
numberColonnade = headed "Nr" (fromString.show)
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]
@ -39,8 +42,8 @@ encodeHeadedWidgetTableNumbered attrs colo tdata =
encodeWidgetTable attrs (mconcat [numberCol, lmap snd colo]) (zip [1..] tdata)
where
numberCol :: Colonnade Headed (Int,a) (WidgetT site IO ())
numberCol = headed "Nr" (fromString.show.fst)
numberCol = headed "Nr" (fromString.show.fst)
headedRowSelector :: ( PathPiece b
, Eq b
)
@ -74,7 +77,7 @@ headedRowSelector toExternal fromExternal attrs colonnade tdata = do
selectionIdent <- newFormIdent
(selectionResults, selectionBoxes) <- fmap unzip . forM externalIds $ \ident -> mopt (checkbox ident) ("" { fsName = Just selectionIdent }) Nothing
let
selColonnade :: Colonnade Headed Int (Cell UniWorX)
selColonnade = headed "Markiert" $ cell . fvInput . (selectionBoxes !!)

View File

@ -0,0 +1,22 @@
{-# LANGUAGE NoImplicitPrelude, TemplateHaskell #-}
module Handler.Utils.Templates where
import Import.NoFoundation
lipsum :: WidgetT site IO ()
lipsum = $(widgetFile "widgets/lipsum")
modal :: [Char] -> Maybe [Char] -> WidgetT site IO ()
modal modalTrigger (Just modalContent) = do
let
modalId :: Int32
modalId = 13
$(widgetFile "widgets/modal")
modal modalTrigger Nothing = do
let
modalId :: Int32
modalId = 13
modalContent :: [Char]
modalContent = "placeholder"
$(widgetFile "widgets/modal")

View File

@ -11,7 +11,7 @@ module Handler.Utils.Zip
, produceZip
, consumeZip
, modifyFileTitle
, sourceFiles
, sourceFiles, acceptFile
) where
import Import
@ -102,11 +102,13 @@ modifyFileTitle f = mapC $ \x@File{..} -> x{ fileTitle = f fileTitle }
sourceFiles :: (MonadResource m, MonadThrow m, MonadIO m) => FileInfo -> Source m File
sourceFiles fInfo
| mimeType == "application/zip" = fileSource fInfo =$= void consumeZip
| otherwise = do
let fileTitle = unpack $ fileName fInfo
fileModified <- liftIO getCurrentTime
yieldM $ do
fileContent <- Just <$> runConduit (fileSource fInfo =$= foldC)
return File{..}
| otherwise = yieldM $ acceptFile fInfo
where
mimeType = defaultMimeLookup (fileName fInfo)
acceptFile :: (MonadResource m, MonadThrow m, MonadIO m) => FileInfo -> m File
acceptFile fInfo = do
let fileTitle = unpack $ fileName fInfo
fileModified <- liftIO getCurrentTime
fileContent <- Just <$> runConduit (fileSource fInfo =$= foldC)
return File{..}

View File

@ -16,3 +16,6 @@ import Data.Fixed as Import
import CryptoID as Import
import Data.UUID as Import (UUID)
import Text.Lucius as Import

View File

@ -43,7 +43,7 @@ toPoints :: Integral a => a -> Points
toPoints = MkFixed . fromIntegral
fromPoints :: Integral a => Points -> a
fromPoints = error "TODO: Types.fromPoints not yet implemented"
fromPoints (MkFixed c) = fromInteger c
data SheetType
= Bonus { maxPoints :: Points }
@ -54,9 +54,13 @@ data SheetType
deriveJSON defaultOptions ''SheetType
derivePersistFieldJSON "SheetType"
data ExamStatus = Attended | NoShow | Voided
deriving (Show, Read, Eq, Ord, Enum, Bounded)
derivePersistField "ExamStatus"
data SheetGroup
= Arbitrary { maxParticipants :: Int } -- Distinguish Limited/Arbitrary
| RegisteredGroups
| NoGroups
deriving (Show, Read, Eq)
deriveJSON defaultOptions ''SheetGroup
derivePersistFieldJSON "SheetGroup"
data SheetFileType = SheetExercise | SheetHint | SheetSolution | SheetMarking
deriving (Show, Read, Eq, Ord, Enum, Bounded)
@ -70,9 +74,11 @@ instance PathPiece SheetFileType where
fromPathPiece t =
lookup (CI.mk t) [(CI.mk $ toPathPiece ty,ty) | ty <- [minBound..maxBound]]
data ExamStatus = Attended | NoShow | Voided
deriving (Show, Read, Eq, Ord, Enum, Bounded)
derivePersistField "ExamStatus"
data Load = ByTutorial | ByProportion Double
data Load = ByTutorial | ByProportion Rational
deriving (Show, Read, Eq)
derivePersistField "Load"
@ -96,6 +102,10 @@ data TermIdentifier = TermIdentifier
, season :: Season
} deriving (Show, Read, Eq, Ord, Generic, Typeable)
-- Conversion TermId <-> TermIdentifier::
-- from_TermId_to_TermIdentifier = unTermKey
-- from_TermIdentifier_to_TermId = TermKey
--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.pack $ seasonToChar season : show year

7022
static/css/bootstrap.css vendored

File diff suppressed because it is too large Load Diff

9
static/css/fonts.css Normal file
View File

@ -0,0 +1,9 @@
@font-face {
font-family: 'Glyphicons Halflings';
src: url('../fonts/glyphicons-halflings-regular.eot');
src: url('../fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'),
url('../fonts/glyphicons-halflings-regular.woff2') format('woff2'),
url('../fonts/glyphicons-halflings-regular.woff') format('woff'),
url('../fonts/glyphicons-halflings-regular.ttf') format('truetype'),
url('../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg');
}

34
static/css/icons.css Normal file
View File

@ -0,0 +1,34 @@
.glyphicon {
position: relative;
display: inline-block;
width: 40px;
height: 40px;
}
.glyphicon::before {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-family: 'Glyphicons Halflings';
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.glyphicon--home::before {
content: '\e021';
}
.glyphicon--book::before {
content: '\e043';
}
.glyphicon--profile::before {
content: '\e019';
}
.glyphicon--user::before {
content: '\e008';
}
.glyphicon--login::before {
content: '\e161';
}
.glyphicon--logout::before {
content: '\e163';
}

View File

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

View File

@ -1,38 +1,34 @@
<div .masthead>
<div .container>
<div .row>
<h1 .header>
#{courseName course}
$maybe school <- schoolMB
<h4>
#{schoolName school}
<div .course-header>
<div .course-header__info>
<table>
<tr>
<th>Teilnehmer
<td>
#{participants}
$maybe capacity <- courseCapacity course
\ von #{capacity}
<tr>
<th>Anmeldezeitraum
<td>
$maybe regFrom <- courseRegisterFrom course
#{formatTimeGerWD regFrom}
$maybe regTo <- courseRegisterTo course
\ bis #{formatTimeGerWD regTo}
<div>
<form method=post action=@{CourseR tid csh CourseShowR} enctype=#{regEnctype}>
^{regWidget}
<div .course-header__title>
<h1>#{courseName course}
$maybe school <- schoolMB
<h4>#{schoolName school}
<div .container>
<div .bs-docs-section>
<div .row>
<div .col-lg-12>
<div .page-header>
$maybe descr <- courseDescription course
<h2 #description>Beschreibung
<p> #{descr}
$maybe link <- courseLinkExternal course
<h4 #linl>Homepage
<a href=#{link}>#{link}
<div .row>
<div .col-lg-12>
<h4>Teilnehmer
#{participants}
$maybe capacity <- courseCapacity course
\ von #{capacity}
<br>
$maybe regFrom <- courseRegisterFrom course
Anmeldezeitraum: #{formatTimeGerWD regFrom}
$maybe regTo <- courseRegisterTo course
\ bis #{formatTimeGerWD regTo}
<form method=post action=@{CourseShowR tid csh} enctype=#{regEnctype}>
^{regWidget}
<hr>
$maybe descr <- courseDescription course
<h2 #description>Beschreibung
<p> #{descr}
$maybe link <- courseLinkExternal course
<h4 #linl>Homepage
<a href=#{link}>#{link}

19
templates/course.lucius Normal file
View File

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

4
templates/courses.hamlet Normal file
View File

@ -0,0 +1,4 @@
<div .container>
<h1>Kursübersicht für Semester #{termToText $ unTermKey tidini}
<div .scrolltable>
^{coursesTable}

View File

@ -19,12 +19,7 @@ $newline never
\<!--[if lt IE 9]>
\<script src="http://html5shiv.googlecode.com/svn/trunk/html5.js"></script>
\<![endif]-->
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.4/jquery.js">
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/js-cookie/2.0.3/js.cookie.min.js">
\<!-- Bootstrap-3.3.7 compiled and minified JavaScript -->
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous">
<script>
/* The `defaultCsrfMiddleware` Middleware added in Foundation.hs adds a CSRF token to the request cookies. */
/* AJAX requests should add that token to a header to be validated by the server. */
@ -43,9 +38,12 @@ $newline never
\ });
}
<body .no-js>
<!-- removes no-js class from body if client supports javascript -->
<script>
document.documentElement.className = document.documentElement.className.replace(/\bno-js\b/,'js');
<body>
document.body.classList.remove('no-js');
^{pageBody pc}
$maybe analytics <- appAnalytics $ appSettings master

View File

@ -1,60 +1,20 @@
<!-- navigation -->
^{navbar}
<!-- Static navbar -->
<nav .navbar.navbar-default.navbar-static-top>
<div .container>
<div .navbar-header>
<button type="button" .navbar-toggle.collapsed data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<div .main>
<div #navbar .collapse.navbar-collapse>
<ul .nav.navbar-nav>
$forall menuType <- menuTypes
$case menuType
$of NavbarLeft (MenuItem label route _)
<li :Just route == mcurrentRoute:.active>
<a href=@{route}>#{label}
$of NavbarExtra (MenuItem label route _)
<li :Just route == mcurrentRoute:.active>
<a href=@{route}>#{label}
$of _
<!-- secondary navigation at the side -->
^{asidenav}
<ul .nav.navbar-nav.navbar-right>
$forall menuType <- menuTypes
$case menuType
$of NavbarRight (MenuItem label route _)
<li :Just route == mcurrentRoute:.active>
<a href=@{route}>#{label}
$of _
<!-- Page Contents -->
<div .container>
$if not $ Just HomeR == mcurrentRoute
<ul .breadcrumb>
$forall bc <- parents
<li>
<a href="@{fst bc}">#{snd bc}
<li .active>#{title}
<div .main__content>
<!-- alerts -->
$forall (status, msg) <- mmsgs
$with status2 <- bool status "info" (status == "")
<div class="alert alert-#{status2}">#{msg}
<!-- prime page actions -->
^{pageactionprime}
$if (Just HomeR == mcurrentRoute)
<!-- actual content -->
^{widget}
$else
<div .container>
<div .row>
<div .col-md-12>
^{widget}
<!-- Footer -->
<footer .footer>
<div .container>
<p .text-muted>
#{appCopyright $ appSettings master}

View File

@ -1,73 +1,221 @@
.masthead,
.navbar {
background-color: rgb(27, 28, 29);
}
.navbar-default .navbar-nav > .active > a {
background-color: transparent;
border-bottom: 2px solid white;
}
.navbar-nav {
padding-bottom: 1em;
}
.masthead {
margin-top: -21px;
color: white;
text-align: center;
min-height: 500px;
}
.masthead .header {
max-width: 700px;
margin: 0 auto;
font-family: Lato,'Helvetica Neue',Arial,Helvetica,sans-serif;
}
.masthead h1.header {
margin-top: 1em;
margin-bottom: 0em;
font-size: 4.5em;
line-height: 1.2em;
font-weight: normal;
}
.masthead h2 {
font-size: 1.7em;
font-weight: normal;
}
.masthead .btn {
margin: 1em 0;
}
:root {
/* THEME 1 */
--base00: #72a85b;
--base-bg-color: #1d1c1d;
--base-font-color: #fff;
--sec-font-color: #fff;
--box-bg-color: #3c3c3c;
/* THEME 2 */
--base00: #38428a;
--base-bg-color: #ffffff;
--base-font-color: rgb(53, 53, 53);
--sec-font-color: #eaf2ff;
--box-bg-color: #dddddd;
/* THEME 3 */
--darkbase: #364B60;
--lightbase: #2490E8;
--lighterbase: #60C2FF;
--whitebase: #FCFFFA;
--greybase: #B1B5C0;
--fontbase: #34303a;
--fontsec: #5b5861;
/* THEME 4 */
--darkbase: #263C4C;
--lightbase: #598EB5;
--lighterbase: #5F98C2;
--whitebase: #FCFFFA;
--greybase: #B1B5C0;
--lightgreybase: #D9DEDB;
--blackbase: #1A2A36;
--fontbase: #34303a;
--fontsec: #5b5861;
--primarybase: #4C7A9C;
/* Common styles for all types */
.bs-callout {
padding: 20px;
margin: 20px 0;
border: 1px solid #eee;
border-left-width: 5px;
border-radius: 3px;
/* THEME INDEPENDENT COLORS */
--errorbase: red;
--warningbase: #fe7700;
--validbase: #2dcc35;
--infobase: var(--darkbase);
/* FONTS */
--fontfamilybase: "Source Sans Pro", Helvetica, sans-serif;
/* DIMENSIONS */
--header-height: 80px;
--header-height-collapsed: 50px;
}
.bs-callout p:last-child {
margin-bottom: 0;
* {
box-sizing: border-box;
padding: 0;
margin: 0;
}
.bs-callout-info {
border-left-color: #1b809e;
body {
background-color: white;
color: var(--fontbase);
font-family: var(--fontfamilybase);
font-weight: 400;
font-size: 16px;
overflow-y: scroll;
}
/* Space things out */
.bs-docs-section {
margin-bottom: 60px;
}
.bs-docs-section:last-child {
margin-bottom: 0;
a,
a:visited {
text-decoration: none;
font-weight: 600;
transition: color .2s ease, background-color .2s ease;
}
#message {
margin-bottom: 40px;
ul {
list-style-type: none;
}
.list--inline > li {
display: inline-block;
}
h1, h2, h3, h4, h5 {
font-weight: 600;
}
h1 {
font-size: 32px;
margin: 20px 0 10px;
}
h2 {
font-size: 24px;
margin: 10px 0 5px;
}
h3 {
font-size: 20px;
margin: 5px 0;
}
h4 {
font-size: 16px;
margin: 0;
}
table {
margin: 21px 0;
}
.scrolltable {
width: 100%;
overflow: auto;
}
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 */
.main {
display: flex;
min-height: calc(100vh - var(--header-height));
overflow: hidden;
}
.main__content {
position: relative;
background-color: white;
padding: 0 40px;
flex: 1;
z-index: 0;
overflow: hidden;
> .container {
margin: 20px 0;
}
p {
margin: 10px 0;
}
a {
color: var(--darkbase);
}
a:hover {
color: var(--lightbase);
}
}
.pseudo-focus {
outline: 5px auto var(--lightbase);
outline: 5px auto -webkit-focus-ring-color;
}
/* GENERAL BUTTON STYLES */
input[type="submit"],
input[type="button"],
button,
.btn, a.btn {
outline: 0;
border: 0;
box-shadow: 0;
background-color: var(--lightbase);
color: white;
padding: 10px 17px;
min-width: 100px;
transition: all .1s;
font-size: 16px;
cursor: pointer;
border-radius: 4px;
display: inline-block;
}
input.btn-primary,
button.btn-primary,
a.btn.btn-primary,
.btn.btn-primary {
background-color: var(--primarybase);
}
input.btn-info,
button.btn-info,
a.btn.btn-info,
.btn.btn-info {
background-color: var(--infobase)
}
input[type="submit"][disabled],
input[type="button"][disabled],
button[disabled],
a.btn[disabled],
.btn[disabled] {
opacity: 0.3;
background-color: var(--greybase);
cursor: default;
}
input[type="submit"]:not([disabled]):hover,
input[type="button"]:not([disabled]):hover,
button:not([disabled]):hover,
a.btn:not([disabled]):hover,
.btn:not([disabled]):hover {
background-color: var(--lighterbase);
text-decoration: underline;
color: white;
}
input[type="submit"].btn-info:hover,
input[type="button"].btn-info:hover,
button.btn-info:hover,
a.btn.btn-info:hover,
.btn.btn-info:hover {
background-color: var(--greybase)
}
.alert-debug {
background-color: rgb(240, 30, 240);
}

View File

@ -1,9 +0,0 @@
$newline never
#{fragment}
$case formLayout
$of FormStandard
$forall view <- views
<div :fvRequired view:.required :not $ fvRequired view:.optional :isJust $ fvErrors view:.has-error>
$if not (Blaze.null $ fvLabel view)
<label for=#{fvId view}>#{fvLabel view}
^{fvInput view}

View File

@ -5,9 +5,8 @@
<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}
^{formWidget}

View File

@ -0,0 +1,15 @@
<div .container>
<div .bs-docs-section>
<div .row>
<div .col-lg-12>
<div .page-header>
<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}

View File

@ -1,82 +1,100 @@
<div .masthead>
<div .container>
<div .row>
<h1 .header>
ReWorX - Demo
<h3>
Testumgebung für die Re-Implementierung von
<a href="https://uniworx.ifi.lmu.de/">
UniWorX
<div .container>
<!-- Starting
================================================== -->
<div .bs-docs-section>
<div .row>
<div .col-lg-12>
<div .page-header>
<h2 #start>Übersicht
<p>
Die Reimplementierung von
UniWorX ist noch nicht abgeschlossen.
<div .alert .alert-danger>
Das System ist noch nicht produktiv
einsetzbar
<div .bs-docs-section>
<div .row>
<div .col-lg-12>
<div .page-header>
<h2 #design>Design
<p>
Wir konzentrieren uns derzeit
ausschließlich auf die Funktionalität.
<p>
Insbesondere Formulare zeigen
alle Eingabefelder und Knöpfe
ohne eine gezielte Anordnung
und Reihenfolge.
Dies läßt sich leicht nachträglich einstellen.
<p>
Momentan werden noch keine speziellen Grafiken oder CSS verwendet;
sondern nur gewöhnliches Bootstrap3.
<div .bs-docs-section>
<div .row>
<div .col-lg-12>
<div .page-header>
<h3 #design>Teilweise funktionierende Abschnitte
<ul .list-group>
<h1>ReWorX - Demo
<h3>
Testumgebung für die Re-Implementierung von <a href="https://uniworx.ifi.lmu.de/">UniWorX</a>
<p>
Die Reimplementierung von
UniWorX ist noch nicht abgeschlossen.
<li .list-group-item>
<a href=@{UsersR}>Benutzer Verwaltung
<p .alert .alert-danger>Das System ist noch nicht produktiv einsetzbar
<li .list-group-item>
<a href=@{TermShowR}>Semester Verwaltung
<a href=@{TermEditR}>Neues Semester anlegen
<hr>
<div .container>
<h2 .js-show-hide__toggle>Teilweise funktionierende Abschnitte
<li .list-group-item>
<a href=@{CourseEditR}>Kurse anlegen
editieren und anzeigen
<ul>
<li .list-group-item>
<a href=@{UsersR}>Benutzer Verwaltung
<li .list-group-item>
<a href=@{SubmissionListR}>Dateien hochladen und abrufen
<h3 #design>Funktionen zum Testen
<ul .list-group>
<li .list-group-item>
<a href=@{TermShowR}>Semester Verwaltung
<a href=@{TermEditR}>Neues Semester anlegen
<li .list-group-item>
Knopf-Test:
<form .form-inline method=post action=@{HomeR} enctype=#{btnEnctype}>
^{btnWdgt}
<li .list-group-item>
<a href=@{CourseEditR}>Kurse anlegen
editieren und anzeigen
<li .list-group-item>
<a href=@{CourseNewR}>Kurse anlegen
<li .list-group-item>
<a href=@{SubmissionListR}>Dateien hochladen und abrufen
<hr>
<div .container>
<h2 .js-show-hide__toggle data-collapsed=true>Tabellen
<table .js-sortable>
<thead>
<tr>
<th .sorted-asc>ID
<th>TH1
<th>TH2
<th>TH3
<tbody>
<tr>
<td>0
<td>NT2
<td>CON2
<td>3
<tr>
<td>1
<td>5
<td>ONT2
<td>13
<tr>
<td>2
<td>CONT1
<td>NT2
<td>43
<tr>
<td>3
<td>43
<td>T2C2
<td>35
<tr>
<td>4
<td>73
<td>CA62
<td>7
<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 wegen 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">

0
templates/home.julius Normal file
View File

View File

@ -0,0 +1,17 @@
$newline never
<input type=checkbox id=#{fieldId}_zip name=#{fieldName} value=#{unpackZips} :req:required>
<label for=#{fieldId}_zip>
ZIPs entpacken
<ul>
$forall FileUploadInfo{..} <- fileInfos
<li>
<input type=checkbox name=#{fieldName} value=#{toPathPiece fuiId} id=#{fuiHtmlId} :fuiChecked:checked>
<span style="display:none">
#{fuiTitle}
<label for=#{fuiHtmlId}>
#{fuiTitle}
<li>
<input type=file id=#{fieldId} name=#{fieldName} multiple>

View File

@ -1,20 +0,0 @@
<div .container>
<div .bs-docs-section>
<div .row>
<div .col-lg-12>
<div .page-header>
<h1 #forms>Neuen Blatt anlegen:
<p>
Bitte alles ausfüllen!
<div .row>
<div .col-lg-6>
<div .bs-callout bs-callout-info well>
<form .form-horizontal method=post #forms enctype=#{enc}>
^{wdgt}
<button .btn.btn-primary type="submit">
Blatt anlegen

View File

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

View File

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

View File

@ -0,0 +1,236 @@
(function() {
'use strict';
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
window.utils.reactiveFileUpload = function(input, parent) {
var currValidInputCount = 0;
var addMore = false;
var inputName = input.getAttribute('name');
var isMulti = input.getAttribute('multiple') ? true : false;
// 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) {
parentElement.appendChild(this.container);
}
this.remove = function() {
this.container.remove();
}
this.wasValid = function() {
return this.container.classList.contains('file-input__container--valid');
}
}
function addNextInput() {
var inputs = parent.querySelectorAll('.file-input__container');
if (inputs[inputs.length - 1].classList.contains('file-input__container--valid')) {
makeInput(inputName).addTo(parent);
}
}
// updates submitbutton and form-group-stripe
function updateForm() {
var submitBtn = parent.parentElement.querySelector('[type=submit]');
parent.classList.remove('form-group--has-error');
if (currValidInputCount > 0) {
if (parent.classList.contains('form-group')) {
parent.classList.add('form-group--valid')
}
submitBtn.removeAttribute('disabled');
if (isMulti) {
addNextInput();
}
} else {
if (parent.classList.contains('form-group')) {
parent.classList.remove('form-group--valid')
}
submitBtn.setAttribute('disabled', 'disabled');
}
}
// 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 clearInput(fileInput) {
if (isMulti) {
fileInput.remove();
} else {
fileInput.container.classList.remove('file-input__container--valid')
fileInput.label.innerHTML = '';
}
updateForm();
}
// create new wrapped input element with name name
function makeInput(name) {
var cont = document.createElement('div');
var desc = document.createElement('label');
var nextInput = document.createElement('input');
var remover = document.createElement('div');
cont.classList.add('file-input__container');
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);
}
// initial setup
function setup() {
var newInput = makeInput(inputName);
input.remove();
newInput.addTo(parent);
updateForm();
}
setup();
}
// to remove previously uploaded files
window.utils.reactiveFileCheckbox = function(input, label, parent) {
// adds eventlistener(s)
function addListener(container) {
container.addEventListener('click', function() {
input.click();
});
input.addEventListener('change', function(event) {
container.classList.toggle('file-checkbox__container--valid', this.checked);
});
}
// initial setup
function setup() {
var cont = input.parentNode;
while (cont !== document.body) {
if (cont.classList.contains('file-checkbox__container')) {
break;
}
cont = cont.parentNode;
}
// take care of properly moving elements
if (input.parentNode.classList.contains('checkbox')) {
input.parentNode.classList.add('file-checkbox__checkbox');
} else {
input.classList.add('file-checkbox__checkbox');
}
addListener(cont);
}
setup();
}
window.utils.reactiveFormGroup = function(formGroup, input) {
// updates to dom
if (input.value.length > 0) {
formGroup.classList.add('form-group--valid');
} else {
formGroup.classList.remove('form-group--valid');
}
input.addEventListener('input', function() {
formGroup.classList.remove('form-group--has-error');
if (input.value.length > 0) {
formGroup.classList.add('form-group--valid');
} else {
formGroup.classList.remove('form-group--valid');
}
});
};
})();
document.addEventListener('DOMContentLoaded', function() {
// setup reactive labels
Array.from(document.querySelectorAll('.reactive-label')).forEach(function(label) {
var input = document.querySelector('#' + label.getAttribute('for'));
if (!input) {
console.error('No input found for ReactiveLabel! Targeted input: \'#%s\'', label.getAttribute('for'));
return false;
}
var parent = label.parentElement;
var type = input.getAttribute('type');
var isFileUpload = /file/i.test(type);
var isFileCheckbox = input.classList.contains('file-checkbox');
var isListening = !RegExp(['date', 'checkbox', 'radio', 'hidden', 'file'].join('|')).test(type);
var isInFormGroup = parent.classList.contains('form-group') && parent.classList.contains('form-group--required');
if (isInFormGroup) {
window.utils.reactiveFormGroup(parent, input);
}
if (isFileUpload) {
window.utils.reactiveFileUpload(input, parent);
}
if (isFileCheckbox) {
window.utils.reactiveFileCheckbox(input, label, parent);
}
if (isListening) {
window.utils.reactiveInputLabel(input, label);
} else {
label.classList.remove('reactive-label');
}
});
});

View File

@ -0,0 +1,360 @@
/* GENERAL STYLES FOR FORMS */
form {
margin: 20px 0;
}
/* FORM GROUPS */
.form-group {
position: relative;
display: flex;
display: grid;
grid-template-columns: 25% max-content;
grid-auto-columns: 25%;
grid-gap: 5px;
justify-content: flex-start;
align-items: center;
margin: 10px 0;
padding-left: 10px;
border-left: 8px solid transparent;
}
.form-group--required {
border-left: 8px solid var(--lighterbase);
}
.form-group--valid {
border-left: 8px solid var(--validbase);
}
.form-group--has-error {
border-left: 8px solid var(--errorbase) !important;
}
.form-group__label {
width: 25%;
white-space: nowrap;
font-weight: 600;
}
@media (max-width: 999px) {
.form-group {
grid-template-columns: 1fr;
grid-template-rows: 30px;
align-items: baseline;
margin-top: 17px;
flex-direction: column;
> * {
width: 100%;
}
}
}
/* TEXT INPUTS */
input[type="text"],
input[type="password"],
input[type="url"],
input[type="number"],
input[type="email"] {
background-color: rgba(0, 0, 0, 0.05);
padding: 7px 3px 7px;
outline: 0;
border: 0;
border-bottom: 2px solid var(--darkbase);
box-shadow: 0 2px 13px rgba(0, 0, 0, 0.05);
color: var(--fontbase);
transition: all .1s;
font-size: 16px;
min-width: 400px;
}
input[type="text"]:focus,
input[type="password"]:focus,
input[type="url"]:focus,
input[type="number"]:focus,
input[type="email"]:focus {
border-bottom-color: var(--lightbase);
background-color: transparent;
}
/* BUTTON STYLE SEE default-layout.lucius */
/* TEXTAREAS */
textarea {
outline: 0;
border: 0;
padding: 7px 4px;
min-width: 400px;
min-height: 100px;
font-family: var(--fontfamilybase);
font-size: 16px;
color: var(--fontbase);
background-color: rgba(0, 0, 0, 0.05);
box-shadow: 0 2px 13px rgba(0, 0, 0, 0.05);
border-bottom: 2px solid var(--darkbase);
}
textarea:focus {
background-color: transparent;
border-bottom-color: var(--lightbase);
}
/* CUSTOM LEGACY CHECKBOX AND RADIO BOXES */
input[type="checkbox"] {
position: relative;
height: 20px;
width: 20px;
-webkit-appearance: none;
cursor: pointer;
}
input[type="checkbox"]::before {
content: '';
position: absolute;
width: 20px;
height: 20px;
background-color: var(--lighterbase);
display: flex;
align-items: center;
justify-content: center;
border-radius: 2px;
}
input[type="checkbox"]:checked::before {
background-color: var(--lightbase);
}
input[type="checkbox"]:checked::after {
content: '✓';
position: absolute;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 20px;
}
/* CUSTOM CHECKBOXES AND RADIO BOXES */
/* Completely replaces legacy checkbox and radiobox */
.checkbox,
.radio {
position: relative;
> [type="checkbox"],
> [type="radio"] {
display: none;
}
> label {
display: block;
height: 30px;
width: 30px;
background-color: var(--greybase);
border-radius: 4px;
color: white;
cursor: pointer;
}
> label::before,
> label::after {
content: '';
position: absolute;
top: 14px;
left: 5px;
display: block;
width: 20px;
height: 20px;
background-color: white;
transition: all .2s;
}
> label::before {
width: 20px;
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;
}
&:hover > label {
background-color: var(--lighterbase);
}
&:hover > label::before {
transform: scale(0.8, 0.4);
}
> :checked + label::before {
transform: scale(1, 1) rotate(45deg);
}
> :checked + label:hover::after,
> :checked + label:hover::before {
transform: scale(1, 1) rotate(0deg);
}
&:hover > label::after {
transform: scale(0.8, 0.4);
}
> :checked + label::after {
transform: scale(1, 1) rotate(-45deg);
}
}
.radio > label::before {
transform: scale(0.01, 0.01) rotate(45deg);
}
.radio > label::after {
transform: scale(0.01, 0.01) rotate(-45deg);
}
.radio::before {
content: '';
position: absolute;
top: 2px;
left: 2px;
right: 2px;
bottom: 2px;
border-radius: 4px;
border: 2px solid white;
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 */
input[type="file"].js-file-input {
color: white;
width: 0.1px;
height: 0.1px;
opacity: 0;
overflow: hidden;
position: absolute;
z-index: -1;
outline: 0;
border: 0;
}
.file-input__container,
.file-checkbox__container {
grid-column-start: 2;
display: flex;
justify-content: space-between;
}
.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;
}
.file-input__label,
.file-checkbox__label {
text-align: left;
position: relative;
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 {
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;
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--valid > .file-checkbox__label {
text-decoration: none;
background-color: var(--lighterbase);
&:hover {
background-color: var(--greybase);
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__container,
.file-checkbox__container {
grid-column-start: 1;
}
}

View File

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

View File

@ -0,0 +1,100 @@
(function() {
'use strict';
window.utils = window.utils || {};
window.utils.modal = function(modal) {
var overlay = document.createElement('div');
var closer = document.createElement('div');
var trigger = document.querySelector(modal.dataset.trigger);
var origParent = modal.parentNode;
function open(event) {
if (event) {
event.preventDefault();
}
modal.classList.add('modal--open');
overlay.classList.add('modal__overlay');
document.body.insertBefore(modal, null);
document.body.insertBefore(overlay, modal);
overlay.classList.add('modal__overlay--open');
toggleScroll(false);
if (modal.dataset.closeable === 'true') {
closer.classList.add('modal__closer');
modal.insertBefore(closer, null);
closer.addEventListener('click', close, false);
overlay.addEventListener('click', close, false);
}
}
// open this modal with an event:
// document.dispatchEvent(new CustomEvent('modal-open', { dateils: {for: 'modal-13'}}))
function openOnEvent(event) {
if (event.detail.for === modal.getAttribute('id')) {
open();
}
}
function close(event) {
if (typeof event === 'undefined' || event.target === closer || event.target === overlay) {
overlay.remove();
origParent.insertBefore(modal, null);
modal.classList.remove('modal--open');
toggleScroll(true);
closer.removeEventListener('click', close, false);
}
};
function setup() {
// every modal can be openend via document-wide event, see openOnEvent
document.addEventListener('modal-open', openOnEvent, false);
// if modal has trigger assigned to it open modal on click
if (trigger) {
trigger.classList.add('modal__trigger');
trigger.addEventListener('click', open, false);
}
// if there is no content specified for the modal we assume that
// the content is supposed to be the page the trigger links to.
// so we check if the trigger has a href-attribute, fetch that page
// and replace the modal content with the response
var replaceMe = modal.querySelector('.replace-me');
var replaceWith = trigger ? trigger.getAttribute('href') : '';
if (replaceMe) {
replaceMe.classList.remove('replace-me');
replaceMe.innerText = '...loading';
if (replaceWith.length > 0) {
fetch(replaceWith).then(function(response) {
return response.text();
}).then(function(body) {
var modalContent = document.createElement('div');
modalContent.innerHTML = body;
var main = modalContent.querySelector('.main__content');
if (main) {
replaceMe.innerText = '';
replaceMe.insertBefore(main, null);
} else {
replaceMe.innerHTML = body;
}
});
}
}
// tell further modals, that this one already got initialized
modal.classList.add('js-modal-initialized');
}
setup();
};
// make sure document doesn't scroll when modal is active
function toggleScroll(scrollable) {
document.body.classList.toggle('no-scroll', !scrollable);
}
})();
document.addEventListener('DOMContentLoaded', function() {
Array.from(document.querySelectorAll('.js-modal:not(.js-modal-initialized)')).map(function(modal) {
new utils.modal(modal);
});
}, false);

View File

@ -0,0 +1,87 @@
.modal {
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%) scale(0.8, 0.8);
display: block;
background-color: rgba(255, 255, 255, 0.9);
min-width: 60vw;
min-height: 100px;
max-height: calc(100vh - 30px);
border-radius: 7px;
z-index: -1;
color: var(--fontbase);
padding: 20px;
overflow: auto;
opacity: 0;
transition: all .15s ease;
&.modal--open {
opacity: 1;
z-index: 200;
transform: translate(-50%, -50%) scale(1, 1);
}
}
@media (max-width: 999px) {
.modal {
min-width: 80vw;
}
}
@media (max-width: 666px) {
.modal {
min-width: 90vw;
}
}
@media (max-width: 444px) {
.modal {
min-width: calc(100vw - 20px);
}
}
.modal__overlay {
position: fixed;
left: 0;
top: 0;
height: 100%;
width: 100%;
background-color: transparent;
z-index: -1;
opacity: 0;
transition: all .2s ease;
&.modal__overlay--open {
z-index: 199;
opacity: 1;
background-color: rgba(0, 0, 0, 0.4);
}
}
.modal__trigger {
cursor: pointer;
}
.modal__closer {
position: absolute;
top: 20px;
right: 20px;
display: flex;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
background-color: var(--darkbase);
border-radius: 2px;
cursor: pointer;
z-index: 20;
&::before {
content: '\e014';
font-family: 'Glyphicons Halflings';
color: white;
}
}
.no-scroll {
overflow: hidden;
}

View File

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

View File

@ -0,0 +1,59 @@
/**
* div.js-show-hide
* div.js-show-hide-toggle
* toggle here
* div
* content here
*/
document.addEventListener('DOMContentLoaded', function() {
var elements = Array.from(document.querySelectorAll('.js-show-hide__toggle')),
toggles = [],
path = new URL(window.location.href).pathname;
function addEventHandler(el) {
el.addEventListener('click', function elClickListener() {
var toggle = toggles[el.dataset.index];
toggle.collapsed = !toggle.collapsed;
toggle.parent.classList.toggle('js-show-hide--collapsed', toggle.collapsed);
updateLocalStorage();
});
}
function updateLocalStorage(id) {
var lsData = getStateFromLocalStorage();
lsData[path] = toggles.map(t => {
return {id: t.index, collapsed: t.collapsed};
});
window.localStorage.setItem('showHidesToggles', JSON.stringify(lsData));
}
function collapsedStateInLocalStorage(id, fallBack) {
var lsData = getStateFromLocalStorage();
if (lsData[path] && lsData[path][id] && lsData[path][id].id === id) {
return lsData[path][id].collapsed;
}
return fallBack;
}
function getStateFromLocalStorage() {
return JSON.parse(window.localStorage.getItem('showHidesToggles')) || {};
}
elements.forEach(function(el, i) {
el.dataset.index = i;
var coll = collapsedStateInLocalStorage(i, el.dataset.collapsed === 'true');
if (coll) {
el.parentElement.classList.add('js-show-hide--collapsed')
}
Array.from(el.parentElement.children).forEach(function(el) {
if (!el.classList.contains('js-show-hide__toggle')) {
el.classList.add('js-show-hide__target');
}
});
toggles.push({index: i, collapsed: coll, parent: el.parentElement});
addEventHandler(el);
});
});

View File

@ -0,0 +1,49 @@
.js-show-hide {
position: relative;
}
.js-show-hide__toggle {
position: relative;
}
.js-show-hide__toggle:hover {
cursor: pointer;
}
.js-show-hide__toggle::before {
content: '';
position: absolute;
width: 0;
height: 0;
border-right: 8px solid transparent;
border-bottom: 8px solid transparent;
}
.js-show-hide__toggle::before,
.js-show-hide--collapsed .js-show-hide__toggle:hover::before {
left: -28px;
top: 10px;
border-left: 8px solid transparent;
border-top: 8px solid var(--lightbase);
}
.js-show-hide__toggle:hover::before,
.js-show-hide--collapsed .js-show-hide__toggle::before {
border-left: 8px solid var(--lightbase);
border-top: 8px solid transparent;
top: 5px;
left: -22px;
}
.js-show-hide__target {
transition: all .2s ease;
overflow: hidden;
}
.js-show-hide--collapsed > .js-show-hide__target {
display: block;
height: 0;
margin: 0;
padding: 0;
max-height: 0;
}

View File

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

View File

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

View File

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

View File

@ -1,6 +1,8 @@
<form method=POST enctype=#{uploadEnctype} action=@{SubmissionListR}>
^{uploadWidget}
<div .container>
<form method=POST enctype=#{uploadEnctype} action=@{SubmissionListR}>
^{uploadWidget}
<form method=POST enctype=#{selectEncoding} target=_blank action=@{SubmissionDownloadMultiArchiveR}>
^{subTable}
<button .btn .btn-default type=submit >Markierte herunterladen
<div .container>
<form method=POST enctype=#{selectEncoding} target=_blank action=@{SubmissionDownloadMultiArchiveR}>
^{subTable}
<button .btn .btn-default type=submit >Markierte herunterladen

5
templates/terms.hamlet Normal file
View File

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

View File

@ -0,0 +1,48 @@
$newline never
<aside .main__aside>
<div .asidenav>
<div .asidenav__box--dont-hide>
<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--dont-hide>
<h3 .asidenav__box-title>
WiSe 17/18
<ul .asidenav__list>
<li .asidenav__list-item>
<a .asidenav__link-wrapper href="/course/S2018/ixd/show">
<div .asidenav__link-triple>IXD
<div .asidenav__link-label>Interaction Design
<ul .asidenav__nested-list>
<li .asidenav__list-item>
<a .asidenav__link-wrapper href="/course/S2018/ixd/ex">Übungsblätter
<li .asidenav__list-item>
<a .asidenav__link-wrapper href="/course/S2018/ixd/show">Klausuren
<li .asidenav__list-item>
<a .asidenav__link-wrapper href="/course/S2018/ixd/show">Übungsgruppen
<li .asidenav__list-item>
<a .asidenav__link-wrapper href="/course/S2018/ffp/show">
<div .asidenav__link-triple>FFP
<div .asidenav__link-label>Fortgeschrittene Funktionale Programmierung
<ul .asidenav__nested-list>
<li .asidenav__list-item>
<a .asidenav__link-wrapper href="/course/S2018/ffp/ex">Abgaben
<li .asidenav__list-item>
<a .asidenav__link-wrapper href="/course/S2018/ffp/show">Klausuren
<li .asidenav__list-item>
<a .asidenav__link-wrapper href="/course/S2018/dbs/show">
<div .asidenav__link-triple>DBS
<div .asidenav__link-label>Datenbanksysteme
<ul .asidenav__nested-list>
<li .asidenav__list-item>
<a .asidenav__link-wrapper href="/course/S2018/dbs/ex">Übungsgruppen
<div .asidenav__toggler>

View File

@ -0,0 +1,85 @@
(function() {
'use strict';
window.utils = window.utils || {};
// Defines a function to turn an element into an interactive aside-navigation.
// If the small is smaller than 999px the navigation is automatically
// collapsed - even when dynamically resized (e.g. switching from portatit
// to landscape).
// The can user may also manually collapse and expand the navigation by
// using the little arrow at the bottom.
window.utils.aside = function(asideEl) {
var collapsed = false;
var collClass = 'main__aside--collapsed';
// animClass used to enable transitions only when needed so that
// (potentially happening) initial collapse of the asidenav
// goes unnoticed by the user.
var animClass = 'main__aside--transitioning';
var aboveCollapsedNav = false;
init();
function init() {
var collLS = window.localStorage.getItem('asidenavCollapsed') === 'true';
if (document.body.getBoundingClientRect().width < 999 || collLS) {
asideEl.classList.add(collClass);
collapsed = true;
}
addListener();
}
function check() {
if (collapsed && !hasCollapsedClass() || !collapsed && hasCollapsedClass()) {
asideEl.classList.add(animClass);
asideEl.classList.toggle(collClass, collapsed);
window.localStorage.setItem('asidenavCollapsed', collapsed);
}
}
function hasCollapsedClass() {
return asideEl.classList.contains(collClass);
}
function addListener() {
asideEl.querySelector('.asidenav__toggler').addEventListener('click', function(event) {
collapsed = !collapsed;
check();
}, false);
asideEl.addEventListener('transitionend', function(event) {
if (event.propertyName === 'opacity') {
asideEl.classList.remove(animClass);
}
}, false);
window.addEventListener('resize', function() {
collapsed = document.body.getBoundingClientRect().width < 999;
check();
}, false);
asideEl.addEventListener('mouseover', function(event) {
if (!hasCollapsedClass()) {
return false;
}
aboveCollapsedNav = true;
window.setTimeout(function() {
if (aboveCollapsedNav && !document.body.classList.contains('touch-supported')) {
asideEl.classList.add('pseudo-hover');
}
}, 800);
}, false);
asideEl.addEventListener('mouseleave', function(event) {
aboveCollapsedNav = false;
asideEl.classList.remove('pseudo-hover');
}, false);
}
};
})();
document.addEventListener('DOMContentLoaded', function() {
utils.aside(document.querySelector('.main__aside'));
});

View File

@ -0,0 +1,204 @@
.main__aside {
position: relative;
background-color: var(--darkbase);
box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
z-index: 1;
flex: 0 0 300px;
}
.main__aside--transitioning {
transition: flex-basis .2s ease;
}
.main__aside--transitioning .asidenav__box{
transition: opacity .2s ease;
}
.main__aside--collapsed.pseudo-hover {
overflow: visible;
}
.main__aside--collapsed {
width: 50px;
flex-basis: 50px;
overflow: hidden;
.asidenav__box-title {
width: 50px;
padding: 0;
}
}
.main__aside--collapsed .asidenav__box {
opacity: 0;
}
.main__aside--collapsed .asidenav__box--dont-hide {
opacity: 1;
}
.main__aside--collapsed:not(.main__aside--transitioning) .asidenav__box {
height: 0;
padding: 0;
margin: 0;
visibility: hidden;
}
.asidenav {
width: 300px;
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 {
margin: 10px 0;
padding: 10px 0;
width: 100%;
border-bottom: 4px solid var(--whitebase);
background-color: var(--darkbase);
}
.asidenav__box-title {
padding: 7px 13px;
a {
color: white;
}
}
.asidenav__nested-list {
position: absolute;
top: 0;
right: 0;
color: var(--fontbase);
transform: translateX(0);
opacity: 0;
transition: all .2s ease;
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;
}
.asidenav__link-wrapper,
.asidenav__link-label {
color: white;
}
.asidenav__link-triple {
transform: scale(1.2, 1);
}
}
}
.asidenav__list-item--active {
background-color: var(--darkbase);
color: white;
.asidenav__link-wrapper {
pointer-events: none;
color: white;
}
}
.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-triple {
background-color: var(--darkbase);
color: var(--whitebase);
height: 50px;
width: 50px;
display: inline-block;
line-height: 50px;
text-align: center;
margin-right: 13px;
flex-shrink: 0;
outline: 1px solid white;
}
}
.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';
}

View File

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

View File

@ -0,0 +1,14 @@
.breadcrumbs__container {
position: relative;
color: white;
z-index: 10;
align-self: flex-end;
margin-bottom: 20px;
transition: margin-bottom .2s ease;
}
.breadcrumbs__container--animated {
transition: left .2s ease;
}
.breadcrumbs__container .breadcrumbs__link {
color: white;
}

View File

@ -0,0 +1,9 @@
$newline never
#{fragment}
$case formLayout
$of FormStandard
$forall view <- views
<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)
<label .form-group__label .reactive-label for=#{fvId view}>#{fvLabel view}
^{fvInput view}

View File

@ -0,0 +1,51 @@
(function() {
'use strict';
window.utils = window.utils || {};
// registers input-listener for each element in <elements> (array) and
// enables <button> if <fn> for these elements returns true
window.utils.reactiveButton = function(elements, button, fn) {
if (elements.length == 0) {
return false;
}
var checkboxes = elements[0].getAttribute('type') === 'checkbox';
var eventType = checkboxes ? 'change' : 'input';
updateButtonState();
elements.forEach(function(el) {
el.addEventListener(eventType, function() {
updateButtonState();
});
});
function updateButtonState() {
if (fn.call(null, elements)) {
button.removeAttribute('disabled');
} else {
button.setAttribute('disabled', 'true');
}
}
};
})();
document.addEventListener('DOMContentLoaded', function() {
// auto reactiveButton submit-buttons with required fields
var forms = document.querySelectorAll('form');
Array.from(forms).forEach(function(form) {
var requireds = form.querySelectorAll('[required]');
var submitBtn = form.querySelector('[type=submit]');
if (submitBtn && requireds) {
window.utils.reactiveButton(Array.from(requireds), submitBtn, function(inputs) {
var done = true;
inputs.forEach(function(inp) {
var len = inp.value.trim().length;
if (done && len === 0) {
done = false;
}
});
return done;
});
}
});
});

View File

View File

@ -0,0 +1,10 @@
<div>
<h2>A little lorem ipsum to make the page scrollable
Aute velit consectetur consequat excepteur ut in qui do reprehenderit sit consequat occaecat incididunt sint eu. Nulla cillum quis et sit labore aliquip et ad excepteur duis elit deserunt aute. Anim esse reprehenderit et ipsum et sit quis minim enim sint et pariatur duis nostrud. Do minim dolore adipisicing ullamco ad aute veniam id magna est proident tempor labore non. Mollit et laboris duis esse commodo ex tempor.
Elit labore qui dolor ea non ut anim occaecat do aliqua cillum qui esse. Aliqua nisi non veniam labore esse irure velit qui veniam labore consectetur ut. Tempor laboris anim officia veniam nostrud cupidatat reprehenderit aute incididunt nostrud dolore occaecat deserunt pariatur in non dolore.
Proident anim pariatur eu laborum Lorem duis cillum est anim magna sit pariatur eu. Nulla consectetur quis ea sunt proident sint laborum. Esse est ea Lorem mollit aute excepteur fugiat ipsum ipsum ad irure do reprehenderit voluptate. Sint tempor nulla mollit voluptate pariatur aliquip culpa amet ullamco fugiat incididunt minim sint ipsum exercitation eiusmod ad. Nostrud ipsum nulla cillum ex aute elit voluptate proident proident magna ut. Ipsum officia cillum officia nostrud enim fugiat ut. Voluptate laboris aute dolore incididunt aliquip sit sit. Ea ut laboris Lorem ea ad non nostrud aute tempor non nisi.
Qui cupidatat nisi id et excepteur sint aliquip fugiat sint reprehenderit aliquip enim anim aliqua sint dolore proident. Voluptate labore tempor laboris nisi eiusmod sunt occaecat deserunt adipisicing. Consectetur fugiat quis non ad laboris aliqua voluptate in eu id. Duis velit cillum aliquip dolor non ea mollit incididunt elit ex excepteur aute consequat amet.
Aute velit consectetur consequat excepteur ut in qui do reprehenderit sit consequat occaecat incididunt sint eu. Nulla cillum quis et sit labore aliquip et ad excepteur duis elit deserunt aute. Anim esse reprehenderit et ipsum et sit quis minim enim sint et pariatur duis nostrud. Do minim dolore adipisicing ullamco ad aute veniam id magna est proident tempor labore non. Mollit et laboris duis esse commodo ex tempor.
Elit labore qui dolor ea non ut anim occaecat do aliqua cillum qui esse. Aliqua nisi non veniam labore esse irure velit qui veniam labore consectetur ut. Tempor laboris anim officia veniam nostrud cupidatat reprehenderit aute incididunt nostrud dolore occaecat deserunt pariatur in non dolore.
Proident anim pariatur eu laborum Lorem duis cillum est anim magna sit pariatur eu. Nulla consectetur quis ea sunt proident sint laborum. Esse est ea Lorem mollit aute excepteur fugiat ipsum ipsum ad irure do reprehenderit voluptate. Sint tempor nulla mollit voluptate pariatur aliquip culpa amet ullamco fugiat incididunt minim sint ipsum exercitation eiusmod ad. Nostrud ipsum nulla cillum ex aute elit voluptate proident proident magna ut. Ipsum officia cillum officia nostrud enim fugiat ut. Voluptate laboris aute dolore incididunt aliquip sit sit. Ea ut laboris Lorem ea ad non nostrud aute tempor non nisi.
Qui cupidatat nisi id et excepteur sint aliquip fugiat sint reprehenderit aliquip enim anim aliqua sint dolore proident. Voluptate labore tempor laboris nisi eiusmod sunt occaecat deserunt adipisicing. Consectetur fugiat quis non ad laboris aliqua voluptate in eu id. Duis velit cillum aliquip dolor non ea mollit incididunt elit ex excepteur aute consequat amet.

View File

@ -0,0 +1,18 @@
<div .modal.js-modal #modal-#{modalId} data-trigger=#{modalTrigger} data-closeable=true>
$if 11 == length modalContent
<div .replace-me>
$else
<h2>Neue Veranstaltung
#{modalContent}
<form>
<div .form-group>
<label .reactive-label for="inp1">Name
<input type="text" id="inp1">
<div .form-group>
<label .reactive-label for="inp2">Kürzel
<input type="text" id="inp2">
<div .form-group>
<label .reactive-label for="inp3">Semester
<input type="text" id="inp3">
<div .form-group>
<input type="submit" value="Submit">

View File

@ -0,0 +1,26 @@
$newline never
<div .navbar-container>
<nav .navbar.js-sticky-navbar>
<!-- breadcrumbs -->
$if not $ Just HomeR == mcurrentRoute
^{breadcrumbs}
<ul .navbar__list.list--inline>
$forall menuType <- menuTypes
$case menuType
$of NavbarRight (MenuItem label mIcon route _)
<li .navbar__list-item :Just route == mcurrentRoute:.navbar__list-item--active>
<a .navbar__link-wrapper href=@{route}>
$if isJust mIcon
<div .glyphicon.glyphicon--#{fromMaybe "" mIcon}>
<div .navbar__link-label>#{label}
$of NavbarSecondary (MenuItem label mIcon route _)
<li .navbar__list-item.navbar__list-item--secondary :Just route == mcurrentRoute:.navbar__list-item--active>
<a .navbar__link-wrapper href=@{route}>
$if isJust mIcon
<div .glyphicon.glyphicon--#{fromMaybe "" mIcon}>
<div .navbar__link-label>#{label}
$of _
<div .navbar__pushdown>

View File

@ -0,0 +1,48 @@
(function() {
'use strict';
window.utils = window.utils || {};
window.utils.stickynav = function(nav) {
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);
}
})();
document.addEventListener('DOMContentLoaded', function() {
utils.stickynav(document.querySelector('.js-sticky-navbar'));
});

View File

@ -0,0 +1,127 @@
.navbar {
position: fixed;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
width: 100%;
height: var(--header-height);
padding-right: 5vw;
padding-left: 340px;
background: var(--darkbase); /* Old browsers */
background: -moz-linear-gradient(bottom, var(--darkbase) 0%, #425d79 100%); /* FF3.6-15 */
background: -webkit-linear-gradient(bottom, var(--darkbase) 0%,#425d79 100%); /* Chrome10-25,Safari5.1-6 */
background: linear-gradient(to top, var(--darkbase) 0%,#425d79 100%); /* W3C, IE10+, FF16+, Chrome26+, Opera12+, Safari7+ */
color: white;
box-shadow: 0 1px 10px rgba(0, 0, 0, 0.1);
z-index: 10;
top: 0;
left: 0;
overflow: hidden;
transition: height 0.2s ease;
}
.navbar__list {
align-self: flex-end;
white-space: nowrap;
}
.navbar__list-item {
position: relative;
transition: background-color .1s ease;
.glyphicon {
position: relative;
width: 100%;
height: 20px;
}
.glyphicon::before {
height: 20px;
}
}
.navbar :last-child {
margin-left: auto;
}
.navbar .navbar__link-wrapper {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 80px;
color: var(--whitebase);
transition: height .2s ease;
}
.navbar__link-label {
transition: opacity .2s ease;
padding: 0 13px;
color: white;
text-transform: uppercase;
}
.navbar__list-item--secondary {
margin-left: 20px;
color: var(--greybase);
}
.navbar__list-item--secondary + .navbar__list-item--secondary {
margin-left: 0;
border-left: 0;
}
.navbar__list-item--active {
background-color: white;
color: var(--darkbase);
.navbar__link-wrapper {
color: var(--darkbase);
}
}
.navbar__list-item--active .navbar__link-wrapper {
pointer-events: none;
}
.navbar__list-item--active .navbar__link-label {
color: var(--darkbase);
}
.navbar .navbar__list-item:not(.navbar__list-item--active):hover {
background-color: var(--darkbase);
color: var(--whitebase);
}
.navbar .navbar__list-item:not(.navbar__list-item--active):hover .navbar__link-wrapper {
color: var(--whitebase);
}
.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--sticky {
height: var(--header-height-collapsed);
z-index: 100;
.navbar__link-wrapper {
height: 50px;
}
.breadcrumbs__container {
margin-bottom: 7px;
}
}
.navbar--animated {
transition: all .2s ease;
}
.navbar__pushdown {
/*display: none;*/
height: var(--header-height);
transition: height .2s ease;
}
.navbar--sticky + .navbar__pushdown {
display: block;
height: var(--header-height-collapsed);
}

View File

@ -0,0 +1,11 @@
$newline never
$if hasPageActions
<div .page-nav-prime>
<h3>Aktionen:
<ul .pagenav__list>
$forall menuType <- menuTypes
$case menuType
$of PageActionPrime (MenuItem label mIcon route _)
<li .pagenav__list-item>
<a .pagenav__link-wrapper href=@{route}>#{label}
$of _

View File

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