Merge branch 'master' of gitlab.cip.ifi.lmu.de:jost/UniWorX into feat/pagination
This commit is contained in:
commit
54cd2bd266
6
.directory
Normal file
6
.directory
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
[Dolphin]
|
||||||
|
Timestamp=2018,3,14,10,57,55
|
||||||
|
Version=4
|
||||||
|
|
||||||
|
[Settings]
|
||||||
|
HiddenFilesShown=true
|
||||||
4
.kateproject
Normal file
4
.kateproject
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"name": "ReWorX"
|
||||||
|
, "files": [ { "git": 1, "filters": ["*.hs", "*.hamlet", "*.lucius", "*.cassius", "*.julius"], "recursive": 1 } ]
|
||||||
|
}
|
||||||
@ -1,10 +1,11 @@
|
|||||||
* Datensparsamkeit: nur Speichern was notwendig ist; Dokumentieren, warum was gespeichert wird!
|
* Datensparsamkeit: nur Speichern was notwendig ist; Dokumentieren, warum was gespeichert wird!
|
||||||
* Verfügbarkeit: Backup / aktuelles System; nicht nur eine Person, Anfragen werden organisiert beantwortet
|
* 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
|
* 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)
|
* Nichtverkettbarkeit: (eher irrelevant für unseren Anwendungsfall)
|
||||||
* Transparenz: User weiß was über ihn gespeichert wird; Dokumentation; Vorfälle schnell melden?
|
* 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!?!
|
* Wer ist Datenschutzverantwortlicher? Steffen!?!
|
||||||
=> Sofort anzeigen, wenn etwas schiefläuft.
|
=> 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;
|
- 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.
|
-> 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
38
FragenSJ.txt
Normal 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!!!
|
||||||
94
README.md
94
README.md
@ -1,21 +1,24 @@
|
|||||||
# Quick Start Guide
|
# 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
|
## LDAP
|
||||||
install:<br>
|
install:
|
||||||
`sudo apt-get install slapd ldap-utils`
|
`sudo apt-get install slapd ldap-utils`
|
||||||
|
|
||||||
|
|
||||||
## PostgreSQL
|
## PostgreSQL
|
||||||
install:<br>
|
install:
|
||||||
`sudo apt-get install postgresql`
|
`sudo apt-get install postgresql`
|
||||||
|
|
||||||
switch to user *postgres* (got created during installation):<br>
|
switch to user *postgres* (got created during installation):
|
||||||
`sudo -i -u postgres`
|
`sudo -i -u postgres`
|
||||||
|
|
||||||
add db user *uniworx*:<br>
|
add db user *uniworx*:
|
||||||
`createuser --interactive`
|
`createuser --interactive`
|
||||||
|
|
||||||
you'll get a prompt:
|
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?]
|
Shall the new role be a superuser? (y/n)` - [not exactly sure. Guess not?]
|
||||||
```
|
```
|
||||||
|
|
||||||
create database *uniworx*:<br>
|
create database *uniworx*:
|
||||||
`createdb uniworx`
|
`createdb uniworx`
|
||||||
|
|
||||||
to access the database as user *uniworx* you now need to add a new linux-user called *uniworx*:<br>
|
to access the database as user *uniworx* you now need to add a new linux-user called *uniworx*:
|
||||||
`sudo adduser uniworx`
|
`sudo adduser uniworx`
|
||||||
|
|
||||||
log-in as new user *uniworx*:<br>
|
log-in as new user *uniworx*:
|
||||||
`sudo -i -u uniworx`
|
`sudo -i -u uniworx`
|
||||||
|
|
||||||
you can now use `psql uniworx` to execute SQL-commands and such.
|
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
|
## stack
|
||||||
Install with:<br>
|
Install with:
|
||||||
`curl -sSL https://get.haskellstack.org/ | sh`
|
`curl -sSL https://get.haskellstack.org/ | sh`
|
||||||
|
|
||||||
setup stack and install dependencies:<br>
|
setup stack and install dependencies:
|
||||||
`stack setup`
|
`stack setup`
|
||||||
|
|
||||||
There might be packages missing during setup. You most probably simply need to install them and try again.<br>
|
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
|
||||||
Instructions are easy to find via search engine of your choice and the specific error you got.<br>
|
`sudo apt-get install libsasl2-dev libldap2-dev`
|
||||||
Example from experience: For LDAP `ldab` and `lber` header files were missing.
|
|
||||||
|
|
||||||
Build the app:<br>
|
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.*
|
||||||
`stack build`
|
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>
|
Build the app:
|
||||||
`env DUMMY_LOGIN=true stack exec -- yesod devel`
|
`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.
|
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
|
# Sources and more infos
|
||||||
PostgreSQl: <br>
|
PostgreSQl:
|
||||||
https://www.digitalocean.com/community/tutorials/how-to-install-and-use-postgresql-on-ubuntu-16-04
|
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
|
--Zeige Tabellen
|
||||||
\dt
|
\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:
|
--Lösche Tabelle "course" und alle davon abhängigen:
|
||||||
DROP TABLE "course" CASCADE;
|
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
|
||||||
|
|||||||
123
fill-db.hs
123
fill-db.hs
@ -14,6 +14,7 @@ main :: IO ()
|
|||||||
main = db $ do
|
main = db $ do
|
||||||
now <- liftIO getCurrentTime
|
now <- liftIO getCurrentTime
|
||||||
let
|
let
|
||||||
|
summer2017 = TermIdentifier 2017 Summer
|
||||||
winter2017 = TermIdentifier 2017 Winter
|
winter2017 = TermIdentifier 2017 Winter
|
||||||
summer2018 = TermIdentifier 2018 Summer
|
summer2018 = TermIdentifier 2018 Summer
|
||||||
gkleen <- insert User
|
gkleen <- insert User
|
||||||
@ -23,6 +24,29 @@ main = db $ do
|
|||||||
, userEmail = "G.Kleen@campus.lmu.de"
|
, userEmail = "G.Kleen@campus.lmu.de"
|
||||||
, userDisplayName = "Gregor Kleen"
|
, 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
|
void . insert $ Term
|
||||||
{ termName = winter2017
|
{ termName = winter2017
|
||||||
, termStart = fromGregorian 2017 10 16
|
, termStart = fromGregorian 2017 10 16
|
||||||
@ -45,9 +69,16 @@ main = db $ do
|
|||||||
mi <- insert $ School "Institut für Mathematik" "MI"
|
mi <- insert $ School "Institut für Mathematik" "MI"
|
||||||
void . insert $ UserAdmin gkleen ifi
|
void . insert $ UserAdmin gkleen ifi
|
||||||
void . insert $ UserAdmin gkleen mi
|
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 gkleen ifi
|
||||||
|
void . insert $ UserLecturer fhamann ifi
|
||||||
|
void . insert $ UserLecturer jost ifi
|
||||||
ifiBsc <- insert $ Degree "Bachelor Informatik" ifi
|
ifiBsc <- insert $ Degree "Bachelor Informatik" ifi
|
||||||
ifiMsc <- insert $ Degree "Master Informatik" ifi
|
ifiMsc <- insert $ Degree "Master Informatik" ifi
|
||||||
|
miBsc <- insert $ Degree "Bachelor Mathematik" mi
|
||||||
|
-- FFP
|
||||||
ffp <- insert Course
|
ffp <- insert Course
|
||||||
{ courseName = "Fortgeschrittene Funktionale Programmierung"
|
{ courseName = "Fortgeschrittene Funktionale Programmierung"
|
||||||
, courseDescription = Nothing
|
, courseDescription = Nothing
|
||||||
@ -56,16 +87,96 @@ main = db $ do
|
|||||||
, courseTermId = TermKey summer2018
|
, courseTermId = TermKey summer2018
|
||||||
, courseSchoolId = ifi
|
, courseSchoolId = ifi
|
||||||
, courseCapacity = Just 20
|
, courseCapacity = Just 20
|
||||||
, courseCreated = now
|
|
||||||
, courseChanged = now
|
|
||||||
, courseCreatedBy = gkleen
|
|
||||||
, courseChangedBy = gkleen
|
|
||||||
, courseHasRegistration = True
|
, courseHasRegistration = True
|
||||||
, courseRegisterFrom = Just now
|
, courseRegisterFrom = Just now
|
||||||
, courseRegisterTo = Just ((3600 * 24 * 60) `addUTCTime` now )
|
, courseRegisterTo = Just ((3600 * 24 * 60) `addUTCTime` now )
|
||||||
}
|
}
|
||||||
|
insert_ $ CourseEdit jost now ffp
|
||||||
void . insert $ DegreeCourse ifiBsc ffp
|
void . insert $ DegreeCourse ifiBsc ffp
|
||||||
void . insert $ DegreeCourse ifiMsc ffp
|
void . insert $ DegreeCourse ifiMsc ffp
|
||||||
void . insert $ Lecturer gkleen ffp
|
void . insert $ Lecturer gkleen ffp
|
||||||
void . insert $ Corrector gkleen ffp (ByProportion 1)
|
insert_ $ Corrector gkleen ffp (ByProportion 1)
|
||||||
void . insert $ Sheet ffp "Blatt 1" Nothing NotGraded Nothing now now Nothing Nothing now now gkleen gkleen
|
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
|
||||||
|
|||||||
@ -1,4 +1,24 @@
|
|||||||
SummerTerm year@Integer: Sommersemester #{tshow year}
|
SummerTerm year@Integer: Sommersemester #{tshow year}
|
||||||
WinterTerm year@Integer: Wintersemester #{tshow year}/#{tshow $ succ year}
|
WinterTerm year@Integer: Wintersemester #{tshow year}/#{tshow $ succ year}
|
||||||
PSLimitNonPositive: “pagesize” muss größer als null sein
|
PSLimitNonPositive: “pagesize” muss größer als null sein
|
||||||
Page n@Int64 num@Int64: Seite #{tshow n} von #{tshow num}
|
Page n@Int64 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
61
models
@ -11,7 +11,7 @@ UserAdmin
|
|||||||
UserLecturer
|
UserLecturer
|
||||||
user UserId
|
user UserId
|
||||||
school SchoolId
|
school SchoolId
|
||||||
UniqueSchoolLecturer school user
|
UniqueSchoolLecturer user school
|
||||||
StudyFeatures
|
StudyFeatures
|
||||||
user UserId
|
user UserId
|
||||||
degree StudyDegreeId
|
degree StudyDegreeId
|
||||||
@ -30,8 +30,8 @@ StudyTerms
|
|||||||
name Text Maybe
|
name Text Maybe
|
||||||
Primary key
|
Primary key
|
||||||
Term json
|
Term json
|
||||||
name TermIdentifier
|
name TermIdentifier -- unTermKey :: TermId -> TermIdentifier
|
||||||
start Day
|
start Day -- TermKey :: TermIdentifier -< TermId
|
||||||
end Day
|
end Day
|
||||||
holidays [Day]
|
holidays [Day]
|
||||||
lectureStart Day
|
lectureStart Day
|
||||||
@ -61,14 +61,14 @@ Course
|
|||||||
termId TermId
|
termId TermId
|
||||||
schoolId SchoolId
|
schoolId SchoolId
|
||||||
capacity Int Maybe
|
capacity Int Maybe
|
||||||
created UTCTime
|
|
||||||
changed UTCTime
|
|
||||||
createdBy UserId
|
|
||||||
changedBy UserId
|
|
||||||
hasRegistration Bool -- canRegisterNow = hasRegistration && maybe False (<= currentTime) registerFrom && maybe True (>= currentTime) registerTo
|
hasRegistration Bool -- canRegisterNow = hasRegistration && maybe False (<= currentTime) registerFrom && maybe True (>= currentTime) registerTo
|
||||||
registerFrom UTCTime Maybe
|
registerFrom UTCTime Maybe
|
||||||
registerTo UTCTime Maybe
|
registerTo UTCTime Maybe
|
||||||
CourseTermShort termId shorthand
|
CourseTermShort termId shorthand
|
||||||
|
CourseEdit
|
||||||
|
user UserId
|
||||||
|
time UTCTime
|
||||||
|
course CourseId
|
||||||
Lecturer
|
Lecturer
|
||||||
userId UserId
|
userId UserId
|
||||||
courseId CourseId
|
courseId CourseId
|
||||||
@ -90,22 +90,24 @@ CourseParticipant
|
|||||||
courseId CourseId
|
courseId CourseId
|
||||||
userId UserId
|
userId UserId
|
||||||
registration UTCTime
|
registration UTCTime
|
||||||
UniqueCourseParticipant courseId userId
|
UniqueParticipant userId courseId
|
||||||
Sheet
|
Sheet
|
||||||
courseId CourseId
|
courseId CourseId
|
||||||
name Text
|
name Text
|
||||||
description Html Maybe
|
description Html Maybe
|
||||||
type SheetType
|
type SheetType
|
||||||
|
grouping SheetGroup
|
||||||
markingText Html Maybe
|
markingText Html Maybe
|
||||||
|
visibleFrom UTCTime Maybe
|
||||||
activeFrom UTCTime
|
activeFrom UTCTime
|
||||||
activeTo UTCTime
|
activeTo UTCTime
|
||||||
hintFrom UTCTime Maybe
|
hintFrom UTCTime Maybe
|
||||||
solutionFrom UTCTime Maybe
|
solutionFrom UTCTime Maybe
|
||||||
created UTCTime
|
|
||||||
changed UTCTime
|
|
||||||
createdBy UserId
|
|
||||||
changedBy UserId
|
|
||||||
CourseSheet courseId name
|
CourseSheet courseId name
|
||||||
|
SheetEdit
|
||||||
|
user UserId
|
||||||
|
time UTCTime
|
||||||
|
sheet SheetId
|
||||||
SheetFile
|
SheetFile
|
||||||
sheetId SheetId
|
sheetId SheetId
|
||||||
fileId FileId
|
fileId FileId
|
||||||
@ -122,11 +124,11 @@ Submission
|
|||||||
ratingComment Text Maybe
|
ratingComment Text Maybe
|
||||||
ratingBy UserId Maybe
|
ratingBy UserId Maybe
|
||||||
ratingTime UTCTime Maybe
|
ratingTime UTCTime Maybe
|
||||||
created UTCTime
|
|
||||||
changed UTCTime
|
|
||||||
createdBy UserId
|
|
||||||
changedBy UserId
|
|
||||||
deriving Show
|
deriving Show
|
||||||
|
SubmissionEdit
|
||||||
|
user UserId
|
||||||
|
time UTCTime
|
||||||
|
submission SubmissionId
|
||||||
SubmissionFile
|
SubmissionFile
|
||||||
submissionId SubmissionId
|
submissionId SubmissionId
|
||||||
fileId FileId
|
fileId FileId
|
||||||
@ -141,10 +143,10 @@ SubmissionUser
|
|||||||
SubmissionGroup
|
SubmissionGroup
|
||||||
courseId CourseId
|
courseId CourseId
|
||||||
name Text
|
name Text
|
||||||
created UTCTime
|
SubmissionGroupEdit
|
||||||
changed UTCTime
|
user UserId
|
||||||
createdBy UserId
|
time UTCTime
|
||||||
changedBy UserId
|
submissionGroup SubmissionGroupId
|
||||||
SubmissionGroupUser
|
SubmissionGroupUser
|
||||||
submissionGroupId SubmissionGroupId
|
submissionGroupId SubmissionGroupId
|
||||||
userId UserId
|
userId UserId
|
||||||
@ -163,13 +165,12 @@ Booking
|
|||||||
end UTCTime
|
end UTCTime
|
||||||
weekly Bool
|
weekly Bool
|
||||||
exceptions [Day] -- only if weekly, begin in exception
|
exceptions [Day] -- only if weekly, begin in exception
|
||||||
created UTCTime
|
|
||||||
changed UTCTime
|
|
||||||
createdBy UserId
|
|
||||||
changedBy UserId
|
|
||||||
|
|
||||||
bookedFor RoomForId
|
bookedFor RoomForId
|
||||||
room RoomId
|
room RoomId
|
||||||
|
BookingEdit
|
||||||
|
user UserId
|
||||||
|
time UTCTime
|
||||||
|
boooking BookingId
|
||||||
Room
|
Room
|
||||||
name Text
|
name Text
|
||||||
capacity Int Maybe
|
capacity Int Maybe
|
||||||
@ -195,10 +196,10 @@ Exam
|
|||||||
deregistrationEnd UTCTime
|
deregistrationEnd UTCTime
|
||||||
ratingVisible Bool
|
ratingVisible Bool
|
||||||
statisticsVisible Bool
|
statisticsVisible Bool
|
||||||
created UTCTime
|
ExamEdit
|
||||||
changed UTCTime
|
user UserId
|
||||||
createdBy UserId
|
time UTCTime
|
||||||
changedBy UserId
|
exam ExamId
|
||||||
ExamUser
|
ExamUser
|
||||||
userId UserId
|
userId UserId
|
||||||
examId ExamId
|
examId ExamId
|
||||||
|
|||||||
34
routes
34
routes
@ -6,25 +6,29 @@
|
|||||||
|
|
||||||
/ HomeR GET POST
|
/ HomeR GET POST
|
||||||
/profile ProfileR GET
|
/profile ProfileR GET
|
||||||
/users UsersR GET
|
/users UsersR GET !adminAny
|
||||||
|
|
||||||
/term TermShowR GET
|
/term TermShowR GET
|
||||||
/term/edit TermEditR GET POST
|
/term/edit TermEditR GET POST !adminAny
|
||||||
/term/#TermId/edit TermEditExistR GET
|
/term/#TermId/edit TermEditExistR GET !adminAny
|
||||||
|
|
||||||
/course/ CourseListR GET
|
/course/ CourseListR GET
|
||||||
!/course/new CourseEditR GET POST
|
!/course/new CourseNewR GET POST !lecturerAny
|
||||||
!/course/#TermId CourseListTermR GET
|
!/course/#TermId CourseListTermR GET
|
||||||
/course/#TermId/#Text/edit CourseEditExistR GET
|
/course/#TermId/#Text CourseR:
|
||||||
/course/#TermId/#Text/show CourseShowR GET POST
|
/show CourseShowR GET POST
|
||||||
|
/edit CourseEditR GET POST !lecturer
|
||||||
|
|
||||||
/course/#TermId/#Text/sheet/ SheetListR GET
|
/ex SheetR !registered:
|
||||||
/course/#TermId/#Text/sheet/#Text/show SheetShowR GET
|
/ SheetListR GET
|
||||||
/course/#TermId/#Text/sheet/#Text/#SheetFileType/#FilePath SheetFileR GET
|
/#Text/show SheetShowR GET !time
|
||||||
/course/#TermId/#Text/sheet/new SheetNewR GET POST
|
/#Text/#SheetFileType/#FilePath SheetFileR GET !time
|
||||||
/course/#TermId/#Text/sheet/#SheetId/edit SheetEditR GET POST
|
/new SheetNewR GET POST !lecturer
|
||||||
/course/#TermId/#Text/sheet/#SheetId/delete SheetDelR GET POST
|
/#Text/edit SheetEditR GET POST !lecturer
|
||||||
|
/#Text/delete SheetDelR GET POST !lecturer
|
||||||
|
|
||||||
|
|
||||||
|
-- TODO below
|
||||||
/submission SubmissionListR GET POST
|
/submission SubmissionListR GET POST
|
||||||
/submission/#CryptoUUIDSubmission SubmissionR GET POST
|
/submission/#CryptoUUIDSubmission SubmissionR GET POST
|
||||||
/submissions.zip SubmissionDownloadMultiArchiveR POST
|
/submissions.zip SubmissionDownloadMultiArchiveR POST
|
||||||
@ -34,4 +38,4 @@
|
|||||||
!/#UUID CryptoUUIDDispatchR GET
|
!/#UUID CryptoUUIDDispatchR GET
|
||||||
|
|
||||||
-- For demonstration
|
-- For demonstration
|
||||||
/course/#CryptoUUIDCourse/edit CourseEditExistIDR GET
|
/course/#CryptoUUIDCourse/edit CourseEditIDR GET
|
||||||
|
|||||||
@ -34,5 +34,6 @@ instance PathPiece UUID where
|
|||||||
decCryptoIDs [ ''SubmissionId
|
decCryptoIDs [ ''SubmissionId
|
||||||
, ''CourseId
|
, ''CourseId
|
||||||
, ''SheetId
|
, ''SheetId
|
||||||
|
, ''FileId
|
||||||
]
|
]
|
||||||
{- TODO: Do we need/want CryptoUUIDs for Sheet numbers? -}
|
{- TODO: Do we need/want CryptoUUIDs for Sheet numbers? -}
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
{-# LANGUAGE MultiParamTypeClasses #-}
|
{-# LANGUAGE MultiParamTypeClasses #-}
|
||||||
{-# LANGUAGE TypeFamilies #-}
|
{-# LANGUAGE TypeFamilies #-}
|
||||||
{-# LANGUAGE ViewPatterns #-}
|
{-# LANGUAGE ViewPatterns #-}
|
||||||
|
{-# LANGUAGE PatternSynonyms #-}
|
||||||
{-# LANGUAGE DataKinds #-}
|
{-# LANGUAGE DataKinds #-}
|
||||||
{-# LANGUAGE RecordWildCards #-}
|
{-# LANGUAGE RecordWildCards #-}
|
||||||
{-# LANGUAGE NamedFieldPuns #-}
|
{-# LANGUAGE NamedFieldPuns #-}
|
||||||
@ -51,6 +52,11 @@ import Handler.Utils.StudyFeatures
|
|||||||
|
|
||||||
import System.FilePath
|
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
|
-- | The foundation datatype for your application. This can be a good place to
|
||||||
-- keep settings and values requiring initialization before your application
|
-- keep settings and values requiring initialization before your application
|
||||||
-- starts running, such as database connections. Every handler will have
|
-- starts running, such as database connections. Every handler will have
|
||||||
@ -78,18 +84,27 @@ data UniWorX = UniWorX
|
|||||||
-- type Widget = WidgetT UniWorX IO ()
|
-- type Widget = WidgetT UniWorX IO ()
|
||||||
mkYesodData "UniWorX" $(parseRoutesFile "routes")
|
mkYesodData "UniWorX" $(parseRoutesFile "routes")
|
||||||
|
|
||||||
|
-- Pattern Synonyms for convenience
|
||||||
|
pattern CSheetR tid csh ptn = CourseR tid csh (SheetR ptn)
|
||||||
|
|
||||||
|
|
||||||
data MenuItem = MenuItem
|
data MenuItem = MenuItem
|
||||||
{ menuItemLabel :: Text
|
{ menuItemLabel :: Text
|
||||||
|
, menuItemIcon :: Maybe Text
|
||||||
, menuItemRoute :: Route UniWorX
|
, menuItemRoute :: Route UniWorX
|
||||||
, menuItemAccessCallback :: Handler Bool
|
, menuItemAccessCallback :: Handler Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
data MenuTypes
|
data MenuTypes -- Semantische Rolle:
|
||||||
= NavbarLeft { menuItem :: MenuItem }
|
= NavbarAside { menuItem :: MenuItem } -- TODO
|
||||||
| NavbarRight { menuItem :: MenuItem }
|
| NavbarExtra { menuItem :: MenuItem } -- TODO
|
||||||
| NavbarExtra { menuItem :: MenuItem }
|
| 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)
|
type Form x = Html -> MForm (HandlerT UniWorX IO) (FormResult x, Widget)
|
||||||
|
|
||||||
mkMessage "UniWorX" "messages" "de"
|
mkMessage "UniWorX" "messages" "de"
|
||||||
@ -145,7 +160,7 @@ instance Yesod UniWorX where
|
|||||||
isAuthorized TermShowR _ = return Authorized
|
isAuthorized TermShowR _ = return Authorized
|
||||||
isAuthorized CourseListR _ = return Authorized
|
isAuthorized CourseListR _ = return Authorized
|
||||||
isAuthorized (CourseListTermR _) _ = return Authorized
|
isAuthorized (CourseListTermR _) _ = return Authorized
|
||||||
isAuthorized (CourseShowR _ _) _ = return Authorized
|
isAuthorized (CourseR _ _ CourseShowR) _ = return Authorized
|
||||||
isAuthorized (CryptoUUIDDispatchR _) _ = return Authorized
|
isAuthorized (CryptoUUIDDispatchR _) _ = return Authorized
|
||||||
isAuthorized SubmissionListR _ = isAuthenticated
|
isAuthorized SubmissionListR _ = isAuthenticated
|
||||||
isAuthorized SubmissionDownloadMultiArchiveR _ = isAuthenticated
|
isAuthorized SubmissionDownloadMultiArchiveR _ = isAuthenticated
|
||||||
@ -192,16 +207,28 @@ instance Yesod UniWorX where
|
|||||||
makeLogger = return . appLogger
|
makeLogger = return . appLogger
|
||||||
|
|
||||||
isAuthorizedDB :: Route UniWorX -> Bool -> YesodDB UniWorX AuthResult
|
isAuthorizedDB :: Route UniWorX -> Bool -> YesodDB UniWorX AuthResult
|
||||||
|
isAuthorizedDB route@(routeAttrs -> attrs) writeable
|
||||||
|
| "adminAny" `member` attrs = adminAccess Nothing
|
||||||
|
| "lecturerAny" `member` attrs = lecturerAccess Nothing
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
isAuthorizedDB UsersR _ = adminAccess Nothing
|
isAuthorizedDB UsersR _ = adminAccess Nothing
|
||||||
isAuthorizedDB (SubmissionR cID) _ = submissionAccess $ Right cID
|
isAuthorizedDB (SubmissionR cID) _ = submissionAccess $ Right cID
|
||||||
isAuthorizedDB (SubmissionDownloadSingleR cID _) _ = submissionAccess $ Right cID
|
isAuthorizedDB (SubmissionDownloadSingleR cID _) _ = submissionAccess $ Right cID
|
||||||
isAuthorizedDB (SubmissionDownloadArchiveR (splitExtension -> (baseName, _))) _ = submissionAccess . Left . CryptoID $ CI.mk baseName
|
isAuthorizedDB (SubmissionDownloadArchiveR (splitExtension -> (baseName, _))) _ = submissionAccess . Left . CryptoID $ CI.mk baseName
|
||||||
isAuthorizedDB TermEditR _ = adminAccess Nothing
|
isAuthorizedDB TermEditR _ = adminAccess Nothing
|
||||||
isAuthorizedDB (TermEditExistR _) _ = adminAccess Nothing
|
isAuthorizedDB (TermEditExistR _) _ = adminAccess Nothing
|
||||||
isAuthorizedDB CourseEditR _ = lecturerAccess Nothing
|
isAuthorizedDB CourseNewR _ = lecturerAccess Nothing
|
||||||
isAuthorizedDB (CourseEditExistR t c) _ = courseLecturerAccess . entityKey =<< getBy404 (CourseTermShort t c)
|
isAuthorizedDB (CourseR t c CourseEditR) _ = courseLecturerAccess . entityKey =<< getBy404 (CourseTermShort t c)
|
||||||
isAuthorizedDB (SheetNewR t c) _ = courseLecturerAccess . entityKey =<< getBy404 (CourseTermShort t c)
|
isAuthorizedDB (CourseR t c (SheetR SheetListR)) False = return Authorized --
|
||||||
isAuthorizedDB (CourseEditExistIDR cID) _ = do
|
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
|
courseId <- decrypt cID
|
||||||
courseLecturerAccess courseId
|
courseLecturerAccess courseId
|
||||||
isAuthorizedDB _route _isWrite = return $ Unauthorized "No access to this route." -- Calling isAuthorized here creates infinite loop!
|
isAuthorizedDB _route _isWrite = return $ Unauthorized "No access to this route." -- Calling isAuthorized here creates infinite loop!
|
||||||
@ -219,29 +246,45 @@ submissionAccess cID = do
|
|||||||
|
|
||||||
adminAccess :: Maybe SchoolId -- ^ If @Just@, matched exactly against 'userAdminSchool'
|
adminAccess :: Maybe SchoolId -- ^ If @Just@, matched exactly against 'userAdminSchool'
|
||||||
-> YesodDB UniWorX AuthResult
|
-> YesodDB UniWorX AuthResult
|
||||||
adminAccess school = do
|
adminAccess school = do
|
||||||
authId <- lift requireAuthId
|
authId <- lift requireAuthId
|
||||||
adrights <- selectList ((UserAdminUser ==. authId) : maybe [] (\s -> [UserAdminSchool ==. s]) school) []
|
adrights <- selectList ((UserAdminUser ==. authId) : maybe [] (\s -> [UserAdminSchool ==. s]) school) []
|
||||||
return $ if (not $ null adrights)
|
return $ if (not $ null adrights)
|
||||||
then Authorized
|
then Authorized
|
||||||
else Unauthorized "No admin access"
|
else Unauthorized "No admin access" -- TODO internationalize
|
||||||
|
|
||||||
lecturerAccess :: Maybe SchoolId
|
lecturerAccess :: Maybe SchoolId
|
||||||
-> YesodDB UniWorX AuthResult
|
-> YesodDB UniWorX AuthResult
|
||||||
lecturerAccess school = do
|
lecturerAccess school = do
|
||||||
authId <- lift requireAuthId
|
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)
|
return $ if (not $ null lecrights)
|
||||||
then Authorized
|
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 -> 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
|
authId <- lift requireAuthId
|
||||||
users <- map (lecturerUserId . entityVal ) <$> selectList [ LecturerCourseId ==. courseId ] []
|
access <- getBy $ authType authId courseId
|
||||||
return $ case authId `elem` users of
|
case access of
|
||||||
True -> Authorized
|
(Just _) -> return Authorized
|
||||||
False -> Unauthorized "No lecturer access for this course"
|
Nothing -> unauthorizedI msg
|
||||||
|
|
||||||
isAuthorizedDB' :: Route UniWorX -> Bool -> YesodDB UniWorX Bool
|
isAuthorizedDB' :: Route UniWorX -> Bool -> YesodDB UniWorX Bool
|
||||||
isAuthorizedDB' route isWrite = (== Authorized) <$> isAuthorizedDB route isWrite
|
isAuthorizedDB' route isWrite = (== Authorized) <$> isAuthorizedDB route isWrite
|
||||||
@ -250,61 +293,82 @@ isAuthorized' :: Route UniWorX -> Bool -> Handler Bool
|
|||||||
isAuthorized' route isWrite = runDB $ isAuthorizedDB' route isWrite
|
isAuthorized' route isWrite = runDB $ isAuthorizedDB' route isWrite
|
||||||
|
|
||||||
-- Define breadcrumbs.
|
-- Define breadcrumbs.
|
||||||
instance YesodBreadcrumbs UniWorX where
|
instance YesodBreadcrumbs UniWorX where
|
||||||
breadcrumb TermShowR = return ("Semester", Just HomeR)
|
breadcrumb TermShowR = return ("Semester", Just HomeR)
|
||||||
breadcrumb TermEditR = return ("Neu", Just TermShowR)
|
breadcrumb TermEditR = return ("Neu", Just TermShowR)
|
||||||
breadcrumb (TermEditExistR _) = return ("Editieren", 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 CourseListR = return ("Kurs", Just HomeR)
|
||||||
breadcrumb (SheetShowR tid csh _shn) = return ("Übungen", Just $ SheetListR tid csh)
|
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 SubmissionListR = return ("Abgaben", Just HomeR)
|
||||||
breadcrumb (SubmissionR _) = return ("Abgabe", Just SubmissionListR)
|
breadcrumb (SubmissionR _) = return ("Abgabe", Just SubmissionListR)
|
||||||
|
|
||||||
breadcrumb HomeR = return ("ReWorX", Nothing)
|
breadcrumb HomeR = return ("ReWorX", Nothing)
|
||||||
breadcrumb (AuthR _) = return ("Login", Just HomeR)
|
breadcrumb (AuthR _) = return ("Login", Just HomeR)
|
||||||
breadcrumb ProfileR = return ("Profile", Just HomeR)
|
breadcrumb ProfileR = return ("Profile", Just HomeR)
|
||||||
breadcrumb _ = return ("home", Nothing)
|
breadcrumb _ = return ("home", Nothing)
|
||||||
|
|
||||||
|
|
||||||
defaultLinks :: [MenuTypes]
|
defaultLinks :: [MenuTypes]
|
||||||
defaultLinks = -- Define the menu items of the header.
|
defaultLinks = -- Define the menu items of the header.
|
||||||
[ NavbarLeft $ MenuItem
|
[ NavbarRight $ MenuItem
|
||||||
{ menuItemLabel = "Home"
|
{ menuItemLabel = "Home"
|
||||||
|
, menuItemIcon = Just "home"
|
||||||
, menuItemRoute = HomeR
|
, menuItemRoute = HomeR
|
||||||
, menuItemAccessCallback = return True
|
, 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
|
, NavbarRight $ MenuItem
|
||||||
{ menuItemLabel = "Profile"
|
{ menuItemLabel = "Profile"
|
||||||
|
, menuItemIcon = Just "profile"
|
||||||
, menuItemRoute = ProfileR
|
, menuItemRoute = ProfileR
|
||||||
, menuItemAccessCallback = isJust <$> maybeAuthPair
|
, menuItemAccessCallback = isJust <$> maybeAuthPair
|
||||||
}
|
}
|
||||||
, NavbarRight $ MenuItem
|
, NavbarSecondary $ MenuItem
|
||||||
{ menuItemLabel = "Login"
|
{ menuItemLabel = "Login"
|
||||||
|
, menuItemIcon = Just "login"
|
||||||
, menuItemRoute = AuthR LoginR
|
, menuItemRoute = AuthR LoginR
|
||||||
, menuItemAccessCallback = isNothing <$> maybeAuthPair
|
, menuItemAccessCallback = isNothing <$> maybeAuthPair
|
||||||
}
|
}
|
||||||
, NavbarRight $ MenuItem
|
, NavbarSecondary $ MenuItem
|
||||||
{ menuItemLabel = "Logout"
|
{ menuItemLabel = "Logout"
|
||||||
|
, menuItemIcon = Just "logout"
|
||||||
, menuItemRoute = AuthR LogoutR
|
, menuItemRoute = AuthR LogoutR
|
||||||
, menuItemAccessCallback = isJust <$> maybeAuthPair
|
, menuItemAccessCallback = isJust <$> maybeAuthPair
|
||||||
}
|
}
|
||||||
|
, NavbarAside $ MenuItem
|
||||||
|
{ menuItemLabel = "Aktuelle Veranstaltungen"
|
||||||
|
, menuItemIcon = Just "book"
|
||||||
|
, menuItemRoute = CourseListR -- should be CourseListActiveR or similar in the future
|
||||||
|
, menuItemAccessCallback = return True
|
||||||
|
}
|
||||||
|
, NavbarAside $ MenuItem
|
||||||
|
{ menuItemLabel = "Alte Veranstaltungen"
|
||||||
|
, menuItemIcon = Just "book"
|
||||||
|
, menuItemRoute = CourseListR -- should be CourseListInactiveR or similar in the future
|
||||||
|
, menuItemAccessCallback = return True
|
||||||
|
}
|
||||||
|
, NavbarAside $ MenuItem
|
||||||
|
{ 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
|
defaultLinkLayout :: [MenuTypes] -> Widget -> Handler Html
|
||||||
@ -328,12 +392,34 @@ defaultMenuLayout menu widget = do
|
|||||||
-- value passed to hamletToRepHtml cannot be a widget, this allows
|
-- value passed to hamletToRepHtml cannot be a widget, this allows
|
||||||
-- you to use normal widget features in default-layout.
|
-- you to use normal widget features in default-layout.
|
||||||
|
|
||||||
|
let
|
||||||
|
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
|
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 "default-layout")
|
||||||
|
$(widgetFile "standalone/modal")
|
||||||
|
$(widgetFile "standalone/showHide")
|
||||||
|
$(widgetFile "standalone/sortable")
|
||||||
|
$(widgetFile "standalone/inputs")
|
||||||
withUrlRenderer $(hamletFile "templates/default-layout-wrapper.hamlet")
|
withUrlRenderer $(hamletFile "templates/default-layout-wrapper.hamlet")
|
||||||
|
|
||||||
|
|
||||||
-- How to run database actions.
|
-- How to run database actions.
|
||||||
instance YesodPersist UniWorX where
|
instance YesodPersist UniWorX where
|
||||||
type YesodPersistBackend UniWorX = SqlBackend
|
type YesodPersistBackend UniWorX = SqlBackend
|
||||||
@ -363,7 +449,7 @@ instance YesodAuth UniWorX where
|
|||||||
|
|
||||||
$logDebugS "auth" $ tshow ((userPlugin, userIdent), creds)
|
$logDebugS "auth" $ tshow ((userPlugin, userIdent), creds)
|
||||||
|
|
||||||
when isDummy . (throwError =<<) . lift $
|
when isDummy . (throwError =<<) . lift $
|
||||||
maybe (UserError $ IdentifierNotFound credsIdent) (Authenticated . entityKey) <$> getBy uAuth
|
maybe (UserError $ IdentifierNotFound credsIdent) (Authenticated . entityKey) <$> getBy uAuth
|
||||||
|
|
||||||
let
|
let
|
||||||
@ -373,7 +459,7 @@ instance YesodAuth UniWorX where
|
|||||||
|
|
||||||
userEmail <- maybe (throwError $ ServerError "Could not retrieve user email") return userEmail'
|
userEmail <- maybe (throwError $ ServerError "Could not retrieve user email") return userEmail'
|
||||||
userDisplayName <- maybe (throwError $ ServerError "Could not retrieve user name") return userDisplayName'
|
userDisplayName <- maybe (throwError $ ServerError "Could not retrieve user name") return userDisplayName'
|
||||||
|
|
||||||
let
|
let
|
||||||
newUser = User{..}
|
newUser = User{..}
|
||||||
userUpdate = [ UserMatrikelnummer =. userMatrikelnummer
|
userUpdate = [ UserMatrikelnummer =. userMatrikelnummer
|
||||||
@ -388,13 +474,13 @@ instance YesodAuth UniWorX where
|
|||||||
userStudyFeatures' = [ v | (k, v) <- credsExtra, k == "dfnEduPersonFeaturesOfStudy" ]
|
userStudyFeatures' = [ v | (k, v) <- credsExtra, k == "dfnEduPersonFeaturesOfStudy" ]
|
||||||
|
|
||||||
fs <- either (\err -> throwError . ServerError $ "Could not parse features of study: " <> err) return userStudyFeatures
|
fs <- either (\err -> throwError . ServerError $ "Could not parse features of study: " <> err) return userStudyFeatures
|
||||||
|
|
||||||
lift $ deleteWhere [StudyFeaturesUser ==. userId]
|
lift $ deleteWhere [StudyFeaturesUser ==. userId]
|
||||||
|
|
||||||
forM_ fs $ \StudyFeatures{..} -> do
|
forM_ fs $ \StudyFeatures{..} -> do
|
||||||
lift . insertMaybe studyFeaturesDegree $ StudyDegree (unStudyDegreeKey studyFeaturesDegree) Nothing Nothing
|
lift . insertMaybe studyFeaturesDegree $ StudyDegree (unStudyDegreeKey studyFeaturesDegree) Nothing Nothing
|
||||||
lift . insertMaybe studyFeaturesField $ StudyTerms (unStudyTermsKey studyFeaturesField) Nothing Nothing
|
lift . insertMaybe studyFeaturesField $ StudyTerms (unStudyTermsKey studyFeaturesField) Nothing Nothing
|
||||||
|
|
||||||
lift $ insertMany_ fs
|
lift $ insertMany_ fs
|
||||||
return $ Authenticated userId
|
return $ Authenticated userId
|
||||||
where
|
where
|
||||||
@ -419,7 +505,7 @@ ldapConfig _app@(appSettings -> settings) = LDAPConfig
|
|||||||
}
|
}
|
||||||
where
|
where
|
||||||
principalName :: IsString a => a
|
principalName :: IsString a => a
|
||||||
principalName = "userPrincipalName"
|
principalName = "userPrincipalName"
|
||||||
identifierModifier _ entry = case lookup principalName $ leattrs entry of
|
identifierModifier _ entry = case lookup principalName $ leattrs entry of
|
||||||
Just [n] -> Text.pack n
|
Just [n] -> Text.pack n
|
||||||
_ -> error "Could not determine user principal name"
|
_ -> error "Could not determine user principal name"
|
||||||
|
|||||||
@ -9,38 +9,38 @@
|
|||||||
|
|
||||||
module Handler.Course where
|
module Handler.Course where
|
||||||
|
|
||||||
import Import
|
import Import
|
||||||
import Handler.Utils
|
import Handler.Utils
|
||||||
|
|
||||||
-- import Data.Time
|
-- import Data.Time
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
import Data.Function ((&))
|
import Data.Function ((&))
|
||||||
import Yesod.Form.Bootstrap3
|
import Yesod.Form.Bootstrap3
|
||||||
|
|
||||||
import Colonnade hiding (fromMaybe)
|
import Colonnade hiding (fromMaybe)
|
||||||
import Yesod.Colonnade
|
import Yesod.Colonnade
|
||||||
|
|
||||||
import qualified Data.UUID.Cryptographic as UUID
|
import qualified Data.UUID.Cryptographic as UUID
|
||||||
|
|
||||||
|
|
||||||
getCourseListR :: Handler TypedContent
|
getCourseListR :: Handler TypedContent
|
||||||
getCourseListR = redirect TermShowR
|
getCourseListR = redirect TermShowR
|
||||||
|
|
||||||
getCourseListTermR :: TermId -> Handler Html
|
getCourseListTermR :: TermId -> Handler Html
|
||||||
getCourseListTermR tidini = do
|
getCourseListTermR tidini = do
|
||||||
(term,courses) <- runDB $ (,)
|
(term,courses) <- runDB $ (,)
|
||||||
<$> get tidini
|
<$> get tidini
|
||||||
<*> selectList [CourseTermId ==. tidini] [Asc CourseShorthand]
|
<*> selectList [CourseTermId ==. tidini] [Asc CourseShorthand]
|
||||||
when (isNothing term) $ do
|
when (isNothing term) $ do
|
||||||
addMessage "warning" [shamlet| Semester #{toPathPiece tidini} nicht gefunden. |]
|
addMessage "warning" [shamlet| Semester #{toPathPiece tidini} nicht gefunden. |]
|
||||||
redirect TermShowR
|
redirect TermShowR
|
||||||
-- TODO: several runDBs per TableRow are probably too inefficient!
|
-- TODO: several runDBs per TableRow are probably too inefficient!
|
||||||
let colonnadeTerms = mconcat
|
let colonnadeTerms = mconcat
|
||||||
[ headed "Kürzel" $ (\ckv ->
|
[ headed "Kürzel" $ (\ckv ->
|
||||||
let c = entityVal ckv
|
let c = entityVal ckv
|
||||||
shd = courseShorthand c
|
shd = courseShorthand c
|
||||||
tid = courseTermId 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 "Institut" $ [shamlet| #{course} |]
|
||||||
, headed "Beginn Anmeldung" $ fromString.(maybe "" formatTimeGerWD).courseRegisterFrom.entityVal
|
, headed "Beginn Anmeldung" $ fromString.(maybe "" formatTimeGerWD).courseRegisterFrom.entityVal
|
||||||
, headed "Ende Anmeldung" $ fromString.(maybe "" formatTimeGerWD).courseRegisterTo.entityVal
|
, headed "Ende Anmeldung" $ fromString.(maybe "" formatTimeGerWD).courseRegisterTo.entityVal
|
||||||
@ -49,201 +49,237 @@ getCourseListTermR tidini = do
|
|||||||
partiNum <- handlerToWidget $ runDB $ count [CourseParticipantCourseId ==. cid]
|
partiNum <- handlerToWidget $ runDB $ count [CourseParticipantCourseId ==. cid]
|
||||||
[whamlet| #{show partiNum} |]
|
[whamlet| #{show partiNum} |]
|
||||||
)
|
)
|
||||||
, headed " " $ (\ckv ->
|
, headed " " $ (\ckv ->
|
||||||
let c = entityVal ckv
|
let c = entityVal ckv
|
||||||
shd = courseShorthand c
|
shd = courseShorthand c
|
||||||
tid = courseTermId c
|
tid = courseTermId c
|
||||||
in do
|
in do
|
||||||
adminLink <- handlerToWidget $ isAuthorized (CourseEditExistR tid shd ) False
|
adminLink <- handlerToWidget $ isAuthorized (CourseR tid shd CourseEditR) False
|
||||||
-- if (adminLink==Authorized) then linkButton "Ändern" BCWarning (CourseEditExistR tid shd) else ""
|
-- if (adminLink==Authorized) then linkButton "Ändern" BCWarning (CourseEditR tid shd) else ""
|
||||||
[whamlet|
|
[whamlet|
|
||||||
$if adminLink == Authorized
|
$if adminLink == Authorized
|
||||||
<a href=@{CourseEditExistR tid shd}>
|
<a href=@{CourseR tid shd CourseEditR}>
|
||||||
editieren
|
editieren
|
||||||
|]
|
|]
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
let pageLinks =
|
let pageLinks =
|
||||||
[ NavbarLeft $ MenuItem
|
[ PageActionPrime $ MenuItem
|
||||||
{ menuItemLabel = "Neuer Kurs"
|
{ menuItemLabel = "Neuer Kurs"
|
||||||
, menuItemRoute = CourseEditR
|
, menuItemIcon = Just "book"
|
||||||
, menuItemAccessCallback = (== Authorized) <$> isAuthorized CourseEditR False
|
, menuItemRoute = CourseNewR
|
||||||
|
, menuItemAccessCallback = (== Authorized) <$> isAuthorized CourseNewR False
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
let coursesTable = encodeWidgetTable tableSortable colonnadeTerms courses
|
||||||
defaultLinkLayout pageLinks $ do
|
defaultLinkLayout pageLinks $ do
|
||||||
-- defaultLayout $ do
|
setTitle "Semesterkurse"
|
||||||
setTitle "Semesterkurse"
|
$(widgetFile "courses")
|
||||||
linkButton "Neuen Kurs anlegen" BCPrimary CourseEditR
|
|
||||||
encodeWidgetTable tableDefault colonnadeTerms courses -- (map entityVal courses)
|
|
||||||
|
|
||||||
getCourseShowR :: TermId -> Text -> Handler Html
|
getCourseShowR :: TermId -> Text -> Handler Html
|
||||||
getCourseShowR tid csh = do
|
getCourseShowR tid csh = do
|
||||||
mbAid <- maybeAuthId
|
mbAid <- maybeAuthId
|
||||||
(courseEnt,(schoolMB,participants,mbRegistered)) <- runDB $ do
|
(courseEnt,(schoolMB,participants,mbRegistered)) <- runDB $ do
|
||||||
courseEnt@(Entity cid course) <- getBy404 $ CourseTermShort tid csh
|
courseEnt@(Entity cid course) <- getBy404 $ CourseTermShort tid csh
|
||||||
dependent <- (,,)
|
dependent <- (,,)
|
||||||
<$> get (courseSchoolId course) -- join
|
<$> get (courseSchoolId course) -- join
|
||||||
<*> count [CourseParticipantCourseId ==. cid] -- join
|
<*> count [CourseParticipantCourseId ==. cid] -- join
|
||||||
<*> (case mbAid of -- TODO: Someone please refactor this late-night mess here!
|
<*> (case mbAid of -- TODO: Someone please refactor this late-night mess here!
|
||||||
Nothing -> return False
|
Nothing -> return False
|
||||||
(Just aid) -> do
|
(Just aid) -> do
|
||||||
regL <- getBy (UniqueCourseParticipant cid aid)
|
regL <- getBy (UniqueParticipant aid cid)
|
||||||
return $ isJust regL)
|
return $ isJust regL)
|
||||||
return $ (courseEnt,dependent)
|
return $ (courseEnt,dependent)
|
||||||
let course = entityVal courseEnt
|
let course = entityVal courseEnt
|
||||||
(regWidget, regEnctype) <- generateFormPost $ identifyForm "registerBtn" $ registerButton $ mbRegistered
|
(regWidget, regEnctype) <- generateFormPost $ identifyForm "registerBtn" $ registerButton $ mbRegistered
|
||||||
defaultLayout $ do
|
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}|]
|
setTitle $ [shamlet| #{toPathPiece tid} - #{csh}|]
|
||||||
$(widgetFile "course")
|
$(widgetFile "course")
|
||||||
|
|
||||||
registerButton :: Bool -> Form ()
|
registerButton :: Bool -> Form ()
|
||||||
registerButton registered = renderAForm FormStandard $
|
registerButton registered = renderAForm FormStandard $
|
||||||
pure () <* bootstrapSubmit regMsg
|
pure () <* bootstrapSubmit regMsg
|
||||||
where
|
where
|
||||||
msg = if registered then "Abmelden" else "Anmelden"
|
msg = if registered then "Abmelden" else "Anmelden"
|
||||||
regMsg = msg :: BootstrapSubmit Text
|
regMsg = msg :: BootstrapSubmit Text
|
||||||
|
|
||||||
postCourseShowR :: TermId -> Text -> Handler Html
|
postCourseShowR :: TermId -> Text -> Handler Html
|
||||||
postCourseShowR tid csh = do
|
postCourseShowR tid csh = do
|
||||||
aid <- requireAuthId
|
aid <- requireAuthId
|
||||||
(cid, registered) <- runDB $ do
|
(cid, registered) <- runDB $ do
|
||||||
(Entity cid _) <- getBy404 $ CourseTermShort tid csh
|
(Entity cid _) <- getBy404 $ CourseTermShort tid csh
|
||||||
registered <- isJust <$> (getBy $ UniqueCourseParticipant cid aid)
|
registered <- isJust <$> (getBy $ UniqueParticipant aid cid)
|
||||||
return (cid, registered)
|
return (cid, registered)
|
||||||
((regResult,_), _) <- runFormPost $ identifyForm "registerBtn" $ registerButton registered
|
((regResult,_), _) <- runFormPost $ identifyForm "registerBtn" $ registerButton registered
|
||||||
case regResult of
|
case regResult of
|
||||||
(FormSuccess _)
|
(FormSuccess _)
|
||||||
| registered -> do
|
| registered -> do
|
||||||
runDB $ deleteBy $ UniqueCourseParticipant cid aid
|
runDB $ deleteBy $ UniqueParticipant aid cid
|
||||||
addMessage "info" "Sie wurden abgemeldet."
|
addMessage "info" "Sie wurden abgemeldet."
|
||||||
| otherwise -> do
|
| otherwise -> do
|
||||||
actTime <- liftIO $ getCurrentTime
|
actTime <- liftIO $ getCurrentTime
|
||||||
regOk <- runDB $ insertUnique $ CourseParticipant cid aid actTime
|
regOk <- runDB $ insertUnique $ CourseParticipant cid aid actTime
|
||||||
when (isJust regOk) $ addMessage "success" "Erfolgreich angemeldet!"
|
when (isJust regOk) $ addMessage "success" "Erfolgreich angemeldet!"
|
||||||
(_other) -> return () -- TODO check this!
|
(_other) -> return () -- TODO check this!
|
||||||
-- redirect or not?! I guess not, since we want GET now
|
-- redirect or not?! I guess not, since we want GET now
|
||||||
getCourseShowR tid csh
|
getCourseShowR tid csh
|
||||||
|
|
||||||
getCourseEditR :: Handler Html
|
getCourseNewR :: Handler Html
|
||||||
getCourseEditR = do
|
getCourseNewR = do
|
||||||
-- TODO: Defaults für Semester hier ermitteln und übergeben
|
-- TODO: Defaults für Semester hier ermitteln und übergeben
|
||||||
courseEditHandler Nothing
|
courseEditHandler Nothing
|
||||||
|
|
||||||
postCourseEditR :: Handler Html
|
postCourseNewR :: Handler Html
|
||||||
postCourseEditR = courseEditHandler Nothing
|
postCourseNewR = courseEditHandler Nothing
|
||||||
|
|
||||||
getCourseEditExistR :: TermId -> Text -> Handler Html
|
getCourseEditR :: TermId -> Text -> Handler Html
|
||||||
getCourseEditExistR tid csh = do
|
getCourseEditR tid csh = do
|
||||||
course <- runDB $ getBy $ CourseTermShort tid csh
|
course <- runDB $ getBy $ CourseTermShort tid csh
|
||||||
courseEditHandler course
|
courseEditHandler course
|
||||||
|
|
||||||
getCourseEditExistIDR :: CryptoUUIDCourse -> Handler Html
|
postCourseEditR :: TermId -> Text -> Handler Html
|
||||||
getCourseEditExistIDR cID = do
|
postCourseEditR = getCourseEditR
|
||||||
|
|
||||||
|
getCourseEditIDR :: CryptoUUIDCourse -> Handler Html
|
||||||
|
getCourseEditIDR cID = do
|
||||||
cIDKey <- getsYesod appCryptoIDKey
|
cIDKey <- getsYesod appCryptoIDKey
|
||||||
courseID <- UUID.decrypt cIDKey cID
|
courseID <- UUID.decrypt cIDKey cID
|
||||||
courseEditHandler =<< runDB (getEntity courseID)
|
courseEditHandler =<< runDB (getEntity courseID)
|
||||||
|
|
||||||
|
|
||||||
courseEditHandler :: Maybe (Entity Course) -> Handler Html
|
courseDeleteHandler :: Handler Html -- not called anywhere yet
|
||||||
courseEditHandler course = do
|
courseDeleteHandler = undefined
|
||||||
aid <- requireAuthId
|
{- TODO
|
||||||
((result, formWidget), formEnctype) <- runFormPost $ newCourseForm $ courseToForm <$> course
|
| False -- DELETE -- TODO: This no longer works that way!!! See new way in Handler.Term.termEditHandler
|
||||||
action <- lookupPostParam "formaction"
|
, Just cid <- cfCourseId res -> do
|
||||||
case (result,action) of
|
|
||||||
(FormSuccess res, fAct)
|
|
||||||
| fAct == formActionDelete
|
|
||||||
, Just cid <- cfCourseId res -> do
|
|
||||||
runDB $ deleteCascade cid -- TODO Sicherheitsabfrage einbauen!
|
runDB $ deleteCascade cid -- TODO Sicherheitsabfrage einbauen!
|
||||||
let cti = toPathPiece $ cfTerm res
|
let cti = toPathPiece $ cfTerm res
|
||||||
addMessage "info" [shamlet| Kurs #{cti}/#{cfShort res} wurde gelöscht!|]
|
addMessage "info" [shamlet| Kurs #{cti}/#{cfShort res} wurde gelöscht!|]
|
||||||
redirect $ CourseListTermR $ cfTerm res
|
redirect $ CourseListTermR $ cfTerm res
|
||||||
| fAct == formActionSave
|
-}
|
||||||
, Just cid <- cfCourseId res -> do
|
|
||||||
let tid = cfTerm res
|
courseEditHandler :: Maybe (Entity Course) -> Handler Html
|
||||||
actTime <- liftIO getCurrentTime
|
courseEditHandler course = do
|
||||||
updateokay <- runDB $ do
|
aid <- requireAuthId -- TODO: Verify that Editor is owner of the Course to be Edited!!!
|
||||||
exists <- getBy $ CourseTermShort tid $ cfShort res
|
((result, formWidget), formEnctype) <- runFormPost $ newCourseForm $ courseToForm <$> course
|
||||||
let upokay = isNothing exists
|
case result of
|
||||||
when upokay $ update cid
|
(FormSuccess res@(
|
||||||
[ CourseName =. cfName res
|
CourseForm { cfCourseId = Nothing
|
||||||
, CourseDescription =. cfDesc res
|
, cfShort = csh
|
||||||
, CourseLinkExternal =. cfLink res
|
, cfTerm = tid
|
||||||
, CourseShorthand =. cfShort res -- TODO: change here should generate a warning, or only allowed for Admins?!
|
})) -> do -- create new course
|
||||||
, CourseTermId =. tid -- TODO: change here should generate a warning, or only allowed for Admins?!
|
let tident = unTermKey tid
|
||||||
, CourseSchoolId =. cfSchool res
|
now <- liftIO getCurrentTime
|
||||||
, CourseCapacity =. cfCapacity res
|
insertOkay <- runDB $ insertUnique $ Course
|
||||||
, CourseRegisterFrom =. cfRegFrom res
|
{ courseName = cfName res
|
||||||
, CourseRegisterTo =. cfRegTo res
|
, courseDescription = cfDesc res
|
||||||
, CourseChangedBy =. aid
|
, courseLinkExternal = cfLink res
|
||||||
, CourseChanged =. actTime
|
, courseShorthand = cfShort res
|
||||||
]
|
, courseTermId = cfTerm res
|
||||||
return upokay
|
, courseSchoolId = cfSchool res
|
||||||
let cti = toPathPiece $ cfTerm res
|
, courseCapacity = cfCapacity res
|
||||||
if updateokay
|
, courseHasRegistration = cfHasReg res
|
||||||
then do
|
, courseRegisterFrom = cfRegFrom res
|
||||||
addMessage "info" [shamlet| Kurs #{cti}/#{cfShort res} wurde geändert. |]
|
, courseRegisterTo = cfRegTo res
|
||||||
redirect $ CourseListTermR $ cfTerm res
|
}
|
||||||
else do
|
case insertOkay of
|
||||||
addMessage "danger" [shamlet| Kurs #{cti}/#{cfShort res} konnte nicht geändert werden.
|
(Just cid) -> do
|
||||||
\ Es gibt bereits einen anderen Kurs mit diesem Kürzel in diesem Semester.|]
|
runDB $ do
|
||||||
| fAct == formActionSave
|
insert_ $ CourseEdit aid now cid
|
||||||
, Nothing <- cfCourseId res -> do
|
insert_ $ Lecturer aid cid
|
||||||
actTime <- liftIO getCurrentTime
|
addMessageI "info" $ MsgCourseNewOk tident csh
|
||||||
insertOkay <- runDB $ insertUnique $ Course
|
redirect $ CourseListTermR tid
|
||||||
{ courseName = cfName res
|
Nothing ->
|
||||||
, courseDescription = cfDesc res
|
addMessageI "danger" $ MsgCourseNewDupShort tident csh
|
||||||
, courseLinkExternal = cfLink res
|
|
||||||
, courseShorthand = cfShort res
|
(FormSuccess res@(
|
||||||
, courseTermId = cfTerm res
|
CourseForm { cfCourseId = Just cid
|
||||||
, courseSchoolId = cfSchool res
|
, cfShort = csh
|
||||||
, courseCapacity = cfCapacity res
|
, cfTerm = tid
|
||||||
, courseHasRegistration = cfHasReg res
|
})) -> do -- edit existing course
|
||||||
, courseRegisterFrom = cfRegFrom res
|
let tident = unTermKey tid
|
||||||
, courseRegisterTo = cfRegTo res
|
now <- liftIO getCurrentTime
|
||||||
, courseCreated = actTime
|
-- addMessage "debug" [shamlet| #{show res}|]
|
||||||
, courseChanged = actTime
|
runDB $ do
|
||||||
, courseCreatedBy = aid
|
old <- get cid
|
||||||
, courseChangedBy = aid
|
case old of
|
||||||
}
|
Nothing -> addMessageI "error" $ MsgInvalidInput
|
||||||
case insertOkay of
|
(Just oldCourse) -> do
|
||||||
(Just cid) -> do
|
-- existing <- getBy $ CourseTermShort tid csh
|
||||||
runDB $ insert_ $ Lecturer aid cid
|
-- if ((entityKey <$> existing) /= Just cid)
|
||||||
let cti = toPathPiece $ cfTerm res
|
-- then addMessageI "danger" $ MsgCourseEditDupShort tident csh
|
||||||
addMessage "info" [shamlet|Kurs #{cti}/#{cfShort res} wurde angelegt.|]
|
-- else do
|
||||||
redirect $ CourseListTermR $ cfTerm res
|
-- addMessage "debug" $ fromMaybe [shamlet|No description given.|] $ cfDesc res
|
||||||
Nothing -> do
|
-- update cid
|
||||||
let cti = toPathPiece $ cfTerm res
|
-- [ CourseName =. cfName res
|
||||||
addMessage "danger" [shamlet|Es gibt bereits einen Kurs #{cfShort res} in Semester #{cti}.|]
|
-- , CourseDescription =. cfDesc res
|
||||||
(FormFailure _,_) -> addMessage "warning" "Bitte Eingabe korrigieren."
|
-- , CourseLinkExternal =. cfLink res
|
||||||
_other -> return ()
|
-- , 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 formTitle = "Kurs editieren/anlegen" :: Text
|
||||||
let actionUrl = CourseEditR
|
actionUrl <- fromMaybe CourseNewR <$> getCurrentRoute
|
||||||
let formActions = defaultFormActions
|
|
||||||
defaultLayout $ do
|
defaultLayout $ do
|
||||||
setTitle [shamlet| #{formTitle} |]
|
setTitle [shamlet| #{formTitle} |]
|
||||||
$(widgetFile "formPage")
|
$(widgetFile "formPage")
|
||||||
|
|
||||||
|
|
||||||
data CourseForm = CourseForm
|
data CourseForm = CourseForm
|
||||||
{ cfCourseId :: Maybe CourseId -- Maybe CryptoUUIDCourse
|
{ cfCourseId :: Maybe CourseId -- Maybe CryptoUUIDCourse
|
||||||
, cfName :: Text
|
, cfName :: Text
|
||||||
, cfDesc :: Maybe Html
|
, cfDesc :: Maybe Html
|
||||||
, cfLink :: Maybe Text
|
, cfLink :: Maybe Text
|
||||||
, cfShort :: Text
|
, cfShort :: Text
|
||||||
, cfTerm :: TermId
|
, cfTerm :: TermId
|
||||||
, cfSchool :: SchoolId
|
, cfSchool :: SchoolId
|
||||||
, cfCapacity :: Maybe Int
|
, cfCapacity :: Maybe Int
|
||||||
, cfHasReg :: Bool
|
, cfHasReg :: Bool
|
||||||
, cfRegFrom :: Maybe UTCTime
|
, cfRegFrom :: Maybe UTCTime
|
||||||
, cfRegTo :: Maybe UTCTime
|
, cfRegTo :: Maybe UTCTime
|
||||||
}
|
}
|
||||||
|
|
||||||
instance Show CourseForm where
|
instance Show CourseForm where
|
||||||
show cf = T.unpack (cfShort cf) ++ ' ':(show $ cfCourseId cf)
|
show cf = T.unpack (cfShort cf) ++ ' ':(show $ cfCourseId cf)
|
||||||
|
|
||||||
|
|
||||||
courseToForm :: Entity Course -> CourseForm
|
courseToForm :: Entity Course -> CourseForm
|
||||||
courseToForm cEntity = CourseForm
|
courseToForm cEntity = CourseForm
|
||||||
{ cfCourseId = Just $ entityKey cEntity
|
{ cfCourseId = Just $ entityKey cEntity
|
||||||
, cfName = courseName course
|
, cfName = courseName course
|
||||||
, cfDesc = courseDescription course
|
, cfDesc = courseDescription course
|
||||||
@ -253,26 +289,26 @@ courseToForm cEntity = CourseForm
|
|||||||
, cfSchool = courseSchoolId course
|
, cfSchool = courseSchoolId course
|
||||||
, cfCapacity = courseCapacity course
|
, cfCapacity = courseCapacity course
|
||||||
, cfHasReg = courseHasRegistration course
|
, cfHasReg = courseHasRegistration course
|
||||||
, cfRegFrom = courseRegisterFrom course
|
, cfRegFrom = courseRegisterFrom course
|
||||||
, cfRegTo = courseRegisterTo course
|
, cfRegTo = courseRegisterTo course
|
||||||
}
|
}
|
||||||
where
|
where
|
||||||
course = entityVal cEntity
|
course = entityVal cEntity
|
||||||
|
|
||||||
newCourseForm :: Maybe CourseForm -> Form CourseForm
|
newCourseForm :: Maybe CourseForm -> Form CourseForm
|
||||||
newCourseForm template = identForm FIDcourse $ \html -> do
|
newCourseForm template = identForm FIDcourse $ \html -> do
|
||||||
-- mopt hiddenField
|
-- mopt hiddenField
|
||||||
-- cidKey <- getsYesod appCryptoIDKey
|
-- cidKey <- getsYesod appCryptoIDKey
|
||||||
-- courseId <- runMaybeT $ do
|
-- courseId <- runMaybeT $ do
|
||||||
-- cid <- cfCourseId template
|
-- cid <- cfCourseId template
|
||||||
-- UUID.encrypt cidKey cid
|
-- UUID.encrypt cidKey cid
|
||||||
(result, widget) <- flip (renderAForm FormStandard) html $ CourseForm
|
(result, widget) <- flip (renderAForm FormStandard) html $ CourseForm
|
||||||
-- <$> pure cid -- $ join $ cfCourseId <$> template -- why doesnt this work?
|
-- <$> pure cid -- $ join $ cfCourseId <$> template -- why doesnt this work?
|
||||||
<$> aopt hiddenField "KursId" (cfCourseId <$> template)
|
<$> aopt hiddenField "KursId" (cfCourseId <$> template)
|
||||||
<*> areq textField (fsb "Name") (cfName <$> template)
|
<*> areq textField (fsb "Name") (cfName <$> template)
|
||||||
<*> aopt htmlField (fsb "Beschreibung") (cfDesc <$> template)
|
<*> aopt htmlField (fsb "Beschreibung") (cfDesc <$> template)
|
||||||
<*> aopt urlField (fsb "Homepage") (cfLink <$> template)
|
<*> aopt urlField (fsb "Homepage") (cfLink <$> template)
|
||||||
<*> areq textField (fsb "Kürzel"
|
<*> areq textField (fsb "Kürzel"
|
||||||
-- & addAttr "disabled" "disabled"
|
-- & addAttr "disabled" "disabled"
|
||||||
& setTooltip "Muss innerhalb des Semesters eindeutig sein")
|
& setTooltip "Muss innerhalb des Semesters eindeutig sein")
|
||||||
(cfShort <$> template)
|
(cfShort <$> template)
|
||||||
@ -282,9 +318,9 @@ newCourseForm template = identForm FIDcourse $ \html -> do
|
|||||||
<*> areq checkBoxField (fsb "Anmeldung") (cfHasReg <$> template)
|
<*> areq checkBoxField (fsb "Anmeldung") (cfHasReg <$> template)
|
||||||
<*> aopt utcTimeField (fsb "Anmeldung von:") (cfRegFrom <$> template)
|
<*> aopt utcTimeField (fsb "Anmeldung von:") (cfRegFrom <$> template)
|
||||||
<*> aopt utcTimeField (fsb "Anmeldung bis:") (cfRegTo <$> template)
|
<*> aopt utcTimeField (fsb "Anmeldung bis:") (cfRegTo <$> template)
|
||||||
-- <* bootstrapSubmit (bsSubmit (show cid))
|
<* submitButton
|
||||||
return $ case result of
|
return $ case result of
|
||||||
FormSuccess courseResult
|
FormSuccess courseResult
|
||||||
| errorMsgs <- validateCourse courseResult
|
| errorMsgs <- validateCourse courseResult
|
||||||
, not $ null errorMsgs ->
|
, not $ null errorMsgs ->
|
||||||
(FormFailure errorMsgs,
|
(FormFailure errorMsgs,
|
||||||
@ -293,18 +329,18 @@ newCourseForm template = identForm FIDcourse $ \html -> do
|
|||||||
<h4> Fehler:
|
<h4> Fehler:
|
||||||
<ul>
|
<ul>
|
||||||
$forall errmsg <- errorMsgs
|
$forall errmsg <- errorMsgs
|
||||||
<li> #{errmsg}
|
<li> #{errmsg}
|
||||||
^{widget}
|
^{widget}
|
||||||
|]
|
|]
|
||||||
)
|
)
|
||||||
_ -> (result, widget)
|
_ -> (result, widget)
|
||||||
-- where
|
-- where
|
||||||
-- cid :: Maybe CourseId
|
-- cid :: Maybe CourseId
|
||||||
-- cid = join $ cfCourseId <$> template
|
-- cid = join $ cfCourseId <$> template
|
||||||
|
|
||||||
|
|
||||||
validateCourse :: CourseForm -> [Text]
|
validateCourse :: CourseForm -> [Text]
|
||||||
validateCourse (CourseForm{..}) =
|
validateCourse (CourseForm{..}) =
|
||||||
[ msg | (False, msg) <-
|
[ msg | (False, msg) <-
|
||||||
[
|
[
|
||||||
( cfRegFrom <= cfRegTo
|
( cfRegFrom <= cfRegTo
|
||||||
@ -324,5 +360,3 @@ validateCourse (CourseForm{..}) =
|
|||||||
, "Anmeldungen aktivieren oder Anmeldezeitraum löschen"
|
, "Anmeldungen aktivieren oder Anmeldezeitraum löschen"
|
||||||
)
|
)
|
||||||
] ]
|
] ]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -9,13 +9,13 @@
|
|||||||
|
|
||||||
module Handler.Home where
|
module Handler.Home where
|
||||||
|
|
||||||
import Import
|
import Import
|
||||||
import Handler.Utils
|
import Handler.Utils
|
||||||
|
|
||||||
-- import Data.Time
|
-- import Data.Time
|
||||||
-- import qualified Data.Text as T
|
-- import qualified Data.Text as T
|
||||||
-- import Yesod.Form.Bootstrap3
|
-- import Yesod.Form.Bootstrap3
|
||||||
|
|
||||||
import Web.PathPieces (showToPathPiece, readFromPathPiece)
|
import Web.PathPieces (showToPathPiece, readFromPathPiece)
|
||||||
|
|
||||||
-- import Colonnade
|
-- import 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
|
instance PathPiece CreateButton where -- for displaying the button only, not really for paths
|
||||||
toPathPiece = showToPathPiece
|
toPathPiece = showToPathPiece
|
||||||
fromPathPiece = readFromPathPiece
|
fromPathPiece = readFromPathPiece
|
||||||
|
|
||||||
instance Button CreateButton where
|
instance Button CreateButton where
|
||||||
label CreateMath = [whamlet|Ma<i>thema</i>tik|]
|
label CreateMath = [whamlet|Ma<i>thema</i>tik|]
|
||||||
label CreateInf = "Informatik"
|
label CreateInf = "Informatik"
|
||||||
|
|
||||||
cssClass CreateMath = BCInfo
|
cssClass CreateMath = BCInfo
|
||||||
cssClass CreateInf = BCPrimary
|
cssClass CreateInf = BCPrimary
|
||||||
-- END Button needed here
|
-- END Button needed here
|
||||||
|
|
||||||
getHomeR :: Handler Html
|
getHomeR :: Handler Html
|
||||||
getHomeR = do
|
getHomeR = do
|
||||||
(btnWdgt, btnEnctype) <- generateFormPost (buttonForm :: Form CreateButton)
|
(btnWdgt, btnEnctype) <- generateFormPost (buttonForm :: Form CreateButton)
|
||||||
defaultLayout $ do
|
defaultLayout $ do
|
||||||
setTitle "Willkommen zum ReWorX Test!"
|
setTitle "Willkommen zum ReWorX Test!"
|
||||||
$(widgetFile "home")
|
$(widgetFile "home")
|
||||||
|
|
||||||
|
|
||||||
postHomeR :: Handler Html
|
postHomeR :: Handler Html
|
||||||
postHomeR = do
|
postHomeR = do
|
||||||
((btnResult,_), _) <- runFormPost $ buttonForm
|
((btnResult,_), _) <- runFormPost $ buttonForm
|
||||||
case btnResult of
|
case btnResult of
|
||||||
(FormSuccess CreateInf) -> setMessage "Informatik-Knopf gedrückt"
|
(FormSuccess CreateInf) -> setMessage "Informatik-Knopf gedrückt"
|
||||||
(FormSuccess CreateMath) -> addMessage "warning" "Knopf Mathematik erkannt"
|
(FormSuccess CreateMath) -> addMessage "warning" "Knopf Mathematik erkannt"
|
||||||
_other -> return ()
|
_other -> return ()
|
||||||
getHomeR
|
getHomeR
|
||||||
|
|
||||||
|
|||||||
@ -6,28 +6,40 @@
|
|||||||
{-# LANGUAGE MultiParamTypeClasses #-}
|
{-# LANGUAGE MultiParamTypeClasses #-}
|
||||||
{-# LANGUAGE TypeFamilies #-}
|
{-# LANGUAGE TypeFamilies #-}
|
||||||
{-# LANGUAGE FlexibleContexts #-}
|
{-# LANGUAGE FlexibleContexts #-}
|
||||||
|
{-# LANGUAGE FlexibleInstances #-}
|
||||||
|
|
||||||
module Handler.Sheet where
|
module Handler.Sheet where
|
||||||
|
|
||||||
import Import
|
import Import
|
||||||
|
import System.FilePath (takeFileName)
|
||||||
|
|
||||||
import Handler.Utils
|
import Handler.Utils
|
||||||
import Handler.Utils.Zip
|
import Handler.Utils.Zip
|
||||||
|
|
||||||
|
|
||||||
-- import Data.Time
|
-- import Data.Time
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
-- import Data.Function ((&))
|
-- import Data.Function ((&))
|
||||||
--
|
--
|
||||||
import Colonnade -- hiding (fromMaybe)
|
import Colonnade hiding (fromMaybe, singleton)
|
||||||
import Yesod.Colonnade
|
import Yesod.Colonnade
|
||||||
--
|
--
|
||||||
import qualified Data.UUID.Cryptographic as UUID
|
import qualified Data.UUID.Cryptographic as UUID
|
||||||
import qualified Data.Conduit.List as C
|
import qualified Data.Conduit.List as C
|
||||||
|
|
||||||
import qualified Database.Esqueleto as E
|
import qualified Database.Esqueleto as E
|
||||||
|
import qualified Database.Esqueleto.Internal.Sql as E
|
||||||
|
|
||||||
|
import Control.Monad.Writer (MonadWriter(..), execWriterT)
|
||||||
|
|
||||||
import Network.Mime
|
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 Handlers
|
||||||
* Implement Breadcrumbs in Foundation
|
* Implement Breadcrumbs in Foundation
|
||||||
@ -36,36 +48,47 @@ import Network.Mime
|
|||||||
|
|
||||||
data SheetForm = SheetForm
|
data SheetForm = SheetForm
|
||||||
{ sfName :: Text
|
{ sfName :: Text
|
||||||
, sfComment :: Maybe Html
|
, sfDescription :: Maybe Html
|
||||||
, sfType :: SheetType
|
, sfType :: SheetType
|
||||||
|
, sfGrouping :: SheetGroup
|
||||||
, sfMarkingText :: Maybe Html
|
, sfMarkingText :: Maybe Html
|
||||||
|
, sfVisibleFrom :: Maybe UTCTime
|
||||||
, sfActiveFrom :: UTCTime
|
, sfActiveFrom :: UTCTime
|
||||||
, sfActiveTo :: UTCTime
|
, sfActiveTo :: UTCTime
|
||||||
, sfSheetF :: Maybe FileInfo
|
, sfSheetF :: Maybe (Source Handler (Either FileId File))
|
||||||
, sfHintFrom :: Maybe UTCTime
|
, sfHintFrom :: Maybe UTCTime
|
||||||
, sfHintF :: Maybe FileInfo
|
, sfHintF :: Maybe FileInfo
|
||||||
, sfSolutionFrom :: Maybe UTCTime
|
, sfSolutionFrom :: Maybe UTCTime
|
||||||
, sfSolutionF :: Maybe FileInfo
|
, sfSolutionF :: Maybe FileInfo
|
||||||
|
-- Keine SheetId im Formular!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
makeSheetForm :: CourseId -> Maybe SheetForm -> Form SheetForm
|
makeSheetForm :: Maybe SheetId -> Maybe SheetForm -> Form SheetForm
|
||||||
makeSheetForm cid template = identForm FIDsheet $ \html -> do
|
makeSheetForm msId template = identForm FIDsheet $ \html -> do
|
||||||
-- TODO: Yesod.Form.MassInput.inputList arbeitet Server-seitig :(
|
let oldFileIds fType
|
||||||
-- Erstmal nur mit ZIP arbeiten
|
| 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
|
(result, widget) <- flip (renderAForm FormStandard) html $ SheetForm
|
||||||
<$> areq textField (fsb "Name") (sfName <$> template)
|
<$> areq textField (fsb "Name") (sfName <$> template)
|
||||||
<*> aopt htmlField (fsb "Hinweise für Teilnehmer") (sfMarkingText <$> template)
|
<*> aopt htmlField (fsb "Hinweise für Teilnehmer") (sfDescription <$> template)
|
||||||
<*> sheetTypeAFormReq (fsb "Bewertung") (sfType <$> template)
|
<*> sheetTypeAFormReq (fsb "Bewertung") (sfType <$> template)
|
||||||
--TODO: SICHTBARKEIT hinzunehmen
|
<*> sheetGroupAFormReq (fsb "Abgabegruppengröße") (sfGrouping <$> template)
|
||||||
<*> aopt htmlField (fsb "Hinweise für Korrektoren") (sfMarkingText <$> template)
|
<*> aopt htmlField (fsb "Hinweise für Korrektoren") (sfMarkingText <$> template)
|
||||||
<*> areq utcTimeField (fsb "Abgabe ab") (sfActiveFrom <$> template)
|
<*> aopt utcTimeField (fsb "Sichtbar ab") (sfVisibleFrom <$> template)
|
||||||
<*> areq utcTimeField (fsb "Abgabefrist") (sfActiveTo <$> template)
|
<*> areq utcTimeField (fsb "Abgabe ab") (sfActiveFrom <$> template)
|
||||||
<*> fileAFormOpt (fsb "Aufgaben")
|
<*> areq utcTimeField (fsb "Abgabefrist") (sfActiveTo <$> template)
|
||||||
<*> aopt utcTimeField (fsb "Hinweis ab") (sfHintFrom <$> template)
|
<*> aopt (multiFileField $ oldFileIds SheetExercise) (fsb "Aufgabenstellung") (sfSheetF <$> template)
|
||||||
<*> fileAFormOpt (fsb "Hinweis")
|
<*> aopt utcTimeField (fsb "Hinweis ab") (sfHintFrom <$> template)
|
||||||
<*> aopt utcTimeField (fsb "Lösung ab") (sfSolutionFrom <$> template)
|
<*> fileAFormOpt (fsb "Hinweis")
|
||||||
<*> fileAFormOpt (fsb "Lösung")
|
<*> aopt utcTimeField (fsb "Lösung ab") (sfSolutionFrom <$> template)
|
||||||
|
<*> fileAFormOpt (fsb "Lösung")
|
||||||
|
<* submitButton
|
||||||
return $ case result of
|
return $ case result of
|
||||||
FormSuccess sheetResult
|
FormSuccess sheetResult
|
||||||
| errorMsgs <- validateSheet sheetResult
|
| errorMsgs <- validateSheet sheetResult
|
||||||
@ -82,14 +105,48 @@ makeSheetForm cid template = identForm FIDsheet $ \html -> do
|
|||||||
)
|
)
|
||||||
_ -> (result, widget)
|
_ -> (result, widget)
|
||||||
where
|
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 :: TermId -> Text -> Text -> YesodDB UniWorX (Entity Sheet)
|
||||||
fetchSheet tid csh shn = do
|
fetchSheet = fetchSheetAux id
|
||||||
-- TODO: More efficient with Esquleto?
|
|
||||||
(Entity cid _course) <- getBy404 $ CourseTermShort tid csh
|
fetchSheetId :: TermId -> Text -> Text -> YesodDB UniWorX (Key Sheet)
|
||||||
getBy404 $ CourseSheet cid shn
|
fetchSheetId tid cid shn = E.unValue <$> fetchSheetAux (E.^. SheetId) tid cid shn
|
||||||
|
|
||||||
-- List Sheets
|
-- List Sheets
|
||||||
getSheetListCID :: CourseId -> Handler Html
|
getSheetListCID :: CourseId -> Handler Html
|
||||||
@ -114,17 +171,35 @@ getSheetList courseEnt = do
|
|||||||
submissions <- count sheetsub
|
submissions <- count sheetsub
|
||||||
rated <- count $ (SubmissionRatingTime !=. Nothing):sheetsub
|
rated <- count $ (SubmissionRatingTime !=. Nothing):sheetsub
|
||||||
return (sid, sheet, (submissions, rated))
|
return (sid, sheet, (submissions, rated))
|
||||||
let colSheets = mconcat
|
let colBase = mconcat
|
||||||
[ headed "Blatt" $ toWgt . sheetName . snd3
|
[ 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 ab" $ toWgt . formatTimeGerWD . sheetActiveFrom . snd3
|
||||||
, headed "Abgabe bis" $ toWgt . formatTimeGerWD . sheetActiveTo . snd3
|
, headed "Abgabe bis" $ toWgt . formatTimeGerWD . sheetActiveTo . snd3
|
||||||
, headed "Bewertung" $ toWgt . show . sheetType . snd3
|
, headed "Bewertung" $ toWgt . show . sheetType . snd3
|
||||||
, headed "Korrigiert" $ toWgt . snd . trd3
|
]
|
||||||
, headed "Eingereicht" $ toWgt . fst . trd3
|
let colAdmin = mconcat -- only show edit button for allowed course assistants
|
||||||
-- TODO: only show edit button for allowed course assistants
|
[ headed "Korrigiert" $ toWgt . snd . trd3
|
||||||
, headed "" $ \s -> linkButton "Edit" BCLink $ SheetEditR tid csh $ fst3 s
|
, headed "Eingereicht" $ toWgt . fst . trd3
|
||||||
]
|
, headed "" $ \s -> linkButton "Edit" BCLink $ CourseR tid csh $ SheetR $ SheetEditR $ sheetName $ snd3 s
|
||||||
defaultLayout $ do
|
, 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
|
setTitle $ toHtml $ T.append "Übungsblätter " csh
|
||||||
if null sheets
|
if null sheets
|
||||||
then [whamlet|Es wurden noch keine Übungsblätter angelegt.|]
|
then [whamlet|Es wurden noch keine Übungsblätter angelegt.|]
|
||||||
@ -132,29 +207,27 @@ getSheetList courseEnt = do
|
|||||||
|
|
||||||
-- Show single sheet
|
-- Show single sheet
|
||||||
getSheetShowR :: TermId -> Text -> Text -> Handler Html
|
getSheetShowR :: TermId -> Text -> Text -> Handler Html
|
||||||
getSheetShowR tid csh shn = getSheetShow =<<
|
getSheetShowR tid csh shn = do
|
||||||
(runDB $ fetchSheet tid csh shn)
|
entSheet <- runDB $ fetchSheet tid csh shn
|
||||||
|
|
||||||
{- Nur per UUID
|
|
||||||
getSheetIdShowR :: SheetId -> Handler Html
|
|
||||||
getSheetIdShowR sheetId = getSheetShow =<<
|
|
||||||
(Entity sheetId) <$> (runDB $ get404 sheetId)\
|
|
||||||
-}{-
|
|
||||||
getSheetUUIDShowR :: CryptoUUIDSheet -> Handler Html
|
|
||||||
getSheetUUIDShowR sUUID = do
|
|
||||||
cIDKey <- getsYesod appCryptoIDKey
|
|
||||||
sheetId <- UUID.decrypt cIDKey sUUID
|
|
||||||
sheetEnt <- runDB $ get404 sheetId
|
|
||||||
getSheetShow $ Entity sheetId sheetEnt
|
|
||||||
-}
|
|
||||||
|
|
||||||
getSheetShow :: (Entity Sheet) -> Handler Html
|
|
||||||
getSheetShow entSheet = do
|
|
||||||
let sheet = entityVal entSheet
|
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
|
defaultLayout $ do
|
||||||
setTitle $ toHtml $ T.append "Übung " $ sheetName sheet
|
setTitle $ toHtml $ T.append "Übung " $ sheetName sheet
|
||||||
|
$(widgetFile "sheetShow")
|
||||||
[whamlet| Under Construction !!! |] -- TODO
|
[whamlet| Under Construction !!! |] -- TODO
|
||||||
$(widgetFile "sheetAdmin")
|
|
||||||
|
|
||||||
getSheetFileR :: TermId -> Text -> Text -> SheetFileType -> FilePath -> Handler TypedContent
|
getSheetFileR :: TermId -> Text -> Text -> SheetFileType -> FilePath -> Handler TypedContent
|
||||||
getSheetFileR tid csh shn typ title = do
|
getSheetFileR tid csh shn typ title = do
|
||||||
@ -181,72 +254,149 @@ getSheetFileR tid csh shn typ title = do
|
|||||||
[] -> notFound
|
[] -> notFound
|
||||||
_other -> error "Multiple matching files found."
|
_other -> error "Multiple matching files found."
|
||||||
|
|
||||||
|
|
||||||
getSheetNewR :: TermId -> Text -> Handler Html
|
getSheetNewR :: TermId -> Text -> Handler Html
|
||||||
getSheetNewR tid csh = do
|
getSheetNewR tid csh = do
|
||||||
(Entity cid course) <- runDB $ getBy404 $ CourseTermShort tid csh
|
|
||||||
let template = Nothing -- TODO: provide convenience by interpolating name/nr/dates+7days
|
let template = Nothing -- TODO: provide convenience by interpolating name/nr/dates+7days
|
||||||
((res,wdgt), enc) <- runFormPost $ makeSheetForm cid template
|
let action newSheet = -- More specific error message for new sheet could go here, if insertUnique returns Nothing
|
||||||
|
insertUnique $ newSheet
|
||||||
case res of
|
handleSheetEdit tid csh Nothing template action
|
||||||
(FormSuccess SheetForm{..}) -> do
|
|
||||||
|
|
||||||
|
|
||||||
let sid = undefined -- TODO after first insert
|
|
||||||
let sname = undefined -- TODO after first insert
|
|
||||||
|
|
||||||
-- Prüfe, das FileTitle innerhalb des Sheets eindeutig ist für diesen SheetFileTpye
|
|
||||||
whenIsJust sfSheetF $ \sinfo -> do
|
|
||||||
let sheetInsert file = do
|
|
||||||
fid <- insert file
|
|
||||||
void . insert $ SheetFile sid fid SheetExercise
|
|
||||||
runDB . runConduit $ (sourceFiles sinfo) =$= C.mapM_ sheetInsert
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
addMessage "info" "Blatt angelegt"
|
|
||||||
redirect $ SheetShowR tid csh sname
|
|
||||||
(FormFailure msgs) -> forM_ msgs $ (addMessage "warning") . toHtml
|
|
||||||
_ -> return ()
|
|
||||||
defaultLayout $ do
|
|
||||||
$(widgetFile "newSheet")
|
|
||||||
|
|
||||||
postSheetNewR :: TermId -> Text -> Handler Html
|
postSheetNewR :: TermId -> Text -> Handler Html
|
||||||
postSheetNewR = getSheetNewR
|
postSheetNewR = getSheetNewR
|
||||||
|
|
||||||
getSheetEditR :: TermId -> Text -> SheetId -> Handler Html
|
|
||||||
getSheetEditR _ _ _ = defaultLayout [whamlet| Under Construction !!! |] -- TODO
|
|
||||||
|
|
||||||
postSheetEditR :: TermId -> Text -> SheetId -> Handler Html
|
getSheetEditR :: TermId -> Text -> Text -> Handler Html
|
||||||
postSheetEditR _ _ _ = defaultLayout [whamlet| Under Construction !!! |] -- TODO
|
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
|
handleSheetEdit :: TermId -> Text -> Maybe SheetId -> Maybe SheetForm -> (Sheet -> YesodDB UniWorX (Maybe SheetId)) -> Handler Html
|
||||||
getSheetDelR _ _ _ = defaultLayout [whamlet| Under Construction !!! |] -- TODO
|
handleSheetEdit tid csh msId template dbAction = do
|
||||||
-- Sicherheitsabfrage
|
let tident = unTermKey tid
|
||||||
|
let mbshn = sfName <$> template
|
||||||
postSheetDelR :: TermId -> Text -> SheetId -> Handler Html
|
aid <- requireAuthId
|
||||||
postSheetDelR _ _ _ = defaultLayout [whamlet| Under Construction !!! |] -- TODO
|
((res,formWidget), formEnctype) <- runFormPost $ makeSheetForm msId template
|
||||||
-- Tatsächlich löschen
|
case res of
|
||||||
|
(FormSuccess SheetForm{..}) -> do
|
||||||
|
saveOkay <- runDB $ do
|
||||||
{-
|
actTime <- liftIO getCurrentTime
|
||||||
getCourseShowR :: TermId -> Text -> Handler Html
|
cid <- getKeyBy404 $ CourseTermShort tid csh
|
||||||
getCourseShowR tid csh = do
|
let newSheet = Sheet
|
||||||
mbAid <- maybeAuthId
|
{ sheetCourseId = cid
|
||||||
(courseEnt,(schoolMB,participants,mbRegistered)) <- runDB $ do
|
, sheetName = sfName
|
||||||
courseEnt@(Entity cid course) <- getBy404 $ CourseTermShort (TermKey tid) csh
|
, sheetDescription = sfDescription
|
||||||
dependent <- (,,)
|
, sheetType = sfType
|
||||||
<$> get (courseSchoolId course) -- join
|
, sheetGrouping = sfGrouping
|
||||||
<*> count [CourseParticipantCourseId ==. cid] -- join
|
, sheetMarkingText = sfMarkingText
|
||||||
<*> (case mbAid of -- TODO: Someone please refactor this late-night mess here!
|
, sheetVisibleFrom = sfVisibleFrom
|
||||||
Nothing -> return False
|
, sheetActiveFrom = sfActiveFrom
|
||||||
(Just aid) -> do
|
, sheetActiveTo = sfActiveTo
|
||||||
regL <- getBy (UniqueCourseParticipant cid aid)
|
, sheetHintFrom = sfHintFrom
|
||||||
return $ isJust regL)
|
, sheetSolutionFrom = sfSolutionFrom
|
||||||
return $ (courseEnt,dependent)
|
}
|
||||||
let course = entityVal courseEnt
|
mbsid <- dbAction newSheet
|
||||||
(regWidget, regEnctype) <- generateFormPost $ identifyForm "registerBtn" $ registerButton $ mbRegistered
|
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
|
defaultLayout $ do
|
||||||
setTitle $ [shamlet| #{termToText tid} - #{csh}|]
|
setTitleI pageTitle
|
||||||
$(widgetFile "course")
|
$(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
|
||||||
|
|||||||
@ -8,6 +8,7 @@
|
|||||||
{-# LANGUAGE FlexibleContexts #-}
|
{-# LANGUAGE FlexibleContexts #-}
|
||||||
{-# LANGUAGE TupleSections #-}
|
{-# LANGUAGE TupleSections #-}
|
||||||
{-# LANGUAGE LambdaCase #-}
|
{-# LANGUAGE LambdaCase #-}
|
||||||
|
{-# LANGUAGE ViewPatterns #-}
|
||||||
|
|
||||||
module Handler.Submission where
|
module Handler.Submission where
|
||||||
|
|
||||||
@ -53,7 +54,7 @@ submissionTable = do
|
|||||||
(,,) <$> encrypt submissionId <*> encrypt submissionId <*> pure s
|
(,,) <$> encrypt submissionId <*> encrypt submissionId <*> pure s
|
||||||
|
|
||||||
let
|
let
|
||||||
anchorCourse (_, _, (_, _, Entity _ Course{..})) = CourseShowR courseTermId courseShorthand
|
anchorCourse (_, _, (_, _, Entity _ Course{..})) = CourseR courseTermId courseShorthand CourseShowR
|
||||||
courseText (_, _, (_, _, Entity _ Course{..})) = toWidget courseName
|
courseText (_, _, (_, _, Entity _ Course{..})) = toWidget courseName
|
||||||
anchorSubmission (_, cUUID, _) = SubmissionR cUUID
|
anchorSubmission (_, cUUID, _) = SubmissionR cUUID
|
||||||
submissionText (cID, _, _) = toWidget . toPathPiece . CI.foldedCase $ ciphertext cID
|
submissionText (cID, _, _) = toWidget . toPathPiece . CI.foldedCase $ ciphertext cID
|
||||||
@ -177,8 +178,12 @@ postSubmissionDownloadMultiArchiveR = do
|
|||||||
|
|
||||||
withinDirectory f@File{..} = f { fileTitle = directoryName </> fileTitle }
|
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
|
yield $ File
|
||||||
{ fileModified = submissionChanged
|
{ fileModified = lastEditTime
|
||||||
, fileTitle = directoryName
|
, fileTitle = directoryName
|
||||||
, fileContent = Nothing
|
, fileContent = Nothing
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,11 +9,11 @@
|
|||||||
|
|
||||||
module Handler.Term where
|
module Handler.Term where
|
||||||
|
|
||||||
import Import
|
import Import
|
||||||
import Handler.Utils
|
import Handler.Utils
|
||||||
|
|
||||||
import qualified Data.Text as T
|
-- import qualified Data.Text as T
|
||||||
import Yesod.Form.Bootstrap3
|
import Yesod.Form.Bootstrap3
|
||||||
|
|
||||||
import Colonnade hiding (bool)
|
import Colonnade hiding (bool)
|
||||||
import Yesod.Colonnade
|
import Yesod.Colonnade
|
||||||
@ -27,47 +27,47 @@ getTermShowR = do
|
|||||||
-- term <- runDB $ E.select . E.from $ \(term) -> do
|
-- term <- runDB $ E.select . E.from $ \(term) -> do
|
||||||
-- E.orderBy [E.desc $ term E.^. TermStart ]
|
-- E.orderBy [E.desc $ term E.^. TermStart ]
|
||||||
-- return term
|
-- return term
|
||||||
--
|
--
|
||||||
let
|
let
|
||||||
termData = E.from $ \term -> do
|
termData = E.from $ \term -> do
|
||||||
-- E.orderBy [E.desc $ term E.^. TermStart ]
|
-- E.orderBy [E.desc $ term E.^. TermStart ]
|
||||||
let courseCount :: E.SqlExpr (E.Value Int)
|
let courseCount :: E.SqlExpr (E.Value Int)
|
||||||
courseCount = E.sub_select . E.from $ \course -> do
|
courseCount = E.sub_select . E.from $ \course -> do
|
||||||
E.where_ $ term E.^. TermId E.==. course E.^. CourseTermId
|
E.where_ $ term E.^. TermId E.==. course E.^. CourseTermId
|
||||||
return E.countRows
|
return E.countRows
|
||||||
return (term, courseCount)
|
return (term, courseCount)
|
||||||
selectRep $ do
|
selectRep $ do
|
||||||
provideRep $ toJSON . map fst <$> runDB (E.select termData)
|
provideRep $ toJSON . map fst <$> runDB (E.select termData)
|
||||||
provideRep $ do
|
provideRep $ do
|
||||||
let colonnadeTerms = mconcat
|
let colonnadeTerms = mconcat
|
||||||
[ headed "Kürzel" $ \(Entity tid Term{..},_) -> cell $ do
|
[ headed "Kürzel" $ \(Entity tid Term{..},_) -> cell $ do
|
||||||
-- Scrap this if to slow, create term edit page instead
|
-- Scrap this if to slow, create term edit page instead
|
||||||
adminLink <- handlerToWidget $ isAuthorized (TermEditExistR tid) False
|
adminLink <- handlerToWidget $ isAuthorized (TermEditExistR tid) False
|
||||||
[whamlet|
|
[whamlet|
|
||||||
$if adminLink == Authorized
|
$if adminLink == Authorized
|
||||||
<a href=@{TermEditExistR tid}>
|
<a href=@{TermEditExistR tid}>
|
||||||
#{termToText termName}
|
#{termToText termName}
|
||||||
$else
|
$else
|
||||||
#{termToText termName}
|
#{termToText termName}
|
||||||
|]
|
|]
|
||||||
, headed "Beginn Vorlesungen" $ \(Entity _ Term{..},_) ->
|
, headed "Beginn Vorlesungen" $ \(Entity _ Term{..},_) ->
|
||||||
stringCell $ formatTimeGerWD termLectureStart
|
stringCell $ formatTimeGerWD termLectureStart
|
||||||
, headed "Ende Vorlesungen" $ \(Entity _ Term{..},_) ->
|
, headed "Ende Vorlesungen" $ \(Entity _ Term{..},_) ->
|
||||||
stringCell $ formatTimeGerWD termLectureEnd
|
stringCell $ formatTimeGerWD termLectureEnd
|
||||||
, headed "Aktiv" $ \(Entity _ Term{..},_) ->
|
, headed "Aktiv" $ \(Entity _ Term{..},_) ->
|
||||||
textCell $ bool "" tickmark termActive
|
textCell $ bool "" tickmark termActive
|
||||||
, headed "Kursliste" $ \(Entity tid Term{..}, E.Value numCourses) ->
|
, headed "Kursliste" $ \(Entity tid Term{..}, E.Value numCourses) ->
|
||||||
cell [whamlet|
|
cell [whamlet|
|
||||||
<a href=@{CourseListTermR tid}>
|
<a href=@{CourseListTermR tid}>
|
||||||
#{show numCourses} Kurse
|
#{show numCourses} Kurse
|
||||||
|]
|
|]
|
||||||
, headed "Semesteranfang" $ \(Entity _ Term{..},_) ->
|
, headed "Semesteranfang" $ \(Entity _ Term{..},_) ->
|
||||||
stringCell $ formatTimeGerWD termStart
|
stringCell $ formatTimeGerWD termStart
|
||||||
, headed "Semesterende" $ \(Entity _ Term{..},_) ->
|
, headed "Semesterende" $ \(Entity _ Term{..},_) ->
|
||||||
stringCell $ formatTimeGerWD termEnd
|
stringCell $ formatTimeGerWD termEnd
|
||||||
, headed "Feiertage im Semester" $ \(Entity _ Term{..},_) ->
|
, headed "Feiertage im Semester" $ \(Entity _ Term{..},_) ->
|
||||||
stringCell $ (intercalate ", ") $ map formatTimeGerWD termHolidays
|
stringCell $ (intercalate ", ") $ map formatTimeGerWD termHolidays
|
||||||
]
|
]
|
||||||
table <- dbTable def $ DBTable
|
table <- dbTable def $ DBTable
|
||||||
{ dbtSQLQuery = termData
|
{ dbtSQLQuery = termData
|
||||||
, dbtColonnade = colonnadeTerms
|
, dbtColonnade = colonnadeTerms
|
||||||
@ -75,44 +75,54 @@ getTermShowR = do
|
|||||||
, dbtAttrs = tableDefault
|
, dbtAttrs = tableDefault
|
||||||
, dbtIdent = "terms" :: Text
|
, 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"
|
setTitle "Freigeschaltete Semester"
|
||||||
table
|
$(widgetFile "terms")
|
||||||
|
|
||||||
|
|
||||||
getTermEditR :: Handler Html
|
getTermEditR :: Handler Html
|
||||||
getTermEditR = do
|
getTermEditR = do
|
||||||
-- TODO: Defaults für Semester hier ermitteln und übergeben
|
-- TODO: Defaults für Semester hier ermitteln und übergeben
|
||||||
termEditHandler Nothing
|
termEditHandler Nothing
|
||||||
|
|
||||||
postTermEditR :: Handler Html
|
postTermEditR :: Handler Html
|
||||||
postTermEditR = termEditHandler Nothing
|
postTermEditR = termEditHandler Nothing
|
||||||
|
|
||||||
getTermEditExistR :: TermId -> Handler Html
|
getTermEditExistR :: TermId -> Handler Html
|
||||||
getTermEditExistR tid = do
|
getTermEditExistR tid = do
|
||||||
term <- runDB $ get tid
|
term <- runDB $ get tid
|
||||||
termEditHandler term
|
termEditHandler term
|
||||||
|
|
||||||
|
|
||||||
termEditHandler :: Maybe Term -> Handler Html
|
termEditHandler :: Maybe Term -> Handler Html
|
||||||
termEditHandler term = do
|
termEditHandler term = do
|
||||||
((result, formWidget), formEnctype) <- runFormPost $ newTermForm term
|
((result, formWidget), formEnctype) <- runFormPost $ newTermForm term
|
||||||
case result of
|
case result of
|
||||||
(FormSuccess res) -> do
|
(FormSuccess res) -> do
|
||||||
-- term <- runDB $ get $ TermKey termName
|
-- term <- runDB $ get $ TermKey termName
|
||||||
runDB $ repsert (TermKey $ termName res) res
|
runDB $ repsert (TermKey $ termName res) res
|
||||||
let tid = termToText $ termName res
|
-- VOR INTERNATIONALISIERUNG:
|
||||||
let msg = "Semester " `T.append` tid `T.append` " erfolgreich editiert."
|
-- let tid = termToText $ termName res
|
||||||
addMessage "success" [shamlet| #{msg} |]
|
-- let msg = "Semester " `T.append` tid `T.append` " erfolgreich editiert."
|
||||||
|
-- addMessage "success" [shamlet| #{msg} |]
|
||||||
|
-- MIT INTERNATIONALISIERUNG:
|
||||||
|
addMessageI "success" $ MsgTermEdited $ termName res
|
||||||
redirect TermShowR
|
redirect TermShowR
|
||||||
(FormMissing ) -> return ()
|
(FormMissing ) -> return ()
|
||||||
(FormFailure _) -> addMessage "warning" "Bitte Eingabe korrigieren."
|
(FormFailure _) -> addMessageI "warning" MsgInvalidInput
|
||||||
let formTitle = "Semester editieren/anlegen" :: Text
|
let formTitle = "Semester editieren/anlegen" :: Text
|
||||||
let actionUrl = TermEditR
|
let actionUrl = TermEditR
|
||||||
defaultLayout $ do
|
defaultLayout $ do
|
||||||
setTitle [shamlet| #{formTitle} |]
|
setTitle [shamlet| #{formTitle} |]
|
||||||
$(widgetFile "formPage")
|
$(widgetFile "formPage")
|
||||||
|
|
||||||
newTermForm :: Maybe Term -> Form Term
|
newTermForm :: Maybe Term -> Form Term
|
||||||
newTermForm template html = do
|
newTermForm template html = do
|
||||||
(result, widget) <- flip (renderAForm FormStandard) html $ Term
|
(result, widget) <- flip (renderAForm FormStandard) html $ Term
|
||||||
@ -124,8 +134,8 @@ newTermForm template html = do
|
|||||||
<*> areq dayField (bfs ("Ende Vorlesungen" :: Text)) (termLectureEnd <$> template)
|
<*> areq dayField (bfs ("Ende Vorlesungen" :: Text)) (termLectureEnd <$> template)
|
||||||
<*> areq checkBoxField (bfs ("Aktiv" :: Text)) (termActive <$> template)
|
<*> areq checkBoxField (bfs ("Aktiv" :: Text)) (termActive <$> template)
|
||||||
<* submitButton
|
<* submitButton
|
||||||
return $ case result of
|
return $ case result of
|
||||||
FormSuccess termResult
|
FormSuccess termResult
|
||||||
| errorMsgs <- validateTerm termResult
|
| errorMsgs <- validateTerm termResult
|
||||||
, not $ null errorMsgs ->
|
, not $ null errorMsgs ->
|
||||||
(FormFailure errorMsgs,
|
(FormFailure errorMsgs,
|
||||||
@ -134,13 +144,13 @@ newTermForm template html = do
|
|||||||
<h4> Fehler:
|
<h4> Fehler:
|
||||||
<ul>
|
<ul>
|
||||||
$forall errmsg <- errorMsgs
|
$forall errmsg <- errorMsgs
|
||||||
<li> #{errmsg}
|
<li> #{errmsg}
|
||||||
^{widget}
|
^{widget}
|
||||||
|]
|
|]
|
||||||
)
|
)
|
||||||
_ -> (result, widget)
|
_ -> (result, widget)
|
||||||
{-
|
{-
|
||||||
where
|
where
|
||||||
set :: Text -> FieldSettings site
|
set :: Text -> FieldSettings site
|
||||||
set = bfs
|
set = bfs
|
||||||
-}
|
-}
|
||||||
|
|||||||
@ -41,5 +41,5 @@ getUsersR = do
|
|||||||
-- ++ map (\school -> headed (text2widget $ schoolName $ entityVal school) (\u -> "xx")) schools
|
-- ++ map (\school -> headed (text2widget $ schoolName $ entityVal school) (\u -> "xx")) schools
|
||||||
defaultLayout $ do
|
defaultLayout $ do
|
||||||
setTitle "Comprehensive User List"
|
setTitle "Comprehensive User List"
|
||||||
let userList = encodeWidgetTable tableDefault colonnadeUsers users
|
let userList = encodeWidgetTable tableSortable colonnadeUsers users
|
||||||
$(widgetFile "users")
|
$(widgetFile "users")
|
||||||
|
|||||||
@ -1,12 +1,15 @@
|
|||||||
{-# LANGUAGE NoImplicitPrelude #-}
|
{-# LANGUAGE NoImplicitPrelude #-}
|
||||||
{-# LANGUAGE OverloadedStrings #-}
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
{-# LANGUAGE FlexibleContexts #-}
|
{-# LANGUAGE TypeFamilies, FlexibleContexts, ConstraintKinds #-}
|
||||||
{-# LANGUAGE QuasiQuotes #-}
|
{-# LANGUAGE QuasiQuotes #-}
|
||||||
|
|
||||||
|
|
||||||
module Handler.Utils
|
module Handler.Utils
|
||||||
( module Handler.Utils
|
( module Handler.Utils
|
||||||
) where
|
) where
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
import Import.NoFoundation
|
import Import.NoFoundation
|
||||||
|
|
||||||
import Handler.Utils.DateTime as Handler.Utils
|
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.Zip as Handler.Utils
|
||||||
import Handler.Utils.Rating as Handler.Utils
|
import Handler.Utils.Rating as Handler.Utils
|
||||||
import Handler.Utils.Submission as Handler.Utils
|
import Handler.Utils.Submission as Handler.Utils
|
||||||
|
import Handler.Utils.Templates as Handler.Utils
|
||||||
|
|
||||||
import Text.Blaze (Markup, ToMarkup)
|
import Text.Blaze (Markup, ToMarkup)
|
||||||
|
|
||||||
import Data.Map (Map)
|
import Data.Map (Map)
|
||||||
import qualified Data.Map as Map
|
import qualified Data.Map as Map
|
||||||
|
import qualified Data.List as List
|
||||||
|
|
||||||
|
import Database.Persist.Class
|
||||||
|
|
||||||
tickmark :: IsString a => a
|
tickmark :: IsString a => a
|
||||||
tickmark = fromString "✔"
|
tickmark = fromString "✔"
|
||||||
@ -54,9 +61,69 @@ whenIsJust :: Monad m => Maybe a -> (a -> m ()) -> m ()
|
|||||||
whenIsJust (Just x) f = f x
|
whenIsJust (Just x) f = f x
|
||||||
whenIsJust Nothing _ = return ()
|
whenIsJust Nothing _ = return ()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
----------
|
----------
|
||||||
-- Maps --
|
-- Maps --
|
||||||
----------
|
----------
|
||||||
entities2map :: PersistEntity record => [Entity record] -> Map (Key record) record
|
entities2map :: PersistEntity record => [Entity record] -> Map (Key record) record
|
||||||
entities2map = foldl' (\m entity -> Map.insert (entityKey entity) (entityVal entity) m) Map.empty
|
entities2map = foldl' (\m entity -> Map.insert (entityKey entity) (entityVal entity) m) Map.empty
|
||||||
|
|
||||||
|
|
||||||
|
------------
|
||||||
|
-- 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)
|
||||||
|
|||||||
@ -7,8 +7,13 @@
|
|||||||
{-# LANGUAGE TypeFamilies #-}
|
{-# LANGUAGE TypeFamilies #-}
|
||||||
{-# LANGUAGE FlexibleContexts #-}
|
{-# LANGUAGE FlexibleContexts #-}
|
||||||
{-# LANGUAGE ViewPatterns #-}
|
{-# 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 Import
|
||||||
import qualified Data.Char as Char
|
import qualified Data.Char as Char
|
||||||
@ -21,12 +26,20 @@ import qualified Data.Foldable as Foldable
|
|||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
-- import Yesod.Form.Types
|
-- import Yesod.Form.Types
|
||||||
import Yesod.Form.Functions (parseHelper)
|
import Yesod.Form.Functions (parseHelper)
|
||||||
import Yesod.Form.Bootstrap3
|
import Yesod.Form.Bootstrap3
|
||||||
|
|
||||||
import qualified Text.Blaze.Internal as Blaze (null)
|
import qualified Text.Blaze.Internal as Blaze (null)
|
||||||
|
|
||||||
import Web.PathPieces (showToPathPiece, readFromPathPiece)
|
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 --
|
-- Unique Form Identifiers to avoid accidents --
|
||||||
------------------------------------------------
|
------------------------------------------------
|
||||||
@ -35,9 +48,14 @@ data FormIdentifier = FIDcourse | FIDsheet
|
|||||||
deriving (Enum, Eq, Ord, Bounded, Read, Show)
|
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)
|
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 --
|
-- Form Renderer --
|
||||||
-------------------
|
-------------------
|
||||||
@ -48,7 +66,7 @@ data FormLayout = FormStandard
|
|||||||
renderAForm :: Monad m => FormLayout -> FormRender m a
|
renderAForm :: Monad m => FormLayout -> FormRender m a
|
||||||
renderAForm formLayout aform fragment = do
|
renderAForm formLayout aform fragment = do
|
||||||
(res, (($ []) -> views)) <- aFormToForm aform
|
(res, (($ []) -> views)) <- aFormToForm aform
|
||||||
let widget = $(widgetFile "form")
|
let widget = $(widgetFile "widgets/form")
|
||||||
return (res, widget)
|
return (res, widget)
|
||||||
|
|
||||||
----------------------------
|
----------------------------
|
||||||
@ -67,26 +85,23 @@ class (Enum a, Bounded a, Ord a, PathPiece a) => Button a where
|
|||||||
|
|
||||||
cssClass :: a -> ButtonCssClass
|
cssClass :: a -> ButtonCssClass
|
||||||
cssClass _ = BCDefault
|
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)
|
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
|
toPathPiece = showToPathPiece
|
||||||
fromPathPiece = readFromPathPiece
|
fromPathPiece = readFromPathPiece
|
||||||
|
|
||||||
instance Button StandardButton where
|
instance Button BtnDelete where
|
||||||
label BtnDelete = "Löschen"
|
label BtnDelete = "Löschen"
|
||||||
label BtnAbort = "Abbrechen"
|
label BtnAbort = "Abrechen"
|
||||||
label BtnSave = "Speichern"
|
|
||||||
|
cssClass BtnDelete = BCDanger
|
||||||
cssClass BtnDelete = BCWarning
|
|
||||||
cssClass BtnAbort = BCDefault
|
cssClass BtnAbort = BCDefault
|
||||||
cssClass BtnSave = BCPrimary
|
|
||||||
-}
|
|
||||||
|
|
||||||
data SubmitButton = BtnSubmit
|
data SubmitButton = BtnSubmit
|
||||||
deriving (Enum, Eq, Ord, Bounded, Read, Show)
|
deriving (Enum, Eq, Ord, Bounded, Read, Show)
|
||||||
@ -99,45 +114,45 @@ instance Button SubmitButton where
|
|||||||
label BtnSubmit = "Submit"
|
label BtnSubmit = "Submit"
|
||||||
|
|
||||||
cssClass BtnSubmit = BCPrimary
|
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)
|
-- data LinkButton = LinkButton (Route UniWorX)
|
||||||
-- deriving (Enum, Eq, Ord, Bounded, Read, Show)
|
-- deriving (Enum, Eq, Ord, Bounded, Read, Show)
|
||||||
--
|
--
|
||||||
-- instance PathPiece LinkButton where
|
-- instance PathPiece LinkButton where
|
||||||
-- LinkButton route = ???
|
-- LinkButton route = ???
|
||||||
|
|
||||||
linkButton :: Widget -> ButtonCssClass -> Route UniWorX -> Widget
|
linkButton :: Widget -> ButtonCssClass -> Route UniWorX -> Widget
|
||||||
linkButton lbl cls url = [whamlet| <a href=@{url} .btn .#{bcc2txt cls} role=button>^{lbl} |]
|
linkButton lbl cls url = [whamlet| <a href=@{url} .btn .#{bcc2txt cls} role=button>^{lbl} |]
|
||||||
-- [whamlet|
|
-- [whamlet|
|
||||||
-- <form method=post action=@{url}>
|
-- <form method=post action=@{url}>
|
||||||
-- <input type="hidden" name="_formid" value="identify-linkButton">
|
-- <input type="hidden" name="_formid" value="identify-linkButton">
|
||||||
-- <button .btn .#{bcc2txt cls} type=submit value="Link to @{url}">^{lbl}
|
-- <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 :: Button a => a -> Field Handler a
|
||||||
buttonField btn = Field {fieldParse, fieldView, fieldEnctype}
|
buttonField btn = Field {fieldParse, fieldView, fieldEnctype}
|
||||||
where
|
where
|
||||||
fieldEnctype = UrlEncoded
|
fieldEnctype = UrlEncoded
|
||||||
|
|
||||||
fieldView fid name attrs _val _ =
|
fieldView fid name attrs _val _ =
|
||||||
[whamlet|
|
[whamlet|
|
||||||
<button .btn .#{bcc2txt $ cssClass btn} type=submit name=#{name} value=#{toPathPiece btn} *{attrs} ##{fid}>^{label btn}
|
<button .btn .#{bcc2txt $ cssClass btn} type=submit name=#{name} value=#{toPathPiece btn} *{attrs} ##{fid}>^{label btn}
|
||||||
|]
|
|]
|
||||||
|
|
||||||
fieldParse [] _ = return $ Right Nothing
|
fieldParse [] _ = return $ Right Nothing
|
||||||
fieldParse [str] _
|
fieldParse [str] _
|
||||||
| str == toPathPiece btn = return $ Right $ Just btn
|
| str == toPathPiece btn = return $ Right $ Just btn
|
||||||
| otherwise = return $ Left "Wrong button value"
|
| otherwise = return $ Left "Wrong button value"
|
||||||
fieldParse _ _ = return $ Left "Multiple button values"
|
fieldParse _ _ = return $ Left "Multiple button values"
|
||||||
|
|
||||||
|
|
||||||
combinedButtonField :: Button a => [a] -> AForm Handler [Maybe a]
|
combinedButtonField :: Button a => [a] -> AForm Handler [Maybe a]
|
||||||
combinedButtonField btns = traverse b2f btns
|
combinedButtonField btns = traverse b2f btns
|
||||||
where
|
where
|
||||||
b2f b = aopt (buttonField b) "" Nothing
|
b2f b = aopt (buttonField b) "" Nothing
|
||||||
|
|
||||||
submitButton :: AForm Handler ()
|
submitButton :: AForm Handler ()
|
||||||
submitButton = void $ combinedButtonField [BtnSubmit]
|
submitButton = void $ combinedButtonField [BtnSubmit]
|
||||||
@ -146,10 +161,10 @@ submitButton = void $ combinedButtonField [BtnSubmit]
|
|||||||
combinedButtonField :: Button a => [a] -> Form m -> Form (a,m)
|
combinedButtonField :: Button a => [a] -> Form m -> Form (a,m)
|
||||||
combinedButtonField btns inner csrf = do
|
combinedButtonField btns inner csrf = do
|
||||||
buttonIdent <- newFormIdent
|
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]
|
(results, btnViews) <- unzip <$> mapM button [minBound..maxBound]
|
||||||
(innerRes,innerWdgt) <- inner
|
(innerRes,innerWdgt) <- inner
|
||||||
let widget = do
|
let widget = do
|
||||||
[whamlet|
|
[whamlet|
|
||||||
#{csrf}
|
#{csrf}
|
||||||
^{innerWdgt}
|
^{innerWdgt}
|
||||||
@ -171,14 +186,14 @@ combinedButtonField btns inner csrf = do
|
|||||||
accResult' _ x@(FormSuccess _) = x --SJ: Is this safe? Shouldn't Failure override Success?
|
accResult' _ x@(FormSuccess _) = x --SJ: Is this safe? Shouldn't Failure override Success?
|
||||||
accResult' (FormSuccess Nothing) x = x
|
accResult' (FormSuccess Nothing) x = x
|
||||||
accResult' FormMissing _ = FormMissing
|
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 => Markup -> MForm (HandlerT UniWorX IO) (FormResult a, (WidgetT UniWorX IO ()))
|
||||||
buttonForm :: (Button a) => Form a
|
buttonForm :: (Button a) => Form a
|
||||||
buttonForm csrf = do
|
buttonForm csrf = do
|
||||||
buttonIdent <- newFormIdent
|
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]
|
(results, btnViews) <- unzip <$> mapM button [minBound..maxBound]
|
||||||
let widget = do
|
let widget = do
|
||||||
[whamlet|
|
[whamlet|
|
||||||
@ -199,44 +214,8 @@ buttonForm csrf = do
|
|||||||
accResult' FormMissing _ = FormMissing
|
accResult' FormMissing _ = FormMissing
|
||||||
accResult' (FormFailure errs) _ = FormFailure errs
|
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 --
|
-- Fields --
|
||||||
------------
|
------------
|
||||||
@ -244,6 +223,9 @@ postButtonForm lblId = identifyForm lblId buttonF
|
|||||||
natField :: (Monad m, Integral i, RenderMessage (HandlerSite m) FormMessage) => Text -> Field m i
|
natField :: (Monad m, Integral i, RenderMessage (HandlerSite m) FormMessage) => Text -> Field m i
|
||||||
natField d = checkBool (>= 0) (T.append d " muss eine natürliche Zahl sein.") $ intField
|
natField d = checkBool (>= 0) (T.append d " muss eine natürliche Zahl sein.") $ intField
|
||||||
|
|
||||||
|
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 :: (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
|
posIntField d = checkBool (>= 1) (T.append d " muss eine positive Zahl sein.") $ intField
|
||||||
|
|
||||||
@ -263,6 +245,55 @@ schoolEntField = selectField schools
|
|||||||
where
|
where
|
||||||
schools = optionsPersist [] [Asc SchoolName] schoolName
|
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 :: FieldSettings UniWorX -> Maybe SheetType -> AForm Handler SheetType
|
||||||
sheetTypeAFormReq d Nothing =
|
sheetTypeAFormReq d Nothing =
|
||||||
-- TODO, offer options to choose between Normal/Bonus/Pass
|
-- 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
|
-- TODO, offer options to choose between Normal/Bonus/Pass
|
||||||
(Normal . toPoints) <$> areq (natField "Punkte") d (Just $ fromPoints p)
|
(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
|
utcTimeField :: (Monad m, RenderMessage (HandlerSite m) FormMessage) => Field m UTCTime
|
||||||
-- StackOverflow: dayToUTC <$> (areq (jqueryDayField def {...}) settings Nothing)
|
-- 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
|
{ fieldParse = parseHelper $ readTime
|
||||||
, fieldView = \theId name attrs val isReq ->
|
, fieldView = \theId name attrs val isReq ->
|
||||||
[whamlet|
|
[whamlet|
|
||||||
@ -282,42 +322,42 @@ utcTimeField = Field
|
|||||||
|]
|
|]
|
||||||
, fieldEnctype = UrlEncoded
|
, fieldEnctype = UrlEncoded
|
||||||
}
|
}
|
||||||
where
|
where
|
||||||
fieldTimeFormat :: String
|
fieldTimeFormat :: String
|
||||||
--fieldTimeFormat = "%e.%m.%y %k:%M"
|
--fieldTimeFormat = "%e.%m.%y %k:%M"
|
||||||
fieldTimeFormat = "%Y-%m-%eT%H:%M"
|
fieldTimeFormat = "%Y-%m-%eT%H:%M"
|
||||||
|
|
||||||
readTime :: Text -> Either FormMessage UTCTime
|
readTime :: Text -> Either FormMessage UTCTime
|
||||||
readTime t =
|
readTime t =
|
||||||
case parseTimeM True germanTimeLocale fieldTimeFormat (T.unpack t) of
|
case parseTimeM True germanTimeLocale fieldTimeFormat (T.unpack t) of
|
||||||
(Just time) -> Right time
|
(Just time) -> Right time
|
||||||
Nothing -> Left $ MsgInvalidEntry $ "Datum/Zeit Format: tt.mm.yy hh:mm " ++ t
|
Nothing -> Left $ MsgInvalidEntry $ "Datum/Zeit Format: tt.mm.yy hh:mm " ++ t
|
||||||
|
|
||||||
showTime :: UTCTime -> Text
|
showTime :: UTCTime -> Text
|
||||||
showTime = fromString . (formatTime germanTimeLocale fieldTimeFormat)
|
showTime = fromString . (formatTime germanTimeLocale fieldTimeFormat)
|
||||||
|
|
||||||
|
|
||||||
fsb :: Text -> FieldSettings site
|
fsb :: Text -> FieldSettings site
|
||||||
fsb = bfs -- Just to avoid annoying Ambiguous Type Errors
|
fsb = bfs -- Just to avoid annoying Ambiguous Type Errors
|
||||||
|
|
||||||
addAttr :: Text -> Text -> FieldSettings site -> FieldSettings site
|
addAttr :: Text -> Text -> FieldSettings site -> FieldSettings site
|
||||||
addAttr attr valu fs = fs { fsAttrs=newAttrs (fsAttrs fs) }
|
addAttr attr valu fs = fs { fsAttrs=newAttrs (fsAttrs fs) }
|
||||||
where
|
where
|
||||||
newAttrs :: [(Text,Text)] -> [(Text,Text)]
|
newAttrs :: [(Text,Text)] -> [(Text,Text)]
|
||||||
newAttrs [] = [(attr,valu)]
|
newAttrs [] = [(attr,valu)]
|
||||||
newAttrs (p@(a,v):t)
|
newAttrs (p@(a,v):t)
|
||||||
| attr==a = (a,T.append valu $ cons ' ' 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 :: Text -> [Text] -> FieldSettings site -> FieldSettings site
|
||||||
addAttrs attr valus fs = fs { fsAttrs=newAttrs (fsAttrs fs) }
|
addAttrs attr valus fs = fs { fsAttrs=newAttrs (fsAttrs fs) }
|
||||||
where
|
where
|
||||||
newAttrs :: [(Text,Text)] -> [(Text,Text)]
|
newAttrs :: [(Text,Text)] -> [(Text,Text)]
|
||||||
newAttrs [] = [(attr,T.intercalate " " valus)]
|
newAttrs [] = [(attr,T.intercalate " " valus)]
|
||||||
newAttrs (p@(a,v):t)
|
newAttrs (p@(a,v):t)
|
||||||
| attr==a = (a,T.intercalate " " (v:valus)):t
|
| attr==a = (a,T.intercalate " " (v:valus)):t
|
||||||
| otherwise = p:(newAttrs t)
|
| otherwise = p:(newAttrs t)
|
||||||
|
|
||||||
addClass :: Text -> FieldSettings site -> FieldSettings site
|
addClass :: Text -> FieldSettings site -> FieldSettings site
|
||||||
addClass = addAttr "class"
|
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) }
|
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) }
|
setClass fs c = fs { fsAttrs=("class",c):(fsAttrs fs) }
|
||||||
|
|
||||||
setNameClass :: FieldSettings site -> Text -> Text -> FieldSettings site -- deprecated
|
setNameClass :: FieldSettings site -> Text -> Text -> FieldSettings site -- deprecated
|
||||||
@ -345,3 +385,24 @@ setTooltip tt fs
|
|||||||
| null tt = fs { fsTooltip = Nothing }
|
| null tt = fs { fsTooltip = Nothing }
|
||||||
| otherwise = fs { fsTooltip = Just $ fromString tt }
|
| otherwise = fs { fsTooltip = Just $ fromString tt }
|
||||||
|
|
||||||
|
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
|
||||||
|
|||||||
12
src/Handler/Utils/Form/Types.hs
Normal file
12
src/Handler/Utils/Form/Types.hs
Normal 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
|
||||||
|
}
|
||||||
@ -70,12 +70,9 @@ sinkSubmission sheetId userId mExists = do
|
|||||||
submissionRatingComment = Nothing
|
submissionRatingComment = Nothing
|
||||||
submissionRatingBy = Nothing
|
submissionRatingBy = Nothing
|
||||||
submissionRatingTime = 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
|
sId <$ sinkSubmission' sId isUpdate
|
||||||
where
|
where
|
||||||
@ -184,9 +181,9 @@ sinkSubmission sheetId userId mExists = do
|
|||||||
alreadyTouched <- gets $ getAny . sinkSubmissionTouched
|
alreadyTouched <- gets $ getAny . sinkSubmissionTouched
|
||||||
when (not alreadyTouched) $ do
|
when (not alreadyTouched) $ do
|
||||||
now <- liftIO getCurrentTime
|
now <- liftIO getCurrentTime
|
||||||
lift . update submissionId $ case isUpdate of
|
lift $ case isUpdate of
|
||||||
False -> [ SubmissionChangedBy =. userId, SubmissionChanged =. now ]
|
False -> insert_ $ SubmissionEdit userId now submissionId
|
||||||
True -> [ SubmissionRatingBy =. Just userId, SubmissionRatingTime =. Just now ]
|
True -> update submissionId [ SubmissionRatingBy =. Just userId, SubmissionRatingTime =. Just now ]
|
||||||
tell $ mempty{ sinkSubmissionTouched = Any True }
|
tell $ mempty{ sinkSubmissionTouched = Any True }
|
||||||
|
|
||||||
finalize :: SubmissionSinkState -> YesodDB UniWorX ()
|
finalize :: SubmissionSinkState -> YesodDB UniWorX ()
|
||||||
|
|||||||
@ -23,12 +23,15 @@ import Data.Either
|
|||||||
|
|
||||||
-- Table design
|
-- Table design
|
||||||
tableDefault :: Attribute
|
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
|
-- Colonnade Tools
|
||||||
numberColonnade :: (IsString c) => Colonnade Headed Int c
|
numberColonnade :: (IsString c) => Colonnade Headed Int c
|
||||||
numberColonnade = headed "Nr" (fromString.show)
|
numberColonnade = headed "Nr" (fromString.show)
|
||||||
|
|
||||||
pairColonnade :: (Functor h) => Colonnade h a c -> Colonnade h b c -> Colonnade h (a,b) c
|
pairColonnade :: (Functor h) => Colonnade h a c -> Colonnade h b c -> Colonnade h (a,b) c
|
||||||
pairColonnade a b = mconcat [ lmap fst a, lmap snd b]
|
pairColonnade a b = mconcat [ lmap fst a, lmap snd b]
|
||||||
|
|
||||||
@ -39,8 +42,8 @@ encodeHeadedWidgetTableNumbered attrs colo tdata =
|
|||||||
encodeWidgetTable attrs (mconcat [numberCol, lmap snd colo]) (zip [1..] tdata)
|
encodeWidgetTable attrs (mconcat [numberCol, lmap snd colo]) (zip [1..] tdata)
|
||||||
where
|
where
|
||||||
numberCol :: Colonnade Headed (Int,a) (WidgetT site IO ())
|
numberCol :: Colonnade Headed (Int,a) (WidgetT site IO ())
|
||||||
numberCol = headed "Nr" (fromString.show.fst)
|
numberCol = headed "Nr" (fromString.show.fst)
|
||||||
|
|
||||||
headedRowSelector :: ( PathPiece b
|
headedRowSelector :: ( PathPiece b
|
||||||
, Eq b
|
, Eq b
|
||||||
)
|
)
|
||||||
@ -74,7 +77,7 @@ headedRowSelector toExternal fromExternal attrs colonnade tdata = do
|
|||||||
selectionIdent <- newFormIdent
|
selectionIdent <- newFormIdent
|
||||||
|
|
||||||
(selectionResults, selectionBoxes) <- fmap unzip . forM externalIds $ \ident -> mopt (checkbox ident) ("" { fsName = Just selectionIdent }) Nothing
|
(selectionResults, selectionBoxes) <- fmap unzip . forM externalIds $ \ident -> mopt (checkbox ident) ("" { fsName = Just selectionIdent }) Nothing
|
||||||
|
|
||||||
let
|
let
|
||||||
selColonnade :: Colonnade Headed Int (Cell UniWorX)
|
selColonnade :: Colonnade Headed Int (Cell UniWorX)
|
||||||
selColonnade = headed "Markiert" $ cell . fvInput . (selectionBoxes !!)
|
selColonnade = headed "Markiert" $ cell . fvInput . (selectionBoxes !!)
|
||||||
|
|||||||
22
src/Handler/Utils/Templates.hs
Normal file
22
src/Handler/Utils/Templates.hs
Normal 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")
|
||||||
@ -11,7 +11,7 @@ module Handler.Utils.Zip
|
|||||||
, produceZip
|
, produceZip
|
||||||
, consumeZip
|
, consumeZip
|
||||||
, modifyFileTitle
|
, modifyFileTitle
|
||||||
, sourceFiles
|
, sourceFiles, acceptFile
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import Import
|
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 :: (MonadResource m, MonadThrow m, MonadIO m) => FileInfo -> Source m File
|
||||||
sourceFiles fInfo
|
sourceFiles fInfo
|
||||||
| mimeType == "application/zip" = fileSource fInfo =$= void consumeZip
|
| mimeType == "application/zip" = fileSource fInfo =$= void consumeZip
|
||||||
| otherwise = do
|
| otherwise = yieldM $ acceptFile fInfo
|
||||||
let fileTitle = unpack $ fileName fInfo
|
|
||||||
fileModified <- liftIO getCurrentTime
|
|
||||||
yieldM $ do
|
|
||||||
fileContent <- Just <$> runConduit (fileSource fInfo =$= foldC)
|
|
||||||
return File{..}
|
|
||||||
where
|
where
|
||||||
mimeType = defaultMimeLookup (fileName fInfo)
|
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{..}
|
||||||
|
|||||||
@ -16,3 +16,6 @@ import Data.Fixed as Import
|
|||||||
|
|
||||||
import CryptoID as Import
|
import CryptoID as Import
|
||||||
import Data.UUID as Import (UUID)
|
import Data.UUID as Import (UUID)
|
||||||
|
|
||||||
|
|
||||||
|
import Text.Lucius as Import
|
||||||
|
|||||||
@ -43,7 +43,7 @@ toPoints :: Integral a => a -> Points
|
|||||||
toPoints = MkFixed . fromIntegral
|
toPoints = MkFixed . fromIntegral
|
||||||
|
|
||||||
fromPoints :: Integral a => Points -> a
|
fromPoints :: Integral a => Points -> a
|
||||||
fromPoints = error "TODO: Types.fromPoints not yet implemented"
|
fromPoints (MkFixed c) = fromInteger c
|
||||||
|
|
||||||
data SheetType
|
data SheetType
|
||||||
= Bonus { maxPoints :: Points }
|
= Bonus { maxPoints :: Points }
|
||||||
@ -54,9 +54,13 @@ data SheetType
|
|||||||
deriveJSON defaultOptions ''SheetType
|
deriveJSON defaultOptions ''SheetType
|
||||||
derivePersistFieldJSON "SheetType"
|
derivePersistFieldJSON "SheetType"
|
||||||
|
|
||||||
data ExamStatus = Attended | NoShow | Voided
|
data SheetGroup
|
||||||
deriving (Show, Read, Eq, Ord, Enum, Bounded)
|
= Arbitrary { maxParticipants :: Int } -- Distinguish Limited/Arbitrary
|
||||||
derivePersistField "ExamStatus"
|
| RegisteredGroups
|
||||||
|
| NoGroups
|
||||||
|
deriving (Show, Read, Eq)
|
||||||
|
deriveJSON defaultOptions ''SheetGroup
|
||||||
|
derivePersistFieldJSON "SheetGroup"
|
||||||
|
|
||||||
data SheetFileType = SheetExercise | SheetHint | SheetSolution | SheetMarking
|
data SheetFileType = SheetExercise | SheetHint | SheetSolution | SheetMarking
|
||||||
deriving (Show, Read, Eq, Ord, Enum, Bounded)
|
deriving (Show, Read, Eq, Ord, Enum, Bounded)
|
||||||
@ -70,9 +74,11 @@ instance PathPiece SheetFileType where
|
|||||||
fromPathPiece t =
|
fromPathPiece t =
|
||||||
lookup (CI.mk t) [(CI.mk $ toPathPiece ty,ty) | ty <- [minBound..maxBound]]
|
lookup (CI.mk t) [(CI.mk $ toPathPiece ty,ty) | ty <- [minBound..maxBound]]
|
||||||
|
|
||||||
|
data ExamStatus = Attended | NoShow | Voided
|
||||||
|
deriving (Show, Read, Eq, Ord, Enum, Bounded)
|
||||||
|
derivePersistField "ExamStatus"
|
||||||
|
|
||||||
|
data Load = ByTutorial | ByProportion Rational
|
||||||
data Load = ByTutorial | ByProportion Double
|
|
||||||
deriving (Show, Read, Eq)
|
deriving (Show, Read, Eq)
|
||||||
derivePersistField "Load"
|
derivePersistField "Load"
|
||||||
|
|
||||||
@ -96,6 +102,10 @@ data TermIdentifier = TermIdentifier
|
|||||||
, season :: Season
|
, season :: Season
|
||||||
} deriving (Show, Read, Eq, Ord, Generic, Typeable)
|
} 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
|
--TODO: Enforce the number of digits within year, with parsing filling in the current leading digits? Goal: short urls
|
||||||
termToText :: TermIdentifier -> Text
|
termToText :: TermIdentifier -> Text
|
||||||
termToText TermIdentifier{..} = Text.pack $ seasonToChar season : show year
|
termToText TermIdentifier{..} = Text.pack $ seasonToChar season : show year
|
||||||
|
|||||||
7022
static/css/bootstrap.css
vendored
7022
static/css/bootstrap.css
vendored
File diff suppressed because it is too large
Load Diff
9
static/css/fonts.css
Normal file
9
static/css/fonts.css
Normal 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
34
static/css/icons.css
Normal 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';
|
||||||
|
}
|
||||||
7
static/js/featureChecker.js
Normal file
7
static/js/featureChecker.js
Normal 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);
|
||||||
@ -1,38 +1,34 @@
|
|||||||
<div .masthead>
|
<div .course-header>
|
||||||
<div .container>
|
<div .course-header__info>
|
||||||
<div .row>
|
<table>
|
||||||
<h1 .header>
|
<tr>
|
||||||
#{courseName course}
|
<th>Teilnehmer
|
||||||
$maybe school <- schoolMB
|
<td>
|
||||||
<h4>
|
#{participants}
|
||||||
#{schoolName school}
|
$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 .container>
|
||||||
<div .bs-docs-section>
|
$maybe descr <- courseDescription course
|
||||||
<div .row>
|
<h2 #description>Beschreibung
|
||||||
<div .col-lg-12>
|
<p> #{descr}
|
||||||
<div .page-header>
|
$maybe link <- courseLinkExternal course
|
||||||
|
<h4 #linl>Homepage
|
||||||
$maybe descr <- courseDescription course
|
<a href=#{link}>#{link}
|
||||||
<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>
|
|
||||||
|
|
||||||
|
|||||||
19
templates/course.lucius
Normal file
19
templates/course.lucius
Normal 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
4
templates/courses.hamlet
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<div .container>
|
||||||
|
<h1>Kursübersicht für Semester #{termToText $ unTermKey tidini}
|
||||||
|
<div .scrolltable>
|
||||||
|
^{coursesTable}
|
||||||
@ -19,12 +19,7 @@ $newline never
|
|||||||
\<!--[if lt IE 9]>
|
\<!--[if lt IE 9]>
|
||||||
\<script src="http://html5shiv.googlecode.com/svn/trunk/html5.js"></script>
|
\<script src="http://html5shiv.googlecode.com/svn/trunk/html5.js"></script>
|
||||||
\<![endif]-->
|
\<![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">
|
<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>
|
<script>
|
||||||
/* The `defaultCsrfMiddleware` Middleware added in Foundation.hs adds a CSRF token to the request cookies. */
|
/* 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. */
|
/* 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>
|
<script>
|
||||||
document.documentElement.className = document.documentElement.className.replace(/\bno-js\b/,'js');
|
document.body.classList.remove('no-js');
|
||||||
<body>
|
|
||||||
^{pageBody pc}
|
^{pageBody pc}
|
||||||
|
|
||||||
$maybe analytics <- appAnalytics $ appSettings master
|
$maybe analytics <- appAnalytics $ appSettings master
|
||||||
|
|||||||
@ -1,60 +1,20 @@
|
|||||||
|
<!-- navigation -->
|
||||||
|
^{navbar}
|
||||||
|
|
||||||
<!-- Static navbar -->
|
<div .main>
|
||||||
<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 #navbar .collapse.navbar-collapse>
|
<!-- secondary navigation at the side -->
|
||||||
<ul .nav.navbar-nav>
|
^{asidenav}
|
||||||
$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 _
|
|
||||||
|
|
||||||
<ul .nav.navbar-nav.navbar-right>
|
<div .main__content>
|
||||||
$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}
|
|
||||||
|
|
||||||
|
<!-- alerts -->
|
||||||
$forall (status, msg) <- mmsgs
|
$forall (status, msg) <- mmsgs
|
||||||
$with status2 <- bool status "info" (status == "")
|
$with status2 <- bool status "info" (status == "")
|
||||||
<div class="alert alert-#{status2}">#{msg}
|
<div class="alert alert-#{status2}">#{msg}
|
||||||
|
|
||||||
|
<!-- prime page actions -->
|
||||||
|
^{pageactionprime}
|
||||||
|
|
||||||
$if (Just HomeR == mcurrentRoute)
|
<!-- actual content -->
|
||||||
^{widget}
|
^{widget}
|
||||||
$else
|
|
||||||
<div .container>
|
|
||||||
<div .row>
|
|
||||||
<div .col-md-12>
|
|
||||||
^{widget}
|
|
||||||
|
|
||||||
<!-- Footer -->
|
|
||||||
<footer .footer>
|
|
||||||
<div .container>
|
|
||||||
<p .text-muted>
|
|
||||||
#{appCopyright $ appSettings master}
|
|
||||||
|
|||||||
@ -1,73 +1,221 @@
|
|||||||
.masthead,
|
:root {
|
||||||
.navbar {
|
/* THEME 1 */
|
||||||
background-color: rgb(27, 28, 29);
|
--base00: #72a85b;
|
||||||
}
|
--base-bg-color: #1d1c1d;
|
||||||
|
--base-font-color: #fff;
|
||||||
.navbar-default .navbar-nav > .active > a {
|
--sec-font-color: #fff;
|
||||||
background-color: transparent;
|
--box-bg-color: #3c3c3c;
|
||||||
border-bottom: 2px solid white;
|
/* THEME 2 */
|
||||||
}
|
--base00: #38428a;
|
||||||
|
--base-bg-color: #ffffff;
|
||||||
.navbar-nav {
|
--base-font-color: rgb(53, 53, 53);
|
||||||
padding-bottom: 1em;
|
--sec-font-color: #eaf2ff;
|
||||||
}
|
--box-bg-color: #dddddd;
|
||||||
|
/* THEME 3 */
|
||||||
.masthead {
|
--darkbase: #364B60;
|
||||||
margin-top: -21px;
|
--lightbase: #2490E8;
|
||||||
color: white;
|
--lighterbase: #60C2FF;
|
||||||
text-align: center;
|
--whitebase: #FCFFFA;
|
||||||
min-height: 500px;
|
--greybase: #B1B5C0;
|
||||||
}
|
--fontbase: #34303a;
|
||||||
|
--fontsec: #5b5861;
|
||||||
.masthead .header {
|
/* THEME 4 */
|
||||||
max-width: 700px;
|
--darkbase: #263C4C;
|
||||||
margin: 0 auto;
|
--lightbase: #598EB5;
|
||||||
font-family: Lato,'Helvetica Neue',Arial,Helvetica,sans-serif;
|
--lighterbase: #5F98C2;
|
||||||
}
|
--whitebase: #FCFFFA;
|
||||||
|
--greybase: #B1B5C0;
|
||||||
.masthead h1.header {
|
--lightgreybase: #D9DEDB;
|
||||||
margin-top: 1em;
|
--blackbase: #1A2A36;
|
||||||
margin-bottom: 0em;
|
--fontbase: #34303a;
|
||||||
font-size: 4.5em;
|
--fontsec: #5b5861;
|
||||||
line-height: 1.2em;
|
--primarybase: #4C7A9C;
|
||||||
font-weight: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
.masthead h2 {
|
|
||||||
font-size: 1.7em;
|
|
||||||
font-weight: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
.masthead .btn {
|
|
||||||
margin: 1em 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* Common styles for all types */
|
/* THEME INDEPENDENT COLORS */
|
||||||
.bs-callout {
|
--errorbase: red;
|
||||||
padding: 20px;
|
--warningbase: #fe7700;
|
||||||
margin: 20px 0;
|
--validbase: #2dcc35;
|
||||||
border: 1px solid #eee;
|
--infobase: var(--darkbase);
|
||||||
border-left-width: 5px;
|
|
||||||
border-radius: 3px;
|
|
||||||
|
/* 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 {
|
body {
|
||||||
border-left-color: #1b809e;
|
background-color: white;
|
||||||
|
color: var(--fontbase);
|
||||||
|
font-family: var(--fontfamilybase);
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 16px;
|
||||||
|
overflow-y: scroll;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Space things out */
|
a,
|
||||||
.bs-docs-section {
|
a:visited {
|
||||||
margin-bottom: 60px;
|
text-decoration: none;
|
||||||
}
|
font-weight: 600;
|
||||||
.bs-docs-section:last-child {
|
transition: color .2s ease, background-color .2s ease;
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#message {
|
ul {
|
||||||
margin-bottom: 40px;
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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}
|
|
||||||
@ -5,9 +5,8 @@
|
|||||||
<div .page-header>
|
<div .page-header>
|
||||||
<h1 #forms>
|
<h1 #forms>
|
||||||
#{formTitle}
|
#{formTitle}
|
||||||
|
|
||||||
<div .row>
|
<div .row>
|
||||||
<div .col-md-10 .col-lg-9>
|
<div .col-md-10 .col-lg-9>
|
||||||
<div .bs-callout bs-callout-info well>
|
<div .bs-callout bs-callout-info well>
|
||||||
<form .form-horizontal method=post action=@{actionUrl}#forms enctype=#{formEnctype}>
|
<form .form-horizontal method=post action=@{actionUrl}#forms enctype=#{formEnctype}>
|
||||||
^{formWidget}
|
^{formWidget}
|
||||||
|
|||||||
15
templates/formPageI18n.hamlet
Normal file
15
templates/formPageI18n.hamlet
Normal 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}
|
||||||
@ -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>
|
<div .container>
|
||||||
<!-- Starting
|
<h1>ReWorX - Demo
|
||||||
================================================== -->
|
<h3>
|
||||||
<div .bs-docs-section>
|
Testumgebung für die Re-Implementierung von <a href="https://uniworx.ifi.lmu.de/">UniWorX</a>
|
||||||
<div .row>
|
<p>
|
||||||
<div .col-lg-12>
|
Die Reimplementierung von
|
||||||
<div .page-header>
|
UniWorX ist noch nicht abgeschlossen.
|
||||||
<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>
|
|
||||||
|
|
||||||
<li .list-group-item>
|
<p .alert .alert-danger>Das System ist noch nicht produktiv einsetzbar
|
||||||
<a href=@{UsersR}>Benutzer Verwaltung
|
|
||||||
|
|
||||||
<li .list-group-item>
|
<hr>
|
||||||
<a href=@{TermShowR}>Semester Verwaltung
|
<div .container>
|
||||||
<a href=@{TermEditR}>Neues Semester anlegen
|
<h2 .js-show-hide__toggle>Teilweise funktionierende Abschnitte
|
||||||
|
|
||||||
|
<ul>
|
||||||
<li .list-group-item>
|
<li .list-group-item>
|
||||||
<a href=@{CourseEditR}>Kurse anlegen
|
<a href=@{UsersR}>Benutzer Verwaltung
|
||||||
editieren und anzeigen
|
|
||||||
|
|
||||||
<li .list-group-item>
|
<li .list-group-item>
|
||||||
<a href=@{SubmissionListR}>Dateien hochladen und abrufen
|
<a href=@{TermShowR}>Semester Verwaltung
|
||||||
|
<a href=@{TermEditR}>Neues Semester anlegen
|
||||||
<h3 #design>Funktionen zum Testen
|
|
||||||
|
|
||||||
<ul .list-group>
|
|
||||||
|
|
||||||
<li .list-group-item>
|
<li .list-group-item>
|
||||||
Knopf-Test:
|
<a href=@{CourseNewR}>Kurse anlegen
|
||||||
<form .form-inline method=post action=@{HomeR} enctype=#{btnEnctype}>
|
|
||||||
^{btnWdgt}
|
<li .list-group-item>
|
||||||
|
<a href=@{SubmissionListR}>Dateien hochladen und abrufen
|
||||||
<li .list-group-item>
|
|
||||||
<a href=@{CourseEditR}>Kurse anlegen
|
<hr>
|
||||||
editieren und anzeigen
|
<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
0
templates/home.julius
Normal file
17
templates/multiFileField.hamlet
Normal file
17
templates/multiFileField.hamlet
Normal 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>
|
||||||
@ -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
|
|
||||||
|
|
||||||
|
|
||||||
@ -24,7 +24,16 @@
|
|||||||
|
|
||||||
<div .row>
|
<div .row>
|
||||||
<div .col-lg-12>
|
<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>
|
<hr>
|
||||||
|
|
||||||
1
templates/standalone/inputs.hamlet
Normal file
1
templates/standalone/inputs.hamlet
Normal file
@ -0,0 +1 @@
|
|||||||
|
<!-- only here to be able to include inputs using `toWidget` -->
|
||||||
236
templates/standalone/inputs.julius
Normal file
236
templates/standalone/inputs.julius
Normal 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');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
360
templates/standalone/inputs.lucius
Normal file
360
templates/standalone/inputs.lucius
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
1
templates/standalone/modal.hamlet
Normal file
1
templates/standalone/modal.hamlet
Normal file
@ -0,0 +1 @@
|
|||||||
|
<!-- only here to be able to include modal using `toWidget` -->
|
||||||
100
templates/standalone/modal.julius
Normal file
100
templates/standalone/modal.julius
Normal 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);
|
||||||
87
templates/standalone/modal.lucius
Normal file
87
templates/standalone/modal.lucius
Normal 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;
|
||||||
|
}
|
||||||
1
templates/standalone/showHide.hamlet
Normal file
1
templates/standalone/showHide.hamlet
Normal file
@ -0,0 +1 @@
|
|||||||
|
<!-- only here to be able to include showHide using `toWidget` -->
|
||||||
59
templates/standalone/showHide.julius
Normal file
59
templates/standalone/showHide.julius
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
49
templates/standalone/showHide.lucius
Normal file
49
templates/standalone/showHide.lucius
Normal 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;
|
||||||
|
}
|
||||||
1
templates/standalone/sortable.hamlet
Normal file
1
templates/standalone/sortable.hamlet
Normal file
@ -0,0 +1 @@
|
|||||||
|
<!-- only here to be able to include sortable using `toWidget` -->
|
||||||
107
templates/standalone/sortable.julius
Normal file
107
templates/standalone/sortable.julius
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
31
templates/standalone/sortable.lucius
Normal file
31
templates/standalone/sortable.lucius
Normal 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);
|
||||||
|
}
|
||||||
@ -1,6 +1,8 @@
|
|||||||
<form method=POST enctype=#{uploadEnctype} action=@{SubmissionListR}>
|
<div .container>
|
||||||
^{uploadWidget}
|
<form method=POST enctype=#{uploadEnctype} action=@{SubmissionListR}>
|
||||||
|
^{uploadWidget}
|
||||||
|
|
||||||
<form method=POST enctype=#{selectEncoding} target=_blank action=@{SubmissionDownloadMultiArchiveR}>
|
<div .container>
|
||||||
^{subTable}
|
<form method=POST enctype=#{selectEncoding} target=_blank action=@{SubmissionDownloadMultiArchiveR}>
|
||||||
<button .btn .btn-default type=submit >Markierte herunterladen
|
^{subTable}
|
||||||
|
<button .btn .btn-default type=submit >Markierte herunterladen
|
||||||
|
|||||||
5
templates/terms.hamlet
Normal file
5
templates/terms.hamlet
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<div .container>
|
||||||
|
<h1>Semesterübersicht
|
||||||
|
|
||||||
|
<div .scrolltable>
|
||||||
|
^{table}
|
||||||
48
templates/widgets/asidenav.hamlet
Normal file
48
templates/widgets/asidenav.hamlet
Normal 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>
|
||||||
85
templates/widgets/asidenav.julius
Normal file
85
templates/widgets/asidenav.julius
Normal 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'));
|
||||||
|
|
||||||
|
});
|
||||||
204
templates/widgets/asidenav.lucius
Normal file
204
templates/widgets/asidenav.lucius
Normal 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';
|
||||||
|
}
|
||||||
7
templates/widgets/breadcrumbs.hamlet
Normal file
7
templates/widgets/breadcrumbs.hamlet
Normal 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}
|
||||||
|
>
|
||||||
|
<li .breadcrumbs__item--active>#{title}
|
||||||
14
templates/widgets/breadcrumbs.lucius
Normal file
14
templates/widgets/breadcrumbs.lucius
Normal 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;
|
||||||
|
}
|
||||||
9
templates/widgets/form.hamlet
Normal file
9
templates/widgets/form.hamlet
Normal 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}
|
||||||
51
templates/widgets/form.julius
Normal file
51
templates/widgets/form.julius
Normal 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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
0
templates/widgets/form.lucius
Normal file
0
templates/widgets/form.lucius
Normal file
10
templates/widgets/lipsum.hamlet
Normal file
10
templates/widgets/lipsum.hamlet
Normal 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.
|
||||||
18
templates/widgets/modal.hamlet
Normal file
18
templates/widgets/modal.hamlet
Normal 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">
|
||||||
26
templates/widgets/navbar.hamlet
Normal file
26
templates/widgets/navbar.hamlet
Normal 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>
|
||||||
48
templates/widgets/navbar.julius
Normal file
48
templates/widgets/navbar.julius
Normal 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'));
|
||||||
|
|
||||||
|
});
|
||||||
127
templates/widgets/navbar.lucius
Normal file
127
templates/widgets/navbar.lucius
Normal 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);
|
||||||
|
}
|
||||||
11
templates/widgets/pageactionprime.hamlet
Normal file
11
templates/widgets/pageactionprime.hamlet
Normal 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 _
|
||||||
21
templates/widgets/pageactionprime.lucius
Normal file
21
templates/widgets/pageactionprime.lucius
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user