Merge branch 'master' into course-teaser
This commit is contained in:
commit
c98acacfec
@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
- ignore: { name: "Parse error" }
|
- ignore: { name: "Parse error" }
|
||||||
- ignore: { name: "Reduce duplication" }
|
- ignore: { name: "Reduce duplication" }
|
||||||
|
- ignore: { name: "Redundant lambda" }
|
||||||
- ignore: { name: "Use ||" }
|
- ignore: { name: "Use ||" }
|
||||||
- ignore: { name: "Use &&" }
|
- ignore: { name: "Use &&" }
|
||||||
- ignore: { name: "Use ++" }
|
- ignore: { name: "Use ++" }
|
||||||
|
|||||||
@ -1,3 +1,9 @@
|
|||||||
|
* Version 27.03.2019
|
||||||
|
|
||||||
|
Kurse Veranstalter können nun mehrere Dozenten und Assistenten selbst eintragen
|
||||||
|
|
||||||
|
Erfassung Studiengangsdaten
|
||||||
|
|
||||||
* Version 20.03.2019
|
* Version 20.03.2019
|
||||||
|
|
||||||
Kursanmeldung benötigen assoziertes Hauptfach (für Studierende mit mehreren Hauptfächern)
|
Kursanmeldung benötigen assoziertes Hauptfach (für Studierende mit mehreren Hauptfächern)
|
||||||
|
|||||||
38
FragenSJ.txt
38
FragenSJ.txt
@ -1,38 +0,0 @@
|
|||||||
** Sicherheitsabfragen?
|
|
||||||
- Verschlüsselung des Zugriffs?
|
|
||||||
|
|
||||||
- SDelR 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.postSDelR: 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.getTermCourseListR, 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!!!
|
|
||||||
34
PageActionPrime.txt
Normal file
34
PageActionPrime.txt
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
Übersicht über PageActions und Workflow im Vergleich zum alten UniWorX:
|
||||||
|
-----
|
||||||
|
|
||||||
|
Course Actions im alten UniWorX:
|
||||||
|
- Studenten
|
||||||
|
- Übungsgruppen
|
||||||
|
- Übungsblätter
|
||||||
|
- Klausuren
|
||||||
|
- E-Mails
|
||||||
|
- Online-Evaluation
|
||||||
|
|
||||||
|
-----
|
||||||
|
|
||||||
|
CourseActions in Uni2Work:
|
||||||
|
-1 Übungsblätter (öfter)
|
||||||
|
[ Auch aus SheetList übernommen un hier ebenfalls angzeigt:
|
||||||
|
-1 Aktuelles Blatt (häufig)
|
||||||
|
-1 Letztes unzugewiesenes (häufig, nur Assistenten)
|
||||||
|
-1 Abgaben (häufig, nur Assistenten)
|
||||||
|
-1 Korrekturen (häufig, nur Assistenten)
|
||||||
|
-1 Neues Blatt (häufig, nur Assistenten)
|
||||||
|
]
|
||||||
|
-2 Teilnehmerliste (gelegentlich, nur Assistenten)
|
||||||
|
-2 Kurs editieren (selten, nur Assistenten)
|
||||||
|
-2 Kurs klonen (selten, nur Assistenten)
|
||||||
|
-2 Kurs löschen (sehr selten, nur Assistenten/Admins)
|
||||||
|
|
||||||
|
SheetList:
|
||||||
|
- Aktuelles Blatt
|
||||||
|
- Letztes unzugewiesenes
|
||||||
|
- Abgaben
|
||||||
|
- Korrekturen
|
||||||
|
- Neues Blatt
|
||||||
|
|
||||||
2
build.sh
2
build.sh
@ -1,4 +1,4 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
exec -- stack build --fast --flag uniworx:library-only --flag uniworx:dev
|
exec -- stack build --fast --flag uniworx:-library-only --flag uniworx:dev
|
||||||
echo Build task completed.
|
echo Build task completed.
|
||||||
|
|||||||
2
db.sh
2
db.sh
@ -1,4 +1,4 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Options: see /test/Database.hs (Main)
|
# Options: see /test/Database.hs (Main)
|
||||||
stack build --fast --flag uniworx:library-only --flag uniworx:dev
|
stack build --fast --flag uniworx:-library-only --flag uniworx:dev
|
||||||
stack exec uniworxdb -- $@
|
stack exec uniworxdb -- $@
|
||||||
|
|||||||
3
hlint.sh
Executable file
3
hlint.sh
Executable file
@ -0,0 +1,3 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
exec -- ./test.sh uniworx:test:hlint
|
||||||
@ -1,11 +1,18 @@
|
|||||||
|
PrintDebugForStupid name@Text: Debug message "#{name}"
|
||||||
|
|
||||||
BtnSubmit: Senden
|
BtnSubmit: Senden
|
||||||
BtnAbort: Abbrechen
|
BtnAbort: Abbrechen
|
||||||
BtnDelete: Löschen
|
BtnDelete: Löschen
|
||||||
BtnRegister: Anmelden
|
BtnRegister: Anmelden
|
||||||
BtnDeregister: Abmelden
|
BtnDeregister: Abmelden
|
||||||
BtnHijack: Sitzung übernehmen
|
BtnHijack: Sitzung übernehmen
|
||||||
|
BtnSave: Speichern
|
||||||
|
BtnCandidatesInfer: Studienfachzuordnung automatisch lernen
|
||||||
|
BtnCandidatesDeleteConflicts: Konflikte löschen
|
||||||
|
BtnCandidatesDeleteAll: Alle Beobachtungen löschen
|
||||||
|
|
||||||
Aborted: Abgebrochen
|
Aborted: Abgebrochen
|
||||||
|
Remarks: Hinweise
|
||||||
Registered: Angemeldet
|
Registered: Angemeldet
|
||||||
RegisteredHeader: Anmeldung
|
RegisteredHeader: Anmeldung
|
||||||
RegisteredSince date@Text: Angemeldet seit #{date}
|
RegisteredSince date@Text: Angemeldet seit #{date}
|
||||||
@ -13,6 +20,11 @@ RegisterFrom: Anmeldungen von
|
|||||||
RegisterTo: Anmeldungen bis
|
RegisterTo: Anmeldungen bis
|
||||||
DeRegUntil: Abmeldungen bis
|
DeRegUntil: Abmeldungen bis
|
||||||
|
|
||||||
|
GenericKey: Schlüssel
|
||||||
|
GenericShort: Kürzel
|
||||||
|
GenericIsNew: Neu
|
||||||
|
GenericHasConflict: Konflikt
|
||||||
|
|
||||||
SummerTerm year@Integer: Sommersemester #{display year}
|
SummerTerm year@Integer: Sommersemester #{display year}
|
||||||
WinterTerm year@Integer: Wintersemester #{display year}/#{display $ succ year}
|
WinterTerm year@Integer: Wintersemester #{display year}/#{display $ succ year}
|
||||||
SummerTermShort year@Integer: SoSe #{display year}
|
SummerTermShort year@Integer: SoSe #{display year}
|
||||||
@ -34,6 +46,8 @@ TermStartDay: Erster Tag
|
|||||||
TermStartDayTooltip: Üblicherweise immer 1.April oder 1.Oktober
|
TermStartDayTooltip: Üblicherweise immer 1.April oder 1.Oktober
|
||||||
TermEndDay: Letzter Tag
|
TermEndDay: Letzter Tag
|
||||||
TermEndDayTooltip: Üblicherweise immer 30.September oder 31.März
|
TermEndDayTooltip: Üblicherweise immer 30.September oder 31.März
|
||||||
|
TermHolidays: Feiertage
|
||||||
|
TermHolidayPlaceholder: Feiertag
|
||||||
TermLectureStart: Beginn Vorlesungen
|
TermLectureStart: Beginn Vorlesungen
|
||||||
TermLectureEnd: Ende Vorlesungen
|
TermLectureEnd: Ende Vorlesungen
|
||||||
TermLectureEndTooltip: Meistens dauer das Sommersemester 14 Wochen und das Wintersemester 15 Wochen.
|
TermLectureEndTooltip: Meistens dauer das Sommersemester 14 Wochen und das Wintersemester 15 Wochen.
|
||||||
@ -51,8 +65,8 @@ CourseCapacity: Kapazität
|
|||||||
CourseCapacityTip: Anzahl erlaubter Kursanmeldungen, leer lassen für unbeschränkte Kurskapazität
|
CourseCapacityTip: Anzahl erlaubter Kursanmeldungen, leer lassen für unbeschränkte Kurskapazität
|
||||||
CourseNoCapacity: In diesem Kurs sind keine Plätze mehr frei.
|
CourseNoCapacity: In diesem Kurs sind keine Plätze mehr frei.
|
||||||
CourseNotEmpty: In diesem Kurs sind momentan Teilnehmer angemeldet.
|
CourseNotEmpty: In diesem Kurs sind momentan Teilnehmer angemeldet.
|
||||||
CourseRegisterOk: Sie wurden angemeldet
|
CourseRegisterOk: Anmeldung erfolgreich
|
||||||
CourseDeregisterOk: Sie wurden abgemeldet
|
CourseDeregisterOk: Erfolgreich abgemeldet
|
||||||
CourseStudyFeature: Assoziiertes Hauptfach
|
CourseStudyFeature: Assoziiertes Hauptfach
|
||||||
CourseStudyFeatureTooltip: Korrekte Angabe kann Notenweiterleitungen beschleunigen
|
CourseStudyFeatureTooltip: Korrekte Angabe kann Notenweiterleitungen beschleunigen
|
||||||
CourseSecretWrong: Falsches Kennwort
|
CourseSecretWrong: Falsches Kennwort
|
||||||
@ -71,8 +85,10 @@ CourseNewHeading: Neuen Kurs anlegen
|
|||||||
CourseEditHeading tid@TermId ssh@SchoolId csh@CourseShorthand: Kurs #{display tid}-#{display ssh}-#{csh} editieren
|
CourseEditHeading tid@TermId ssh@SchoolId csh@CourseShorthand: Kurs #{display tid}-#{display ssh}-#{csh} editieren
|
||||||
CourseEditTitle: Kurs editieren/anlegen
|
CourseEditTitle: Kurs editieren/anlegen
|
||||||
CourseMembers: Teilnehmer
|
CourseMembers: Teilnehmer
|
||||||
|
CourseMemberOf: Teilnehmer
|
||||||
CourseMembersCount n@Int: #{display n}
|
CourseMembersCount n@Int: #{display n}
|
||||||
CourseMembersCountLimited n@Int max@Int: #{display n}/#{display max}
|
CourseMembersCountLimited n@Int max@Int: #{display n}/#{display max}
|
||||||
|
CourseMembersCountOf n@Int mbNum@IntMaybe: #{display n} Anmeldungen #{maybeDisplay " von " mbNum " möglichen"}
|
||||||
CourseName: Name
|
CourseName: Name
|
||||||
CourseDescription: Beschreibung
|
CourseDescription: Beschreibung
|
||||||
CourseDescriptionTip: Beliebiges HTML-Markup ist gestattet
|
CourseDescriptionTip: Beliebiges HTML-Markup ist gestattet
|
||||||
@ -93,6 +109,20 @@ CourseFilterNone: Egal
|
|||||||
CourseDeleteQuestion: Wollen Sie den unten aufgeführten Kurs wirklich löschen?
|
CourseDeleteQuestion: Wollen Sie den unten aufgeführten Kurs wirklich löschen?
|
||||||
CourseDeleted: Kurs gelöscht
|
CourseDeleted: Kurs gelöscht
|
||||||
CourseUserNote: Notiz
|
CourseUserNote: Notiz
|
||||||
|
CourseUserNoteTooltip: Nur für Dozenten dieses Kurses einsehbar
|
||||||
|
CourseUserNoteSaved: Notizänderungen gespeichert
|
||||||
|
CourseUserNoteDeleted: Teilnehmernotiz gelöscht
|
||||||
|
CourseUserDeregister: Abmelden
|
||||||
|
CourseUsersDeregistered count@Int64: #{show count} Teilnehmer abgemeldet
|
||||||
|
|
||||||
|
CourseLecturers: Kursverwalter
|
||||||
|
CourseLecturer: Dozent
|
||||||
|
CourseAssistant: Assistent
|
||||||
|
CourseLecturerAlreadyAdded email@UserEmail: Es gibt bereits einen Kursverwalter mit E-Mail #{email}
|
||||||
|
CourseRegistrationEndMustBeAfterStart: Ende des Anmeldezeitraums muss nach dem Anfang liegen
|
||||||
|
CourseDeregistrationEndMustBeAfterStart: Ende des Abmeldezeitraums muss nach dem Anfang des Anmeldezeitraums liegen
|
||||||
|
CourseUserMustBeLecturer: Aktueller Benutzer muss als Kursverwalter eingetragen sein
|
||||||
|
CourseLecturerRightsIdentical: Alle Sorten von Kursverwalter haben identische Rechte
|
||||||
|
|
||||||
NoSuchTerm tid@TermId: Semester #{display tid} gibt es nicht.
|
NoSuchTerm tid@TermId: Semester #{display tid} gibt es nicht.
|
||||||
NoSuchSchool ssh@SchoolId: Institut #{display ssh} gibt es nicht.
|
NoSuchSchool ssh@SchoolId: Institut #{display ssh} gibt es nicht.
|
||||||
@ -129,21 +159,21 @@ SheetInvisibleUntil date@Text: Dieses Übungsblatt ist für Teilnehmer momentan
|
|||||||
SheetName: Name
|
SheetName: Name
|
||||||
SheetDescription: Hinweise für Teilnehmer
|
SheetDescription: Hinweise für Teilnehmer
|
||||||
SheetGroup: Gruppenabgabe
|
SheetGroup: Gruppenabgabe
|
||||||
SheetVisibleFrom: Sichtbar ab
|
SheetVisibleFrom: Sichtbar für Teilnehmer ab
|
||||||
SheetVisibleFromTip: Ohne Datum wird das Blatt nie sichtbar, z.B. weil es noch nicht fertig ist
|
SheetVisibleFromTip: Ohne Datum nie sichtbar und keine Abgabe möglich; nur für unfertige Blätter leer lassen, deren Fristen/Bewertung sich noch ändern kann
|
||||||
SheetActiveFrom: Aktiv ab
|
SheetActiveFrom: Beginn Abgabezeitraum
|
||||||
SheetActiveFromTip: Abgabe und Download der Aufgabenstellung ist erst ab diesem Datum möglich
|
SheetActiveFromTip: Download der Aufgabenstellung erst ab diesem Datum möglich
|
||||||
SheetActiveTo: Abgabefrist
|
SheetActiveTo: Ende Abgabezeitraum
|
||||||
SheetHintFromTip: Ohne Datum nie für Teilnehmer sichtbar, Korrektoren können diese Dateien immer herunterladen
|
SheetHintFromTip: Ohne Datum nie für Teilnehmer sichtbar, Korrektoren können diese Dateien immer herunterladen
|
||||||
SheetSolutionFromTip: Ohne Datum nie für Teilnehmer sichtbar, Korrektoren können diese Dateien immer herunterladen
|
SheetSolutionFromTip: Ohne Datum nie für Teilnehmer sichtbar, Korrektoren können diese Dateien immer herunterladen
|
||||||
SheetMarkingTip: Hinweise zur Korrektur, sichtbar nur für Korrektoren
|
SheetMarkingTip: Hinweise zur Korrektur, sichtbar nur für Korrektoren
|
||||||
SheetPseudonym: Persönliches Abgabe-Pseudonym
|
SheetPseudonym: Persönliches Abgabe-Pseudonym
|
||||||
SheetGeneratePseudonym: Generieren
|
SheetGeneratePseudonym: Generieren
|
||||||
|
|
||||||
SheetErrVisibility: Sichtbarkeit muss vor Beginn der Abgabefrist liegen
|
SheetErrVisibility: "Beginn Abgabezeitraum" muss nach "Sichbar für Teilnehmer ab" liegen
|
||||||
SheetErrDeadlineEarly: Ende der Abgabefrist muss nach deren Beginn liegen
|
SheetErrDeadlineEarly: "Ende Abgabezeitraum" muss nach "Beginn Abzeitraum" liegen
|
||||||
SheetErrHintEarly: Hinweise dürfen erst nach Beginn der Abgabefrist herausgegeben werden
|
SheetErrHintEarly: Hinweise dürfen erst nach Beginn des Abgabezeitraums herausgegeben werden
|
||||||
SheetErrSolutionEarly: Die Lösung sollte erst nach Ende der Abgabefrist herausgegeben werden
|
SheetErrSolutionEarly: Lösungen dürfen erst nach Ende der Abgabezeitraums herausgegeben werden
|
||||||
|
|
||||||
|
|
||||||
Deadline: Abgabe
|
Deadline: Abgabe
|
||||||
@ -214,7 +244,7 @@ AddCorrector: Zusätzlicher Korrektor
|
|||||||
CorrectorExists email@UserEmail: #{email} ist bereits als Korrektor eingetragen
|
CorrectorExists email@UserEmail: #{email} ist bereits als Korrektor eingetragen
|
||||||
SheetCorrectorsTitle tid@TermId ssh@SchoolId csh@CourseShorthand sheetName@SheetName: Korrektoren für #{display tid}-#{display ssh}-#{csh} #{sheetName}
|
SheetCorrectorsTitle tid@TermId ssh@SchoolId csh@CourseShorthand sheetName@SheetName: Korrektoren für #{display tid}-#{display ssh}-#{csh} #{sheetName}
|
||||||
CountTutProp: Tutorien zählen gegen Proportion
|
CountTutProp: Tutorien zählen gegen Proportion
|
||||||
AutoAssignCorrs: Korrekturen am Ende der Abgabefrist automatisch zuteilen
|
AutoAssignCorrs: Korrekturen nach Ablauf des Abgabezeitraums automatisch zuteilen
|
||||||
Corrector: Korrektor
|
Corrector: Korrektor
|
||||||
Correctors: Korrektoren
|
Correctors: Korrektoren
|
||||||
CorState: Status
|
CorState: Status
|
||||||
@ -246,11 +276,15 @@ DataProtHeading: Datenschutzerklärung
|
|||||||
SystemMessageHeading: Uni2work Statusmeldung
|
SystemMessageHeading: Uni2work Statusmeldung
|
||||||
SystemMessageListHeading: Uni2work Statusmeldungen
|
SystemMessageListHeading: Uni2work Statusmeldungen
|
||||||
|
|
||||||
|
HomeOpenCourses: Kurse mit offener Registrierung
|
||||||
|
HomeUpcomingSheets: Anstehende Übungsblätter
|
||||||
|
|
||||||
NumCourses num@Int64: #{display num} Kurse
|
NumCourses num@Int64: #{display num} Kurse
|
||||||
CloseAlert: Schliessen
|
CloseAlert: Schliessen
|
||||||
|
|
||||||
Name: Name
|
Name: Name
|
||||||
MatrikelNr: Matrikelnummer
|
MatrikelNr: Matrikelnummer
|
||||||
|
NoMatrikelKnown: Keine Matrikelnummer
|
||||||
Theme: Oberflächen Design
|
Theme: Oberflächen Design
|
||||||
Favoriten: Anzahl gespeicherter Favoriten
|
Favoriten: Anzahl gespeicherter Favoriten
|
||||||
Plugin: Plugin
|
Plugin: Plugin
|
||||||
@ -351,6 +385,8 @@ AccessRightsFor: Berechtigungen für
|
|||||||
AdminFor: Administrator
|
AdminFor: Administrator
|
||||||
LecturerFor: Dozent
|
LecturerFor: Dozent
|
||||||
LecturersFor: Dozenten
|
LecturersFor: Dozenten
|
||||||
|
AssistantFor: Assistent
|
||||||
|
AssistantsFor: Assistenten
|
||||||
ForSchools n@Int: für #{pluralDE n "Institut" "Institute"}
|
ForSchools n@Int: für #{pluralDE n "Institut" "Institute"}
|
||||||
UserListTitle: Komprehensive Benutzerliste
|
UserListTitle: Komprehensive Benutzerliste
|
||||||
AccessRightsSaved: Berechtigungsänderungen wurden gespeichert.
|
AccessRightsSaved: Berechtigungsänderungen wurden gespeichert.
|
||||||
@ -408,17 +444,23 @@ SheetCorrectorSubmissionsTip: Abgabe erfolgt über ein Uni2work-externes Verfahr
|
|||||||
SubmissionNoUploadExpected: Es ist keine Abgabe von Dateien vorgesehen.
|
SubmissionNoUploadExpected: Es ist keine Abgabe von Dateien vorgesehen.
|
||||||
|
|
||||||
AdminFeaturesHeading: Studiengänge
|
AdminFeaturesHeading: Studiengänge
|
||||||
|
StudyTerms: Studiengänge
|
||||||
|
StudyTerm: Studiengang
|
||||||
|
NoStudyTermsKnown: Nicht eingeschrieben
|
||||||
StudyFeatureInference: Studiengangschlüssel-Inferenz
|
StudyFeatureInference: Studiengangschlüssel-Inferenz
|
||||||
StudyFeatureAge: Fachsemester
|
StudyFeatureAge: Fachsemester
|
||||||
StudyFeatureDegree: Abschluss
|
StudyFeatureDegree: Abschluss
|
||||||
FieldPrimary: Hauptfach
|
FieldPrimary: Hauptfach
|
||||||
FieldSecondary: Nebenfach
|
FieldSecondary: Nebenfach
|
||||||
NoPrimaryStudyField: (kein Hauptfach registriert)
|
NoPrimaryStudyField: (kein Hauptfach registriert)
|
||||||
|
StudyFeatureType:
|
||||||
|
StudyFeatureValid: Aktiv
|
||||||
|
StudyFeatureUpdate: Abgeglichen
|
||||||
|
|
||||||
DegreeKey: Schlüssel Abschluss
|
DegreeKey: Abschlussschlüssel
|
||||||
DegreeName: Abschluss
|
DegreeName: Abschluss
|
||||||
DegreeShort: Abschlusskürzel
|
DegreeShort: Abschlusskürzel
|
||||||
StudyTermsKey: Schlüssel Studiengang
|
StudyTermsKey: Studiengangschlüssel
|
||||||
StudyTermsName: Studiengang
|
StudyTermsName: Studiengang
|
||||||
StudyTermsShort: Studiengangkürzel
|
StudyTermsShort: Studiengangkürzel
|
||||||
StudyTermsChangeSuccess: Zuordnung Abschlüsse aktualisiert
|
StudyTermsChangeSuccess: Zuordnung Abschlüsse aktualisiert
|
||||||
@ -428,7 +470,10 @@ AmbiguousCandidatesRemoved n@Int: #{show n} #{pluralDE n "uneindeutiger Kandidat
|
|||||||
RedundantCandidatesRemoved n@Int: #{show n} bereits #{pluralDE n "bekannter Kandidat" "bekannte Kandiaten"} entfernt
|
RedundantCandidatesRemoved n@Int: #{show n} bereits #{pluralDE n "bekannter Kandidat" "bekannte Kandiaten"} entfernt
|
||||||
CandidatesInferred n@Int: #{show n} neue #{pluralDE n "Studiengangszuordnung" "Studiengangszuordnungen"} inferiert
|
CandidatesInferred n@Int: #{show n} neue #{pluralDE n "Studiengangszuordnung" "Studiengangszuordnungen"} inferiert
|
||||||
NoCandidatesInferred: Keine neuen Studienganszuordnungen inferiert
|
NoCandidatesInferred: Keine neuen Studienganszuordnungen inferiert
|
||||||
|
AllIncidencesDeleted: Alle Beobachtungen wurden gelöscht.
|
||||||
|
IncidencesDeleted n@Int: #{show n} #{pluralDE n "Beobachtung" "Beobachtungen"} gelöscht
|
||||||
StudyTermIsNew: Neu
|
StudyTermIsNew: Neu
|
||||||
|
StudyFeatureConflict: Es wurden Konflikte in der Studiengang-Zuordnung gefunden
|
||||||
|
|
||||||
MailTestFormEmail: Email-Addresse
|
MailTestFormEmail: Email-Addresse
|
||||||
MailTestFormLanguages: Spracheinstellungen
|
MailTestFormLanguages: Spracheinstellungen
|
||||||
@ -451,7 +496,7 @@ MailSubmissionsUnassignedIntro n@Int courseName@Text termDesc@Text sheetName@She
|
|||||||
|
|
||||||
MailSubjectSheetSoonInactive csh@CourseShorthand sheetName@SheetName: #{sheetName} in #{csh} kann nur noch kurze Zeit abgegeben werden
|
MailSubjectSheetSoonInactive csh@CourseShorthand sheetName@SheetName: #{sheetName} in #{csh} kann nur noch kurze Zeit abgegeben werden
|
||||||
MailSheetSoonInactiveIntro courseName@Text termDesc@Text sheetName@SheetName: Abgabefirst für #{sheetName} im Kurs #{courseName} (#{termDesc}) endet in Kürze.
|
MailSheetSoonInactiveIntro courseName@Text termDesc@Text sheetName@SheetName: Abgabefirst für #{sheetName} im Kurs #{courseName} (#{termDesc}) endet in Kürze.
|
||||||
MailSubjectSheetInactive csh@CourseShorthand sheetName@SheetName: Abgabfrist für #{sheetName} in #{csh} abgelaufen
|
MailSubjectSheetInactive csh@CourseShorthand sheetName@SheetName: Abgabezeitraum für #{sheetName} in #{csh} abgelaufen
|
||||||
MailSheetInactiveIntro courseName@Text termDesc@Text sheetName@SheetName n@Int num@Int64: Die Abgabefirst für #{sheetName} im Kurs #{courseName} (#{termDesc}) beendet. Es gab #{noneOneMoreDE n "Keine Abgaben" "Nur eine Abgabe von " (display n <> " Abgaben von ")}#{noneOneMoreDE num "" "einem Teilnehmer" (display num <> " Teilnehmern")}.
|
MailSheetInactiveIntro courseName@Text termDesc@Text sheetName@SheetName n@Int num@Int64: Die Abgabefirst für #{sheetName} im Kurs #{courseName} (#{termDesc}) beendet. Es gab #{noneOneMoreDE n "Keine Abgaben" "Nur eine Abgabe von " (display n <> " Abgaben von ")}#{noneOneMoreDE num "" "einem Teilnehmer" (display num <> " Teilnehmern")}.
|
||||||
|
|
||||||
MailSubjectCorrectionsAssigned csh@CourseShorthand sheetName@SheetName: Ihnen wurden Korrekturen zu #{sheetName} in #{csh} zugeteilt
|
MailSubjectCorrectionsAssigned csh@CourseShorthand sheetName@SheetName: Ihnen wurden Korrekturen zu #{sheetName} in #{csh} zugeteilt
|
||||||
@ -465,6 +510,7 @@ MailLecturerRights n@Int: Als Dozent dürfen Sie Veranstaltungen innerhalb #{plu
|
|||||||
|
|
||||||
MailEditNotifications: Benachrichtigungen ein-/ausschalten
|
MailEditNotifications: Benachrichtigungen ein-/ausschalten
|
||||||
MailSubjectSupport: Supportanfrage
|
MailSubjectSupport: Supportanfrage
|
||||||
|
MailSubjectSupportCustom customSubject@Text: [Support] #{customSubject}
|
||||||
|
|
||||||
SheetGrading: Bewertung
|
SheetGrading: Bewertung
|
||||||
SheetGradingPoints maxPoints@Points: #{tshow maxPoints} Punkte
|
SheetGradingPoints maxPoints@Points: #{tshow maxPoints} Punkte
|
||||||
@ -508,7 +554,7 @@ NotificationTriggerSubmissionRatedGraded: Meine Abgabe in einem gewerteten Übun
|
|||||||
NotificationTriggerSubmissionRated: Meine Abgabe wurde korrigiert
|
NotificationTriggerSubmissionRated: Meine Abgabe wurde korrigiert
|
||||||
NotificationTriggerSheetActive: Ich kann ein neues Übungsblatt herunterladen
|
NotificationTriggerSheetActive: Ich kann ein neues Übungsblatt herunterladen
|
||||||
NotificationTriggerSheetSoonInactive: Ich kann ein Übungsblatt bald nicht mehr abgeben
|
NotificationTriggerSheetSoonInactive: Ich kann ein Übungsblatt bald nicht mehr abgeben
|
||||||
NotificationTriggerSheetInactive: Abgabefrist eines meiner Übungsblätter ist abgelaufen
|
NotificationTriggerSheetInactive: Abgabezeitraum eines meiner Übungsblätter ist abgelaufen
|
||||||
NotificationTriggerCorrectionsAssigned: Mir wurden Abgaben zur Korrektur zugeteilt
|
NotificationTriggerCorrectionsAssigned: Mir wurden Abgaben zur Korrektur zugeteilt
|
||||||
NotificationTriggerCorrectionsNotDistributed: Abgaben eines meiner Übungsblätter konnten keinem Korrektur zugeteilt werden
|
NotificationTriggerCorrectionsNotDistributed: Abgaben eines meiner Übungsblätter konnten keinem Korrektur zugeteilt werden
|
||||||
NotificationTriggerUserRightsUpdate: Meine Berechtigungen wurden geändert
|
NotificationTriggerUserRightsUpdate: Meine Berechtigungen wurden geändert
|
||||||
@ -539,6 +585,7 @@ HelpAnswer: Antworten an
|
|||||||
HelpUser: Meinen Benutzeraccount
|
HelpUser: Meinen Benutzeraccount
|
||||||
HelpAnonymous: Keine Antwort (Anonym)
|
HelpAnonymous: Keine Antwort (Anonym)
|
||||||
HelpEmail: E-Mail
|
HelpEmail: E-Mail
|
||||||
|
HelpSubject: Betreff
|
||||||
HelpRequest: Supportanfrage / Verbesserungsvorschlag
|
HelpRequest: Supportanfrage / Verbesserungsvorschlag
|
||||||
HelpProblemPage: Problematische Seite
|
HelpProblemPage: Problematische Seite
|
||||||
HelpIntroduction: Wenn Ihnen die Benutzung dieser Webseite Schwierigkeiten bereitet oder Sie einen verbesserbaren Umstand entdecken bitten wir Sie uns das zu melden, auch wenn Sie Ihr Problem bereits selbst lösen konnten. Wir passen die Seite ständig an und versuchen sie auch für zukünftige Benutzer so einsichtig wie möglich zu halten.
|
HelpIntroduction: Wenn Ihnen die Benutzung dieser Webseite Schwierigkeiten bereitet oder Sie einen verbesserbaren Umstand entdecken bitten wir Sie uns das zu melden, auch wenn Sie Ihr Problem bereits selbst lösen konnten. Wir passen die Seite ständig an und versuchen sie auch für zukünftige Benutzer so einsichtig wie möglich zu halten.
|
||||||
@ -683,3 +730,5 @@ DBTIRowsMissing n@Int: #{pluralDE n "Eine Zeile ist" "Einige Zeile sind"} aus de
|
|||||||
|
|
||||||
MassInputAddDimension: Hinzufügen
|
MassInputAddDimension: Hinzufügen
|
||||||
MassInputDeleteCell: Entfernen
|
MassInputDeleteCell: Entfernen
|
||||||
|
|
||||||
|
NavigationFavourites: Favoriten
|
||||||
@ -33,6 +33,7 @@ CourseFavourite -- which user accessed which course when, only display
|
|||||||
Lecturer -- course ownership
|
Lecturer -- course ownership
|
||||||
user UserId
|
user UserId
|
||||||
course CourseId
|
course CourseId
|
||||||
|
type LecturerType default='"lecturer"'
|
||||||
UniqueLecturer user course -- note: multiple lecturers per course are allowed, but no duplicated rows in this table
|
UniqueLecturer user course -- note: multiple lecturers per course are allowed, but no duplicated rows in this table
|
||||||
CourseParticipant -- course enrolement
|
CourseParticipant -- course enrolement
|
||||||
course CourseId
|
course CourseId
|
||||||
@ -40,12 +41,20 @@ CourseParticipant -- course enrolement
|
|||||||
registration UTCTime -- time of last enrolement for this course
|
registration UTCTime -- time of last enrolement for this course
|
||||||
field StudyFeaturesId Maybe -- associated degree course, user-defined; required for communicating grades
|
field StudyFeaturesId Maybe -- associated degree course, user-defined; required for communicating grades
|
||||||
UniqueParticipant user course
|
UniqueParticipant user course
|
||||||
|
-- Replace the last two by the following, once an audit log is available
|
||||||
|
-- CourseUserNote -- lecturers of a specific course may share a text note on each enrolled student
|
||||||
|
-- course CourseId
|
||||||
|
-- user UserId
|
||||||
|
-- note Html -- arbitrary user-defined text; visible only to lecturer of this course
|
||||||
|
-- time UTCTime -- PROBLEM: deleted note has no modification date
|
||||||
|
-- editor UserId -- who edited this note last
|
||||||
|
-- UniqueCourseUserNote user course
|
||||||
CourseUserNote -- lecturers of a specific course may share a text note on each enrolled student
|
CourseUserNote -- lecturers of a specific course may share a text note on each enrolled student
|
||||||
course CourseId
|
course CourseId
|
||||||
user UserId
|
user UserId
|
||||||
note Text -- arbitrary user-defined text; visible only to lecturer of this course
|
note Html -- arbitrary user-defined text; visible only to lecturer of this course
|
||||||
UniqueCourseUserNotes user course
|
UniqueCourseUserNote user course
|
||||||
CourseUserNoteEdit -- who edited a participants course note whenl
|
CourseUserNoteEdit -- who edited a participants course note when
|
||||||
user UserId
|
user UserId
|
||||||
time UTCTime
|
time UTCTime
|
||||||
note CourseUserNoteId
|
note CourseUserNoteId -- PROBLEM: deleted notes have no modification date any more
|
||||||
|
|||||||
@ -116,6 +116,7 @@ dependencies:
|
|||||||
- lifted-base
|
- lifted-base
|
||||||
- lattices
|
- lattices
|
||||||
- hsass
|
- hsass
|
||||||
|
- semigroupoids
|
||||||
|
|
||||||
other-extensions:
|
other-extensions:
|
||||||
- GeneralizedNewtypeDeriving
|
- GeneralizedNewtypeDeriving
|
||||||
@ -217,6 +218,9 @@ executables:
|
|||||||
dependencies:
|
dependencies:
|
||||||
- uniworx
|
- uniworx
|
||||||
other-modules: []
|
other-modules: []
|
||||||
|
when:
|
||||||
|
- condition: flag(library-only)
|
||||||
|
buildable: false
|
||||||
|
|
||||||
# Test suite
|
# Test suite
|
||||||
tests:
|
tests:
|
||||||
|
|||||||
11
routes
11
routes
@ -36,7 +36,8 @@
|
|||||||
|
|
||||||
/ HomeR GET !free
|
/ HomeR GET !free
|
||||||
/users UsersR GET -- no tags, i.e. admins only
|
/users UsersR GET -- no tags, i.e. admins only
|
||||||
/users/#CryptoUUIDUser AdminUserR GET POST !development
|
/users/#CryptoUUIDUser AdminUserR GET POST
|
||||||
|
/users/#CryptoUUIDUser/delete AdminUserDeleteR POST
|
||||||
/users/#CryptoUUIDUser/hijack AdminHijackUserR POST !adminANDno-escalation
|
/users/#CryptoUUIDUser/hijack AdminHijackUserR POST !adminANDno-escalation
|
||||||
/admin AdminR GET
|
/admin AdminR GET
|
||||||
/admin/features AdminFeaturesR GET POST
|
/admin/features AdminFeaturesR GET POST
|
||||||
@ -52,13 +53,13 @@
|
|||||||
/help HelpR GET POST !free
|
/help HelpR GET POST !free
|
||||||
|
|
||||||
/user ProfileR GET POST !free
|
/user ProfileR GET POST !free
|
||||||
/user/profile ProfileDataR GET POST !free
|
/user/profile ProfileDataR GET !free
|
||||||
/user/authpreds AuthPredsR GET POST !free
|
/user/authpreds AuthPredsR GET POST !free
|
||||||
|
|
||||||
/term TermShowR GET !free
|
/term TermShowR GET !free
|
||||||
/term/current TermCurrentR GET !free
|
/term/current TermCurrentR GET !free
|
||||||
/term/edit TermEditR GET POST
|
/term/edit TermEditR GET POST
|
||||||
/term/#TermId/edit TermEditExistR GET
|
/term/#TermId/edit TermEditExistR GET POST
|
||||||
!/term/#TermId TermCourseListR GET !free
|
!/term/#TermId TermCourseListR GET !free
|
||||||
!/term/#TermId/#SchoolId TermSchoolCourseListR GET !free
|
!/term/#TermId/#SchoolId TermSchoolCourseListR GET !free
|
||||||
|
|
||||||
@ -74,8 +75,8 @@
|
|||||||
/register CRegisterR POST !timeANDcapacity
|
/register CRegisterR POST !timeANDcapacity
|
||||||
/edit CEditR GET POST
|
/edit CEditR GET POST
|
||||||
/delete CDeleteR GET POST !lecturerANDempty
|
/delete CDeleteR GET POST !lecturerANDempty
|
||||||
/users CUsersR GET
|
/users CUsersR GET POST
|
||||||
/users/#CryptoUUIDUser CUserR GET !lecturerANDparticipant
|
/users/#CryptoUUIDUser CUserR GET POST !lecturerANDparticipant
|
||||||
/correctors CHiWisR GET
|
/correctors CHiWisR GET
|
||||||
/notes CNotesR GET POST !corrector
|
/notes CNotesR GET POST !corrector
|
||||||
/subs CCorrectionsR GET POST
|
/subs CCorrectionsR GET POST
|
||||||
|
|||||||
@ -19,7 +19,7 @@ let
|
|||||||
'';
|
'';
|
||||||
|
|
||||||
override = oldAttrs: {
|
override = oldAttrs: {
|
||||||
nativeBuildInputs = oldAttrs.nativeBuildInputs ++ (with pkgs; [ postgresql ]) ++ (with haskellPackages; [ stack yesod-bin hlint cabal-install ]);
|
nativeBuildInputs = oldAttrs.nativeBuildInputs ++ (with pkgs; [ postgresql pgadmin openldap ]) ++ (with haskellPackages; [ stack yesod-bin hlint cabal-install ]);
|
||||||
shellHook = ''
|
shellHook = ''
|
||||||
export PROMPT_INFO="${oldAttrs.name}"
|
export PROMPT_INFO="${oldAttrs.name}"
|
||||||
|
|
||||||
|
|||||||
@ -76,6 +76,8 @@ import qualified Database.Memcached.Binary.IO as Memcached
|
|||||||
-- (HPack takes care to add new modules to our cabal file nowadays.)
|
-- (HPack takes care to add new modules to our cabal file nowadays.)
|
||||||
import Handler.Common
|
import Handler.Common
|
||||||
import Handler.Home
|
import Handler.Home
|
||||||
|
import Handler.Info
|
||||||
|
import Handler.Help
|
||||||
import Handler.Profile
|
import Handler.Profile
|
||||||
import Handler.Users
|
import Handler.Users
|
||||||
import Handler.Admin
|
import Handler.Admin
|
||||||
|
|||||||
@ -6,7 +6,7 @@ module Auth.LDAP
|
|||||||
, Ldap.AttrList, Ldap.Attr(..), Ldap.AttrValue
|
, Ldap.AttrList, Ldap.Attr(..), Ldap.AttrValue
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import Import.NoFoundation
|
import Import.NoFoundation hiding (userEmail, userDisplayName)
|
||||||
import Control.Lens
|
import Control.Lens
|
||||||
import Network.Connection
|
import Network.Connection
|
||||||
|
|
||||||
@ -39,9 +39,16 @@ data CampusMessage = MsgCampusIdentNote
|
|||||||
|
|
||||||
|
|
||||||
findUser :: LdapConf -> Ldap -> Text -> [Ldap.Attr] -> IO [Ldap.SearchEntry]
|
findUser :: LdapConf -> Ldap -> Text -> [Ldap.Attr] -> IO [Ldap.SearchEntry]
|
||||||
findUser LdapConf{..} ldap campusIdent = Ldap.search ldap ldapBase userSearchSettings userFilter
|
findUser LdapConf{..} ldap ident retAttrs = fromMaybe [] <$> findM (assertM (not . null) . lift . flip (Ldap.search ldap ldapBase userSearchSettings) retAttrs) userFilters
|
||||||
where
|
where
|
||||||
userFilter = userPrincipalName Ldap.:= Text.encodeUtf8 campusIdent
|
userFilters =
|
||||||
|
[ userPrincipalName Ldap.:= Text.encodeUtf8 ident
|
||||||
|
, userPrincipalName Ldap.:= Text.encodeUtf8 [st|#{ident}@campus.lmu.de|]
|
||||||
|
, userEmail Ldap.:= Text.encodeUtf8 ident
|
||||||
|
, userEmail Ldap.:= Text.encodeUtf8 [st|#{ident}@lmu.de|]
|
||||||
|
, userEmail Ldap.:= Text.encodeUtf8 [st|#{ident}@campus.lmu.de|]
|
||||||
|
, userDisplayName Ldap.:= Text.encodeUtf8 ident
|
||||||
|
]
|
||||||
userSearchSettings = mconcat
|
userSearchSettings = mconcat
|
||||||
[ Ldap.scope ldapScope
|
[ Ldap.scope ldapScope
|
||||||
, Ldap.size 2
|
, Ldap.size 2
|
||||||
@ -49,17 +56,18 @@ findUser LdapConf{..} ldap campusIdent = Ldap.search ldap ldapBase userSearchSet
|
|||||||
, Ldap.derefAliases Ldap.DerefAlways
|
, Ldap.derefAliases Ldap.DerefAlways
|
||||||
]
|
]
|
||||||
|
|
||||||
userPrincipalName :: Ldap.Attr
|
userPrincipalName, userEmail, userDisplayName :: Ldap.Attr
|
||||||
userPrincipalName = Ldap.Attr "userPrincipalName"
|
userPrincipalName = Ldap.Attr "userPrincipalName"
|
||||||
|
userEmail = Ldap.Attr "mail"
|
||||||
|
userDisplayName = Ldap.Attr "displayName"
|
||||||
|
|
||||||
campusForm :: ( RenderMessage site FormMessage
|
campusForm :: ( RenderMessage site FormMessage
|
||||||
, RenderMessage site CampusMessage
|
, RenderMessage site CampusMessage
|
||||||
, Button site ButtonSubmit
|
, Button site ButtonSubmit
|
||||||
) => AForm (HandlerT site IO) CampusLogin
|
) => AForm (HandlerT site IO) CampusLogin
|
||||||
campusForm = CampusLogin
|
campusForm = CampusLogin
|
||||||
<$> areq ciField (fslpI MsgCampusIdent "user.name@campus.lmu.de" & setTooltip MsgCampusIdentNote) Nothing
|
<$> areq ciField (fslpI MsgCampusIdent "user.name@campus.lmu.de" & setTooltip MsgCampusIdentNote & addAttr "autofocus" "") Nothing
|
||||||
<*> areq passwordField (fslI MsgCampusPassword) Nothing
|
<*> areq passwordField (fslI MsgCampusPassword) Nothing
|
||||||
<* submitButton
|
|
||||||
|
|
||||||
campusLogin :: forall site.
|
campusLogin :: forall site.
|
||||||
( YesodAuth site
|
( YesodAuth site
|
||||||
@ -80,9 +88,14 @@ campusLogin conf@LdapConf{..} pool = AuthPlugin{..}
|
|||||||
FormMissing -> redirect LoginR
|
FormMissing -> redirect LoginR
|
||||||
FormSuccess CampusLogin{ campusIdent = CI.original -> campusIdent, ..} -> do
|
FormSuccess CampusLogin{ campusIdent = CI.original -> campusIdent, ..} -> do
|
||||||
ldapResult <- withLdap pool $ \ldap -> do
|
ldapResult <- withLdap pool $ \ldap -> do
|
||||||
Ldap.bind ldap (Ldap.Dn campusIdent) (Ldap.Password $ Text.encodeUtf8 campusPassword)
|
|
||||||
Ldap.bind ldap ldapDn ldapPassword
|
Ldap.bind ldap ldapDn ldapPassword
|
||||||
findUser conf ldap campusIdent [userPrincipalName]
|
searchResults <- findUser conf ldap campusIdent [userPrincipalName]
|
||||||
|
case searchResults of
|
||||||
|
[Ldap.SearchEntry (Ldap.Dn userDN) userAttrs]
|
||||||
|
| Just [principalName] <- lookup userPrincipalName userAttrs
|
||||||
|
, Right credsIdent <- Text.decodeUtf8' principalName
|
||||||
|
-> Right (userDN, credsIdent) <$ Ldap.bind ldap (Ldap.Dn credsIdent) (Ldap.Password $ Text.encodeUtf8 campusPassword)
|
||||||
|
other -> return $ Left other
|
||||||
case ldapResult of
|
case ldapResult of
|
||||||
Left err
|
Left err
|
||||||
| LdapError (Ldap.ResponseError (Ldap.ResponseErrorCode _ Ldap.InvalidCredentials _ _)) <- err
|
| LdapError (Ldap.ResponseError (Ldap.ResponseErrorCode _ Ldap.InvalidCredentials _ _)) <- err
|
||||||
@ -92,16 +105,11 @@ campusLogin conf@LdapConf{..} pool = AuthPlugin{..}
|
|||||||
| otherwise -> do
|
| otherwise -> do
|
||||||
$logErrorS "LDAP" $ "Error during login: " <> tshow err
|
$logErrorS "LDAP" $ "Error during login: " <> tshow err
|
||||||
loginErrorMessageI LoginR Msg.AuthError
|
loginErrorMessageI LoginR Msg.AuthError
|
||||||
Right searchResults
|
Right (Right (userDN, credsIdent)) ->
|
||||||
| [Ldap.SearchEntry (Ldap.Dn userDN) userAttrs] <- searchResults
|
lift . setCredsRedirect $ Creds apName credsIdent [("DN", userDN)]
|
||||||
, Just [principalName] <- lookup userPrincipalName userAttrs
|
Right (Left searchResults) -> do
|
||||||
, Right credsIdent <- Text.decodeUtf8' principalName
|
$logWarnS "LDAP" $ "Could not extract principal name: " <> tshow searchResults
|
||||||
-> do
|
loginErrorMessageI LoginR Msg.AuthError
|
||||||
$logDebugS "LDAP" $ tshow searchResults
|
|
||||||
lift . setCredsRedirect $ Creds apName credsIdent [("DN", userDN)]
|
|
||||||
| otherwise -> do
|
|
||||||
$logWarnS "LDAP" $ "Could not extract principal name: " <> tshow searchResults
|
|
||||||
loginErrorMessageI LoginR Msg.AuthError
|
|
||||||
apDispatch _ _ = notFound
|
apDispatch _ _ = notFound
|
||||||
apLogin toMaster = do
|
apLogin toMaster = do
|
||||||
(login, loginEnctype) <- handlerToWidget . generateFormPost $ renderAForm FormStandard campusForm
|
(login, loginEnctype) <- handlerToWidget . generateFormPost $ renderAForm FormStandard campusForm
|
||||||
|
|||||||
12
src/Data/List/NonEmpty/Instances.hs
Normal file
12
src/Data/List/NonEmpty/Instances.hs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
||||||
|
|
||||||
|
module Data.List.NonEmpty.Instances
|
||||||
|
(
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Data.List.NonEmpty
|
||||||
|
|
||||||
|
import Language.Haskell.TH.Syntax (Lift(..))
|
||||||
|
|
||||||
|
instance Lift a => Lift (NonEmpty a) where
|
||||||
|
lift (toList -> xs) = [e|fromList xs|]
|
||||||
@ -5,8 +5,9 @@ module Database.Esqueleto.Utils
|
|||||||
, isInfixOf, hasInfix
|
, isInfixOf, hasInfix
|
||||||
, any, all
|
, any, all
|
||||||
, SqlIn(..)
|
, SqlIn(..)
|
||||||
, mkExactFilter, mkContainsFilter
|
, mkExactFilter, mkExactFilterWith
|
||||||
, anyFilter
|
, mkContainsFilter
|
||||||
|
, anyFilter, allFilter
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import ClassyPrelude.Yesod hiding (isInfixOf, any, all)
|
import ClassyPrelude.Yesod hiding (isInfixOf, any, all)
|
||||||
@ -54,26 +55,42 @@ all :: Foldable f =>
|
|||||||
all test = F.foldr (\needle acc -> acc E.&&. test needle) true
|
all test = F.foldr (\needle acc -> acc E.&&. test needle) true
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- Allow usage of Tuples as DbtRowKey, i.e. SqlIn instances for tuples
|
-- Allow usage of Tuples as DbtRowKey, i.e. SqlIn instances for tuples
|
||||||
$(sqlInTuples [2..16])
|
$(sqlInTuples [2..16])
|
||||||
|
|
||||||
|
-- | Example for usage of unValueN
|
||||||
|
_exampleUnValueN :: (E.Value a, E.Value b, E.Value c) -> (a,b,c)
|
||||||
|
_exampleUnValueN = $(unValueN 3)
|
||||||
|
|
||||||
|
-- | Example for usage of unValueNIs
|
||||||
|
_exampleUnValueNIs :: (E.Value a, b, E.Value c) -> (a,b,c)
|
||||||
|
_exampleUnValueNIs = $(unValueNIs 3 [1,3])
|
||||||
|
|
||||||
-- | Example for usage of sqlIJproj
|
-- | Example for usage of sqlIJproj
|
||||||
-- queryFeaturesDegree :: (a `E.InnerJoin` b `E.InnerJoin` c) -> b
|
_queryFeaturesDegree :: (a `E.InnerJoin` b `E.InnerJoin` c) -> b
|
||||||
-- queryFeaturesDegree = $(sqlIJproj 3 2)
|
_queryFeaturesDegree = $(sqlIJproj 3 2)
|
||||||
|
|
||||||
|
|
||||||
-- | generic filter creation for dbTable
|
-- | generic filter creation for dbTable
|
||||||
-- Given a lens-like function, make filter for exact matches in a collection
|
-- Given a lens-like function, make filter for exact matches in a collection
|
||||||
-- (Generalizing from Set to Foldable ok here, but gives ambigouus types elsewhere)
|
-- (Generalizing from Set to Foldable ok here, but gives ambigouus types elsewhere)
|
||||||
mkExactFilter :: (PersistField a)
|
mkExactFilter :: (PersistField a)
|
||||||
=> (t -> E.SqlExpr (E.Value a)) -- ^ getter from query to searched element
|
=> (t -> E.SqlExpr (E.Value a)) -- ^ getter from query to searched element
|
||||||
-> t -- ^ query row
|
-> t -- ^ query row
|
||||||
-> Set.Set a -- ^ needle collection
|
-> Set.Set a -- ^ needle collection
|
||||||
-> E.SqlExpr (E.Value Bool)
|
-> E.SqlExpr (E.Value Bool)
|
||||||
mkExactFilter lenslike row criterias
|
mkExactFilter = mkExactFilterWith id
|
||||||
|
|
||||||
|
-- | like @mkExactFiler@ but allows for conversion; convenient in conjunction with @anyFilter@ and @allFilter@
|
||||||
|
mkExactFilterWith :: (PersistField b)
|
||||||
|
=> (a -> b) -- ^ type conversion
|
||||||
|
-> (t -> E.SqlExpr (E.Value b)) -- ^ getter from query to searched element
|
||||||
|
-> t -- ^ query row
|
||||||
|
-> Set.Set a -- ^ needle collection
|
||||||
|
-> E.SqlExpr (E.Value Bool)
|
||||||
|
mkExactFilterWith cast lenslike row criterias
|
||||||
| Set.null criterias = true
|
| Set.null criterias = true
|
||||||
| otherwise = lenslike row `E.in_` E.valList (Set.toList criterias)
|
| otherwise = lenslike row `E.in_` E.valList (cast <$> Set.toList criterias)
|
||||||
|
|
||||||
-- | generic filter creation for dbTable
|
-- | generic filter creation for dbTable
|
||||||
-- Given a lens-like function, make filter searching for needles in String-like elements
|
-- Given a lens-like function, make filter searching for needles in String-like elements
|
||||||
@ -87,9 +104,22 @@ mkContainsFilter lenslike row criterias
|
|||||||
| Set.null criterias = true
|
| Set.null criterias = true
|
||||||
| otherwise = any (hasInfix $ lenslike row) criterias
|
| otherwise = any (hasInfix $ lenslike row) criterias
|
||||||
|
|
||||||
|
-- | Combine several filters, using logical or
|
||||||
anyFilter :: (Foldable f) => f (t -> Set.Set Text-> E.SqlExpr (E.Value Bool))
|
anyFilter :: (Foldable f)
|
||||||
-> t -> Set.Set Text-> E.SqlExpr (E.Value Bool)
|
=> f (t -> Set.Set Text-> E.SqlExpr (E.Value Bool))
|
||||||
|
-> t
|
||||||
|
-> Set.Set Text
|
||||||
|
-> E.SqlExpr (E.Value Bool)
|
||||||
anyFilter fltrs needle criterias = F.foldr aux false fltrs
|
anyFilter fltrs needle criterias = F.foldr aux false fltrs
|
||||||
where
|
where
|
||||||
aux fltr acc = fltr needle criterias E.||. acc
|
aux fltr acc = fltr needle criterias E.||. acc
|
||||||
|
|
||||||
|
-- | Combine several filters, using logical and
|
||||||
|
allFilter :: (Foldable f)
|
||||||
|
=> f (t -> Set.Set Text-> E.SqlExpr (E.Value Bool))
|
||||||
|
-> t
|
||||||
|
-> Set.Set Text
|
||||||
|
-> E.SqlExpr (E.Value Bool)
|
||||||
|
allFilter fltrs needle criterias = F.foldr aux true fltrs
|
||||||
|
where
|
||||||
|
aux fltr acc = fltr needle criterias E.&&. acc
|
||||||
@ -1,6 +1,7 @@
|
|||||||
module Database.Esqueleto.Utils.TH
|
module Database.Esqueleto.Utils.TH
|
||||||
( SqlIn(..)
|
( SqlIn(..)
|
||||||
, sqlInTuple, sqlInTuples
|
, sqlInTuple, sqlInTuples
|
||||||
|
, unValueN, unValueNIs
|
||||||
, sqlIJproj, sqlLOJproj
|
, sqlIJproj, sqlLOJproj
|
||||||
) where
|
) where
|
||||||
|
|
||||||
@ -48,6 +49,30 @@ sqlInTuple arity = do
|
|||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
|
-- | Generic unValuing of Tuples of Values, i.e.
|
||||||
|
-- $(unValueN 3) :: (E.Value a, E.Value b, E.Value c) -> (a,b,c)
|
||||||
|
unValueN :: Int -> ExpQ
|
||||||
|
unValueN arity = do
|
||||||
|
vs <- replicateM arity $ newName "v"
|
||||||
|
let pat = tupP $ map varP vs
|
||||||
|
let uvE v = [e|E.unValue $(varE v)|]
|
||||||
|
let rhs = tupE $ map uvE vs
|
||||||
|
lam1E pat rhs
|
||||||
|
|
||||||
|
-- | Generic unValuing of certain indices of a Tuple, i.e.
|
||||||
|
-- $(unValueNIs 3 [1,3]) :: (E.Value a, b, E.Value c) -> (a,b,c)
|
||||||
|
unValueNIs :: Int -> [Int] -> ExpQ
|
||||||
|
unValueNIs arity uvIdx = do
|
||||||
|
vs <- replicateM arity $ newName "v"
|
||||||
|
let pat = tupP $ map varP vs
|
||||||
|
let rhs = tupE $ zipWith (curry uvEi) vs [1 ..]
|
||||||
|
lam1E pat rhs
|
||||||
|
where
|
||||||
|
uvEi (v,i) | i `elem` uvIdx = [e|E.unValue $(varE v)|]
|
||||||
|
| otherwise = varE v
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- | Generic projections for InnerJoin-tuples
|
-- | Generic projections for InnerJoin-tuples
|
||||||
-- gives I-th element of N-tuple of left-associative InnerJoin-pairs,
|
-- gives I-th element of N-tuple of left-associative InnerJoin-pairs,
|
||||||
-- i.e. @$(projN n m) :: (t1 `E.InnerJoin` .. `E.InnerJoin` tn) -> tm@ (for m<=n)
|
-- i.e. @$(projN n m) :: (t1 `E.InnerJoin` .. `E.InnerJoin` tn) -> tm@ (for m<=n)
|
||||||
|
|||||||
@ -26,8 +26,10 @@ import qualified Data.CaseInsensitive as CI
|
|||||||
import qualified Data.CryptoID as E
|
import qualified Data.CryptoID as E
|
||||||
|
|
||||||
import Data.ByteArray (convert)
|
import Data.ByteArray (convert)
|
||||||
import Crypto.Hash (Digest, SHAKE256)
|
import Crypto.Hash (Digest, SHAKE256, SHAKE128)
|
||||||
import Crypto.Hash.Conduit (sinkHash)
|
import Crypto.Hash.Conduit (sinkHash)
|
||||||
|
import qualified Data.UUID as UUID
|
||||||
|
import qualified Data.Binary as Binary
|
||||||
|
|
||||||
import qualified Data.ByteString.Base64.URL as Base64 (encode)
|
import qualified Data.ByteString.Base64.URL as Base64 (encode)
|
||||||
|
|
||||||
@ -62,7 +64,6 @@ import Control.Monad.Memo (MemoT, startEvalMemoT, MonadMemo(..))
|
|||||||
import qualified Control.Monad.Catch as C
|
import qualified Control.Monad.Catch as C
|
||||||
|
|
||||||
import Handler.Utils.StudyFeatures
|
import Handler.Utils.StudyFeatures
|
||||||
import Handler.Utils.Templates
|
|
||||||
import Utils.Lens
|
import Utils.Lens
|
||||||
import Utils.Form
|
import Utils.Form
|
||||||
import Utils.Sheet
|
import Utils.Sheet
|
||||||
@ -172,7 +173,13 @@ noneOneMoreDE num noneText singularForm pluralForm
|
|||||||
| num == 1 = singularForm
|
| num == 1 = singularForm
|
||||||
| otherwise = pluralForm
|
| otherwise = pluralForm
|
||||||
|
|
||||||
|
-- Convenience Type for Messages
|
||||||
|
type IntMaybe = Maybe Int -- Yesod messages cannot deal with compound type identifiers
|
||||||
|
|
||||||
|
-- | Convenience function for i18n messages definitions
|
||||||
|
maybeDisplay :: DisplayAble m => Text -> Maybe m -> Text -> Text
|
||||||
|
maybeDisplay _ Nothing _ = mempty
|
||||||
|
maybeDisplay before (Just x) after = before <> (display x) <> after
|
||||||
|
|
||||||
-- Messages creates type UniWorXMessage and RenderMessage UniWorX instance
|
-- Messages creates type UniWorXMessage and RenderMessage UniWorX instance
|
||||||
mkMessage "UniWorX" "messages/uniworx" "de"
|
mkMessage "UniWorX" "messages/uniworx" "de"
|
||||||
@ -233,6 +240,7 @@ embedRenderMessage ''UniWorX ''SheetGrading ("SheetGrading" <>)
|
|||||||
embedRenderMessage ''UniWorX ''AuthTag $ ("AuthTag" <>) . concat . drop 1 . splitCamel
|
embedRenderMessage ''UniWorX ''AuthTag $ ("AuthTag" <>) . concat . drop 1 . splitCamel
|
||||||
embedRenderMessage ''UniWorX ''SheetSubmissionMode ("Sheet" <>)
|
embedRenderMessage ''UniWorX ''SheetSubmissionMode ("Sheet" <>)
|
||||||
embedRenderMessage ''UniWorX ''EncodedSecretBoxException id
|
embedRenderMessage ''UniWorX ''EncodedSecretBoxException id
|
||||||
|
embedRenderMessage ''UniWorX ''LecturerType id
|
||||||
|
|
||||||
newtype SheetTypeHeader = SheetTypeHeader SheetType
|
newtype SheetTypeHeader = SheetTypeHeader SheetType
|
||||||
embedRenderMessageVariant ''UniWorX ''SheetTypeHeader ("SheetType" <>)
|
embedRenderMessageVariant ''UniWorX ''SheetTypeHeader ("SheetType" <>)
|
||||||
@ -993,15 +1001,15 @@ siteLayout' headingOverride widget = do
|
|||||||
| isModal -> getMessages
|
| isModal -> getMessages
|
||||||
| otherwise -> do
|
| otherwise -> do
|
||||||
applySystemMessages
|
applySystemMessages
|
||||||
authTagPivots <- fromMaybe Set.empty <$> getSessionJson SessionInactiveAuthTags
|
authTagPivots <- fromMaybe Set.empty <$> takeSessionJson SessionInactiveAuthTags
|
||||||
forM_ authTagPivots $
|
forM_ authTagPivots $
|
||||||
\authTag -> addMessageWidget Info $ modal [whamlet|_{MsgUnauthorizedDisabledTag authTag}|] (Left $ SomeRoute (AuthPredsR, catMaybes [(toPathPiece GetReferer, ) . toPathPiece <$> mcurrentRoute]))
|
\authTag -> addMessageWidget Info $ modal [whamlet|_{MsgUnauthorizedDisabledTag authTag}|] (Left $ SomeRoute (AuthPredsR, catMaybes [(toPathPiece GetReferer, ) . toPathPiece <$> mcurrentRoute]))
|
||||||
getMessages
|
getMessages
|
||||||
|
|
||||||
let highlight :: Route UniWorX -> Bool -- highlight last route in breadcrumbs, favorites taking priority
|
let highlight :: Route UniWorX -> Bool -- highlight last route in breadcrumbs, favorites taking priority
|
||||||
highlight = let crumbs = mcons mcurrentRoute $ fst <$> reverse parents
|
highlight = let crumbs = mcons mcurrentRoute $ fst <$> reverse parents
|
||||||
navItems = map snd3 favourites ++ map (urlRoute . menuItemRoute . view _1) menuTypes
|
navItems = map (view _2) favourites ++ map (urlRoute . menuItemRoute . view _1) menuTypes
|
||||||
highR = find (`elem` navItems) . uncurry (++) $ partition (`elem` map snd3 favourites) crumbs
|
highR = find (`elem` navItems) . uncurry (++) $ partition (`elem` map (view _2) favourites) crumbs
|
||||||
in \r -> Just r == highR
|
in \r -> Just r == highR
|
||||||
favouriteTerms :: [TermIdentifier]
|
favouriteTerms :: [TermIdentifier]
|
||||||
favouriteTerms = Set.toDescList $ foldMap (\(Course{..}, _, _) -> Set.singleton $ unTermKey courseTerm) favourites
|
favouriteTerms = Set.toDescList $ foldMap (\(Course{..}, _, _) -> Set.singleton $ unTermKey courseTerm) favourites
|
||||||
@ -1014,6 +1022,17 @@ siteLayout' headingOverride 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.
|
||||||
|
|
||||||
|
navbarModal (MenuItem{..}, menuIdent') = customModal Modal
|
||||||
|
{ modalTriggerId = Just menuIdent'
|
||||||
|
, modalId = Nothing
|
||||||
|
, modalTrigger = \(Just route) menuIdent -> $(widgetFile "widgets/navbar/item")
|
||||||
|
, modalContent = Left menuItemRoute
|
||||||
|
}
|
||||||
|
|
||||||
|
navbarItem (MenuItem{..}, menuIdent) = do
|
||||||
|
route <- toTextUrl menuItemRoute
|
||||||
|
$(widgetFile "widgets/navbar/item")
|
||||||
|
|
||||||
navbar :: Widget
|
navbar :: Widget
|
||||||
navbar = $(widgetFile "widgets/navbar/navbar")
|
navbar = $(widgetFile "widgets/navbar/navbar")
|
||||||
asidenav :: Widget
|
asidenav :: Widget
|
||||||
@ -1141,7 +1160,7 @@ instance YesodBreadcrumbs UniWorX where
|
|||||||
-- (CourseR tid ssh csh CRegisterR) -- is POST only
|
-- (CourseR tid ssh csh CRegisterR) -- is POST only
|
||||||
breadcrumb (CourseR tid ssh csh CEditR) = return ("Editieren" , Just $ CourseR tid ssh csh CShowR)
|
breadcrumb (CourseR tid ssh csh CEditR) = return ("Editieren" , Just $ CourseR tid ssh csh CShowR)
|
||||||
breadcrumb (CourseR tid ssh csh CUsersR) = return ("Anmeldungen", Just $ CourseR tid ssh csh CShowR)
|
breadcrumb (CourseR tid ssh csh CUsersR) = return ("Anmeldungen", Just $ CourseR tid ssh csh CShowR)
|
||||||
breadcrumb (CourseR tid ssh csh (CUserR _)) = return ("Teilnehmer" , Just $ CourseR tid ssh csh CShowR)
|
breadcrumb (CourseR tid ssh csh (CUserR _)) = return ("Teilnehmer" , Just $ CourseR tid ssh csh CUsersR)
|
||||||
breadcrumb (CourseR tid ssh csh CCorrectionsR) = return ("Abgaben" , Just $ CourseR tid ssh csh CShowR)
|
breadcrumb (CourseR tid ssh csh CCorrectionsR) = return ("Abgaben" , Just $ CourseR tid ssh csh CShowR)
|
||||||
breadcrumb (CourseR tid ssh csh SheetListR) = return ("Übungen" , Just $ CourseR tid ssh csh CShowR)
|
breadcrumb (CourseR tid ssh csh SheetListR) = return ("Übungen" , Just $ CourseR tid ssh csh CShowR)
|
||||||
breadcrumb (CourseR tid ssh csh SheetNewR ) = return ("Neu", Just $ CourseR tid ssh csh SheetListR)
|
breadcrumb (CourseR tid ssh csh SheetNewR ) = return ("Neu", Just $ CourseR tid ssh csh SheetListR)
|
||||||
@ -1355,6 +1374,14 @@ pageActions (AdminR) =
|
|||||||
, menuItemModal = False
|
, menuItemModal = False
|
||||||
, menuItemAccessCallback' = return True
|
, menuItemAccessCallback' = return True
|
||||||
}
|
}
|
||||||
|
, MenuItem
|
||||||
|
{ menuItemType = PageActionPrime
|
||||||
|
, menuItemLabel = MsgMenuMessageList
|
||||||
|
, menuItemIcon = Nothing
|
||||||
|
, menuItemRoute = SomeRoute MessageListR
|
||||||
|
, menuItemModal = False
|
||||||
|
, menuItemAccessCallback' = return True
|
||||||
|
}
|
||||||
, MenuItem
|
, MenuItem
|
||||||
{ menuItemType = PageActionPrime
|
{ menuItemType = PageActionPrime
|
||||||
, menuItemLabel = MsgErrMsgHeading
|
, menuItemLabel = MsgErrMsgHeading
|
||||||
@ -2105,7 +2132,6 @@ instance YesodAuth UniWorX where
|
|||||||
[ UserLastAuthentication =. Just now | not isDummy ]
|
[ UserLastAuthentication =. Just now | not isDummy ]
|
||||||
|
|
||||||
userId <- lift $ entityKey <$> upsertBy uAuth newUser userUpdate
|
userId <- lift $ entityKey <$> upsertBy uAuth newUser userUpdate
|
||||||
studyTermCandidateIncidence <- liftIO getRandom
|
|
||||||
|
|
||||||
let
|
let
|
||||||
userStudyFeatures = fmap concat . forM userStudyFeatures' $ parseStudyFeatures userId now
|
userStudyFeatures = fmap concat . forM userStudyFeatures' $ parseStudyFeatures userId now
|
||||||
@ -2126,11 +2152,27 @@ instance YesodAuth UniWorX where
|
|||||||
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
|
||||||
|
|
||||||
let
|
let
|
||||||
studyTermCandidates = do
|
studyTermCandidates = Set.fromList $ do
|
||||||
studyTermCandidateName <- termNames
|
name <- termNames
|
||||||
StudyFeatures{ studyFeaturesField = StudyTermsKey' studyTermCandidateKey } <- fs
|
StudyFeatures{ studyFeaturesField = StudyTermsKey' key } <- fs
|
||||||
return StudyTermCandidate{..}
|
return (key, name)
|
||||||
lift $ insertMany_ studyTermCandidates
|
studyTermCandidateIncidence
|
||||||
|
= fromMaybe (error "Could not convert studyTermCandidateIncidence-Hash to UUID")
|
||||||
|
. UUID.fromByteString
|
||||||
|
. fromStrict
|
||||||
|
. (convert :: Digest (SHAKE128 128) -> ByteString)
|
||||||
|
. runIdentity
|
||||||
|
$ sourceList (toStrict . Binary.encode <$> Set.toList studyTermCandidates) $$ sinkHash
|
||||||
|
|
||||||
|
[E.Value candidatesRecorded] <- lift . E.select . return . E.exists . E.from $ \candidate ->
|
||||||
|
E.where_ $ candidate E.^. StudyTermCandidateIncidence E.==. E.val studyTermCandidateIncidence
|
||||||
|
|
||||||
|
unless candidatesRecorded $ do
|
||||||
|
let
|
||||||
|
studyTermCandidates' = do
|
||||||
|
(studyTermCandidateKey, studyTermCandidateName) <- Set.toList studyTermCandidates
|
||||||
|
return StudyTermCandidate{..}
|
||||||
|
lift $ insertMany_ studyTermCandidates'
|
||||||
|
|
||||||
lift $ E.updateWhere [StudyFeaturesUser ==. userId] [StudyFeaturesValid =. False]
|
lift $ E.updateWhere [StudyFeaturesUser ==. userId] [StudyFeaturesValid =. False]
|
||||||
forM_ fs $ \f@StudyFeatures{..} -> do
|
forM_ fs $ \f@StudyFeatures{..} -> do
|
||||||
@ -2179,12 +2221,9 @@ instance YesodMail UniWorX where
|
|||||||
mailT ctx mail = defMailT ctx $ do
|
mailT ctx mail = defMailT ctx $ do
|
||||||
void setMailObjectId
|
void setMailObjectId
|
||||||
setDateCurrent
|
setDateCurrent
|
||||||
replaceMailHeader "Auto-Submitted" $ Just "auto-generated"
|
replaceMailHeader "Sender" . Just . addressEmail =<< getsYesod (appMailFrom . appSettings)
|
||||||
|
|
||||||
ret <- mail
|
mail <* setMailSmtpData
|
||||||
|
|
||||||
setMailSmtpData
|
|
||||||
return ret
|
|
||||||
|
|
||||||
|
|
||||||
instance (MonadThrow m, MonadHandler m, HandlerSite m ~ UniWorX) => MonadCrypto m where
|
instance (MonadThrow m, MonadHandler m, HandlerSite m ~ UniWorX) => MonadCrypto m where
|
||||||
|
|||||||
@ -170,11 +170,11 @@ postAdminTestR = do
|
|||||||
-> Natural -- ^ Dimension Index, outermost dimension ist 0 i.e. if dimension is 3 hyperplane-adders get passed 0, planes get passed 1, lines get 2, and points get 3
|
-> Natural -- ^ Dimension Index, outermost dimension ist 0 i.e. if dimension is 3 hyperplane-adders get passed 0, planes get passed 1, lines get 2, and points get 3
|
||||||
-> (Text -> Text) -- ^ Nudge deterministic field ids so they're unique
|
-> (Text -> Text) -- ^ Nudge deterministic field ids so they're unique
|
||||||
-> FieldView UniWorX -- ^ Submit-Button for this add-widget
|
-> FieldView UniWorX -- ^ Submit-Button for this add-widget
|
||||||
-> Maybe (Form (ListLength -> (ListPosition, Int))) -- ^ Nothing iff adding further cells in this position/dimension makes no sense; returns callback to determine index of new cell and data needed to initialize cell
|
-> Maybe (Form (Map ListPosition Int -> FormResult (Map ListPosition Int))) -- ^ Nothing iff adding further cells in this position/dimension makes no sense; returns callback to determine index of new cells and data needed to initialize cells
|
||||||
mkAddForm 0 0 nudge submitBtn = Just $ \csrf -> do
|
mkAddForm 0 0 nudge submitBtn = Just $ \csrf -> do
|
||||||
(addRes, addView) <- mpreq textField ("" & addName (nudge "text")) Nothing -- Any old field; for demonstration
|
(addRes, addView) <- mpreq textField ("" & addName (nudge "text")) Nothing -- Any old field; for demonstration
|
||||||
let addRes' = fromMaybe 0 . readMay . Text.filter isDigit <$> addRes -- Do something semi-interesting on the result of the @textField@ to demonstrate that further processing can be done
|
let addRes' = fromMaybe 0 . readMay . Text.filter isDigit <$> addRes -- Do something semi-interesting on the result of the @textField@ to demonstrate that further processing can be done
|
||||||
addRes'' = (\dat l -> (fromIntegral l, dat)) <$> addRes' -- Construct the callback to determine new cell position and data within @FormResult@ as required
|
addRes'' = addRes' <&> \dat prev -> FormSuccess (Map.singleton (maybe 0 succ . fmap fst $ Map.lookupMax prev) dat) -- Construct the callback to determine new cell positions and data within @FormResult@ as required, nested @FormResult@ allows aborting the add depending on previous data
|
||||||
return (addRes'', toWidget csrf >> fvInput addView >> fvInput submitBtn)
|
return (addRes'', toWidget csrf >> fvInput addView >> fvInput submitBtn)
|
||||||
mkAddForm _pos _dim _ _ = error "Dimension and Position is always 0 for our 1-dimensional form"
|
mkAddForm _pos _dim _ _ = error "Dimension and Position is always 0 for our 1-dimensional form"
|
||||||
|
|
||||||
@ -199,9 +199,13 @@ postAdminTestR = do
|
|||||||
-- | Make a decision on whether an add widget should be allowed to further cells, given the current @liveliness@ (i.e. before performing the addition)
|
-- | Make a decision on whether an add widget should be allowed to further cells, given the current @liveliness@ (i.e. before performing the addition)
|
||||||
allowAdd :: ListPosition -> Natural -> ListLength -> Bool
|
allowAdd :: ListPosition -> Natural -> ListLength -> Bool
|
||||||
allowAdd _ _ l = l < 7 -- Limit list length; much more complicated checks are possible (this could in principle be monadic, but @massInput@ is probably already complicated enough to cover just current (2019-03) usecases)
|
allowAdd _ _ l = l < 7 -- Limit list length; much more complicated checks are possible (this could in principle be monadic, but @massInput@ is probably already complicated enough to cover just current (2019-03) usecases)
|
||||||
|
-- | Where to send the user when they click a shape-changing button, given the id of the Wrapper of the `massInput`-`Widget`
|
||||||
|
buttonAction :: PathPiece p => p -> Maybe (SomeRoute UniWorX)
|
||||||
|
buttonAction frag = Just . SomeRoute $ AdminTestR :#: frag
|
||||||
|
|
||||||
-- The actual call to @massInput@ is comparatively simple:
|
-- The actual call to @massInput@ is comparatively simple:
|
||||||
((miResult, fvInput -> miForm), miEnc) <- runFormPost . identifyForm ("massinput" :: Text) $ massInput (MassInput mkAddForm mkCellForm deleteCell allowAdd) "" True Nothing
|
|
||||||
|
((miResult, fvInput -> miForm), miEnc) <- runFormPost . identifyForm ("massinput" :: Text) $ massInput (MassInput mkAddForm mkCellForm deleteCell allowAdd buttonAction) "" True Nothing
|
||||||
|
|
||||||
|
|
||||||
let locallyDefinedPageHeading = [whamlet|Admin TestPage for Uni2work|]
|
let locallyDefinedPageHeading = [whamlet|Admin TestPage for Uni2work|]
|
||||||
@ -265,46 +269,71 @@ postAdminErrMsgR = do
|
|||||||
|
|
||||||
|
|
||||||
-- BEGIN - Buttons needed only for StudyTermCandidateManagement
|
-- BEGIN - Buttons needed only for StudyTermCandidateManagement
|
||||||
data ButtonInferStudyTerms = ButtonInferStudyTerms
|
data ButtonAdminStudyTerms
|
||||||
|
= BtnCandidatesInfer
|
||||||
|
| BtnCandidatesDeleteConflicts
|
||||||
|
| BtnCandidatesDeleteAll
|
||||||
deriving (Enum, Eq, Ord, Bounded, Read, Show, Generic, Typeable)
|
deriving (Enum, Eq, Ord, Bounded, Read, Show, Generic, Typeable)
|
||||||
instance Universe ButtonInferStudyTerms
|
instance Universe ButtonAdminStudyTerms
|
||||||
instance Finite ButtonInferStudyTerms
|
instance Finite ButtonAdminStudyTerms
|
||||||
|
|
||||||
nullaryPathPiece ''ButtonInferStudyTerms camelToPathPiece
|
nullaryPathPiece ''ButtonAdminStudyTerms camelToPathPiece
|
||||||
|
embedRenderMessage ''UniWorX ''ButtonAdminStudyTerms id
|
||||||
|
|
||||||
instance Button UniWorX ButtonInferStudyTerms where
|
instance Button UniWorX ButtonAdminStudyTerms where
|
||||||
btnLabel ButtonInferStudyTerms = "Studienfachzuordnung automatisch lernen"
|
btnClasses BtnCandidatesInfer = [BCIsButton, BCPrimary]
|
||||||
btnClasses ButtonInferStudyTerms = [BCIsButton, BCPrimary]
|
btnClasses BtnCandidatesDeleteConflicts = [BCIsButton, BCDanger]
|
||||||
|
btnClasses BtnCandidatesDeleteAll = [BCIsButton, BCDanger]
|
||||||
-- END Button needed only here
|
-- END Button needed only here
|
||||||
|
|
||||||
|
sessionKeyNewStudyTerms :: Text
|
||||||
|
sessionKeyNewStudyTerms = "key-new-study-terms"
|
||||||
|
|
||||||
getAdminFeaturesR, postAdminFeaturesR :: Handler Html
|
getAdminFeaturesR, postAdminFeaturesR :: Handler Html
|
||||||
getAdminFeaturesR = postAdminFeaturesR
|
getAdminFeaturesR = postAdminFeaturesR
|
||||||
postAdminFeaturesR = do
|
postAdminFeaturesR = do
|
||||||
((btnResult, btnWdgt), btnEnctype) <- runFormPost $ identifyForm ("infer-button" :: Text) (buttonForm :: Form ButtonInferStudyTerms)
|
((btnResult, btnWdgt), btnEnctype) <- runFormPost $ identifyForm ("infer-button" :: Text) (buttonForm :: Form ButtonAdminStudyTerms)
|
||||||
let btnForm = wrapForm btnWdgt def
|
let btnForm = wrapForm btnWdgt def
|
||||||
{ formAction = Just $ SomeRoute AdminFeaturesR
|
{ formAction = Just $ SomeRoute AdminFeaturesR
|
||||||
, formEncoding = btnEnctype
|
, formEncoding = btnEnctype
|
||||||
, formSubmit = FormNoSubmit
|
, formSubmit = FormNoSubmit
|
||||||
}
|
}
|
||||||
(infConflicts,infAccepted) <- case btnResult of
|
infConflicts <- case btnResult of
|
||||||
(FormSuccess ButtonInferStudyTerms) -> do
|
FormSuccess BtnCandidatesInfer -> do
|
||||||
(infConflicts,infAmbiguous,infRedundant,infAccepted) <- Candidates.inferHandler
|
(infConflicts, infAmbiguous, infRedundant, infAccepted) <- Candidates.inferHandler
|
||||||
unless (null infAmbiguous) $ addMessageI Info $ MsgAmbiguousCandidatesRemoved $ length infAmbiguous
|
unless (null infAmbiguous) . addMessageI Info . MsgAmbiguousCandidatesRemoved $ length infAmbiguous
|
||||||
unless (null infRedundant) $ addMessageI Info $ MsgRedundantCandidatesRemoved $ length infRedundant
|
unless (null infRedundant) . addMessageI Info . MsgRedundantCandidatesRemoved $ length infRedundant
|
||||||
if null infAccepted
|
let newKeys = map (StudyTermsKey' . fst) infAccepted
|
||||||
then addMessageI Info MsgNoCandidatesInferred
|
setSessionJson sessionKeyNewStudyTerms newKeys
|
||||||
else addMessageI Success $ MsgCandidatesInferred $ length infAccepted
|
if | null infAccepted
|
||||||
return (infConflicts,infAccepted)
|
-> addMessageI Info MsgNoCandidatesInferred
|
||||||
_other -> (,[]) <$> runDB Candidates.conflicts
|
| otherwise
|
||||||
unless (null infConflicts) $ addMessage Warning "KONFLIKTE vorhanden" --TODO i18n
|
-> addMessageI Success . MsgCandidatesInferred $ length infAccepted
|
||||||
|
return infConflicts
|
||||||
|
FormSuccess BtnCandidatesDeleteConflicts -> runDB $ do
|
||||||
|
confs <- Candidates.conflicts
|
||||||
|
incis <- Candidates.getIncidencesFor (entityKey <$> confs)
|
||||||
|
deleteWhere [StudyTermCandidateIncidence <-. (E.unValue <$> incis)]
|
||||||
|
addMessageI Success $ MsgIncidencesDeleted $ length incis
|
||||||
|
return []
|
||||||
|
FormSuccess BtnCandidatesDeleteAll -> runDB $ do
|
||||||
|
deleteWhere ([] :: [Filter StudyTermCandidate])
|
||||||
|
addMessageI Success MsgAllIncidencesDeleted
|
||||||
|
Candidates.conflicts
|
||||||
|
_other -> runDB Candidates.conflicts
|
||||||
|
|
||||||
|
newStudyTermKeys <- fromMaybe [] <$> lookupSessionJson sessionKeyNewStudyTerms
|
||||||
( (degreeResult,degreeTable)
|
( (degreeResult,degreeTable)
|
||||||
, (studyTermsResult,studytermsTable)
|
, (studyTermsResult,studytermsTable)
|
||||||
, ((),candidateTable)) <- runDB $ (,,)
|
, ((), candidateTable)) <- runDB $ (,,)
|
||||||
<$> mkDegreeTable
|
<$> mkDegreeTable
|
||||||
<*> mkStudytermsTable (Set.fromList $ map (StudyTermsKey' . fst) infAccepted)
|
<*> mkStudytermsTable (Set.fromList newStudyTermKeys)
|
||||||
|
(Set.fromList $ map entityKey infConflicts)
|
||||||
<*> mkCandidateTable
|
<*> mkCandidateTable
|
||||||
|
|
||||||
|
-- This needs to happen after calls to `dbTable` so they can short-circuit correctly
|
||||||
|
unless (null infConflicts) $ addMessageI Warning MsgStudyFeatureConflict
|
||||||
|
|
||||||
let degreeResult' :: FormResult (Map (Key StudyDegree) (Maybe Text, Maybe Text))
|
let degreeResult' :: FormResult (Map (Key StudyDegree) (Maybe Text, Maybe Text))
|
||||||
degreeResult' = degreeResult <&> getDBFormResult
|
degreeResult' = degreeResult <&> getDBFormResult
|
||||||
(\row -> ( row ^. _dbrOutput . _entityVal . _studyDegreeName
|
(\row -> ( row ^. _dbrOutput . _entityVal . _studyDegreeName
|
||||||
@ -344,7 +373,7 @@ postAdminFeaturesR = do
|
|||||||
dbtRowKey = (E.^. StudyDegreeKey)
|
dbtRowKey = (E.^. StudyDegreeKey)
|
||||||
dbtProj = return
|
dbtProj = return
|
||||||
dbtColonnade = formColonnade $ mconcat
|
dbtColonnade = formColonnade $ mconcat
|
||||||
[ sortable (Just "key") (i18nCell MsgDegreeKey) (numCell . view (_dbrOutput . _entityVal . _studyDegreeKey))
|
[ sortable (Just "key") (i18nCell MsgGenericKey) (numCell . view (_dbrOutput . _entityVal . _studyDegreeKey))
|
||||||
, sortable (Just "name") (i18nCell MsgDegreeName) (textInputCell _1 (_dbrOutput . _entityVal . _studyDegreeName))
|
, sortable (Just "name") (i18nCell MsgDegreeName) (textInputCell _1 (_dbrOutput . _entityVal . _studyDegreeName))
|
||||||
, sortable (Just "short") (i18nCell MsgDegreeShort) (textInputCell _2 (_dbrOutput . _entityVal . _studyDegreeShorthand))
|
, sortable (Just "short") (i18nCell MsgDegreeShort) (textInputCell _2 (_dbrOutput . _entityVal . _studyDegreeShorthand))
|
||||||
, dbRow
|
, dbRow
|
||||||
@ -358,11 +387,12 @@ postAdminFeaturesR = do
|
|||||||
dbtFilterUI = mempty
|
dbtFilterUI = mempty
|
||||||
dbtParams = def { dbParamsFormAction = Just . SomeRoute $ AdminFeaturesR :#: ("admin-studydegrees-table-wrapper" :: Text)
|
dbtParams = def { dbParamsFormAction = Just . SomeRoute $ AdminFeaturesR :#: ("admin-studydegrees-table-wrapper" :: Text)
|
||||||
}
|
}
|
||||||
psValidator = def & defaultSorting [SortAscBy "name", SortAscBy "short", SortAscBy "key"]
|
psValidator = def -- & defaultSorting [SortAscBy "name", SortAscBy "short", SortAscBy "key"]
|
||||||
|
& defaultSorting [SortAscBy "key"]
|
||||||
in dbTable psValidator DBTable{..}
|
in dbTable psValidator DBTable{..}
|
||||||
|
|
||||||
mkStudytermsTable :: Set (Key StudyTerms) -> DB (FormResult (DBFormResult (Key StudyTerms) (Maybe Text, Maybe Text) (DBRow (Entity StudyTerms))), Widget)
|
mkStudytermsTable :: Set (Key StudyTerms) -> Set (Key StudyTerms) -> DB (FormResult (DBFormResult (Key StudyTerms) (Maybe Text, Maybe Text) (DBRow (Entity StudyTerms))), Widget)
|
||||||
mkStudytermsTable newKeys =
|
mkStudytermsTable newKeys badKeys =
|
||||||
let dbtIdent = "admin-studyterms" :: Text
|
let dbtIdent = "admin-studyterms" :: Text
|
||||||
dbtStyle = def
|
dbtStyle = def
|
||||||
dbtSQLQuery :: E.SqlExpr (Entity StudyTerms) -> E.SqlQuery ( E.SqlExpr (Entity StudyTerms))
|
dbtSQLQuery :: E.SqlExpr (Entity StudyTerms) -> E.SqlQuery ( E.SqlExpr (Entity StudyTerms))
|
||||||
@ -370,15 +400,18 @@ postAdminFeaturesR = do
|
|||||||
dbtRowKey = (E.^. StudyTermsKey)
|
dbtRowKey = (E.^. StudyTermsKey)
|
||||||
dbtProj = return
|
dbtProj = return
|
||||||
dbtColonnade = formColonnade $ mconcat
|
dbtColonnade = formColonnade $ mconcat
|
||||||
[ sortable (Just "key") (i18nCell MsgStudyTermsKey) (numCell . view (_dbrOutput . _entityVal . _studyTermsKey))
|
[ sortable (Just "key") (i18nCell MsgGenericKey) (numCell . view (_dbrOutput . _entityVal . _studyTermsKey))
|
||||||
, sortable (Just "isnew") (i18nCell MsgStudyTermIsNew) (isNewCell . flip Set.member newKeys . view (_dbrOutput . _entityKey))
|
, sortable (Just "isnew") (i18nCell MsgGenericIsNew) (isNewCell . flip Set.member newKeys . view (_dbrOutput . _entityKey))
|
||||||
, sortable (Just "name") (i18nCell MsgStudyTermsName) (textInputCell _1 (_dbrOutput . _entityVal . _studyTermsName))
|
, sortable (Just "isbad") (i18nCell MsgGenericHasConflict) (isBadCell . flip Set.member badKeys . view (_dbrOutput . _entityKey))
|
||||||
, sortable (Just "short") (i18nCell MsgStudyTermsShort) (textInputCell _2 (_dbrOutput . _entityVal . _studyTermsShorthand))
|
, sortable (Just "name") (i18nCell MsgStudyTermsName) (textInputCell _1 (_dbrOutput . _entityVal . _studyTermsName))
|
||||||
|
, sortable (Just "short") (i18nCell MsgStudyTermsShort) (textInputCell _2 (_dbrOutput . _entityVal . _studyTermsShorthand))
|
||||||
, dbRow
|
, dbRow
|
||||||
]
|
]
|
||||||
dbtSorting = Map.fromList
|
dbtSorting = Map.fromList
|
||||||
[ ("key" , SortColumn (E.^. StudyTermsKey))
|
[ ("key" , SortColumn (E.^. StudyTermsKey))
|
||||||
, ("isnew" , SortColumn (\studyTerm -> studyTerm E.^. StudyTermsId `E.in_` E.valList (Set.toList newKeys)))
|
, ("isnew" , SortColumn (\studyTerm -> studyTerm E.^. StudyTermsKey `E.in_` E.valList (unStudyTermsKey <$> Set.toList newKeys))) -- works only once
|
||||||
|
-- Remember: sorting with E.in_ by StudyTermsId instead will produce esqueleto-error "unsafeSqlBinOp: non-id/composite keys not expected here"
|
||||||
|
, ("isbad" , SortColumn (\studyTerm -> studyTerm E.^. StudyTermsKey `E.in_` E.valList (unStudyTermsKey <$> Set.toList badKeys)))
|
||||||
, ("name" , SortColumn (E.^. StudyTermsName))
|
, ("name" , SortColumn (E.^. StudyTermsName))
|
||||||
, ("short" , SortColumn (E.^. StudyTermsShorthand))
|
, ("short" , SortColumn (E.^. StudyTermsShorthand))
|
||||||
]
|
]
|
||||||
@ -386,7 +419,9 @@ postAdminFeaturesR = do
|
|||||||
dbtFilterUI = mempty
|
dbtFilterUI = mempty
|
||||||
dbtParams = def { dbParamsFormAction = Just . SomeRoute $ AdminFeaturesR :#: ("admin-studyterms-table-wrapper" :: Text)
|
dbtParams = def { dbParamsFormAction = Just . SomeRoute $ AdminFeaturesR :#: ("admin-studyterms-table-wrapper" :: Text)
|
||||||
}
|
}
|
||||||
psValidator = def & defaultSorting [SortAscBy "name", SortAscBy "short", SortAscBy "key"]
|
psValidator = def
|
||||||
|
-- & defaultSorting [SortAscBy "name", SortAscBy "short", SortAscBy "key"]
|
||||||
|
& defaultSorting [SortDescBy "isnew", SortDescBy "isbad", SortAscBy "key"]
|
||||||
in dbTable psValidator DBTable{..}
|
in dbTable psValidator DBTable{..}
|
||||||
|
|
||||||
mkCandidateTable =
|
mkCandidateTable =
|
||||||
|
|||||||
@ -161,6 +161,7 @@ colRating = sortable (Just "rating") (i18nCell MsgRating) $ \DBRow{ dbrOutput=(E
|
|||||||
mkRoute = do
|
mkRoute = do
|
||||||
cid <- encrypt subId
|
cid <- encrypt subId
|
||||||
return $ CSubmissionR tid ssh csh sheetName cid CorrectionR
|
return $ CSubmissionR tid ssh csh sheetName cid CorrectionR
|
||||||
|
mTuple mA mB = (,) <$> mA <*> mB -- Hamlet does not support enough haskell-syntax for this
|
||||||
in mconcat
|
in mconcat
|
||||||
[ anchorCellM mkRoute $(widgetFile "widgets/rating/rating")
|
[ anchorCellM mkRoute $(widgetFile "widgets/rating/rating")
|
||||||
, writerCell $ do
|
, writerCell $ do
|
||||||
|
|||||||
@ -9,15 +9,17 @@ import Utils.Form
|
|||||||
-- import Utils.DB
|
-- import Utils.DB
|
||||||
import Handler.Utils
|
import Handler.Utils
|
||||||
import Handler.Utils.Course
|
import Handler.Utils.Course
|
||||||
|
import Handler.Utils.Form.MassInput
|
||||||
import Handler.Utils.Delete
|
import Handler.Utils.Delete
|
||||||
import Handler.Utils.Database
|
import Handler.Utils.Database
|
||||||
import Handler.Utils.Table.Cells
|
import Handler.Utils.Table.Cells
|
||||||
import Handler.Utils.Table.Columns
|
import Handler.Utils.Table.Columns
|
||||||
import Database.Esqueleto.Utils
|
import Database.Persist.Sql (deleteWhereCount)
|
||||||
|
import qualified Database.Esqueleto.Utils as E
|
||||||
import Database.Esqueleto.Utils.TH
|
import Database.Esqueleto.Utils.TH
|
||||||
|
|
||||||
-- import Data.Time
|
-- import Data.Time
|
||||||
-- import qualified Data.Text as T
|
import qualified Data.CaseInsensitive as CI
|
||||||
import Data.Function ((&))
|
import Data.Function ((&))
|
||||||
-- import Yesod.Form.Bootstrap3
|
-- import Yesod.Form.Bootstrap3
|
||||||
|
|
||||||
@ -29,6 +31,7 @@ import qualified Data.Map as Map
|
|||||||
|
|
||||||
import qualified Database.Esqueleto as E
|
import qualified Database.Esqueleto as E
|
||||||
|
|
||||||
|
import Text.Blaze.Html.Renderer.Text (renderHtml)
|
||||||
|
|
||||||
-- NOTE: Outdated way to use dbTable; see ProfileDataR Handler for a more recent method.
|
-- NOTE: Outdated way to use dbTable; see ProfileDataR Handler for a more recent method.
|
||||||
type CourseTableData = DBRow (Entity Course, Int, Bool, Entity School)
|
type CourseTableData = DBRow (Entity Course, Int, Bool, Entity School)
|
||||||
@ -268,7 +271,7 @@ getTermCourseListR tid = do
|
|||||||
getCShowR :: TermId -> SchoolId -> CourseShorthand -> Handler Html
|
getCShowR :: TermId -> SchoolId -> CourseShorthand -> Handler Html
|
||||||
getCShowR tid ssh csh = do
|
getCShowR tid ssh csh = do
|
||||||
mbAid <- maybeAuthId
|
mbAid <- maybeAuthId
|
||||||
(course,schoolName,participants,registration,defSFid,lecturers) <- runDB . maybeT notFound $ do
|
(course,schoolName,participants,registration,defSFid,lecturers,assistants) <- runDB . maybeT notFound $ do
|
||||||
[(E.Entity cid course, E.Value schoolName, E.Value participants, fmap entityVal -> registration)]
|
[(E.Entity cid course, E.Value schoolName, E.Value participants, fmap entityVal -> registration)]
|
||||||
<- lift . E.select . E.from $
|
<- lift . E.select . E.from $
|
||||||
\((school `E.InnerJoin` course) `E.LeftOuterJoin` participant) -> do
|
\((school `E.InnerJoin` course) `E.LeftOuterJoin` participant) -> do
|
||||||
@ -284,12 +287,17 @@ getCShowR tid ssh csh = do
|
|||||||
return ( E.countRows :: E.SqlExpr (E.Value Int))
|
return ( E.countRows :: E.SqlExpr (E.Value Int))
|
||||||
return (course,school E.^. SchoolName, numParticipants, participant)
|
return (course,school E.^. SchoolName, numParticipants, participant)
|
||||||
defSFid <- ifMaybeM mbAid Nothing $ \uid -> lift $ selectFirst [StudyFeaturesUser ==. uid, StudyFeaturesType ==. FieldPrimary, StudyFeaturesValid ==. True] [Desc StudyFeaturesUpdated, Desc StudyFeaturesDegree, Desc StudyFeaturesField] -- sorting by degree & field is an heuristic only, but this is okay for a default suggestion
|
defSFid <- ifMaybeM mbAid Nothing $ \uid -> lift $ selectFirst [StudyFeaturesUser ==. uid, StudyFeaturesType ==. FieldPrimary, StudyFeaturesValid ==. True] [Desc StudyFeaturesUpdated, Desc StudyFeaturesDegree, Desc StudyFeaturesField] -- sorting by degree & field is an heuristic only, but this is okay for a default suggestion
|
||||||
lecturers <- lift . E.select $ E.from $ \(lecturer `E.InnerJoin` user) -> do
|
staff <- lift . E.select $ E.from $ \(lecturer `E.InnerJoin` user) -> do
|
||||||
E.on $ lecturer E.^. LecturerUser E.==. user E.^. UserId
|
E.on $ lecturer E.^. LecturerUser E.==. user E.^. UserId
|
||||||
E.where_ $ lecturer E.^. LecturerCourse E.==. E.val cid
|
E.where_ $ lecturer E.^. LecturerCourse E.==. E.val cid
|
||||||
E.orderBy [ E.asc $ user E.^. UserSurname, E.asc $ user E.^. UserDisplayName ]
|
E.orderBy [ E.asc $ user E.^. UserSurname, E.asc $ user E.^. UserDisplayName ]
|
||||||
return (user E.^. UserDisplayName, user E.^. UserSurname, user E.^. UserEmail)
|
return ( lecturer E.^. LecturerType
|
||||||
return (course,schoolName,participants,registration,entityKey <$> defSFid,lecturers)
|
, user E.^. UserEmail, user E.^. UserDisplayName, user E.^. UserSurname)
|
||||||
|
let partStaff :: (LecturerType, UserEmail, Text, Text) -> Either (UserEmail, Text, Text) (UserEmail, Text, Text)
|
||||||
|
partStaff (CourseLecturer ,name,surn,mail) = Right (name,surn,mail)
|
||||||
|
partStaff (_courseAssistant,name,surn,mail) = Left (name,surn,mail)
|
||||||
|
(assistants,lecturers) = partitionWith partStaff $ map $(unValueN 4) staff
|
||||||
|
return (course,schoolName,participants,registration,entityKey <$> defSFid,lecturers,assistants)
|
||||||
|
|
||||||
mRegFrom <- traverse (formatTime SelFormatDateTime) $ courseRegisterFrom course
|
mRegFrom <- traverse (formatTime SelFormatDateTime) $ courseRegisterFrom course
|
||||||
mRegTo <- traverse (formatTime SelFormatDateTime) $ courseRegisterTo course
|
mRegTo <- traverse (formatTime SelFormatDateTime) $ courseRegisterTo course
|
||||||
@ -303,7 +311,7 @@ getCShowR tid ssh csh = do
|
|||||||
}
|
}
|
||||||
registrationOpen <- (==Authorized) <$> isAuthorized (CourseR tid ssh csh CRegisterR) True
|
registrationOpen <- (==Authorized) <$> isAuthorized (CourseR tid ssh csh CRegisterR) True
|
||||||
siteLayout (toWgt $ courseName course) $ do
|
siteLayout (toWgt $ courseName course) $ do
|
||||||
setTitle [shamlet| #{toPathPiece tid} - #{csh}|]
|
setTitleI $ prependCourseTitle tid ssh csh (""::Text)
|
||||||
$(widgetFile "course")
|
$(widgetFile "course")
|
||||||
|
|
||||||
-- | Registration button with maybe a userid if logged in
|
-- | Registration button with maybe a userid if logged in
|
||||||
@ -370,8 +378,14 @@ getCourseNewR = do
|
|||||||
<*> iopt ciField "ssh"
|
<*> iopt ciField "ssh"
|
||||||
<*> iopt ciField "csh"
|
<*> iopt ciField "csh"
|
||||||
|
|
||||||
|
let courseEditHandler' = courseEditHandler $ \p -> Just . SomeRoute $ (CourseNewR, getParams) :#: p
|
||||||
|
getParams = concat
|
||||||
|
[ [ ("tid", toPathPiece tid) | FormSuccess (Just tid, _, _) <- [params] ]
|
||||||
|
, [ ("ssh", toPathPiece ssh) | FormSuccess (_, Just ssh, _) <- [params] ]
|
||||||
|
, [ ("csh", toPathPiece csh) | FormSuccess (_, _, Just csh) <- [params] ]
|
||||||
|
]
|
||||||
|
|
||||||
let noTemplateAction = courseEditHandler Nothing
|
let noTemplateAction = courseEditHandler' Nothing
|
||||||
case params of -- DO NOT REMOVE: without this distinction, lecturers would never see an empty makeCourseForm any more!
|
case params of -- DO NOT REMOVE: without this distinction, lecturers would never see an empty makeCourseForm any more!
|
||||||
FormMissing -> noTemplateAction
|
FormMissing -> noTemplateAction
|
||||||
FormFailure msgs -> forM_ msgs (addMessage Error . toHtml) >>
|
FormFailure msgs -> forM_ msgs (addMessage Error . toHtml) >>
|
||||||
@ -402,7 +416,7 @@ getCourseNewR = do
|
|||||||
return course
|
return course
|
||||||
template <- case listToMaybe oldCourses of
|
template <- case listToMaybe oldCourses of
|
||||||
(Just oldTemplate) ->
|
(Just oldTemplate) ->
|
||||||
let newTemplate = courseToForm oldTemplate in
|
let newTemplate = courseToForm oldTemplate [] in
|
||||||
return $ Just $ newTemplate
|
return $ Just $ newTemplate
|
||||||
{ cfCourseId = Nothing
|
{ cfCourseId = Nothing
|
||||||
, cfTerm = TermKey $ TermIdentifier 0 Winter -- invalid, will be ignored; undefined won't work due to strictness
|
, cfTerm = TermKey $ TermIdentifier 0 Winter -- invalid, will be ignored; undefined won't work due to strictness
|
||||||
@ -420,10 +434,10 @@ getCourseNewR = do
|
|||||||
unless cshOk $ addMessageI Warning $ MsgNoSuchCourseShorthand $ fromJust mbCsh
|
unless cshOk $ addMessageI Warning $ MsgNoSuchCourseShorthand $ fromJust mbCsh
|
||||||
when (tidOk && sshOk && cshOk) $ addMessageI Warning MsgNoSuchCourse
|
when (tidOk && sshOk && cshOk) $ addMessageI Warning MsgNoSuchCourse
|
||||||
return Nothing
|
return Nothing
|
||||||
courseEditHandler template
|
courseEditHandler' template
|
||||||
|
|
||||||
postCourseNewR :: Handler Html
|
postCourseNewR :: Handler Html
|
||||||
postCourseNewR = courseEditHandler Nothing -- Note: Nothing is safe here, since we will create a new course.
|
postCourseNewR = courseEditHandler (\p -> Just . SomeRoute $ CourseNewR :#: p) Nothing -- Note: Nothing is safe here, since we will create a new course.
|
||||||
|
|
||||||
getCEditR, postCEditR :: TermId -> SchoolId -> CourseShorthand -> Handler Html
|
getCEditR, postCEditR :: TermId -> SchoolId -> CourseShorthand -> Handler Html
|
||||||
getCEditR = pgCEditR
|
getCEditR = pgCEditR
|
||||||
@ -431,10 +445,13 @@ postCEditR = pgCEditR
|
|||||||
|
|
||||||
pgCEditR :: TermId -> SchoolId -> CourseShorthand -> Handler Html
|
pgCEditR :: TermId -> SchoolId -> CourseShorthand -> Handler Html
|
||||||
pgCEditR tid ssh csh = do
|
pgCEditR tid ssh csh = do
|
||||||
course <- runDB $ getBy $ TermSchoolCourseShort tid ssh csh
|
courseLecs <- runDB $ do
|
||||||
|
mbCourse <- getBy (TermSchoolCourseShort tid ssh csh)
|
||||||
|
mbLecs <- for mbCourse $ \course -> map entityVal <$> selectList [LecturerCourse ==. entityKey course] [Asc LecturerType]
|
||||||
|
return $ (,) <$> mbCourse <*> mbLecs
|
||||||
-- IMPORTANT: both GET and POST Handler must use the same template,
|
-- IMPORTANT: both GET and POST Handler must use the same template,
|
||||||
-- since an Edit is identified via CourseID, which is not embedded in the received form data for security reasons.
|
-- since an Edit is identified via CourseID, which is not embedded in the received form data for security reasons.
|
||||||
courseEditHandler $ courseToForm <$> course
|
courseEditHandler (\p -> Just . SomeRoute $ CourseR tid ssh csh CEditR :#: p) $ uncurry courseToForm <$> courseLecs
|
||||||
|
|
||||||
|
|
||||||
getCDeleteR, postCDeleteR :: TermId -> SchoolId -> CourseShorthand -> Handler Html
|
getCDeleteR, postCDeleteR :: TermId -> SchoolId -> CourseShorthand -> Handler Html
|
||||||
@ -450,48 +467,50 @@ postCDeleteR tid ssh csh = do
|
|||||||
-- | Course Creation and Editing
|
-- | Course Creation and Editing
|
||||||
-- | IMPORTANT: in case of Edit, Post/Get Request is provided with the same CourseForm template (cannot be Nothing),
|
-- | IMPORTANT: in case of Edit, Post/Get Request is provided with the same CourseForm template (cannot be Nothing),
|
||||||
-- | since an edit is identified via cfCourseId which is not contained in the received form data for security reasons!
|
-- | since an edit is identified via cfCourseId which is not contained in the received form data for security reasons!
|
||||||
courseEditHandler :: Maybe CourseForm -> Handler Html
|
courseEditHandler :: (forall p. PathPiece p => p -> Maybe (SomeRoute UniWorX)) -> Maybe CourseForm -> Handler Html
|
||||||
courseEditHandler mbCourseForm = do
|
courseEditHandler miButtonAction mbCourseForm = do
|
||||||
aid <- requireAuthId -- TODO: Verify that Editor is owner of the Course to be Edited!!!
|
aid <- requireAuthId -- TODO: Verify that Editor is owner of the Course to be Edited!!!
|
||||||
((result, formWidget), formEnctype) <- runFormPost $ makeCourseForm mbCourseForm
|
((result, formWidget), formEnctype) <- runFormPost $ makeCourseForm miButtonAction mbCourseForm
|
||||||
case result of
|
formResult result $ \case
|
||||||
(FormSuccess res@CourseForm
|
res@CourseForm
|
||||||
{ cfCourseId = Nothing
|
{ cfCourseId = Nothing
|
||||||
, cfShort = csh
|
, cfShort = csh
|
||||||
, cfSchool = ssh
|
, cfSchool = ssh
|
||||||
, cfTerm = tid
|
, cfTerm = tid
|
||||||
}) -> do -- create new course
|
} -> do -- create new course
|
||||||
now <- liftIO getCurrentTime
|
now <- liftIO getCurrentTime
|
||||||
insertOkay <- runDB $ insertUnique Course
|
insertOkay <- runDB $ do
|
||||||
{ courseName = cfName res
|
insertOkay <- insertUnique Course
|
||||||
, courseDescription = cfDesc res
|
{ courseName = cfName res
|
||||||
, courseLinkExternal = cfLink res
|
, courseDescription = cfDesc res
|
||||||
, courseShorthand = cfShort res
|
, courseLinkExternal = cfLink res
|
||||||
, courseTerm = cfTerm res
|
, courseShorthand = cfShort res
|
||||||
, courseSchool = cfSchool res
|
, courseTerm = cfTerm res
|
||||||
, courseCapacity = cfCapacity res
|
, courseSchool = cfSchool res
|
||||||
, courseRegisterSecret = cfSecret res
|
, courseCapacity = cfCapacity res
|
||||||
, courseMaterialFree = cfMatFree res
|
, courseRegisterSecret = cfSecret res
|
||||||
, courseRegisterFrom = cfRegFrom res
|
, courseMaterialFree = cfMatFree res
|
||||||
, courseRegisterTo = cfRegTo res
|
, courseRegisterFrom = cfRegFrom res
|
||||||
, courseDeregisterUntil = cfDeRegUntil res
|
, courseRegisterTo = cfRegTo res
|
||||||
}
|
, courseDeregisterUntil = cfDeRegUntil res
|
||||||
case insertOkay of
|
}
|
||||||
(Just cid) -> do
|
whenIsJust insertOkay $ \cid -> do
|
||||||
runDB $ do
|
forM_ (cfLecturers res) (\(lid,lty) -> insert_ $ Lecturer lid cid lty)
|
||||||
insert_ $ CourseEdit aid now cid
|
insert_ $ CourseEdit aid now cid
|
||||||
insert_ $ Lecturer aid cid
|
return insertOkay
|
||||||
|
case insertOkay of
|
||||||
|
Just _ -> do
|
||||||
addMessageI Info $ MsgCourseNewOk tid ssh csh
|
addMessageI Info $ MsgCourseNewOk tid ssh csh
|
||||||
redirect $ TermCourseListR tid
|
redirect $ TermCourseListR tid
|
||||||
Nothing ->
|
Nothing ->
|
||||||
addMessageI Warning $ MsgCourseNewDupShort tid ssh csh
|
addMessageI Warning $ MsgCourseNewDupShort tid ssh csh
|
||||||
|
|
||||||
(FormSuccess res@CourseForm
|
res@CourseForm
|
||||||
{ cfCourseId = Just cid
|
{ cfCourseId = Just cid
|
||||||
, cfShort = csh
|
, cfShort = csh
|
||||||
, cfSchool = ssh
|
, cfSchool = ssh
|
||||||
, cfTerm = tid
|
, cfTerm = tid
|
||||||
}) -> do -- edit existing course
|
} -> do -- edit existing course
|
||||||
now <- liftIO getCurrentTime
|
now <- liftIO getCurrentTime
|
||||||
-- addMessage "debug" [shamlet| #{show res}|]
|
-- addMessage "debug" [shamlet| #{show res}|]
|
||||||
success <- runDB $ do
|
success <- runDB $ do
|
||||||
@ -516,13 +535,12 @@ courseEditHandler mbCourseForm = do
|
|||||||
case updOkay of
|
case updOkay of
|
||||||
(Just _) -> addMessageI Warning (MsgCourseEditDupShort tid ssh csh) $> False
|
(Just _) -> addMessageI Warning (MsgCourseEditDupShort tid ssh csh) $> False
|
||||||
Nothing -> do
|
Nothing -> do
|
||||||
|
deleteWhere [LecturerCourse ==. cid]
|
||||||
|
forM_ (cfLecturers res) (\(lid,lty) -> insert_ $ Lecturer lid cid lty)
|
||||||
insert_ $ CourseEdit aid now cid
|
insert_ $ CourseEdit aid now cid
|
||||||
addMessageI Success $ MsgCourseEditOk tid ssh csh
|
addMessageI Success $ MsgCourseEditOk tid ssh csh
|
||||||
return True
|
return True
|
||||||
when success $ redirect $ CourseR tid ssh csh CShowR
|
when success $ redirect $ CourseR tid ssh csh CShowR
|
||||||
|
|
||||||
(FormFailure _) -> addMessageI Warning MsgInvalidInput
|
|
||||||
FormMissing -> return ()
|
|
||||||
actionUrl <- fromMaybe CourseNewR <$> getCurrentRoute
|
actionUrl <- fromMaybe CourseNewR <$> getCurrentRoute
|
||||||
defaultLayout $ do
|
defaultLayout $ do
|
||||||
setTitleI MsgCourseEditTitle
|
setTitleI MsgCourseEditTitle
|
||||||
@ -546,10 +564,11 @@ data CourseForm = CourseForm
|
|||||||
, cfRegFrom :: Maybe UTCTime
|
, cfRegFrom :: Maybe UTCTime
|
||||||
, cfRegTo :: Maybe UTCTime
|
, cfRegTo :: Maybe UTCTime
|
||||||
, cfDeRegUntil :: Maybe UTCTime
|
, cfDeRegUntil :: Maybe UTCTime
|
||||||
|
, cfLecturers :: [(UserId, LecturerType)]
|
||||||
}
|
}
|
||||||
|
|
||||||
courseToForm :: Entity Course -> CourseForm
|
courseToForm :: Entity Course -> [Lecturer] -> CourseForm
|
||||||
courseToForm (Entity cid Course{..}) = CourseForm
|
courseToForm (Entity cid Course{..}) lecs = CourseForm
|
||||||
{ cfCourseId = Just cid
|
{ cfCourseId = Just cid
|
||||||
, cfName = courseName
|
, cfName = courseName
|
||||||
, cfDesc = courseDescription
|
, cfDesc = courseDescription
|
||||||
@ -563,21 +582,21 @@ courseToForm (Entity cid Course{..}) = CourseForm
|
|||||||
, cfRegFrom = courseRegisterFrom
|
, cfRegFrom = courseRegisterFrom
|
||||||
, cfRegTo = courseRegisterTo
|
, cfRegTo = courseRegisterTo
|
||||||
, cfDeRegUntil = courseDeregisterUntil
|
, cfDeRegUntil = courseDeregisterUntil
|
||||||
|
, cfLecturers = [(lecturerUser, lecturerType) | Lecturer{..} <- lecs]
|
||||||
}
|
}
|
||||||
|
|
||||||
makeCourseForm :: Maybe CourseForm -> Form CourseForm
|
makeCourseForm :: (forall p. PathPiece p => p -> Maybe (SomeRoute UniWorX)) -> Maybe CourseForm -> Form CourseForm
|
||||||
makeCourseForm template = identifyForm FIDcourse $ \html -> do
|
makeCourseForm miButtonAction template = identifyForm FIDcourse $ \html -> do
|
||||||
-- TODO: Refactor to avoid the four repeated calls to liftHandlerT and three runDBs
|
-- TODO: Refactor to avoid the four repeated calls to liftHandlerT and three runDBs
|
||||||
-- let editCid = cfCourseId =<< template -- possible start for refactoring
|
-- let editCid = cfCourseId =<< template -- possible start for refactoring
|
||||||
|
|
||||||
mr <- liftHandlerT getMessageRender -- needed for translation of placeholders
|
MsgRenderer mr <- getMsgRenderer
|
||||||
|
|
||||||
userSchools <- liftHandlerT . runDB $ do
|
uid <- liftHandlerT requireAuthId
|
||||||
userId <- liftHandlerT requireAuthId
|
(lecSchools, admSchools) <- liftHandlerT . runDB $ (,)
|
||||||
(fmap concat . sequence)
|
<$> (map (userLecturerSchool . entityVal) <$> selectList [UserLecturerUser ==. uid] [] )
|
||||||
[ map (userLecturerSchool . entityVal) <$> selectList [UserLecturerUser ==. userId] []
|
<*> (map (userAdminSchool . entityVal) <$> selectList [UserAdminUser ==. uid] [] )
|
||||||
, map (userAdminSchool . entityVal) <$> selectList [UserAdminUser ==. userId] []
|
let userSchools = lecSchools ++ admSchools
|
||||||
]
|
|
||||||
|
|
||||||
termsField <- case template of
|
termsField <- case template of
|
||||||
-- Change of term is only allowed if user may delete the course (i.e. no participants) or admin
|
-- Change of term is only allowed if user may delete the course (i.e. no participants) or admin
|
||||||
@ -590,6 +609,48 @@ makeCourseForm template = identifyForm FIDcourse $ \html -> do
|
|||||||
| otherwise -> termsSetField [cfTerm cform]
|
| otherwise -> termsSetField [cfTerm cform]
|
||||||
_allOtherCases -> return termsAllowedField
|
_allOtherCases -> return termsAllowedField
|
||||||
|
|
||||||
|
let miAdd :: ListPosition -> Natural -> (Text -> Text) -> FieldView UniWorX -> Maybe (Form (Map ListPosition UserId -> FormResult (Map ListPosition UserId)))
|
||||||
|
miAdd _ _ nudge btn = Just $ \csrf -> do
|
||||||
|
(addRes, addView) <- mpreq emailField ("" & addName (nudge "user")) Nothing
|
||||||
|
addRes' <- for addRes $ liftHandlerT . runDB . getKeyBy . UniqueEmail . CI.mk
|
||||||
|
let addRes'' = case (,) <$> addRes <*> addRes' of
|
||||||
|
FormSuccess (email, Nothing) -> FormFailure [ mr . MsgEMailUnknown $ CI.mk email ]
|
||||||
|
FormSuccess (email, Just lid) -> FormSuccess $ \prev -> if
|
||||||
|
| lid `elem` Map.elems prev -> FormFailure [ mr . MsgCourseLecturerAlreadyAdded $ CI.mk email ]
|
||||||
|
| otherwise -> FormSuccess $ Map.singleton (maybe 0 succ . fmap fst $ Map.lookupMax prev) lid
|
||||||
|
FormFailure errs -> FormFailure errs
|
||||||
|
FormMissing -> FormMissing
|
||||||
|
addView' = toWidget csrf >> fvInput addView >> fvInput btn
|
||||||
|
return (addRes'', addView')
|
||||||
|
|
||||||
|
miCell :: ListPosition -> UserId -> Maybe LecturerType -> (Text -> Text) -> Form LecturerType
|
||||||
|
miCell _ lid defType nudge = \csrf -> do
|
||||||
|
(lrwRes,lrwView) <- mreq (selectField optionsFinite) ("" & addName (nudge "lecturer-type")) defType
|
||||||
|
User{userEmail, userDisplayName, userSurname} <- liftHandlerT . runDB $ get404 lid
|
||||||
|
let lrwView' = [whamlet|$newline never
|
||||||
|
#{csrf}
|
||||||
|
^{nameEmailWidget userEmail userDisplayName userSurname} #
|
||||||
|
^{fvInput lrwView}
|
||||||
|
|]
|
||||||
|
return (lrwRes,lrwView')
|
||||||
|
|
||||||
|
miDelete :: ListLength -- ^ Current shape
|
||||||
|
-> ListPosition -- ^ Coordinate to delete
|
||||||
|
-> MaybeT (MForm (HandlerT UniWorX IO)) (Map ListPosition ListPosition)
|
||||||
|
miDelete = miDeleteList
|
||||||
|
|
||||||
|
miAllowAdd :: ListPosition -> Natural -> ListLength -> Bool
|
||||||
|
miAllowAdd _ _ _ = True
|
||||||
|
|
||||||
|
|
||||||
|
lecturerForm :: AForm Handler [(UserId,LecturerType)]
|
||||||
|
lecturerForm = formToAForm . over (mapped._2) pure . over (mapped._1.mapped) Map.elems $ massInput
|
||||||
|
MassInput{..}
|
||||||
|
(fslI MsgCourseLecturers & setTooltip MsgCourseLecturerRightsIdentical)
|
||||||
|
True
|
||||||
|
(Just . Map.fromList . zip [0..] $ maybe [(uid, CourseLecturer)] cfLecturers template)
|
||||||
|
mempty
|
||||||
|
|
||||||
(newRegFrom,newRegTo,newDeRegUntil) <- case template of
|
(newRegFrom,newRegTo,newDeRegUntil) <- case template of
|
||||||
(Just cform) | (Just _cid) <- cfCourseId cform -> return (Nothing,Nothing,Nothing)
|
(Just cform) | (Just _cid) <- cfCourseId cform -> return (Nothing,Nothing,Nothing)
|
||||||
_allIOtherCases -> do
|
_allIOtherCases -> do
|
||||||
@ -621,11 +682,11 @@ makeCourseForm template = identifyForm FIDcourse $ \html -> do
|
|||||||
& setTooltip MsgCourseRegisterToTip) (deepAlt (cfRegTo <$> template) newRegTo)
|
& setTooltip MsgCourseRegisterToTip) (deepAlt (cfRegTo <$> template) newRegTo)
|
||||||
<*> aopt utcTimeField (fslpI MsgDeRegUntil (mr MsgDate)
|
<*> aopt utcTimeField (fslpI MsgDeRegUntil (mr MsgDate)
|
||||||
& setTooltip MsgCourseDeregisterUntilTip) (deepAlt (cfDeRegUntil <$> template) newDeRegUntil)
|
& setTooltip MsgCourseDeregisterUntilTip) (deepAlt (cfDeRegUntil <$> template) newDeRegUntil)
|
||||||
<* submitButton
|
<*> lecturerForm
|
||||||
return $ case result of
|
errorMsgs' <- traverse validateCourse result
|
||||||
FormSuccess courseResult
|
return $ case errorMsgs' of
|
||||||
| errorMsgs <- validateCourse courseResult
|
FormSuccess errorMsgs
|
||||||
, not $ null errorMsgs ->
|
| not $ null errorMsgs ->
|
||||||
(FormFailure errorMsgs,
|
(FormFailure errorMsgs,
|
||||||
[whamlet|
|
[whamlet|
|
||||||
<div class="alert alert-danger">
|
<div class="alert alert-danger">
|
||||||
@ -640,23 +701,26 @@ makeCourseForm template = identifyForm FIDcourse $ \html -> do
|
|||||||
_ -> (result, widget)
|
_ -> (result, widget)
|
||||||
|
|
||||||
|
|
||||||
validateCourse :: CourseForm -> [Text]
|
validateCourse :: (MonadHandler m, HandlerSite m ~ UniWorX) => CourseForm -> m [Text]
|
||||||
validateCourse CourseForm{..} =
|
validateCourse CourseForm{..} = do
|
||||||
[ msg | (False, msg) <-
|
uid <- liftHandlerT requireAuthId
|
||||||
[
|
userAdmin <- liftHandlerT . runDB . getBy $ UniqueUserAdmin uid cfSchool -- FIXME: This /needs/ to be a call to `isAuthorized` on a route
|
||||||
( NTop cfRegFrom <= NTop cfRegTo
|
MsgRenderer mr <- getMsgRenderer
|
||||||
, "Ende des Anmeldezeitraums muss nach dem Anfang liegen"
|
|
||||||
)
|
return
|
||||||
,
|
[ mr msg | (False, msg) <-
|
||||||
( NTop cfRegFrom <= NTop cfDeRegUntil
|
[
|
||||||
, "Ende des Abmeldezeitraums muss nach dem Anfang liegen"
|
( NTop cfRegFrom <= NTop cfRegTo
|
||||||
)
|
, MsgCourseRegistrationEndMustBeAfterStart
|
||||||
-- No starting date is okay: effective immediately
|
)
|
||||||
-- ( cfHasReg <= (isNothing cfRegFrom)
|
,
|
||||||
-- , "Beginn der Anmeldung angeben oder Anmeldungen deaktivieren"
|
( NTop cfRegFrom <= NTop cfDeRegUntil
|
||||||
-- )
|
, MsgCourseDeregistrationEndMustBeAfterStart
|
||||||
-- ,
|
)
|
||||||
] ]
|
, ( maybe (any ((== uid) . fst) cfLecturers) (\(Entity _ UserAdmin{}) -> True) userAdmin
|
||||||
|
, MsgCourseUserMustBeLecturer
|
||||||
|
)
|
||||||
|
] ]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -756,77 +820,131 @@ colUserDegreeShort :: IsDBTable m c => Colonnade Sortable UserTableData (DBCell
|
|||||||
colUserDegreeShort = sortable (Just "degree-short") (i18nCell MsgStudyFeatureDegree) $
|
colUserDegreeShort = sortable (Just "degree-short") (i18nCell MsgStudyFeatureDegree) $
|
||||||
foldMap (i18nCell . ShortStudyDegree) . preview (_userTableFeatures . _2 . _Just)
|
foldMap (i18nCell . ShortStudyDegree) . preview (_userTableFeatures . _2 . _Just)
|
||||||
|
|
||||||
makeCourseUserTable :: CourseId -> _ -> _ -> DB Widget
|
|
||||||
makeCourseUserTable cid colChoices psValidator =
|
data CourseUserAction = CourseUserDeregister
|
||||||
-- -- psValidator has default sorting and filtering
|
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic, Typeable)
|
||||||
let dbtIdent = "courseUsers" :: Text
|
|
||||||
dbtStyle = def { dbsFilterLayout = defaultDBSFilterLayout }
|
instance Universe CourseUserAction
|
||||||
dbtSQLQuery = userTableQuery cid
|
instance Finite CourseUserAction
|
||||||
dbtRowKey = queryUser >>> (E.^. UserId)
|
nullaryPathPiece ''CourseUserAction $ camelToPathPiece' 2
|
||||||
dbtProj = traverse $ \(user, E.Value registrationTime , E.Value userNoteId, (feature,degree,terms)) -> return (user, registrationTime, userNoteId, (entityVal <$> feature, entityVal <$> degree, entityVal <$> terms))
|
embedRenderMessage ''UniWorX ''CourseUserAction id
|
||||||
dbtColonnade = colChoices
|
|
||||||
dbtSorting = Map.fromList
|
makeCourseUserTable :: CourseId -> _ -> _ -> DB (FormResult (CourseUserAction, Set UserId), Widget)
|
||||||
[ sortUserNameLink queryUser -- slower sorting through clicking name column header
|
makeCourseUserTable cid colChoices psValidator = do
|
||||||
, sortUserSurname queryUser -- needed for initial sorting
|
Just currentRoute <- liftHandlerT getCurrentRoute
|
||||||
, sortUserDisplayName queryUser -- needed for initial sorting
|
-- -- psValidator has default sorting and filtering
|
||||||
, sortUserEmail queryUser
|
let dbtIdent = "courseUsers" :: Text
|
||||||
, sortUserMatriclenr queryUser
|
dbtStyle = def { dbsFilterLayout = defaultDBSFilterLayout }
|
||||||
, ("degree" , SortColumn $ queryFeaturesDegree >>> (E.?. StudyDegreeName))
|
dbtSQLQuery = userTableQuery cid
|
||||||
, ("degree-short", SortColumn $ queryFeaturesDegree >>> (E.?. StudyDegreeShorthand))
|
dbtRowKey = queryUser >>> (E.^. UserId)
|
||||||
, ("field" , SortColumn $ queryFeaturesField >>> (E.?. StudyTermsName))
|
dbtProj = traverse $ \(user, E.Value registrationTime , E.Value userNoteId, (feature,degree,terms)) -> return (user, registrationTime, userNoteId, (entityVal <$> feature, entityVal <$> degree, entityVal <$> terms))
|
||||||
, ("field-short" , SortColumn $ queryFeaturesField >>> (E.?. StudyTermsShorthand))
|
dbtColonnade = colChoices
|
||||||
, ("semesternr" , SortColumn $ queryFeaturesStudy >>> (E.?. StudyFeaturesSemester))
|
dbtSorting = Map.fromList
|
||||||
, ("registration", SortColumn $ queryParticipant >>> (E.^. CourseParticipantRegistration))
|
[ sortUserNameLink queryUser -- slower sorting through clicking name column header
|
||||||
, ("note" , SortColumn $ queryUserNote >>> \note -> -- sort by last edit date
|
, sortUserSurname queryUser -- needed for initial sorting
|
||||||
E.sub_select . E.from $ \edit -> do
|
, sortUserDisplayName queryUser -- needed for initial sorting
|
||||||
E.where_ $ note E.?. CourseUserNoteId E.==. E.just (edit E.^. CourseUserNoteEditNote)
|
, sortUserEmail queryUser
|
||||||
return . E.max_ $ edit E.^. CourseUserNoteEditTime
|
, sortUserMatriclenr queryUser
|
||||||
)
|
, ("degree" , SortColumn $ queryFeaturesDegree >>> (E.?. StudyDegreeName))
|
||||||
]
|
, ("degree-short", SortColumn $ queryFeaturesDegree >>> (E.?. StudyDegreeShorthand))
|
||||||
dbtFilter = Map.fromList
|
, ("field" , SortColumn $ queryFeaturesField >>> (E.?. StudyTermsName))
|
||||||
[ fltrUserNameLink queryUser
|
, ("field-short" , SortColumn $ queryFeaturesField >>> (E.?. StudyTermsShorthand))
|
||||||
, fltrUserEmail queryUser
|
, ("semesternr" , SortColumn $ queryFeaturesStudy >>> (E.?. StudyFeaturesSemester))
|
||||||
, fltrUserMatriclenr queryUser
|
, ("registration", SortColumn $ queryParticipant >>> (E.^. CourseParticipantRegistration))
|
||||||
, fltrUserNameEmail queryUser
|
, ("note" , SortColumn $ queryUserNote >>> \note -> -- sort by last edit date
|
||||||
-- , ("course-user-degree", error "TODO") -- TODO
|
E.sub_select . E.from $ \edit -> do
|
||||||
-- , ("course-user-field" , error "TODO") -- TODO
|
E.where_ $ note E.?. CourseUserNoteId E.==. E.just (edit E.^. CourseUserNoteEditNote)
|
||||||
, ("course-user-semesternr", FilterColumn $ mkExactFilter $ queryFeaturesStudy >>> (E.?. StudyFeaturesSemester))
|
return . E.max_ $ edit E.^. CourseUserNoteEditTime
|
||||||
-- , ("course-registration", error "TODO") -- TODO
|
)
|
||||||
-- , ("course-user-note", error "TODO") -- TODO
|
]
|
||||||
]
|
dbtFilter = Map.fromList
|
||||||
dbtFilterUI mPrev = mconcat
|
[ fltrUserNameLink queryUser
|
||||||
[ fltrUserNameEmailUI mPrev
|
, fltrUserEmail queryUser
|
||||||
, fltrUserMatriclenrUI mPrev
|
, fltrUserMatriclenr queryUser
|
||||||
]
|
, fltrUserNameEmail queryUser
|
||||||
dbtParams = def
|
, ("field-name" , FilterColumn $ E.mkContainsFilter $ queryFeaturesField >>> (E.?. StudyTermsName))
|
||||||
in dbTableWidget' psValidator DBTable{..}
|
, ("field-short" , FilterColumn $ E.mkContainsFilter $ queryFeaturesField >>> (E.?. StudyTermsShorthand))
|
||||||
|
, ("field-key" , FilterColumn $ E.mkExactFilter $ queryFeaturesField >>> (E.?. StudyTermsKey))
|
||||||
|
, ("field" , FilterColumn $ E.anyFilter
|
||||||
|
[ E.mkContainsFilter $ queryFeaturesField >>> (E.?. StudyTermsName)
|
||||||
|
, E.mkContainsFilter $ queryFeaturesField >>> (E.?. StudyTermsShorthand)
|
||||||
|
, E.mkExactFilterWith readMay $ queryFeaturesField >>> (E.?. StudyTermsKey)
|
||||||
|
] )
|
||||||
|
, ("degree" , FilterColumn $ E.anyFilter
|
||||||
|
[ E.mkContainsFilter $ queryFeaturesDegree >>> (E.?. StudyDegreeName)
|
||||||
|
, E.mkContainsFilter $ queryFeaturesDegree >>> (E.?. StudyDegreeShorthand)
|
||||||
|
, E.mkExactFilterWith readMay $ queryFeaturesDegree >>> (E.?. StudyDegreeKey)
|
||||||
|
] )
|
||||||
|
, ("semesternr" , FilterColumn $ E.mkExactFilter $ queryFeaturesStudy >>> (E.?. StudyFeaturesSemester))
|
||||||
|
-- , ("course-registration", error "TODO") -- TODO
|
||||||
|
-- , ("course-user-note", error "TODO") -- TODO
|
||||||
|
]
|
||||||
|
dbtFilterUI mPrev = mconcat
|
||||||
|
[ fltrUserNameEmailUI mPrev
|
||||||
|
, fltrUserMatriclenrUI mPrev
|
||||||
|
, prismAForm (singletonFilter "degree") mPrev $ aopt (searchField False) (fslI MsgStudyFeatureDegree)
|
||||||
|
, prismAForm (singletonFilter "field") mPrev $ aopt (searchField False) (fslI MsgCourseStudyFeature)
|
||||||
|
]
|
||||||
|
dbtParams = DBParamsForm
|
||||||
|
{ dbParamsFormMethod = POST
|
||||||
|
, dbParamsFormAction = Just $ SomeRoute currentRoute
|
||||||
|
, dbParamsFormAttrs = []
|
||||||
|
, dbParamsFormSubmit = FormSubmit
|
||||||
|
, dbParamsFormAdditional = \csrf -> do
|
||||||
|
(res,vw) <- mreq (selectField optionsFinite) "" Nothing
|
||||||
|
let formWgt = toWidget csrf <> fvInput vw
|
||||||
|
formRes = (, mempty) . First . Just <$> res
|
||||||
|
return (formRes,formWgt)
|
||||||
|
, dbParamsFormEvaluate = liftHandlerT . runFormPost
|
||||||
|
, dbParamsFormResult = id
|
||||||
|
, dbParamsFormIdent = def
|
||||||
|
}
|
||||||
|
over _1 postprocess <$> dbTable psValidator DBTable{..}
|
||||||
|
where
|
||||||
|
postprocess :: FormResult (First CourseUserAction, DBFormResult UserId Bool UserTableData) -> FormResult (CourseUserAction, Set UserId)
|
||||||
|
postprocess inp = do
|
||||||
|
(First (Just act), usrMap) <- inp
|
||||||
|
let usrSet = Map.keysSet . Map.filter id $ getDBFormResult (const False) usrMap
|
||||||
|
return (act, usrSet)
|
||||||
|
|
||||||
|
getCUsersR, postCUsersR :: TermId -> SchoolId -> CourseShorthand -> Handler Html
|
||||||
|
getCUsersR = postCUsersR
|
||||||
|
postCUsersR tid ssh csh = do
|
||||||
|
(Entity cid course, numParticipants, (participantRes,participantTable)) <- runDB $ do
|
||||||
|
let colChoices = mconcat
|
||||||
|
[ dbSelect (applying _2) id (return . view (hasEntity . _entityKey))
|
||||||
|
, colUserNameLink (CourseR tid ssh csh . CUserR)
|
||||||
|
, colUserEmail
|
||||||
|
, colUserMatriclenr
|
||||||
|
, colUserDegreeShort
|
||||||
|
, colUserField
|
||||||
|
, colUserSemester
|
||||||
|
, sortable (Just "registration") (i18nCell MsgRegisteredHeader) (dateCell . view _userTableRegistration)
|
||||||
|
, colUserComment tid ssh csh
|
||||||
|
]
|
||||||
|
psValidator = def & defaultSortingByName
|
||||||
|
ent@(Entity cid _) <- getBy404 $ TermSchoolCourseShort tid ssh csh
|
||||||
|
numParticipants <- count [CourseParticipantCourse ==. cid]
|
||||||
|
table <- makeCourseUserTable cid colChoices psValidator
|
||||||
|
return (ent, numParticipants, table)
|
||||||
|
formResult participantRes $ \case
|
||||||
|
(CourseUserDeregister,selectedUsers) -> do
|
||||||
|
nrDel <- runDB $ deleteWhereCount
|
||||||
|
[ CourseParticipantCourse ==. cid
|
||||||
|
, CourseParticipantUser <-. Set.toList selectedUsers
|
||||||
|
]
|
||||||
|
addMessageI Success $ MsgCourseUsersDeregistered nrDel
|
||||||
|
redirect $ CourseR tid ssh csh CUsersR
|
||||||
|
let headingLong = [whamlet|_{MsgMenuCourseMembers} #{courseName course} #{display tid}|]
|
||||||
|
headingShort = prependCourseTitle tid ssh csh MsgCourseMembers
|
||||||
|
siteLayout headingLong $ do
|
||||||
|
setTitleI headingShort
|
||||||
|
$(widgetFile "course-participants")
|
||||||
|
|
||||||
|
|
||||||
getCUsersR :: TermId -> SchoolId -> CourseShorthand -> Handler Html
|
getCUserR, postCUserR :: TermId -> SchoolId -> CourseShorthand -> CryptoUUIDUser -> Handler Html
|
||||||
getCUsersR tid ssh csh = do
|
getCUserR = postCUserR
|
||||||
Entity cid course <- runDB $ getBy404 $ TermSchoolCourseShort tid ssh csh
|
postCUserR tid ssh csh uCId = do
|
||||||
let heading = [whamlet|_{MsgMenuCourseMembers} #{courseName course} #{display tid}|]
|
|
||||||
colChoices = mconcat
|
|
||||||
[ colUserNameLink (CourseR tid ssh csh . CUserR)
|
|
||||||
, colUserEmail
|
|
||||||
, colUserMatriclenr
|
|
||||||
, colUserDegreeShort
|
|
||||||
, colUserField
|
|
||||||
, colUserSemester
|
|
||||||
, sortable (Just "course-registration") (i18nCell MsgRegisteredHeader) (dateCell . view _userTableRegistration)
|
|
||||||
, colUserComment tid ssh csh
|
|
||||||
]
|
|
||||||
psValidator = def & defaultSortingByName
|
|
||||||
tableWidget <- runDB $ makeCourseUserTable cid colChoices psValidator
|
|
||||||
siteLayout heading $ do
|
|
||||||
setTitle [shamlet| #{toPathPiece tid} - #{csh}|]
|
|
||||||
-- TODO: create hamlet wrapper
|
|
||||||
tableWidget
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
getCUserR :: TermId -> SchoolId -> CourseShorthand -> CryptoUUIDUser -> Handler Html
|
|
||||||
getCUserR _tid _ssh _csh uCId = do
|
|
||||||
-- Has authorization checks (OR):
|
-- Has authorization checks (OR):
|
||||||
--
|
--
|
||||||
-- - User is current member of course
|
-- - User is current member of course
|
||||||
@ -836,18 +954,88 @@ getCUserR _tid _ssh _csh uCId = do
|
|||||||
-- - User is corrector for course
|
-- - User is corrector for course
|
||||||
-- - User is a tutor for course
|
-- - User is a tutor for course
|
||||||
-- - User is a lecturer for course
|
-- - User is a lecturer for course
|
||||||
|
let currentRoute = CourseR tid ssh csh (CUserR uCId)
|
||||||
|
dozentId <- requireAuthId
|
||||||
uid <- decrypt uCId
|
uid <- decrypt uCId
|
||||||
User{..} <- runDB $ get404 uid
|
-- DB reads
|
||||||
-- USE src/utils/Form.formResult
|
(cid, User{..}, registration, thisUniqueNote, noteText, noteEdits, studies ) <- runDB $ do
|
||||||
defaultLayout -- TODO
|
cid <- getKeyBy404 $ TermSchoolCourseShort tid ssh csh
|
||||||
[whamlet|
|
-- Abfrage Benutzerdaten
|
||||||
<p>^{nameWidget userDisplayName userSurname}
|
user <- get404 uid
|
||||||
|]
|
registration <- fmap entityVal <$> getBy (UniqueParticipant uid cid)
|
||||||
|
-- Abfrage Teilnehmernotiz
|
||||||
|
let thisUniqueNote = UniqueCourseUserNote uid cid
|
||||||
|
mbNoteEnt <- getBy thisUniqueNote
|
||||||
|
(noteText,noteEdits) <- case mbNoteEnt of
|
||||||
|
Nothing -> return (Nothing,[])
|
||||||
|
(Just (Entity noteKey CourseUserNote{courseUserNoteNote})) -> do
|
||||||
|
noteEdits <- E.select $ E.from $ \(edit `E.InnerJoin` usr) -> do
|
||||||
|
E.on $ edit E.^. CourseUserNoteEditUser E.==. usr E.^. UserId
|
||||||
|
E.where_ $ edit E.^. CourseUserNoteEditNote E.==. E.val noteKey
|
||||||
|
E.orderBy [E.desc $ edit E.^. CourseUserNoteEditTime]
|
||||||
|
E.limit 1 -- more will be shown, if changed here
|
||||||
|
return (edit E.^. CourseUserNoteEditTime, usr E.^. UserEmail, usr E.^. UserDisplayName, usr E.^. UserSurname)
|
||||||
|
return (Just courseUserNoteNote, $(unValueN 4) <$> noteEdits)
|
||||||
|
-- Abfrage Studiengänge
|
||||||
|
studies <- E.select $ E.from $ \(studydegree `E.InnerJoin` studyfeat `E.InnerJoin` studyterms) -> do
|
||||||
|
E.where_ $ studyfeat E.^. StudyFeaturesUser E.==. E.val uid
|
||||||
|
E.on $ studyfeat E.^. StudyFeaturesField E.==. studyterms E.^. StudyTermsId
|
||||||
|
E.on $ studyfeat E.^. StudyFeaturesDegree E.==. studydegree E.^. StudyDegreeId
|
||||||
|
return (studyfeat, studydegree, studyterms)
|
||||||
|
|
||||||
|
return (cid,user,registration,thisUniqueNote,noteText,noteEdits,studies)
|
||||||
|
let editByWgt = [whamlet|
|
||||||
|
$forall (etime,_eemail,ename,_esurname) <- noteEdits
|
||||||
|
<br>
|
||||||
|
_{MsgLastEdit} ^{editedByW SelFormatDateTime etime ename}
|
||||||
|
|] -- _{MsgLastEdit} ^{formatTimeW SelFormatDateTime etime} ^{nameWidget ename esurname}
|
||||||
|
|
||||||
|
((noteRes, noteView), noteEnctype) <- runFormPost . identifyForm FIDcUserNote . renderAForm FormStandard $
|
||||||
|
aopt (annotateField editByWgt htmlField') (fslpI MsgCourseUserNote "HTML" & setTooltip MsgCourseUserNoteTooltip) (Just noteText)
|
||||||
|
<* saveButton
|
||||||
|
formResult noteRes $ \mbNote -> (do
|
||||||
|
now <- liftIO getCurrentTime
|
||||||
|
case mbNote of
|
||||||
|
Nothing -> do
|
||||||
|
runDB $ do
|
||||||
|
-- must delete all edits due to foreign key constraints, which does not make sense -> refactor!
|
||||||
|
maybeM (return ()) (\nk -> deleteWhere [CourseUserNoteEditNote ==. nk]) (getKeyBy thisUniqueNote)
|
||||||
|
deleteBy thisUniqueNote
|
||||||
|
addMessageI Info MsgCourseUserNoteDeleted
|
||||||
|
redirect currentRoute -- reload page after post
|
||||||
|
_ | (renderHtml <$> mbNote) == (renderHtml <$> noteText) -> return() -- no changes
|
||||||
|
(Just note) -> do
|
||||||
|
runDB $ do
|
||||||
|
(Entity noteKey _) <- upsertBy thisUniqueNote (CourseUserNote cid uid note) [CourseUserNoteNote =. note]
|
||||||
|
void . insert $ CourseUserNoteEdit dozentId now noteKey
|
||||||
|
addMessageI Success MsgCourseUserNoteSaved
|
||||||
|
redirect currentRoute -- reload page after post
|
||||||
|
)
|
||||||
|
-- De-/Register Button for Lecturer
|
||||||
|
mRegAt <- traverse (formatTime SelFormatDateTime) $ courseParticipantRegistration <$> registration
|
||||||
|
((registerRes,registerView), registerEnctype) <- runFormPost $ registerForm (Just uid) registration Nothing Nothing -- Lecturers are never asked their own register secret
|
||||||
|
formResult registerRes $ \(mbSfId, _secretCorrect) -> if -- lecturers need no secret verification
|
||||||
|
| isJust registration -> do
|
||||||
|
runDB $ deleteBy $ UniqueParticipant uid cid
|
||||||
|
addMessageI Info MsgCourseDeregisterOk
|
||||||
|
| otherwise -> do
|
||||||
|
actTime <- liftIO getCurrentTime
|
||||||
|
regOk <- runDB $ insertUnique $ CourseParticipant cid uid actTime mbSfId
|
||||||
|
when (isJust regOk) $ addMessageI Success MsgCourseRegisterOk
|
||||||
|
-- generate output
|
||||||
|
let headingLong = [whamlet|^{nameWidget userDisplayName userSurname} - _{MsgCourseMemberOf} #{csh} #{display tid}|]
|
||||||
|
headingShort = prependCourseTitle tid ssh csh $ SomeMessage userDisplayName
|
||||||
|
siteLayout headingLong $ do
|
||||||
|
setTitleI headingShort
|
||||||
|
$(widgetFile "course-user")
|
||||||
|
|
||||||
|
|
||||||
getCHiWisR :: TermId -> SchoolId -> CourseShorthand -> Handler Html
|
getCHiWisR :: TermId -> SchoolId -> CourseShorthand -> Handler Html
|
||||||
getCHiWisR = error "CHiWisR: Not implemented"
|
getCHiWisR = error "CHiWisR: Not implemented"
|
||||||
|
|
||||||
getCNotesR, postCNotesR :: TermId -> SchoolId -> CourseShorthand -> Handler Html
|
getCNotesR, postCNotesR :: TermId -> SchoolId -> CourseShorthand -> Handler Html
|
||||||
-- NOTE: The route getNotesR is abused for correctorORlecturer access rights!
|
-- NOTE: The route getNotesR is abused for correctorORlecturer access rights!
|
||||||
getCNotesR = error "CNotesR: Not implemented"
|
-- PROBLEM: Correctors usually don't know Participants by name (anonymous), maybe notes are not shared?
|
||||||
|
-- If they are shared, adjust MsgCourseUserNoteTooltip
|
||||||
|
getCNotesR = error "CNotesR: Not implemented"
|
||||||
postCNotesR = error "CNotesR: Not implemented"
|
postCNotesR = error "CNotesR: Not implemented"
|
||||||
|
|||||||
70
src/Handler/Help.hs
Normal file
70
src/Handler/Help.hs
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
module Handler.Help where
|
||||||
|
|
||||||
|
import Import
|
||||||
|
import Handler.Utils
|
||||||
|
import Jobs
|
||||||
|
|
||||||
|
import qualified Data.Map as Map
|
||||||
|
|
||||||
|
data HelpIdentOptions = HIUser | HIEmail | HIAnonymous
|
||||||
|
deriving (Eq, Ord, Bounded, Enum, Show, Read)
|
||||||
|
|
||||||
|
instance Universe HelpIdentOptions
|
||||||
|
instance Finite HelpIdentOptions
|
||||||
|
|
||||||
|
nullaryPathPiece ''HelpIdentOptions (camelToPathPiece' 1)
|
||||||
|
embedRenderMessage ''UniWorX ''HelpIdentOptions (("Help" <>) . dropPrefix "HI")
|
||||||
|
|
||||||
|
data HelpForm = HelpForm
|
||||||
|
{ hfReferer :: Maybe (Route UniWorX)
|
||||||
|
, hfUserId :: Either (Maybe Address) UserId
|
||||||
|
, hfSubject :: Maybe Text
|
||||||
|
, hfRequest :: Text
|
||||||
|
}
|
||||||
|
|
||||||
|
helpForm :: (forall msg. RenderMessage UniWorX msg => msg -> Text) -> Maybe (Route UniWorX) -> Maybe UserId -> AForm _ HelpForm
|
||||||
|
helpForm mr mReferer mUid = HelpForm
|
||||||
|
<$> aopt routeField (fslI MsgHelpProblemPage & inputReadonly) (Just <$> mReferer)
|
||||||
|
<*> multiActionA (fslI MsgHelpAnswer) identActions (HIUser <$ mUid)
|
||||||
|
<*> aopt textField (fslpI MsgHelpSubject $ mr MsgHelpSubject) Nothing
|
||||||
|
<*> (unTextarea <$> areq textareaField (fslpI MsgHelpRequest $ mr MsgHelpRequest) Nothing)
|
||||||
|
where
|
||||||
|
identActions :: Map _ (AForm _ (Either (Maybe Address) UserId))
|
||||||
|
identActions = Map.fromList $ case mUid of
|
||||||
|
(Just uid) -> (HIUser, pure $ Right uid):defaultActions
|
||||||
|
Nothing -> defaultActions
|
||||||
|
|
||||||
|
defaultActions =
|
||||||
|
[ (HIEmail, Left . Just <$> (Address <$> aopt textField (fslpI MsgName $ mr MsgName) Nothing <*> apreq emailField (fslpI MsgEMail $ mr MsgEMail) Nothing))
|
||||||
|
, (HIAnonymous, pure $ Left Nothing)
|
||||||
|
]
|
||||||
|
|
||||||
|
getHelpR, postHelpR :: Handler Html
|
||||||
|
getHelpR = postHelpR
|
||||||
|
postHelpR = do
|
||||||
|
mUid <- maybeAuthId
|
||||||
|
mReferer <- flip formResultMaybe return <=< runInputGetResult $ iopt routeField (toPathPiece GetReferer)
|
||||||
|
isModal <- hasCustomHeader HeaderIsModal
|
||||||
|
MsgRenderer mr <- getMsgRenderer
|
||||||
|
|
||||||
|
((res,formWidget),formEnctype) <- runFormPost $ renderAForm FormStandard $ helpForm mr mReferer mUid
|
||||||
|
|
||||||
|
formResultModal res HelpR $ \HelpForm{..} -> do
|
||||||
|
now <- liftIO getCurrentTime
|
||||||
|
hfReferer' <- traverse toTextUrl hfReferer
|
||||||
|
queueJob' JobHelpRequest
|
||||||
|
{ jSender = hfUserId
|
||||||
|
, jHelpSubject = hfSubject
|
||||||
|
, jHelpRequest = hfRequest
|
||||||
|
, jRequestTime = now
|
||||||
|
, jReferer = hfReferer'
|
||||||
|
}
|
||||||
|
tell . pure =<< messageI Success MsgHelpSent
|
||||||
|
|
||||||
|
defaultLayout $ do
|
||||||
|
setTitleI MsgHelpTitle
|
||||||
|
wrapForm $(widgetFile "help") def
|
||||||
|
{ formAction = Just $ SomeRoute HelpR
|
||||||
|
, formEncoding = formEnctype
|
||||||
|
, formAttrs = [ ("data-ajax-submit", "") | isModal ]
|
||||||
|
}
|
||||||
@ -4,23 +4,20 @@ import Import
|
|||||||
import Handler.Utils
|
import Handler.Utils
|
||||||
|
|
||||||
import qualified Data.Map as Map
|
import qualified Data.Map as Map
|
||||||
import qualified Data.Set as Set
|
|
||||||
import qualified Database.Esqueleto as E
|
import qualified Database.Esqueleto as E
|
||||||
|
|
||||||
import Jobs
|
|
||||||
import Development.GitRev
|
|
||||||
|
|
||||||
|
|
||||||
getHomeR :: Handler Html
|
getHomeR :: Handler Html
|
||||||
getHomeR = do
|
getHomeR = do
|
||||||
muid <- maybeAuthId
|
muid <- maybeAuthId
|
||||||
case muid of
|
defaultLayout $ do
|
||||||
Nothing -> homeAnonymous
|
setTitleI MsgHomeHeading
|
||||||
Just uid -> homeUser uid
|
maybe mempty homeUpcomingSheets muid
|
||||||
|
homeOpenCourses
|
||||||
|
|
||||||
|
|
||||||
homeAnonymous :: Handler Html
|
homeOpenCourses :: Widget
|
||||||
homeAnonymous = do
|
homeOpenCourses = do
|
||||||
cTime <- liftIO getCurrentTime
|
cTime <- liftIO getCurrentTime
|
||||||
let tableData :: E.SqlExpr (Entity Course)
|
let tableData :: E.SqlExpr (Entity Course)
|
||||||
-> E.SqlQuery (E.SqlExpr (Entity Course))
|
-> E.SqlQuery (E.SqlExpr (Entity Course))
|
||||||
@ -47,7 +44,7 @@ homeAnonymous = do
|
|||||||
, sortable (Just "deadline") (i18nCell MsgRegisterTo) $ \DBRow{ dbrOutput=Entity{entityVal = course} } ->
|
, sortable (Just "deadline") (i18nCell MsgRegisterTo) $ \DBRow{ dbrOutput=Entity{entityVal = course} } ->
|
||||||
cell $ traverse (formatTime SelFormatDateTime) (courseRegisterTo course) >>= maybe mempty toWidget
|
cell $ traverse (formatTime SelFormatDateTime) (courseRegisterTo course) >>= maybe mempty toWidget
|
||||||
]
|
]
|
||||||
courseTable <- runDB $ dbTableWidget' def DBTable
|
courseTable <- liftHandlerT . runDB $ dbTableWidget' def DBTable
|
||||||
{ dbtSQLQuery = tableData
|
{ dbtSQLQuery = tableData
|
||||||
, dbtRowKey = (E.^. CourseId)
|
, dbtRowKey = (E.^. CourseId)
|
||||||
, dbtColonnade = colonnade
|
, dbtColonnade = colonnade
|
||||||
@ -75,16 +72,12 @@ homeAnonymous = do
|
|||||||
, dbtFilterUI = mempty
|
, dbtFilterUI = mempty
|
||||||
, dbtStyle = def
|
, dbtStyle = def
|
||||||
, dbtParams = def
|
, dbtParams = def
|
||||||
, dbtIdent = "upcomingdeadlines" :: Text
|
, dbtIdent = "open-courses" :: Text
|
||||||
}
|
}
|
||||||
-- let features = $(widgetFile "featureList")
|
$(widgetFile "home/openCourses")
|
||||||
-- addMessage Warning "Vorabversion! Die Implementierung von Uni2work ist noch nicht abgeschlossen!"
|
|
||||||
defaultLayout
|
|
||||||
-- (widgetFile "dsgvDisclaimer")
|
|
||||||
$(widgetFile "home")
|
|
||||||
|
|
||||||
homeUser :: Key User -> Handler Html
|
homeUpcomingSheets :: UserId -> Widget
|
||||||
homeUser uid = do
|
homeUpcomingSheets uid = do
|
||||||
cTime <- liftIO getCurrentTime
|
cTime <- liftIO getCurrentTime
|
||||||
let tableData :: E.LeftOuterJoin
|
let tableData :: E.LeftOuterJoin
|
||||||
(E.InnerJoin (E.InnerJoin (E.SqlExpr (Entity CourseParticipant)) (E.SqlExpr (Entity Course))) (E.SqlExpr (Entity Sheet)))
|
(E.InnerJoin (E.InnerJoin (E.SqlExpr (Entity CourseParticipant)) (E.SqlExpr (Entity Course))) (E.SqlExpr (Entity Sheet)))
|
||||||
@ -140,7 +133,7 @@ homeUser uid = do
|
|||||||
(toWidget $ hasTickmark True)
|
(toWidget $ hasTickmark True)
|
||||||
]
|
]
|
||||||
let validator = def & defaultSorting [SortDescBy "done", SortAscBy "deadline"]
|
let validator = def & defaultSorting [SortDescBy "done", SortAscBy "deadline"]
|
||||||
sheetTable <- runDB $ dbTableWidget' validator DBTable
|
sheetTable <- liftHandlerT . runDB $ dbTableWidget' validator DBTable
|
||||||
{ dbtSQLQuery = tableData
|
{ dbtSQLQuery = tableData
|
||||||
, dbtRowKey = \((_ `E.InnerJoin` _ `E.InnerJoin` sheet) `E.LeftOuterJoin` _) -> sheet E.^. SheetId
|
, dbtRowKey = \((_ `E.InnerJoin` _ `E.InnerJoin` sheet) `E.LeftOuterJoin` _) -> sheet E.^. SheetId
|
||||||
, dbtColonnade = colonnade
|
, dbtColonnade = colonnade
|
||||||
@ -175,155 +168,6 @@ homeUser uid = do
|
|||||||
, dbtFilterUI = mempty
|
, dbtFilterUI = mempty
|
||||||
, dbtStyle = def { dbsEmptyStyle = DBESNoHeading, dbsEmptyMessage = MsgNoUpcomingSheetDeadlines }
|
, dbtStyle = def { dbsEmptyStyle = DBESNoHeading, dbsEmptyMessage = MsgNoUpcomingSheetDeadlines }
|
||||||
, dbtParams = def
|
, dbtParams = def
|
||||||
, dbtIdent = "upcomingdeadlines" :: Text
|
, dbtIdent = "upcoming-sheets" :: Text
|
||||||
}
|
}
|
||||||
-- addMessage Warning "Vorabversion! Die Implementierung von Uni2work ist noch nicht abgeschlossen."
|
$(widgetFile "home/upcomingSheets")
|
||||||
defaultLayout $
|
|
||||||
-- setTitle "Willkommen zum Uni2work Test!"
|
|
||||||
$(widgetFile "homeUser")
|
|
||||||
-- (widgetFile "dsgvDisclaimer")
|
|
||||||
|
|
||||||
-- | Versionsgeschichte
|
|
||||||
getVersionR :: Handler TypedContent
|
|
||||||
getVersionR = getInfoR -- TODO
|
|
||||||
|
|
||||||
-- | Impressum
|
|
||||||
getImpressumR :: Handler Html
|
|
||||||
getImpressumR = -- do
|
|
||||||
siteLayoutMsg' MsgMenuImpressum $ do
|
|
||||||
setTitleI MsgImpressumHeading
|
|
||||||
$(i18nWidgetFile "imprint")
|
|
||||||
|
|
||||||
|
|
||||||
-- | Hinweise zu Datenschutz und Aufbewahrungspflichten
|
|
||||||
getDataProtR :: Handler Html
|
|
||||||
getDataProtR = -- do
|
|
||||||
siteLayoutMsg' MsgMenuDataProt $ do
|
|
||||||
setTitleI MsgDataProtHeading
|
|
||||||
$(i18nWidgetFile "data-protection")
|
|
||||||
|
|
||||||
|
|
||||||
-- | Allgemeine Informationen
|
|
||||||
getInfoR :: Handler TypedContent
|
|
||||||
getInfoR = selectRep $ do
|
|
||||||
let infoHeading = [whamlet|Re-Implementierung von <a href="https://uniworx.ifi.lmu.de/">UniWorX</a>|]
|
|
||||||
provideRep . siteLayout infoHeading $ do
|
|
||||||
let features = $(widgetFile "featureList")
|
|
||||||
gitInfo :: Text
|
|
||||||
gitInfo = $gitDescribe <> " (" <> $gitCommitDate <> ")"
|
|
||||||
changeLog <- withUrlRenderer $(textFile "ChangeLog.md")
|
|
||||||
$(widgetFile "versionHistory")
|
|
||||||
provideRep $
|
|
||||||
return ($gitDescribe :: Text)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
data HelpIdentOptions = HIUser | HIEmail | HIAnonymous
|
|
||||||
deriving (Eq, Ord, Bounded, Enum, Show, Read)
|
|
||||||
|
|
||||||
instance Universe HelpIdentOptions
|
|
||||||
instance Finite HelpIdentOptions
|
|
||||||
|
|
||||||
nullaryPathPiece ''HelpIdentOptions (camelToPathPiece' 1)
|
|
||||||
embedRenderMessage ''UniWorX ''HelpIdentOptions (("Help" <>) . dropPrefix "HI")
|
|
||||||
|
|
||||||
data HelpForm = HelpForm
|
|
||||||
{ hfReferer:: Maybe (Route UniWorX)
|
|
||||||
, hfUserId :: Either (Maybe Address) UserId
|
|
||||||
, hfRequest:: Text
|
|
||||||
}
|
|
||||||
|
|
||||||
helpForm :: Maybe (Route UniWorX) -> Maybe UserId -> AForm _ HelpForm
|
|
||||||
helpForm mReferer mUid = HelpForm
|
|
||||||
<$> aopt routeField (fslI MsgHelpProblemPage & inputReadonly) (Just <$> mReferer)
|
|
||||||
<*> multiActionA (fslI MsgHelpAnswer) identActions (HIUser <$ mUid)
|
|
||||||
<*> (unTextarea <$> areq textareaField (fslI MsgHelpRequest) Nothing)
|
|
||||||
where
|
|
||||||
identActions :: Map _ (AForm _ (Either (Maybe Address) UserId))
|
|
||||||
identActions = Map.fromList $ case mUid of
|
|
||||||
(Just uid) -> (HIUser, pure $ Right uid):defaultActions
|
|
||||||
Nothing -> defaultActions
|
|
||||||
|
|
||||||
defaultActions =
|
|
||||||
[ (HIEmail, Left . Just <$> (Address <$> aopt textField (fslI MsgName) Nothing <*> apreq emailField (fslI MsgEMail) Nothing))
|
|
||||||
, (HIAnonymous, pure $ Left Nothing)
|
|
||||||
]
|
|
||||||
|
|
||||||
getHelpR, postHelpR :: Handler Html
|
|
||||||
getHelpR = postHelpR
|
|
||||||
postHelpR = do
|
|
||||||
mUid <- maybeAuthId
|
|
||||||
mReferer <- flip formResultMaybe return <=< runInputGetResult $ iopt routeField (toPathPiece GetReferer)
|
|
||||||
isModal <- hasCustomHeader HeaderIsModal
|
|
||||||
|
|
||||||
((res,formWidget),formEnctype) <- runFormPost $ renderAForm FormStandard $ helpForm mReferer mUid
|
|
||||||
let form = wrapForm formWidget def
|
|
||||||
{ formAction = Just $ SomeRoute HelpR
|
|
||||||
, formEncoding = formEnctype
|
|
||||||
, formAttrs = [ ("data-ajax-submit", "") | isModal ]
|
|
||||||
}
|
|
||||||
|
|
||||||
formResultModal res HelpR $ \HelpForm{..} -> do
|
|
||||||
now <- liftIO getCurrentTime
|
|
||||||
hfReferer' <- traverse toTextUrl hfReferer
|
|
||||||
queueJob' JobHelpRequest
|
|
||||||
{ jSender = hfUserId
|
|
||||||
, jHelpRequest = hfRequest
|
|
||||||
, jRequestTime = now
|
|
||||||
, jReferer = hfReferer'
|
|
||||||
}
|
|
||||||
tell . pure =<< messageI Success MsgHelpSent
|
|
||||||
|
|
||||||
defaultLayout $ do
|
|
||||||
setTitleI MsgHelpTitle
|
|
||||||
$(widgetFile "help")
|
|
||||||
|
|
||||||
|
|
||||||
getInfoLecturerR :: Handler Html
|
|
||||||
getInfoLecturerR =
|
|
||||||
siteLayoutMsg' MsgInfoLecturerTitle $ do
|
|
||||||
setTitleI MsgInfoLecturerTitle
|
|
||||||
$(i18nWidgetFile "info-lecturer")
|
|
||||||
|
|
||||||
|
|
||||||
getAuthPredsR, postAuthPredsR :: Handler Html
|
|
||||||
getAuthPredsR = postAuthPredsR
|
|
||||||
postAuthPredsR = do
|
|
||||||
(AuthTagActive authTagCurrentActive) <- fromMaybe def <$> lookupSessionJson SessionActiveAuthTags
|
|
||||||
|
|
||||||
let
|
|
||||||
blacklist = Set.fromList [ AuthFree, AuthDevelopment, AuthDeprecated ]
|
|
||||||
taForm authTag
|
|
||||||
| authTag `Set.member` blacklist = aforced checkBoxField (fslI authTag) (authTagIsActive def authTag)
|
|
||||||
| otherwise = fromMaybe False <$> aopt checkBoxField (fslI authTag) (Just . Just $ authTagCurrentActive authTag)
|
|
||||||
|
|
||||||
((authActiveRes, authActiveWidget), authActiveEnctype) <- runFormPost . renderAForm FormStandard
|
|
||||||
$ AuthTagActive <$> funcForm taForm (fslI MsgActiveAuthTags) True
|
|
||||||
|
|
||||||
mReferer <- runMaybeT $ do
|
|
||||||
param <- MaybeT (lookupGetParam $ toPathPiece GetReferer) <|> MaybeT (lookupPostParam $ toPathPiece GetReferer)
|
|
||||||
MaybeT . return $ fromPathPiece param
|
|
||||||
|
|
||||||
let authActiveForm = wrapForm authActiveWidget' def
|
|
||||||
{ formAction = Just $ SomeRoute AuthPredsR
|
|
||||||
, formEncoding = authActiveEnctype
|
|
||||||
, formSubmit = FormDualSubmit
|
|
||||||
}
|
|
||||||
authActiveWidget'
|
|
||||||
= [whamlet|
|
|
||||||
$newline never
|
|
||||||
$maybe referer <- mReferer
|
|
||||||
<input type=hidden name=#{toPathPiece GetReferer} value=#{toPathPiece referer}>
|
|
||||||
^{authActiveWidget}
|
|
||||||
|]
|
|
||||||
|
|
||||||
formResult authActiveRes $ \authTagActive -> do
|
|
||||||
setSessionJson SessionActiveAuthTags authTagActive
|
|
||||||
modifySessionJson SessionInactiveAuthTags . fmap $ Set.filter (not . authTagIsActive authTagActive)
|
|
||||||
addMessageI Success MsgAuthPredsActiveChanged
|
|
||||||
redirect $ fromMaybe AuthPredsR mReferer
|
|
||||||
|
|
||||||
siteLayoutMsg MsgAuthPredsActive $ do
|
|
||||||
setTitleI MsgAuthPredsActive
|
|
||||||
$(widgetFile "authpreds")
|
|
||||||
|
|||||||
48
src/Handler/Info.hs
Normal file
48
src/Handler/Info.hs
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
module Handler.Info where
|
||||||
|
|
||||||
|
import Import
|
||||||
|
import Handler.Utils
|
||||||
|
|
||||||
|
import Development.GitRev
|
||||||
|
|
||||||
|
-- | Versionsgeschichte
|
||||||
|
getVersionR :: Handler TypedContent
|
||||||
|
getVersionR = selectRep $ do
|
||||||
|
provideRep $
|
||||||
|
return ($gitDescribe :: Text)
|
||||||
|
provideRep getInfoR
|
||||||
|
|
||||||
|
-- | Impressum
|
||||||
|
getImpressumR :: Handler Html
|
||||||
|
getImpressumR = -- do
|
||||||
|
siteLayoutMsg' MsgMenuImpressum $ do
|
||||||
|
setTitleI MsgImpressumHeading
|
||||||
|
$(i18nWidgetFile "imprint")
|
||||||
|
|
||||||
|
|
||||||
|
-- | Hinweise zu Datenschutz und Aufbewahrungspflichten
|
||||||
|
getDataProtR :: Handler Html
|
||||||
|
getDataProtR = -- do
|
||||||
|
siteLayoutMsg' MsgMenuDataProt $ do
|
||||||
|
setTitleI MsgDataProtHeading
|
||||||
|
$(i18nWidgetFile "data-protection")
|
||||||
|
|
||||||
|
|
||||||
|
-- | Allgemeine Informationen
|
||||||
|
getInfoR :: Handler Html
|
||||||
|
getInfoR = do
|
||||||
|
let infoHeading = [whamlet|Re-Implementierung von <a href="https://uniworx.ifi.lmu.de/">UniWorX</a>|]
|
||||||
|
siteLayout infoHeading $ do
|
||||||
|
let features = $(widgetFile "featureList")
|
||||||
|
gitInfo :: Text
|
||||||
|
gitInfo = $gitDescribe <> " (" <> $gitCommitDate <> ")"
|
||||||
|
changeLog <- withUrlRenderer $(textFile "ChangeLog.md")
|
||||||
|
$(widgetFile "versionHistory")
|
||||||
|
|
||||||
|
|
||||||
|
getInfoLecturerR :: Handler Html
|
||||||
|
getInfoLecturerR =
|
||||||
|
siteLayoutMsg' MsgInfoLecturerTitle $ do
|
||||||
|
setTitleI MsgInfoLecturerTitle
|
||||||
|
$(i18nWidgetFile "info-lecturer")
|
||||||
|
|
||||||
@ -10,7 +10,7 @@ import Utils.Lens
|
|||||||
-- import Yesod.Colonnade
|
-- import Yesod.Colonnade
|
||||||
import Data.Monoid (Any(..))
|
import Data.Monoid (Any(..))
|
||||||
import qualified Data.Map as Map
|
import qualified Data.Map as Map
|
||||||
-- import qualified Data.Set as Set
|
import qualified Data.Set as Set
|
||||||
import qualified Database.Esqueleto as E
|
import qualified Database.Esqueleto as E
|
||||||
-- import Database.Esqueleto ((^.))
|
-- import Database.Esqueleto ((^.))
|
||||||
|
|
||||||
@ -121,139 +121,47 @@ postProfileR = do
|
|||||||
, formEncoding = formEnctype
|
, formEncoding = formEnctype
|
||||||
}
|
}
|
||||||
|
|
||||||
postProfileDataR :: Handler Html
|
|
||||||
postProfileDataR = do
|
|
||||||
((btnResult,_), _) <- runFormPost buttonForm
|
|
||||||
case btnResult of
|
|
||||||
(FormSuccess BtnDelete) -> do
|
|
||||||
(uid, User{..}) <- requireAuthPair
|
|
||||||
clearCreds False -- Logout-User
|
|
||||||
((deletedSubmissions,groupSubmissions),deletedSubmissionGroups) <- runDB $ deleteUser uid
|
|
||||||
-- addMessageIHamlet
|
|
||||||
$(addMessageFile Success "templates/deletedUser.hamlet") -- USE THIS ONE
|
|
||||||
-- addMessageI Success $ MsgDeleteUser deletedSubmissions
|
|
||||||
-- when (groupSubmissions > 0) $ addMessageI Info $ MsgDeleteUserGroupSubmissions groupSubmissions
|
|
||||||
defaultLayout
|
|
||||||
$(widgetFile "deletedUser")
|
|
||||||
|
|
||||||
-- (FormSuccess BtnAbort ) -> do
|
|
||||||
-- addMessageI Info MsgAborted
|
|
||||||
-- redirect ProfileDataR
|
|
||||||
_other -> getProfileDataR
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
deleteUser :: UserId -> DB ((Int,Int),Int64) -- TODO: Restrict deletions for lecturers, tutors and students in course that won't allow deregistration
|
|
||||||
deleteUser duid = do
|
|
||||||
-- E.deleteCount for submissions is not cascading, hence we first select and then delete manually
|
|
||||||
-- We delete all files tied to submissions where the user is the lone submissionUser
|
|
||||||
|
|
||||||
-- Do not deleteCascade submissions where duid is the corrector:
|
|
||||||
updateWhere [SubmissionRatingBy ==. Just duid] [SubmissionRatingBy =. Nothing]
|
|
||||||
|
|
||||||
groupSubmissions <- selectSubmissionsWhere (\numBuddies -> numBuddies E.>. E.val (0::Int64))
|
|
||||||
singleSubmissions <- selectSubmissionsWhere (\numBuddies -> numBuddies E.==. E.val (0::Int64))
|
|
||||||
deleteCascade duid
|
|
||||||
forM_ singleSubmissions $ \(E.Value submissionId) -> do
|
|
||||||
deleteFileIds <- map E.unValue <$> getSubmissionFiles submissionId
|
|
||||||
deleteCascade submissionId
|
|
||||||
deleteCascadeWhere [FileId <-. deleteFileIds] -- TODO: throws exception for de-duplicated files
|
|
||||||
|
|
||||||
deletedSubmissionGroups <- deleteSingleSubmissionGroups
|
|
||||||
return ((length singleSubmissions, length groupSubmissions),deletedSubmissionGroups)
|
|
||||||
where
|
|
||||||
selectSubmissionsWhere :: (E.SqlExpr (E.Value Int64) -> E.SqlExpr (E.Value Bool)) -> DB [E.Value (Key Submission)]
|
|
||||||
selectSubmissionsWhere whereBuddies = E.select $ E.from $ \(submission `E.InnerJoin` suser) -> do
|
|
||||||
E.on $ submission E.^. SubmissionId E.==. suser E.^. SubmissionUserSubmission
|
|
||||||
let numBuddies = E.sub_select $ E.from $ \subUsers -> do
|
|
||||||
E.where_ $ subUsers E.^. SubmissionUserSubmission E.==. submission E.^. SubmissionId
|
|
||||||
E.&&. subUsers E.^. SubmissionUserUser E.!=. E.val duid
|
|
||||||
return E.countRows
|
|
||||||
E.where_ $ suser E.^. SubmissionUserUser E.==. E.val duid
|
|
||||||
E.&&. whereBuddies numBuddies
|
|
||||||
return $ submission E.^. SubmissionId
|
|
||||||
|
|
||||||
getSubmissionFiles :: SubmissionId -> DB [E.Value (Key File)]
|
|
||||||
getSubmissionFiles subId = E.select $ E.from $ \file -> do
|
|
||||||
E.where_ $ E.exists $ E.from $ \submissionFile ->
|
|
||||||
E.where_ $ submissionFile E.^. SubmissionFileSubmission E.==. E.val subId
|
|
||||||
E.&&. submissionFile E.^. SubmissionFileFile E.==. file E.^. FileId
|
|
||||||
return $ file E.^. FileId
|
|
||||||
|
|
||||||
deleteSingleSubmissionGroups = E.deleteCount $ E.from $ \submissionGroup -> do
|
|
||||||
E.where_ $ E.exists $ E.from $ \subGroupUser ->
|
|
||||||
E.where_ $ subGroupUser E.^. SubmissionGroupUserSubmissionGroup E.==. submissionGroup E.^. SubmissionGroupId
|
|
||||||
E.&&. subGroupUser E.^. SubmissionGroupUserUser E.==. E.val duid
|
|
||||||
E.where_ $ E.notExists $ E.from $ \subGroupUser ->
|
|
||||||
E.where_ $ subGroupUser E.^. SubmissionGroupUserSubmissionGroup E.==. submissionGroup E.^. SubmissionGroupId
|
|
||||||
E.&&. subGroupUser E.^. SubmissionGroupUserUser E.!=. E.val duid
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
getProfileDataR :: Handler Html
|
getProfileDataR :: Handler Html
|
||||||
getProfileDataR = do
|
getProfileDataR = do
|
||||||
(uid, User{..}) <- requireAuthPair
|
userEnt <- requireAuth
|
||||||
|
dataWidget <- runDB $ makeProfileData userEnt
|
||||||
|
defaultLayout $ do
|
||||||
|
dataWidget
|
||||||
|
$(widgetFile "dsgvDisclaimer")
|
||||||
|
|
||||||
|
makeProfileData :: Entity User -> DB Widget
|
||||||
|
makeProfileData (Entity uid User{..}) = do
|
||||||
-- MsgRenderer mr <- getMsgRenderer
|
-- MsgRenderer mr <- getMsgRenderer
|
||||||
(admin_rights,lecturer_rights,lecture_corrector,studies) <- runDB $ (,,,) <$>
|
admin_rights <- E.select $ E.from $ \(adright `E.InnerJoin` school) -> do
|
||||||
E.select
|
E.where_ $ adright E.^. UserAdminUser E.==. E.val uid
|
||||||
( E.from $ \(adright `E.InnerJoin` school) -> do
|
E.on $ adright E.^. UserAdminSchool E.==. school E.^. SchoolId
|
||||||
E.where_ $ adright E.^. UserAdminUser E.==. E.val uid
|
return (school E.^. SchoolShorthand)
|
||||||
E.on $ adright E.^. UserAdminSchool E.==. school E.^. SchoolId
|
lecturer_rights <- E.select $ E.from $ \(lecright `E.InnerJoin` school) -> do
|
||||||
return (school E.^. SchoolShorthand)
|
E.where_ $ lecright E.^. UserLecturerUser E.==. E.val uid
|
||||||
)
|
E.on $ lecright E.^. UserLecturerSchool E.==. school E.^. SchoolId
|
||||||
<*>
|
return (school E.^. SchoolShorthand)
|
||||||
E.select
|
lecture_corrector <- E.select $ E.distinct $ E.from $ \(sheet `E.InnerJoin` corrector `E.InnerJoin` course) -> do
|
||||||
( E.from $ \(lecright `E.InnerJoin` school) -> do
|
E.on $ sheet E.^. SheetCourse E.==. course E.^. CourseId
|
||||||
E.where_ $ lecright E.^. UserLecturerUser E.==. E.val uid
|
E.on $ sheet E.^. SheetId E.==. corrector E.^. SheetCorrectorSheet
|
||||||
E.on $ lecright E.^. UserLecturerSchool E.==. school E.^. SchoolId
|
E.where_ $ corrector E.^. SheetCorrectorUser E.==. E.val uid
|
||||||
return (school E.^. SchoolShorthand)
|
return (course E.^. CourseTerm, course E.^. CourseSchool, course E.^. CourseShorthand)
|
||||||
)
|
studies <- E.select $ E.from $ \(studydegree `E.InnerJoin` studyfeat `E.InnerJoin` studyterms) -> do
|
||||||
<*>
|
E.where_ $ studyfeat E.^. StudyFeaturesUser E.==. E.val uid
|
||||||
E.select
|
E.on $ studyfeat E.^. StudyFeaturesField E.==. studyterms E.^. StudyTermsId
|
||||||
( E.distinct $ E.from $ \(sheet `E.InnerJoin` corrector `E.InnerJoin` course) -> do
|
E.on $ studyfeat E.^. StudyFeaturesDegree E.==. studydegree E.^. StudyDegreeId
|
||||||
E.on $ sheet E.^. SheetCourse E.==. course E.^. CourseId
|
return (studyfeat, studydegree, studyterms)
|
||||||
E.on $ sheet E.^. SheetId E.==. corrector E.^. SheetCorrectorSheet
|
--Tables
|
||||||
E.where_ $ corrector E.^. SheetCorrectorUser E.==. E.val uid
|
(hasRows, ownedCoursesTable) <- mkOwnedCoursesTable uid -- Tabelle mit eigenen Kursen
|
||||||
return (course E.^. CourseTerm, course E.^. CourseSchool, course E.^. CourseShorthand)
|
enrolledCoursesTable <- mkEnrolledCoursesTable uid -- Tabelle mit allen Teilnehmer: Kurs (link), Datum
|
||||||
)
|
submissionTable <- mkSubmissionTable uid -- Tabelle mit allen Abgaben und Abgabe-Gruppen
|
||||||
<*>
|
submissionGroupTable <- mkSubmissionGroupTable uid -- Tabelle mit allen Abgabegruppen
|
||||||
E.select
|
correctionsTable <- mkCorrectionsTable uid -- Tabelle mit allen Korrektor-Aufgaben
|
||||||
( E.from $ \(studydegree `E.InnerJoin` studyfeat `E.InnerJoin` studyterms) -> do
|
|
||||||
E.where_ $ studyfeat E.^. StudyFeaturesUser E.==. E.val uid
|
|
||||||
E.on $ studyfeat E.^. StudyFeaturesField E.==. studyterms E.^. StudyTermsId
|
|
||||||
E.on $ studyfeat E.^. StudyFeaturesDegree E.==. studydegree E.^. StudyDegreeId
|
|
||||||
return (studyfeat, studydegree, studyterms)
|
|
||||||
)
|
|
||||||
( (hasRows, ownedCoursesTable)
|
|
||||||
, enrolledCoursesTable
|
|
||||||
, submissionTable
|
|
||||||
, submissionGroupTable
|
|
||||||
, correctionsTable
|
|
||||||
) <- runDB $ (,,,,)
|
|
||||||
<$> mkOwnedCoursesTable uid -- Tabelle mit eigenen Kursen
|
|
||||||
<*> mkEnrolledCoursesTable uid -- Tabelle mit allen Teilnehmer: Kurs (link), Datum
|
|
||||||
<*> mkSubmissionTable uid -- Tabelle mit allen Abgaben und Abgabe-Gruppen
|
|
||||||
<*> mkSubmissionGroupTable uid -- Tabelle mit allen Abgabegruppen
|
|
||||||
<*> mkCorrectionsTable uid -- Tabelle mit allen Korrektor-Aufgaben
|
|
||||||
|
|
||||||
|
|
||||||
let examTable = [whamlet|Klausuren werden momentan leider noch nicht unterstützt.|]
|
let examTable = [whamlet|Klausuren werden momentan leider noch nicht unterstützt.|]
|
||||||
let ownTutorialTable = [whamlet|Übungsgruppen werden momentan leider noch nicht unterstützt.|]
|
let ownTutorialTable = [whamlet|Übungsgruppen werden momentan leider noch nicht unterstützt.|]
|
||||||
let tutorialTable = [whamlet|Übungsgruppen werden momentan leider noch nicht unterstützt.|]
|
let tutorialTable = [whamlet|Übungsgruppen werden momentan leider noch nicht unterstützt.|]
|
||||||
|
|
||||||
lastLogin <- traverse (formatTime SelFormatDateTime) userLastAuthentication
|
lastLogin <- traverse (formatTime SelFormatDateTime) userLastAuthentication
|
||||||
|
return $(widgetFile "profileData")
|
||||||
-- Delete Button
|
|
||||||
(btnWdgt, btnEnctype) <- generateFormPost (buttonForm :: Form ButtonDelete)
|
|
||||||
let btnForm = wrapForm btnWdgt def
|
|
||||||
{ formAction = Just $ SomeRoute ProfileDataR
|
|
||||||
, formEncoding = btnEnctype
|
|
||||||
, formSubmit = FormNoSubmit
|
|
||||||
}
|
|
||||||
defaultLayout $ do
|
|
||||||
let delWdgt = $(widgetFile "widgets/data-delete/data-delete")
|
|
||||||
$(widgetFile "profileData")
|
|
||||||
$(widgetFile "dsgvDisclaimer")
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -583,3 +491,44 @@ mkCorrectionsTable =
|
|||||||
in \uid -> let dbtSQLQuery = dbtSQLQuery' uid
|
in \uid -> let dbtSQLQuery = dbtSQLQuery' uid
|
||||||
in dbTableWidget' validator DBTable{..}
|
in dbTableWidget' validator DBTable{..}
|
||||||
|
|
||||||
|
|
||||||
|
getAuthPredsR, postAuthPredsR :: Handler Html
|
||||||
|
getAuthPredsR = postAuthPredsR
|
||||||
|
postAuthPredsR = do
|
||||||
|
(AuthTagActive authTagCurrentActive) <- fromMaybe def <$> lookupSessionJson SessionActiveAuthTags
|
||||||
|
|
||||||
|
let
|
||||||
|
blacklist = Set.fromList [ AuthFree, AuthDevelopment, AuthDeprecated ]
|
||||||
|
taForm authTag
|
||||||
|
| authTag `Set.member` blacklist = aforced checkBoxField (fslI authTag) (authTagIsActive def authTag)
|
||||||
|
| otherwise = fromMaybe False <$> aopt checkBoxField (fslI authTag) (Just . Just $ authTagCurrentActive authTag)
|
||||||
|
|
||||||
|
((authActiveRes, authActiveWidget), authActiveEnctype) <- runFormPost . renderAForm FormStandard
|
||||||
|
$ AuthTagActive <$> funcForm taForm (fslI MsgActiveAuthTags) True
|
||||||
|
|
||||||
|
mReferer <- runMaybeT $ do
|
||||||
|
param <- MaybeT (lookupGetParam $ toPathPiece GetReferer) <|> MaybeT (lookupPostParam $ toPathPiece GetReferer)
|
||||||
|
MaybeT . return $ fromPathPiece param
|
||||||
|
|
||||||
|
let authActiveForm = wrapForm authActiveWidget' def
|
||||||
|
{ formAction = Just $ SomeRoute AuthPredsR
|
||||||
|
, formEncoding = authActiveEnctype
|
||||||
|
, formSubmit = FormDualSubmit
|
||||||
|
}
|
||||||
|
authActiveWidget'
|
||||||
|
= [whamlet|
|
||||||
|
$newline never
|
||||||
|
$maybe referer <- mReferer
|
||||||
|
<input type=hidden name=#{toPathPiece GetReferer} value=#{toPathPiece referer}>
|
||||||
|
^{authActiveWidget}
|
||||||
|
|]
|
||||||
|
|
||||||
|
formResult authActiveRes $ \authTagActive -> do
|
||||||
|
setSessionJson SessionActiveAuthTags authTagActive
|
||||||
|
modifySessionJson SessionInactiveAuthTags . fmap $ Set.filter (not . authTagIsActive authTagActive)
|
||||||
|
addMessageI Success MsgAuthPredsActiveChanged
|
||||||
|
redirect $ fromMaybe AuthPredsR mReferer
|
||||||
|
|
||||||
|
siteLayoutMsg MsgAuthPredsActive $ do
|
||||||
|
setTitleI MsgAuthPredsActive
|
||||||
|
$(widgetFile "authpreds")
|
||||||
|
|||||||
@ -123,7 +123,6 @@ makeSheetForm msId template = identifyForm FIDsheet $ \html -> do
|
|||||||
<*> aopt (multiFileField $ oldFileIds SheetMarking) (fslI MsgSheetMarking
|
<*> aopt (multiFileField $ oldFileIds SheetMarking) (fslI MsgSheetMarking
|
||||||
& setTooltip MsgSheetMarkingTip) (sfMarkingF <$> template)
|
& setTooltip MsgSheetMarkingTip) (sfMarkingF <$> template)
|
||||||
<*> aopt htmlField (fslI MsgSheetMarking) (sfMarkingText <$> template)
|
<*> aopt htmlField (fslI MsgSheetMarking) (sfMarkingText <$> template)
|
||||||
<* submitButton
|
|
||||||
return $ case result of
|
return $ case result of
|
||||||
FormSuccess sheetResult
|
FormSuccess sheetResult
|
||||||
| errorMsgs <- validateSheet mr sheetResult
|
| errorMsgs <- validateSheet mr sheetResult
|
||||||
@ -200,11 +199,12 @@ getSheetListR tid ssh csh = do
|
|||||||
let stats = sheetTypeSum sheetType in -- for statistics over all shown rows
|
let stats = sheetTypeSum sheetType in -- for statistics over all shown rows
|
||||||
case mbSub of
|
case mbSub of
|
||||||
Nothing -> cellTell mempty $ stats Nothing
|
Nothing -> cellTell mempty $ stats Nothing
|
||||||
(Just (Entity sid Submission{..})) ->
|
(Just (Entity sid sub@Submission{..})) ->
|
||||||
let mkCid = encrypt sid
|
let mkCid = encrypt sid
|
||||||
mkRoute = do
|
mkRoute = do
|
||||||
cid' <- mkCid
|
cid' <- mkCid
|
||||||
return $ CSubmissionR tid ssh csh sheetName cid' CorrectionR
|
return $ CSubmissionR tid ssh csh sheetName cid' CorrectionR
|
||||||
|
mTuple mA mB = (,) <$> mA <*> mB -- Hamlet does not support enough haskell-syntax for this
|
||||||
acell = anchorCellM mkRoute $(widgetFile "widgets/rating/rating")
|
acell = anchorCellM mkRoute $(widgetFile "widgets/rating/rating")
|
||||||
in cellTell acell $ stats submissionRatingPoints
|
in cellTell acell $ stats submissionRatingPoints
|
||||||
|
|
||||||
@ -787,7 +787,7 @@ postSCorrR = getSCorrR
|
|||||||
getSCorrR tid ssh csh shn = do
|
getSCorrR tid ssh csh shn = do
|
||||||
Entity shid Sheet{..} <- runDB $ fetchSheet tid ssh csh shn
|
Entity shid Sheet{..} <- runDB $ fetchSheet tid ssh csh shn
|
||||||
|
|
||||||
((res,formWidget), formEnctype) <- runFormPost . identifyForm FIDcorrectors . renderAForm FormStandard $ formToAForm (correctorForm shid) <* submitButton
|
((res,formWidget), formEnctype) <- runFormPost . identifyForm FIDcorrectors . renderAForm FormStandard $ formToAForm (correctorForm shid)
|
||||||
|
|
||||||
case res of
|
case res of
|
||||||
FormFailure errs -> mapM_ (addMessage Error . toHtml) errs
|
FormFailure errs -> mapM_ (addMessage Error . toHtml) errs
|
||||||
|
|||||||
@ -3,7 +3,6 @@ module Handler.SystemMessage where
|
|||||||
import Import
|
import Import
|
||||||
|
|
||||||
import qualified Data.Map.Lazy as Map
|
import qualified Data.Map.Lazy as Map
|
||||||
import qualified Data.Text as Text
|
|
||||||
|
|
||||||
import qualified Data.Set as Set
|
import qualified Data.Set as Set
|
||||||
|
|
||||||
@ -16,13 +15,7 @@ import Utils.Lens
|
|||||||
|
|
||||||
import qualified Database.Esqueleto as E
|
import qualified Database.Esqueleto as E
|
||||||
|
|
||||||
|
-- htmlField' moved to Handler.Utils.Form/Fields
|
||||||
htmlField' :: Field (HandlerT UniWorX IO) Html
|
|
||||||
htmlField' = htmlField
|
|
||||||
{ fieldParse = \vs fis -> fieldParse htmlField (map Text.strip vs) fis
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
getMessageR, postMessageR :: CryptoUUIDSystemMessage -> Handler Html
|
getMessageR, postMessageR :: CryptoUUIDSystemMessage -> Handler Html
|
||||||
getMessageR = postMessageR
|
getMessageR = postMessageR
|
||||||
|
|||||||
@ -3,14 +3,16 @@ module Handler.Term where
|
|||||||
import Import
|
import Import
|
||||||
import Handler.Utils
|
import Handler.Utils
|
||||||
import Handler.Utils.Table.Cells
|
import Handler.Utils.Table.Cells
|
||||||
|
import Handler.Utils.Form.MassInput
|
||||||
import qualified Data.Map as Map
|
import qualified Data.Map as Map
|
||||||
|
|
||||||
-- import qualified Data.Text as T
|
import Utils.Lens
|
||||||
import Yesod.Form.Bootstrap3
|
|
||||||
-- import Colonnade hiding (bool)
|
|
||||||
|
|
||||||
import qualified Database.Esqueleto as E
|
import qualified Database.Esqueleto as E
|
||||||
|
|
||||||
|
import qualified Data.Set as Set
|
||||||
|
|
||||||
|
|
||||||
-- | Default start day of term for season,
|
-- | Default start day of term for season,
|
||||||
-- @True@: start of term, @False@: end of term
|
-- @True@: start of term, @False@: end of term
|
||||||
defaultDay :: Bool -> Season -> Day
|
defaultDay :: Bool -> Season -> Day
|
||||||
@ -148,7 +150,7 @@ getTermShowR = do
|
|||||||
setTitleI MsgTermsHeading
|
setTitleI MsgTermsHeading
|
||||||
$(widgetFile "terms")
|
$(widgetFile "terms")
|
||||||
|
|
||||||
getTermEditR :: Handler Html
|
getTermEditR, postTermEditR :: Handler Html
|
||||||
getTermEditR = do
|
getTermEditR = do
|
||||||
mbLastTerm <- runDB $ selectFirst [] [Desc TermName]
|
mbLastTerm <- runDB $ selectFirst [] [Desc TermName]
|
||||||
let template = case mbLastTerm of
|
let template = case mbLastTerm of
|
||||||
@ -164,18 +166,18 @@ getTermEditR = do
|
|||||||
, tftEnd = Just $ defaultDay False seas & setYear yr'
|
, tftEnd = Just $ defaultDay False seas & setYear yr'
|
||||||
}
|
}
|
||||||
termEditHandler template
|
termEditHandler template
|
||||||
|
|
||||||
postTermEditR :: Handler Html
|
|
||||||
postTermEditR = termEditHandler mempty
|
postTermEditR = termEditHandler mempty
|
||||||
|
|
||||||
getTermEditExistR :: TermId -> Handler Html
|
getTermEditExistR, postTermEditExistR :: TermId -> Handler Html
|
||||||
getTermEditExistR tid = do
|
getTermEditExistR = postTermEditExistR
|
||||||
|
postTermEditExistR tid = do
|
||||||
term <- runDB $ get tid
|
term <- runDB $ get tid
|
||||||
termEditHandler $ termToTemplate term
|
termEditHandler $ termToTemplate term
|
||||||
|
|
||||||
|
|
||||||
termEditHandler :: TermFormTemplate -> Handler Html
|
termEditHandler :: TermFormTemplate -> Handler Html
|
||||||
termEditHandler term = do
|
termEditHandler term = do
|
||||||
|
Just eHandler <- getCurrentRoute
|
||||||
((result, formWidget), formEnctype) <- runFormPost $ newTermForm term
|
((result, formWidget), formEnctype) <- runFormPost $ newTermForm term
|
||||||
case result of
|
case result of
|
||||||
(FormSuccess res) -> do
|
(FormSuccess res) -> do
|
||||||
@ -194,7 +196,7 @@ termEditHandler term = do
|
|||||||
defaultLayout $ do
|
defaultLayout $ do
|
||||||
setTitleI MsgTermEditHeading
|
setTitleI MsgTermEditHeading
|
||||||
wrapForm formWidget def
|
wrapForm formWidget def
|
||||||
{ formAction = Just $ SomeRoute TermEditR
|
{ formAction = Just $ SomeRoute eHandler
|
||||||
, formEncoding = formEnctype
|
, formEncoding = formEnctype
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -247,14 +249,21 @@ termToTemplate (Just Term{..}) = TermFormTemplate
|
|||||||
newTermForm :: TermFormTemplate -> Form Term
|
newTermForm :: TermFormTemplate -> Form Term
|
||||||
newTermForm template html = do
|
newTermForm template html = do
|
||||||
mr <- getMessageRender
|
mr <- getMessageRender
|
||||||
|
let
|
||||||
|
tidForm
|
||||||
|
| Just tid <- tftName template
|
||||||
|
= aforced termNewField (fslpI MsgTerm (mr MsgTermPlaceholder)) tid
|
||||||
|
| otherwise
|
||||||
|
= areq termNewField (fslpI MsgTerm (mr MsgTermPlaceholder)) Nothing
|
||||||
|
holidayForm = formToAForm . over (mapped._2) pure $ massInputList dayField (const $ "" & addPlaceholder (mr MsgTermHolidayPlaceholder)) (const Nothing) (fslI MsgTermHolidays) True (tftHolidays template) mempty
|
||||||
(result, widget) <- flip (renderAForm FormStandard) html $ Term
|
(result, widget) <- flip (renderAForm FormStandard) html $ Term
|
||||||
<$> areq termNewField (fslpI MsgTerm (mr MsgTermPlaceholder)) (tftName template)
|
<$> tidForm
|
||||||
<*> areq dayField (fslI MsgTermStartDay & setTooltip MsgTermStartDayTooltip) (tftStart template)
|
<*> areq dayField (fslI MsgTermStartDay & setTooltip MsgTermStartDayTooltip) (tftStart template)
|
||||||
<*> areq dayField (fslI MsgTermEndDay & setTooltip MsgTermEndDayTooltip) (tftEnd template)
|
<*> areq dayField (fslI MsgTermEndDay & setTooltip MsgTermEndDayTooltip) (tftEnd template)
|
||||||
<*> pure [] -- TODO: List of Day field required, must probably be done as its own form and then combined
|
<*> (Set.toList . Set.fromList <$> holidayForm)
|
||||||
<*> areq dayField (fslI MsgTermLectureStart) (tftLectureStart template)
|
<*> areq dayField (fslI MsgTermLectureStart) (tftLectureStart template)
|
||||||
<*> areq dayField (fslI MsgTermLectureEnd & setTooltip MsgTermLectureEndTooltip) (tftLectureEnd template)
|
<*> areq dayField (fslI MsgTermLectureEnd & setTooltip MsgTermLectureEndTooltip) (tftLectureEnd template)
|
||||||
<*> areq checkBoxField (bfs ("Aktiv" :: Text)) (tftActive template)
|
<*> areq checkBoxField (fslI MsgTermActive) (tftActive template)
|
||||||
return $ case result of
|
return $ case result of
|
||||||
FormSuccess termResult
|
FormSuccess termResult
|
||||||
| errorMsgs <- validateTerm termResult
|
| errorMsgs <- validateTerm termResult
|
||||||
|
|||||||
@ -16,6 +16,8 @@ import qualified Data.Map as Map
|
|||||||
import qualified Database.Esqueleto as E
|
import qualified Database.Esqueleto as E
|
||||||
import qualified Database.Esqueleto.Utils as E
|
import qualified Database.Esqueleto.Utils as E
|
||||||
|
|
||||||
|
import Handler.Profile (makeProfileData)
|
||||||
|
|
||||||
|
|
||||||
hijackUserForm :: CryptoUUIDUser -> Form ()
|
hijackUserForm :: CryptoUUIDUser -> Form ()
|
||||||
hijackUserForm cID csrf = do
|
hijackUserForm cID csrf = do
|
||||||
@ -161,7 +163,7 @@ postAdminUserR uuid = do
|
|||||||
uid <- decrypt uuid
|
uid <- decrypt uuid
|
||||||
let fromSchoolList = Set.fromList . map (userAdminSchool . entityVal)
|
let fromSchoolList = Set.fromList . map (userAdminSchool . entityVal)
|
||||||
let unValueRights (school, E.Value isAdmin, E.Value isLecturer) = (school,isAdmin,isLecturer)
|
let unValueRights (school, E.Value isAdmin, E.Value isLecturer) = (school,isAdmin,isLecturer)
|
||||||
(User{..}, fromSchoolList -> adminSchools, fmap unValueRights -> userRights) <- runDB $ (,,)
|
(user@User{..}, fromSchoolList -> adminSchools, fmap unValueRights -> userRights) <- runDB $ (,,)
|
||||||
<$> get404 uid
|
<$> get404 uid
|
||||||
<*> selectList [UserAdminUser ==. adminId] []
|
<*> selectList [UserAdminUser ==. adminId] []
|
||||||
<*> E.select ( E.from $ \school -> do
|
<*> E.select ( E.from $ \school -> do
|
||||||
@ -176,7 +178,7 @@ postAdminUserR uuid = do
|
|||||||
)
|
)
|
||||||
-- above data is needed for both form generation and result evaluation
|
-- above data is needed for both form generation and result evaluation
|
||||||
let userRightsForm :: Form [(SchoolId, Bool, Bool)]
|
let userRightsForm :: Form [(SchoolId, Bool, Bool)]
|
||||||
userRightsForm csrf = do
|
userRightsForm = identifyForm FIDuserRights $ \csrf -> do
|
||||||
boxRights <- forM userRights $ \(school@(Entity sid _), isAdmin, isLecturer) ->
|
boxRights <- forM userRights $ \(school@(Entity sid _), isAdmin, isLecturer) ->
|
||||||
if Set.member sid adminSchools
|
if Set.member sid adminSchools
|
||||||
then do
|
then do
|
||||||
@ -213,5 +215,84 @@ postAdminUserR uuid = do
|
|||||||
formResult result userRightsAction
|
formResult result userRightsAction
|
||||||
let heading =
|
let heading =
|
||||||
[whamlet|_{MsgAccessRightsFor} ^{nameWidget userDisplayName userSurname}|]
|
[whamlet|_{MsgAccessRightsFor} ^{nameWidget userDisplayName userSurname}|]
|
||||||
siteLayout heading
|
-- Delete Button needed in data-delete
|
||||||
|
(btnWgt, btnEnctype) <- generateFormPost (identifyForm FIDUserDelete buttonForm :: Form ButtonDelete)
|
||||||
|
let btnForm = wrapForm btnWgt def
|
||||||
|
{ formAction = Just $ SomeRoute $ AdminUserDeleteR uuid
|
||||||
|
, formEncoding = btnEnctype
|
||||||
|
, formSubmit = FormNoSubmit
|
||||||
|
}
|
||||||
|
userDataWidget <- runDB $ makeProfileData $ Entity uid user
|
||||||
|
siteLayout heading $ do
|
||||||
|
let deleteWidget = $(widgetFile "widgets/data-delete/data-delete")
|
||||||
$(widgetFile "adminUser")
|
$(widgetFile "adminUser")
|
||||||
|
|
||||||
|
|
||||||
|
postAdminUserDeleteR :: CryptoUUIDUser -> Handler Html
|
||||||
|
postAdminUserDeleteR uuid = do
|
||||||
|
uid <- decrypt uuid
|
||||||
|
((btnResult,_), _) <- runFormPost (identifyForm FIDUserDelete buttonForm :: Form ButtonDelete)
|
||||||
|
case btnResult of
|
||||||
|
(FormSuccess BtnDelete) -> do
|
||||||
|
User{..} <- runDB $ get404 uid
|
||||||
|
-- clearCreds False -- Logout-User
|
||||||
|
((deletedSubmissions,groupSubmissions),deletedSubmissionGroups) <- runDB $ deleteUser uid
|
||||||
|
-- addMessageIHamlet
|
||||||
|
$(addMessageFile Success "templates/deletedUser.hamlet") -- USE THIS ONE
|
||||||
|
-- addMessageI Success $ MsgDeleteUser deletedSubmissions
|
||||||
|
-- when (groupSubmissions > 0) $ addMessageI Info $ MsgDeleteUserGroupSubmissions groupSubmissions
|
||||||
|
defaultLayout
|
||||||
|
$(widgetFile "deletedUser")
|
||||||
|
|
||||||
|
-- (FormSuccess BtnAbort ) -> do
|
||||||
|
-- addMessageI Info MsgAborted
|
||||||
|
-- redirect ProfileDataR
|
||||||
|
_other -> getAdminUserR uuid
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
deleteUser :: UserId -> DB ((Int,Int),Int64) -- TODO: Restrict deletions for lecturers, tutors and students in course that won't allow deregistration
|
||||||
|
deleteUser duid = do
|
||||||
|
-- E.deleteCount for submissions is not cascading, hence we first select and then delete manually
|
||||||
|
-- We delete all files tied to submissions where the user is the lone submissionUser
|
||||||
|
|
||||||
|
-- Do not deleteCascade submissions where duid is the corrector:
|
||||||
|
updateWhere [SubmissionRatingBy ==. Just duid] [SubmissionRatingBy =. Nothing]
|
||||||
|
|
||||||
|
groupSubmissions <- selectSubmissionsWhere (\numBuddies -> numBuddies E.>. E.val (0::Int64))
|
||||||
|
singleSubmissions <- selectSubmissionsWhere (\numBuddies -> numBuddies E.==. E.val (0::Int64))
|
||||||
|
deleteCascade duid
|
||||||
|
forM_ singleSubmissions $ \(E.Value submissionId) -> do
|
||||||
|
deleteFileIds <- map E.unValue <$> getSubmissionFiles submissionId
|
||||||
|
deleteCascade submissionId
|
||||||
|
deleteCascadeWhere [FileId <-. deleteFileIds] -- TODO: throws exception for de-duplicated files
|
||||||
|
|
||||||
|
deletedSubmissionGroups <- deleteSingleSubmissionGroups
|
||||||
|
return ((length singleSubmissions, length groupSubmissions),deletedSubmissionGroups)
|
||||||
|
where
|
||||||
|
selectSubmissionsWhere :: (E.SqlExpr (E.Value Int64) -> E.SqlExpr (E.Value Bool)) -> DB [E.Value (Key Submission)]
|
||||||
|
selectSubmissionsWhere whereBuddies = E.select $ E.from $ \(submission `E.InnerJoin` suser) -> do
|
||||||
|
E.on $ submission E.^. SubmissionId E.==. suser E.^. SubmissionUserSubmission
|
||||||
|
let numBuddies = E.sub_select $ E.from $ \subUsers -> do
|
||||||
|
E.where_ $ subUsers E.^. SubmissionUserSubmission E.==. submission E.^. SubmissionId
|
||||||
|
E.&&. subUsers E.^. SubmissionUserUser E.!=. E.val duid
|
||||||
|
return E.countRows
|
||||||
|
E.where_ $ suser E.^. SubmissionUserUser E.==. E.val duid
|
||||||
|
E.&&. whereBuddies numBuddies
|
||||||
|
return $ submission E.^. SubmissionId
|
||||||
|
|
||||||
|
getSubmissionFiles :: SubmissionId -> DB [E.Value (Key File)]
|
||||||
|
getSubmissionFiles subId = E.select $ E.from $ \file -> do
|
||||||
|
E.where_ $ E.exists $ E.from $ \submissionFile ->
|
||||||
|
E.where_ $ submissionFile E.^. SubmissionFileSubmission E.==. E.val subId
|
||||||
|
E.&&. submissionFile E.^. SubmissionFileFile E.==. file E.^. FileId
|
||||||
|
return $ file E.^. FileId
|
||||||
|
|
||||||
|
deleteSingleSubmissionGroups = E.deleteCount $ E.from $ \submissionGroup -> do
|
||||||
|
E.where_ $ E.exists $ E.from $ \subGroupUser ->
|
||||||
|
E.where_ $ subGroupUser E.^. SubmissionGroupUserSubmissionGroup E.==. submissionGroup E.^. SubmissionGroupId
|
||||||
|
E.&&. subGroupUser E.^. SubmissionGroupUserUser E.==. E.val duid
|
||||||
|
E.where_ $ E.notExists $ E.from $ \subGroupUser ->
|
||||||
|
E.where_ $ subGroupUser E.^. SubmissionGroupUserSubmissionGroup E.==. submissionGroup E.^. SubmissionGroupId
|
||||||
|
E.&&. subGroupUser E.^. SubmissionGroupUserUser E.!=. E.val duid
|
||||||
|
|
||||||
|
|||||||
@ -7,10 +7,11 @@ import Import
|
|||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
-- import qualified Data.Set (Set)
|
-- import qualified Data.Set (Set)
|
||||||
import qualified Data.Set as Set
|
import qualified Data.Set as Set
|
||||||
import Data.CaseInsensitive (CI, original)
|
import Data.CaseInsensitive (original)
|
||||||
-- import qualified Data.CaseInsensitive as CI
|
-- import qualified Data.CaseInsensitive as CI
|
||||||
|
|
||||||
import Language.Haskell.TH (Q, Exp)
|
import Language.Haskell.TH
|
||||||
|
import Language.Haskell.TH.Syntax (qRunIO)
|
||||||
-- import Language.Haskell.TH.Datatype
|
-- import Language.Haskell.TH.Datatype
|
||||||
|
|
||||||
import Text.Hamlet (shamletFile)
|
import Text.Hamlet (shamletFile)
|
||||||
@ -24,9 +25,14 @@ import Handler.Utils.Zip as Handler.Utils
|
|||||||
import Handler.Utils.Rating as Handler.Utils hiding (extractRatings)
|
import Handler.Utils.Rating as Handler.Utils hiding (extractRatings)
|
||||||
-- import Handler.Utils.Submission as Handler.Utils
|
-- import Handler.Utils.Submission as Handler.Utils
|
||||||
import Handler.Utils.Sheet as Handler.Utils
|
import Handler.Utils.Sheet as Handler.Utils
|
||||||
import Handler.Utils.Templates as Handler.Utils
|
|
||||||
import Handler.Utils.Mail as Handler.Utils
|
import Handler.Utils.Mail as Handler.Utils
|
||||||
|
|
||||||
|
import System.Directory (listDirectory)
|
||||||
|
import System.FilePath.Posix (takeBaseName)
|
||||||
|
|
||||||
|
import qualified Data.List as List
|
||||||
|
import qualified Data.List.NonEmpty as NonEmpty
|
||||||
|
|
||||||
|
|
||||||
downloadFiles :: (MonadHandler m, HandlerSite m ~ UniWorX) => m Bool
|
downloadFiles :: (MonadHandler m, HandlerSite m ~ UniWorX) => m Bool
|
||||||
downloadFiles = do
|
downloadFiles = do
|
||||||
@ -44,13 +50,22 @@ simpleLink :: Widget -> Route UniWorX -> Widget
|
|||||||
simpleLink lbl url = [whamlet|<a href=@{url}>^{lbl}|]
|
simpleLink lbl url = [whamlet|<a href=@{url}>^{lbl}|]
|
||||||
|
|
||||||
-- | toWidget-Version of @nameHtml@, for convenience
|
-- | toWidget-Version of @nameHtml@, for convenience
|
||||||
nameWidget :: Text -> Text -> Widget
|
nameWidget :: Text -- ^ userDisplayName
|
||||||
|
-> Text -- ^ userSurname
|
||||||
|
-> Widget
|
||||||
nameWidget displayName surname = toWidget $ nameHtml displayName surname
|
nameWidget displayName surname = toWidget $ nameHtml displayName surname
|
||||||
|
|
||||||
-- | toWidget-Version of @nameEmailHtml@, for convenience
|
-- | toWidget-Version of @nameEmailHtml@, for convenience
|
||||||
nameEmailWidget :: CI Text -> Text -> Text -> Widget
|
nameEmailWidget :: UserEmail -- ^ userEmail
|
||||||
|
-> Text -- ^ userDisplayName
|
||||||
|
-> Text -- ^ userSurname
|
||||||
|
-> Widget
|
||||||
nameEmailWidget email displayName surname = toWidget $ nameEmailHtml email displayName surname
|
nameEmailWidget email displayName surname = toWidget $ nameEmailHtml email displayName surname
|
||||||
|
|
||||||
|
-- | uncurried Version for @nameEmailWidget@ needed in hamlet, where TH cannot be used
|
||||||
|
nameEmailWidget' :: (UserEmail, Text, Text)-> Widget
|
||||||
|
nameEmailWidget' = $(uncurryN 3) nameEmailWidget
|
||||||
|
|
||||||
-- | Show user's displayName, highlighting the surname if possible.
|
-- | Show user's displayName, highlighting the surname if possible.
|
||||||
-- Otherwise appends the surname in parenthesis
|
-- Otherwise appends the surname in parenthesis
|
||||||
nameHtml :: Text -> Text -> Html
|
nameHtml :: Text -> Text -> Html
|
||||||
@ -72,20 +87,47 @@ nameHtml displayName surname
|
|||||||
|
|
||||||
-- | Like nameHtml just show a users displayname with hightlighted surname,
|
-- | Like nameHtml just show a users displayname with hightlighted surname,
|
||||||
-- but also wrap the name with a mailto-link
|
-- but also wrap the name with a mailto-link
|
||||||
nameEmailHtml :: CI Text -> Text -> Text -> Html
|
nameEmailHtml :: UserEmail -> Text -> Text -> Html
|
||||||
nameEmailHtml email displayName surname =
|
nameEmailHtml email displayName surname =
|
||||||
wrapMailto email $ nameHtml displayName surname
|
wrapMailto email $ nameHtml displayName surname
|
||||||
|
|
||||||
-- | Wrap mailto around given Html using single hamlet-file for consistency
|
-- | Wrap mailto around given Html using single hamlet-file for consistency
|
||||||
wrapMailto :: CI Text -> Html -> Html
|
wrapMailto :: UserEmail -> Html -> Html
|
||||||
wrapMailto (original -> email) linkText
|
wrapMailto (original -> email) linkText
|
||||||
| null email = linkText
|
| null email = linkText
|
||||||
| otherwise = $(shamletFile "templates/widgets/link-email.hamlet")
|
| otherwise = $(shamletFile "templates/widgets/link-email.hamlet")
|
||||||
|
|
||||||
-- | Just show an email address in a standard way, for convenience inside hamlet files.
|
-- | Just show an email address in a standard way, for convenience inside hamlet files.
|
||||||
mailtoHtml :: CI Text -> Html
|
mailtoHtml :: UserEmail -> Html
|
||||||
mailtoHtml email = wrapMailto email $ toHtml email
|
mailtoHtml email = wrapMailto email $ toHtml email
|
||||||
|
|
||||||
|
-- | Generic i18n text for "edited at sometime by someone"
|
||||||
|
editedByW :: SelDateTimeFormat -> UTCTime -> Text -> Widget
|
||||||
|
editedByW fmt tm usr = do
|
||||||
|
ft <- handlerToWidget $ formatTime fmt tm
|
||||||
|
[whamlet|_{MsgEditedBy usr ft}|]
|
||||||
|
|
||||||
|
-- | Prefix a message with a short course id,
|
||||||
|
-- eg. for window title bars, etc.
|
||||||
|
-- This function should help to make this consistent everywhere
|
||||||
|
prependCourseTitle :: (RenderMessage UniWorX msg) =>
|
||||||
|
TermId -> SchoolId -> CourseShorthand -> msg -> UniWorXMessages
|
||||||
|
prependCourseTitle tid ssh csh msg = UniWorXMessages
|
||||||
|
[ SomeMessage $ toPathPiece tid
|
||||||
|
, SomeMessage dashText
|
||||||
|
, SomeMessage $ toPathPiece ssh
|
||||||
|
, SomeMessage dashText
|
||||||
|
, SomeMessage csh
|
||||||
|
, SomeMessage colonText
|
||||||
|
, SomeMessage msg
|
||||||
|
]
|
||||||
|
where
|
||||||
|
dashText :: Text
|
||||||
|
dashText = "-"
|
||||||
|
|
||||||
|
colonText :: Text
|
||||||
|
colonText = ":"
|
||||||
|
|
||||||
warnTermDays :: TermId -> [Maybe UTCTime] -> DB ()
|
warnTermDays :: TermId -> [Maybe UTCTime] -> DB ()
|
||||||
warnTermDays tid times = do
|
warnTermDays tid times = do
|
||||||
Term{..} <- get404 tid
|
Term{..} <- get404 tid
|
||||||
@ -100,11 +142,30 @@ warnTermDays tid times = do
|
|||||||
forM_ outoftermdays $ warnI MsgDayIsOutOfTerm
|
forM_ outoftermdays $ warnI MsgDayIsOutOfTerm
|
||||||
|
|
||||||
-- | Add language dependent template files
|
-- | Add language dependent template files
|
||||||
-- For large files which are translated as a whole.
|
--
|
||||||
-- Argument musst be a directory under templates,
|
-- For large files which are translated as a whole.
|
||||||
-- which contains a file for each language,
|
--
|
||||||
-- eg. /templates/imprint/de.hamlet and /templates/imprint/en.hamlet
|
-- Argument musst be a directory under @/templates@,
|
||||||
|
-- which contains a file for each language,
|
||||||
|
-- eg. @imprint@ for choosing between
|
||||||
|
-- @/templates/imprint/de.hamlet@, @/templates/imprint/de-at.hamlet@,
|
||||||
|
-- and @/templates/imprint/en.hamlet@
|
||||||
|
--
|
||||||
|
-- Dependency detection cannot work properly (no `addDependentFile`-equivalent
|
||||||
|
-- for directories)
|
||||||
|
-- @$ stack clean@ is required so new translations show up
|
||||||
i18nWidgetFile :: FilePath -> Q Exp
|
i18nWidgetFile :: FilePath -> Q Exp
|
||||||
i18nWidgetFile =
|
i18nWidgetFile basename = do
|
||||||
-- TODO write code to distinguish languages here
|
-- Construct list of available translations (@de@, @en@, ...) at compile time
|
||||||
widgetFile . (</> "de")
|
let i18nDirectory = "templates" </> basename
|
||||||
|
availableFiles <- qRunIO $ listDirectory i18nDirectory
|
||||||
|
let availableTranslations = sortWith (NTop . flip List.elemIndex (NonEmpty.toList appLanguages)) . List.nub $ pack . takeBaseName <$> availableFiles
|
||||||
|
availableTranslations' <- maybe (fail $ "‘" <> i18nDirectory <> "’ is empty") return $ NonEmpty.nonEmpty availableTranslations
|
||||||
|
|
||||||
|
-- Dispatch to correct language (depending on user settings via `selectLanguage`) at run time
|
||||||
|
ws <- newName "ws" -- Name for dispatch function
|
||||||
|
letE
|
||||||
|
[ funD ws $ [ clause [litP $ stringL l] (normalB . widgetFile $ basename </> l) []
|
||||||
|
| l <- unpack <$> NonEmpty.toList availableTranslations' -- One function definition for every available language
|
||||||
|
] ++ [ clause [wildP] (normalB [e| error "selectLanguage returned an invalid translation" |]) [] ] -- Fallback mostly there so compiler does not complain about non-exhaustive pattern match
|
||||||
|
] [e|selectLanguage availableTranslations' >>= $(varE ws)|]
|
||||||
|
|||||||
@ -1,3 +1,13 @@
|
|||||||
|
{-|
|
||||||
|
Module : Handler.Utils.Delete
|
||||||
|
Description : Generic deletion from database after confirmation
|
||||||
|
|
||||||
|
`postDeleteR`, `getDeleteR`, and `deleteR` provide handlers for calling
|
||||||
|
`deleteCascade` on a `Set` of Record-`Key`s after asking for confirmation, which
|
||||||
|
currently entails asking the user to copy a text, which is dependent on the
|
||||||
|
records to be deleted (i.e. a comma-separated list of user names), into a
|
||||||
|
`Textarea`.
|
||||||
|
-}
|
||||||
module Handler.Utils.Delete
|
module Handler.Utils.Delete
|
||||||
( DeleteRoute(..)
|
( DeleteRoute(..)
|
||||||
, deleteR
|
, deleteR
|
||||||
|
|||||||
@ -12,7 +12,7 @@ import Handler.Utils.DateTime
|
|||||||
|
|
||||||
import Import hiding (cons)
|
import Import hiding (cons)
|
||||||
import qualified Data.Char as Char
|
import qualified Data.Char as Char
|
||||||
|
import qualified Data.Text as Text
|
||||||
import qualified Data.CaseInsensitive as CI
|
import qualified Data.CaseInsensitive as CI
|
||||||
|
|
||||||
-- import Yesod.Core
|
-- import Yesod.Core
|
||||||
@ -45,10 +45,13 @@ import Utils.Lens
|
|||||||
import Data.Aeson (eitherDecodeStrict')
|
import Data.Aeson (eitherDecodeStrict')
|
||||||
import Data.Aeson.Text (encodeToLazyText)
|
import Data.Aeson.Text (encodeToLazyText)
|
||||||
|
|
||||||
|
import Data.Proxy
|
||||||
|
|
||||||
----------------------------
|
----------------------------
|
||||||
-- Buttons (new version ) --
|
-- Buttons (new version ) --
|
||||||
----------------------------
|
----------------------------
|
||||||
|
-- NOTE: ButtonSubmit is defined in Utils.Form !
|
||||||
|
|
||||||
|
|
||||||
data ButtonDelete = BtnDelete
|
data ButtonDelete = BtnDelete
|
||||||
deriving (Enum, Eq, Ord, Bounded, Read, Show, Generic, Typeable)
|
deriving (Enum, Eq, Ord, Bounded, Read, Show, Generic, Typeable)
|
||||||
@ -61,6 +64,23 @@ embedRenderMessage ''UniWorX ''ButtonDelete id
|
|||||||
instance Button UniWorX ButtonDelete where
|
instance Button UniWorX ButtonDelete where
|
||||||
btnClasses BtnDelete = [BCIsButton, BCDanger]
|
btnClasses BtnDelete = [BCIsButton, BCDanger]
|
||||||
|
|
||||||
|
data ButtonSave = BtnSave
|
||||||
|
deriving (Enum, Eq, Ord, Bounded, Read, Show, Generic, Typeable)
|
||||||
|
instance Universe ButtonSave
|
||||||
|
instance Finite ButtonSave
|
||||||
|
|
||||||
|
-- | Save-Button as AForm
|
||||||
|
saveButton :: (Button (HandlerSite m) ButtonSave, MonadHandler m) => AForm m ()
|
||||||
|
saveButton = combinedButtonFieldF_ (Proxy @ButtonSave) ""
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
nullaryPathPiece ''ButtonSave $ camelToPathPiece' 1
|
||||||
|
|
||||||
|
embedRenderMessage ''UniWorX ''ButtonSave id
|
||||||
|
instance Button UniWorX ButtonSave where
|
||||||
|
btnClasses BtnSave = [BCIsButton, BCPrimary]
|
||||||
|
|
||||||
data ButtonRegister = BtnRegister | BtnDeregister
|
data ButtonRegister = BtnRegister | BtnDeregister
|
||||||
deriving (Enum, Eq, Ord, Bounded, Read, Show, Generic, Typeable)
|
deriving (Enum, Eq, Ord, Bounded, Read, Show, Generic, Typeable)
|
||||||
instance Universe ButtonRegister
|
instance Universe ButtonRegister
|
||||||
@ -118,22 +138,20 @@ linkButton lbl cls url = do
|
|||||||
|]
|
|]
|
||||||
|
|
||||||
|
|
||||||
-- buttonForm :: (Button UniWorX a, Finite a) => Markup -> MForm (HandlerT UniWorX IO) (FormResult a, Widget)
|
|
||||||
buttonForm :: (Button UniWorX a, Finite a) => Form a
|
|
||||||
buttonForm csrf = do
|
|
||||||
(res, ($ []) -> fViews) <- aFormToForm . disambiguateButtons $ combinedButtonFieldF ""
|
|
||||||
return (res, [whamlet|
|
|
||||||
$newline never
|
|
||||||
#{csrf}
|
|
||||||
$forall bView <- fViews
|
|
||||||
^{fvInput bView}
|
|
||||||
|])
|
|
||||||
|
|
||||||
|
|
||||||
------------
|
------------
|
||||||
-- Fields --
|
-- Fields --
|
||||||
------------
|
------------
|
||||||
|
|
||||||
|
-- | add some additional text immediately after the field widget; probably not a good idea to use
|
||||||
|
annotateField :: ToWidget (HandlerSite m) wgt => wgt -> Field m a -> Field m a
|
||||||
|
annotateField ann field@Field{fieldView=fvf} =
|
||||||
|
let fvf' idt nmt atts ei bl =
|
||||||
|
[whamlet|
|
||||||
|
^{fvf idt nmt atts ei bl}
|
||||||
|
^{ann}
|
||||||
|
|]
|
||||||
|
in field { fieldView=fvf'}
|
||||||
|
|
||||||
-- ciField moved to Utils.Form
|
-- ciField moved to Utils.Form
|
||||||
|
|
||||||
routeField :: ( Monad m
|
routeField :: ( Monad m
|
||||||
@ -141,6 +159,12 @@ routeField :: ( Monad m
|
|||||||
) => Field m (Route UniWorX)
|
) => Field m (Route UniWorX)
|
||||||
routeField = checkMMap (return . maybe (Left MsgInvalidRoute) Right . fromPathPiece) toPathPiece textField
|
routeField = checkMMap (return . maybe (Left MsgInvalidRoute) Right . fromPathPiece) toPathPiece textField
|
||||||
|
|
||||||
|
-- | Variant that simply removes leading and trailing white space
|
||||||
|
htmlField' :: Field (HandlerT UniWorX IO) Html
|
||||||
|
htmlField' = htmlField
|
||||||
|
{ fieldParse = \vs fis -> fieldParse htmlField (map Text.strip vs) fis
|
||||||
|
}
|
||||||
|
|
||||||
natFieldI :: (Monad m, Integral i, RenderMessage (HandlerSite m) msg, RenderMessage (HandlerSite m) FormMessage) => msg -> Field m i
|
natFieldI :: (Monad m, Integral i, RenderMessage (HandlerSite m) msg, RenderMessage (HandlerSite m) FormMessage) => msg -> Field m i
|
||||||
natFieldI msg = checkBool (>= 0) msg intField
|
natFieldI msg = checkBool (>= 0) msg intField
|
||||||
|
|
||||||
|
|||||||
@ -3,10 +3,11 @@
|
|||||||
module Handler.Utils.Form.MassInput
|
module Handler.Utils.Form.MassInput
|
||||||
( MassInput(..)
|
( MassInput(..)
|
||||||
, massInput
|
, massInput
|
||||||
|
, massInputList
|
||||||
, BoxDimension(..)
|
, BoxDimension(..)
|
||||||
, IsBoxCoord(..), boxDimension
|
, IsBoxCoord(..), boxDimension
|
||||||
, Liveliness(..)
|
, Liveliness(..)
|
||||||
, ListLength(..), ListPosition(..)
|
, ListLength(..), ListPosition(..), miDeleteList
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import Import
|
import Import
|
||||||
@ -29,7 +30,6 @@ import Data.List (genericLength, genericIndex, iterate)
|
|||||||
|
|
||||||
import Control.Monad.Trans.Maybe
|
import Control.Monad.Trans.Maybe
|
||||||
import Control.Monad.Reader.Class (MonadReader(local))
|
import Control.Monad.Reader.Class (MonadReader(local))
|
||||||
import Control.Monad.Fix
|
|
||||||
|
|
||||||
|
|
||||||
data BoxDimension x = forall n. (Enum n, Num n) => BoxDimension (Lens' x n)
|
data BoxDimension x = forall n. (Enum n, Num n) => BoxDimension (Lens' x n)
|
||||||
@ -96,6 +96,13 @@ instance Liveliness ListLength where
|
|||||||
max' = Set.lookupMax ns
|
max' = Set.lookupMax ns
|
||||||
liveCoord (ListPosition n) = prism' (\(ListLength l) -> n < l) (bool (Just 0) (1 <$ guard (n == 0)))
|
liveCoord (ListPosition n) = prism' (\(ListLength l) -> n < l) (bool (Just 0) (1 <$ guard (n == 0)))
|
||||||
|
|
||||||
|
|
||||||
|
miDeleteList :: Applicative m => ListLength -> ListPosition -> m (Map ListPosition ListPosition)
|
||||||
|
miDeleteList l pos
|
||||||
|
-- Close gap left by deleted position with values from further down the list; try prohibiting certain deletions by utilising `guard`
|
||||||
|
| l >= 2 = pure . Map.fromSet (\pos' -> bool pos' (succ pos') $ pos' >= pos) $ Set.fromList [0..fromIntegral (l - 2)]
|
||||||
|
| otherwise = pure Map.empty
|
||||||
|
|
||||||
data ButtonMassInput coord
|
data ButtonMassInput coord
|
||||||
= MassInputAddDimension Natural coord
|
= MassInputAddDimension Natural coord
|
||||||
| MassInputDeleteCell coord
|
| MassInputDeleteCell coord
|
||||||
@ -185,7 +192,7 @@ data MassInput handler liveliness cellData cellResult = MassInput
|
|||||||
-> Natural -- Zero-based dimension index @dimIx@
|
-> Natural -- Zero-based dimension index @dimIx@
|
||||||
-> (Text -> Text) -- Nudge deterministic field ids
|
-> (Text -> Text) -- Nudge deterministic field ids
|
||||||
-> FieldView UniWorX -- Submit button
|
-> FieldView UniWorX -- Submit button
|
||||||
-> Maybe (Markup -> MForm handler (FormResult (liveliness -> (BoxCoord liveliness, cellData)), Widget)) -- ^ Construct a Cell-Addition Widget
|
-> Maybe (Markup -> MForm handler (FormResult (Map (BoxCoord liveliness) cellData -> FormResult (Map (BoxCoord liveliness) cellData)), Widget)) -- ^ Construct a Cell-Addition Widget
|
||||||
, miCell :: BoxCoord liveliness -- Position
|
, miCell :: BoxCoord liveliness -- Position
|
||||||
-> cellData -- @cellData@ from @miAdd@
|
-> cellData -- @cellData@ from @miAdd@
|
||||||
-> Maybe cellResult -- Initial result from Argument to @massInput@
|
-> Maybe cellResult -- Initial result from Argument to @massInput@
|
||||||
@ -198,13 +205,14 @@ data MassInput handler liveliness cellData cellResult = MassInput
|
|||||||
-> Natural
|
-> Natural
|
||||||
-> liveliness
|
-> liveliness
|
||||||
-> Bool -- ^ Decide whether an addition-operation should be permitted
|
-> Bool -- ^ Decide whether an addition-operation should be permitted
|
||||||
|
, miButtonAction :: forall p. PathPiece p => p -> Maybe (SomeRoute UniWorX) -- ^ Override form-tag route for `massInput`-Buttons to keep the user closer to the Widget, the `PathPiece` Argument is to be used for constructiong a `Fragment`
|
||||||
}
|
}
|
||||||
|
|
||||||
massInput :: forall handler cellData cellResult liveliness.
|
massInput :: forall handler cellData cellResult liveliness.
|
||||||
( MonadHandler handler, HandlerSite handler ~ UniWorX
|
( MonadHandler handler, HandlerSite handler ~ UniWorX
|
||||||
, ToJSON cellData, FromJSON cellData
|
, ToJSON cellData, FromJSON cellData
|
||||||
, Liveliness liveliness
|
, Liveliness liveliness
|
||||||
, MonadFix handler, MonadLogger handler
|
, MonadLogger handler
|
||||||
)
|
)
|
||||||
=> MassInput handler liveliness cellData cellResult
|
=> MassInput handler liveliness cellData cellResult
|
||||||
-> FieldSettings UniWorX
|
-> FieldSettings UniWorX
|
||||||
@ -215,6 +223,10 @@ massInput MassInput{..} FieldSettings{..} fvRequired initialResult csrf = do
|
|||||||
let initialShape = fmap fst <$> initialResult
|
let initialShape = fmap fst <$> initialResult
|
||||||
|
|
||||||
miName <- maybe newFormIdent return fsName
|
miName <- maybe newFormIdent return fsName
|
||||||
|
fvId <- maybe newIdent return fsId
|
||||||
|
miAction <- traverse toTextUrl $ miButtonAction fvId
|
||||||
|
let addFormAction = maybe id (addAttr "formaction") miAction
|
||||||
|
|
||||||
let
|
let
|
||||||
shapeName :: MassInputFieldName (BoxCoord liveliness)
|
shapeName :: MassInputFieldName (BoxCoord liveliness)
|
||||||
shapeName = MassInputShape{..}
|
shapeName = MassInputShape{..}
|
||||||
@ -230,19 +242,21 @@ massInput MassInput{..} FieldSettings{..} fvRequired initialResult csrf = do
|
|||||||
| otherwise -> throwM MassInputInvalidShape
|
| otherwise -> throwM MassInputInvalidShape
|
||||||
sentLiveliness <- maybe (throwM MassInputInvalidShape) return $ Map.keysSet sentShape' ^? liveCoords :: MForm handler liveliness
|
sentLiveliness <- maybe (throwM MassInputInvalidShape) return $ Map.keysSet sentShape' ^? liveCoords :: MForm handler liveliness
|
||||||
|
|
||||||
let addForm :: [BoxDimension (BoxCoord liveliness)] -> MForm handler (Map (Natural, BoxCoord liveliness) (FormResult (Maybe (liveliness -> (BoxCoord liveliness, cellData))), Maybe Widget))
|
let addForm :: [BoxDimension (BoxCoord liveliness)] -> MForm handler (Map (Natural, BoxCoord liveliness) (FormResult (Maybe (Map (BoxCoord liveliness) cellData -> FormResult (Map (BoxCoord liveliness) cellData))), Maybe Widget))
|
||||||
addForm = addForm' boxOrigin . zip [0..]
|
addForm = addForm' boxOrigin . zip [0..]
|
||||||
where
|
where
|
||||||
addForm' _ [] = return Map.empty
|
addForm' _ [] = return Map.empty
|
||||||
addForm' miCoord ((dimIx, _) : remDims) = do
|
addForm' miCoord ((dimIx, _) : remDims) = do
|
||||||
let nudgeAddWidgetName :: Text -> Text
|
let nudgeAddWidgetName :: Text -> Text
|
||||||
nudgeAddWidgetName miAddWidgetField = toPathPiece MassInputAddWidget{..}
|
nudgeAddWidgetName miAddWidgetField = toPathPiece MassInputAddWidget{..}
|
||||||
(btnRes', btnView) <- mopt (buttonField $ MassInputAddDimension dimIx miCoord) ("" & addName MassInputAddButton{..}) Nothing
|
(btnRes', btnView) <- mopt (buttonField $ MassInputAddDimension dimIx miCoord) ("" & addName MassInputAddButton{..} & addFormAction) Nothing
|
||||||
let btnRes
|
let btnRes = do
|
||||||
| FormSuccess Nothing <- btnRes' = FormMissing
|
Just x <- btnRes'
|
||||||
| FormSuccess (Just x) <- btnRes' = FormSuccess x
|
return x
|
||||||
| otherwise = error "Value of btnRes should only be inspected if FormSuccess" <$ btnRes'
|
wBtnRes res = do
|
||||||
addRes' <- over (mapped . _Just . _1) (btnRes *>) . local (bool id (set _1 Nothing) $ is _FormMissing btnRes) . traverse ($ mempty) $
|
guard $ isn't _FormMissing btnRes
|
||||||
|
res
|
||||||
|
addRes' <- over (mapped . _Just . _1) wBtnRes . local (bool id (set _1 Nothing) $ is _FormMissing btnRes) . traverse ($ mempty) $
|
||||||
miAdd miCoord dimIx nudgeAddWidgetName btnView
|
miAdd miCoord dimIx nudgeAddWidgetName btnView
|
||||||
let dimRes' = Map.singleton (dimIx, miCoord) (maybe (Nothing <$ btnRes) (fmap Just) $ fmap fst addRes', fmap snd addRes')
|
let dimRes' = Map.singleton (dimIx, miCoord) (maybe (Nothing <$ btnRes) (fmap Just) $ fmap fst addRes', fmap snd addRes')
|
||||||
case remDims of
|
case remDims of
|
||||||
@ -254,9 +268,15 @@ massInput MassInput{..} FieldSettings{..} fvRequired initialResult csrf = do
|
|||||||
return $ dimRes' `Map.union` fold dimRess
|
return $ dimRes' `Map.union` fold dimRess
|
||||||
|
|
||||||
addResults <- addForm boxDimensions
|
addResults <- addForm boxDimensions
|
||||||
|
let
|
||||||
|
addResults' :: Map (Natural, BoxCoord liveliness) (FormResult (Maybe (Map (BoxCoord liveliness) cellData)))
|
||||||
|
addResults' = flip Map.mapWithKey (fst <$> addResults) $ \(dimIx, miCoord) -> \case
|
||||||
|
FormSuccess (Just mkResult)
|
||||||
|
| miAllowAdd miCoord dimIx sentLiveliness -> Just <$> mkResult sentShape'
|
||||||
|
other -> Nothing <$ other
|
||||||
let addShape
|
let addShape
|
||||||
| [((dimIx, miCoord), FormSuccess (Just mkResult))] <- Map.toList . Map.filter (is $ _FormSuccess . _Just) $ fmap fst addResults
|
| [((dimIx, miCoord), FormSuccess (Just mkResult))] <- Map.toList . Map.filter (is $ _FormSuccess . _Just) $ fmap fst addResults
|
||||||
= Just $ maybe id (uncurry Map.insert) (mkResult sentLiveliness <$ guard (miAllowAdd miCoord dimIx sentLiveliness)) sentShape'
|
= Just $ maybe id Map.union (formResultToMaybe $ mkResult sentShape' <* guard (miAllowAdd miCoord dimIx sentLiveliness)) sentShape'
|
||||||
| otherwise = Nothing
|
| otherwise = Nothing
|
||||||
|
|
||||||
addedShape <- if
|
addedShape <- if
|
||||||
@ -267,11 +287,10 @@ massInput MassInput{..} FieldSettings{..} fvRequired initialResult csrf = do
|
|||||||
let
|
let
|
||||||
delForm :: BoxCoord liveliness -> MaybeT (MForm handler) (FormResult (Map (BoxCoord liveliness) (BoxCoord liveliness)), FieldView UniWorX)
|
delForm :: BoxCoord liveliness -> MaybeT (MForm handler) (FormResult (Map (BoxCoord liveliness) (BoxCoord liveliness)), FieldView UniWorX)
|
||||||
delForm miCoord = do
|
delForm miCoord = do
|
||||||
(delRes, delView) <- lift $ mpreq (buttonField $ MassInputDeleteCell miCoord) ("" & addName MassInputDeleteButton{..}) Nothing
|
(delRes, delView) <- lift $ mopt (buttonField $ MassInputDeleteCell miCoord) ("" & addName MassInputDeleteButton{..} & addFormAction) Nothing
|
||||||
-- dollar comment causes build error somehow $ logDebugS "delForm" . tshow $ fmap toPathPiece delRes
|
|
||||||
shapeUpdate <- miDelete addedLiveliness miCoord
|
shapeUpdate <- miDelete addedLiveliness miCoord
|
||||||
guard $ isJust (Map.keysSet shapeUpdate ^? liveCoords :: Maybe liveliness)
|
guard $ isJust (Map.keysSet shapeUpdate ^? liveCoords :: Maybe liveliness)
|
||||||
return (shapeUpdate <$ delRes, delView)
|
return (shapeUpdate <$ assertM (is _Just) delRes, delView)
|
||||||
|
|
||||||
delResults <- fmap (Map.mapMaybe id) . sequence $ Map.fromSet (runMaybeT . delForm) (Map.keysSet addedShape)
|
delResults <- fmap (Map.mapMaybe id) . sequence $ Map.fromSet (runMaybeT . delForm) (Map.keysSet addedShape)
|
||||||
let
|
let
|
||||||
@ -306,12 +325,8 @@ massInput MassInput{..} FieldSettings{..} fvRequired initialResult csrf = do
|
|||||||
| otherwise = prevEnv
|
| otherwise = prevEnv
|
||||||
|
|
||||||
justAdded :: Set (BoxCoord liveliness)
|
justAdded :: Set (BoxCoord liveliness)
|
||||||
justAdded = Set.fromList . mapMaybe (addedCoord . fst) $ Map.elems addResults
|
justAdded = Map.keysSet shape Set.\\ Map.keysSet sentShape'
|
||||||
where
|
|
||||||
addedCoord res
|
|
||||||
| FormSuccess (Just mkResult) <- res
|
|
||||||
= Just . fst $ mkResult sentLiveliness
|
|
||||||
| otherwise = Nothing
|
|
||||||
restrictJustAdded :: BoxCoord liveliness -> Maybe a -> Maybe a
|
restrictJustAdded :: BoxCoord liveliness -> Maybe a -> Maybe a
|
||||||
restrictJustAdded miCoord env = env <* guard (not $ Set.member miCoord justAdded)
|
restrictJustAdded miCoord env = env <* guard (not $ Set.member miCoord justAdded)
|
||||||
|
|
||||||
@ -321,9 +336,11 @@ massInput MassInput{..} FieldSettings{..} fvRequired initialResult csrf = do
|
|||||||
nudgeCellName miCellField = toPathPiece (MassInputCell{..} :: MassInputFieldName (BoxCoord liveliness))
|
nudgeCellName miCellField = toPathPiece (MassInputCell{..} :: MassInputFieldName (BoxCoord liveliness))
|
||||||
local (over _1 (applyDelShapeUpdate . restrictJustAdded miCoord)) $ (cData, ) <$> miCell miCoord cData (fmap snd . Map.lookup miCoord =<< initialResult) nudgeCellName mempty
|
local (over _1 (applyDelShapeUpdate . restrictJustAdded miCoord)) $ (cData, ) <$> miCell miCoord cData (fmap snd . Map.lookup miCoord =<< initialResult) nudgeCellName mempty
|
||||||
let result :: FormResult (Map (BoxCoord liveliness) (cellData, cellResult))
|
let result :: FormResult (Map (BoxCoord liveliness) (cellData, cellResult))
|
||||||
result
|
result = do
|
||||||
| shapeChanged = FormMissing
|
FormSuccess () <|> void (asum $ Map.elems addResults')
|
||||||
| otherwise = traverse (\(cData, (cResult, _)) -> (cData, ) <$> cResult) cellResults
|
FormSuccess () <|> void (asum . Map.elems $ fst <$> delResults)
|
||||||
|
guard $ not shapeChanged
|
||||||
|
for cellResults $ \(cData, (cResult, _)) -> (cData, ) <$> cResult
|
||||||
|
|
||||||
let miWidget' :: BoxCoord liveliness -> [(Natural, BoxDimension (BoxCoord liveliness))] -> Widget
|
let miWidget' :: BoxCoord liveliness -> [(Natural, BoxDimension (BoxCoord liveliness))] -> Widget
|
||||||
miWidget' _ [] = mempty
|
miWidget' _ [] = mempty
|
||||||
@ -343,7 +360,6 @@ massInput MassInput{..} FieldSettings{..} fvRequired initialResult csrf = do
|
|||||||
miWidget = miWidget' boxOrigin $ zip [0..] boxDimensions
|
miWidget = miWidget' boxOrigin $ zip [0..] boxDimensions
|
||||||
|
|
||||||
MsgRenderer mr <- getMsgRenderer
|
MsgRenderer mr <- getMsgRenderer
|
||||||
fvId <- maybe newIdent return fsId
|
|
||||||
|
|
||||||
let
|
let
|
||||||
fvLabel = toHtml $ mr fsLabel
|
fvLabel = toHtml $ mr fsLabel
|
||||||
@ -351,3 +367,29 @@ massInput MassInput{..} FieldSettings{..} fvRequired initialResult csrf = do
|
|||||||
fvInput = $(widgetFile "widgets/massinput/massinput")
|
fvInput = $(widgetFile "widgets/massinput/massinput")
|
||||||
fvErrors = Nothing
|
fvErrors = Nothing
|
||||||
in return (result, FieldView{..})
|
in return (result, FieldView{..})
|
||||||
|
|
||||||
|
|
||||||
|
-- | Wrapper around `massInput` for the common case, that we just want an arbitrary list of single fields without any constraints
|
||||||
|
massInputList :: forall handler cellResult.
|
||||||
|
( MonadHandler handler, HandlerSite handler ~ UniWorX
|
||||||
|
, MonadLogger handler
|
||||||
|
)
|
||||||
|
=> Field handler cellResult
|
||||||
|
-> (ListPosition -> FieldSettings UniWorX)
|
||||||
|
-> (forall p. PathPiece p => p -> Maybe (SomeRoute UniWorX))
|
||||||
|
-> FieldSettings UniWorX
|
||||||
|
-> Bool
|
||||||
|
-> Maybe [cellResult]
|
||||||
|
-> (Markup -> MForm handler (FormResult [cellResult], FieldView UniWorX))
|
||||||
|
massInputList field fieldSettings miButtonAction miSettings miRequired miPrevResult = over (mapped . _1 . mapped) (map snd . Map.elems) . massInput
|
||||||
|
MassInput { miAdd = \_ _ _ submitBtn -> Just $ \csrf ->
|
||||||
|
return (FormSuccess $ \pRes -> FormSuccess $ Map.singleton (maybe 0 succ . fmap fst $ Map.lookupMax pRes) (), toWidget csrf >> fvInput submitBtn)
|
||||||
|
, miCell = \pos () iRes nudge csrf ->
|
||||||
|
over _2 (\FieldView{..} -> $(widgetFile "widgets/massinput/list/cell")) <$> mreq field (fieldSettings pos & addName (nudge "field")) iRes
|
||||||
|
, miDelete = miDeleteList
|
||||||
|
, miAllowAdd = \_ _ _ -> True
|
||||||
|
, miButtonAction
|
||||||
|
}
|
||||||
|
miSettings
|
||||||
|
miRequired
|
||||||
|
(Map.fromList . zip [0..] . map ((), ) <$> miPrevResult)
|
||||||
|
|||||||
@ -462,10 +462,10 @@ sinkSubmission userId mExists isUpdate = do
|
|||||||
case isUpdate of
|
case isUpdate of
|
||||||
False -> lift . insert_ $ SubmissionEdit userId now submissionId
|
False -> lift . insert_ $ SubmissionEdit userId now submissionId
|
||||||
True -> do
|
True -> do
|
||||||
Submission{submissionRatingTime} <- lift $ getJust submissionId
|
Submission{submissionRatingTime, submissionRatingBy} <- lift $ getJust submissionId
|
||||||
when (isNothing submissionRatingTime) $ tellSt mempty { sinkSubmissionNotifyRating = Any True }
|
when (submissionRatingBy == Just userId) $ do
|
||||||
lift $ update submissionId [ SubmissionRatingBy =. Just userId, SubmissionRatingTime =. Just now ]
|
when (isNothing submissionRatingTime) $ tellSt mempty { sinkSubmissionNotifyRating = Any True }
|
||||||
-- TODO: Should submissionRatingAssigned change here if userId changes?
|
lift $ update submissionId [ SubmissionRatingTime =. Just now ]
|
||||||
tellSt $ mempty{ sinkSubmissionTouched = Any True }
|
tellSt $ mempty{ sinkSubmissionTouched = Any True }
|
||||||
|
|
||||||
finalize :: SubmissionSinkState -> YesodJobDB UniWorX ()
|
finalize :: SubmissionSinkState -> YesodJobDB UniWorX ()
|
||||||
|
|||||||
@ -57,6 +57,10 @@ sqlCell act = mempty & cellContents .~ lift act
|
|||||||
tickmarkCell :: (IsDBTable m a) => Bool -> DBCell m a
|
tickmarkCell :: (IsDBTable m a) => Bool -> DBCell m a
|
||||||
tickmarkCell = cell . toWidget . hasTickmark
|
tickmarkCell = cell . toWidget . hasTickmark
|
||||||
|
|
||||||
|
-- | Maybe display an icon for tainted rows
|
||||||
|
isBadCell :: (IsDBTable m a) => Bool -> DBCell m a
|
||||||
|
isBadCell = cell . toWidget . isBad
|
||||||
|
|
||||||
-- | Maybe display a exclamation icon
|
-- | Maybe display a exclamation icon
|
||||||
isNewCell :: (IsDBTable m a) => Bool -> DBCell m a
|
isNewCell :: (IsDBTable m a) => Bool -> DBCell m a
|
||||||
isNewCell = cell . toWidget . isNew
|
isNewCell = cell . toWidget . isNew
|
||||||
|
|||||||
@ -66,8 +66,8 @@ sortUserDisplayName queryUser = ("user-display-name", SortColumn $ queryUser >>>
|
|||||||
|
|
||||||
defaultSortingByName :: PSValidator m x -> PSValidator m x
|
defaultSortingByName :: PSValidator m x -> PSValidator m x
|
||||||
defaultSortingByName =
|
defaultSortingByName =
|
||||||
defaultSorting [SortAscBy "user-surname", SortAscBy "user-display-name"] -- old way, requiring two exta sorters
|
-- defaultSorting [SortAscBy "user-surname", SortAscBy "user-display-name"] -- old way, requiring two exta sorters
|
||||||
-- defaultSorting [SortAscBy "user-name"] -- new way, untested, working with single sorter
|
defaultSorting [SortAscBy "user-name"] -- new way, untested, working with single sorter
|
||||||
|
|
||||||
-- | Alias for sortUserName for consistency
|
-- | Alias for sortUserName for consistency
|
||||||
fltrUserNameLink :: (IsFilterColumn t (a -> Set Text -> E.SqlExpr (E.Value Bool)), IsString d) => (a -> E.SqlExpr (Entity User)) -> (d, FilterColumn t)
|
fltrUserNameLink :: (IsFilterColumn t (a -> Set Text -> E.SqlExpr (E.Value Bool)), IsString d) => (a -> E.SqlExpr (Entity User)) -> (d, FilterColumn t)
|
||||||
|
|||||||
@ -595,7 +595,7 @@ instance Monoid x => Monoid (DBCell (RWST (Maybe (Env, FileEnv), UniWorX, [Lang]
|
|||||||
instance IsDBTable m a => IsString (DBCell m a) where
|
instance IsDBTable m a => IsString (DBCell m a) where
|
||||||
fromString = cell . fromString
|
fromString = cell . fromString
|
||||||
|
|
||||||
|
-- | DB-backed tables with pagination, may short-circuit a handler
|
||||||
dbTable :: forall m x. IsDBTable m x => PSValidator m x -> DBTable m x -> DB (DBResult m x)
|
dbTable :: forall m x. IsDBTable m x => PSValidator m x -> DBTable m x -> DB (DBResult m x)
|
||||||
dbTable PSValidator{..} dbtable@DBTable{ dbtIdent = dbtIdent'@(toPathPiece -> dbtIdent), dbtStyle = DBStyle{..}, .. } = do
|
dbTable PSValidator{..} dbtable@DBTable{ dbtIdent = dbtIdent'@(toPathPiece -> dbtIdent), dbtStyle = DBStyle{..}, .. } = do
|
||||||
let
|
let
|
||||||
@ -639,7 +639,6 @@ dbTable PSValidator{..} dbtable@DBTable{ dbtIdent = dbtIdent'@(toPathPiece -> db
|
|||||||
|
|
||||||
(pagesizeRes'@((pagesizeRes, _), _)) <- lift . runFormGet . identifyForm FIDDBTablePagesize . addPIHiddenField dbtable (prevPi & _piPage .~ Nothing & _piLimit .~ Nothing & _piFilter .~ (formResult' filterRes <|> piFilter prevPi)) . renderAForm FormDBTablePagesize $
|
(pagesizeRes'@((pagesizeRes, _), _)) <- lift . runFormGet . identifyForm FIDDBTablePagesize . addPIHiddenField dbtable (prevPi & _piPage .~ Nothing & _piLimit .~ Nothing & _piFilter .~ (formResult' filterRes <|> piFilter prevPi)) . renderAForm FormDBTablePagesize $
|
||||||
areq (pagesizeField referencePagesize) (fslI MsgDBTablePagesize & addAutosubmit & addName (wIdent "pagesize") & addClass "select--pagesize") (Just referencePagesize)
|
areq (pagesizeField referencePagesize) (fslI MsgDBTablePagesize & addAutosubmit & addName (wIdent "pagesize") & addClass "select--pagesize") (Just referencePagesize)
|
||||||
<* autosubmitButton
|
|
||||||
return (filterRes', pagesizeRes')
|
return (filterRes', pagesizeRes')
|
||||||
|
|
||||||
let
|
let
|
||||||
@ -754,7 +753,7 @@ dbTable PSValidator{..} dbtable@DBTable{ dbtIdent = dbtIdent'@(toPathPiece -> db
|
|||||||
, formAction = Just . SomeRoute $ rawAction :#: wIdent "table-wrapper"
|
, formAction = Just . SomeRoute $ rawAction :#: wIdent "table-wrapper"
|
||||||
, formEncoding = pagesizeEnc
|
, formEncoding = pagesizeEnc
|
||||||
, formAttrs = [("class", "pagesize")]
|
, formAttrs = [("class", "pagesize")]
|
||||||
, formSubmit = FormNoSubmit
|
, formSubmit = FormAutoSubmit
|
||||||
, formAnchor = Just $ wIdent "pagesize-form"
|
, formAnchor = Just $ wIdent "pagesize-form"
|
||||||
}
|
}
|
||||||
uiLayout table = dbsFilterLayout filterWdgt filterEnc (SomeRoute $ rawAction :#: wIdent "table-wrapper") $(widgetFile "table/layout")
|
uiLayout table = dbsFilterLayout filterWdgt filterEnc (SomeRoute $ rawAction :#: wIdent "table-wrapper") $(widgetFile "table/layout")
|
||||||
|
|||||||
@ -1,26 +0,0 @@
|
|||||||
module Handler.Utils.Templates where
|
|
||||||
|
|
||||||
import Data.Either (isLeft)
|
|
||||||
|
|
||||||
import Import.NoFoundation
|
|
||||||
|
|
||||||
modal :: WidgetT site IO () -> Either (SomeRoute site) (WidgetT site IO ()) -> WidgetT site IO ()
|
|
||||||
modal modalTrigger modalContent = do
|
|
||||||
let modalDynamic = isLeft modalContent
|
|
||||||
modalId <- newIdent
|
|
||||||
triggerId <- newIdent
|
|
||||||
$(widgetFile "widgets/modal/modal")
|
|
||||||
case modalContent of
|
|
||||||
Left route -> do
|
|
||||||
route' <- toTextUrl route
|
|
||||||
[whamlet|
|
|
||||||
$newline never
|
|
||||||
<a .modal__trigger href=#{route'} ##{triggerId}>
|
|
||||||
<span .modal__trigger-label>^{modalTrigger}
|
|
||||||
|]
|
|
||||||
Right _ ->
|
|
||||||
[whamlet|
|
|
||||||
$newline never
|
|
||||||
<div .modal__trigger ##{triggerId}>
|
|
||||||
<span .modal__trigger-label>^{modalTrigger}
|
|
||||||
|]
|
|
||||||
@ -180,5 +180,8 @@ conflicts = E.select $ E.from $ \studyTerms -> do
|
|||||||
E.where_ $ studyTerms E.^. StudyTermsName E.==. E.just (candidateTwo E.^. StudyTermCandidateName)
|
E.where_ $ studyTerms E.^. StudyTermsName E.==. E.just (candidateTwo E.^. StudyTermCandidateName)
|
||||||
return studyTerms
|
return studyTerms
|
||||||
|
|
||||||
|
-- | retrieve all incidence keys having containing a certain @StudyTermKey @
|
||||||
|
getIncidencesFor :: [Key StudyTerms] -> DB [E.Value TermCandidateIncidence]
|
||||||
|
getIncidencesFor stks = E.select $ E.distinct $ E.from $ \candidate -> do
|
||||||
|
E.where_ $ candidate E.^. StudyTermCandidateKey `E.in_` E.valList (unStudyTermsKey <$> stks)
|
||||||
|
return $ candidate E.^. StudyTermCandidateIncidence
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import Yesod.Auth as Import
|
|||||||
import Yesod.Core.Types as Import (loggerSet)
|
import Yesod.Core.Types as Import (loggerSet)
|
||||||
import Yesod.Default.Config2 as Import
|
import Yesod.Default.Config2 as Import
|
||||||
import Utils as Import
|
import Utils as Import
|
||||||
|
import Utils.Modal as Import
|
||||||
import Yesod.Core.Json as Import (provideJson)
|
import Yesod.Core.Json as Import (provideJson)
|
||||||
import Yesod.Core.Types.Instances as Import ()
|
import Yesod.Core.Types.Instances as Import ()
|
||||||
|
|
||||||
@ -41,7 +42,8 @@ import GHC.Exts as Import (IsList)
|
|||||||
|
|
||||||
import Data.Hashable as Import
|
import Data.Hashable as Import
|
||||||
import Data.List.NonEmpty as Import (NonEmpty(..))
|
import Data.List.NonEmpty as Import (NonEmpty(..))
|
||||||
import Data.Text.Encoding.Error as Import(UnicodeException(..))
|
import Data.List.NonEmpty.Instances as Import ()
|
||||||
|
import Data.Text.Encoding.Error as Import(UnicodeException(..))
|
||||||
import Data.Semigroup as Import (Semigroup)
|
import Data.Semigroup as Import (Semigroup)
|
||||||
import Data.Monoid as Import (Last(..), First(..))
|
import Data.Monoid as Import (Last(..), First(..))
|
||||||
import Data.Monoid.Instances as Import ()
|
import Data.Monoid.Instances as Import ()
|
||||||
|
|||||||
@ -16,10 +16,11 @@ import Data.Bitraversable
|
|||||||
|
|
||||||
dispatchJobHelpRequest :: Either (Maybe Address) UserId
|
dispatchJobHelpRequest :: Either (Maybe Address) UserId
|
||||||
-> UTCTime
|
-> UTCTime
|
||||||
|
-> Maybe Text -- ^ Help Subject
|
||||||
-> Text -- ^ Help Request
|
-> Text -- ^ Help Request
|
||||||
-> Maybe Text -- ^ Referer
|
-> Maybe Text -- ^ Referer
|
||||||
-> Handler ()
|
-> Handler ()
|
||||||
dispatchJobHelpRequest jSender jRequestTime jHelpRequest jReferer = do
|
dispatchJobHelpRequest jSender jRequestTime jHelpSubject jHelpRequest jReferer = do
|
||||||
supportAddress <- getsYesod $ appMailSupport . appSettings
|
supportAddress <- getsYesod $ appMailSupport . appSettings
|
||||||
userInfo <- bitraverse return (runDB . getEntity) jSender
|
userInfo <- bitraverse return (runDB . getEntity) jSender
|
||||||
let userAddress = either
|
let userAddress = either
|
||||||
@ -28,8 +29,9 @@ dispatchJobHelpRequest jSender jRequestTime jHelpRequest jReferer = do
|
|||||||
userInfo
|
userInfo
|
||||||
mailT def $ do
|
mailT def $ do
|
||||||
_mailTo .= [supportAddress]
|
_mailTo .= [supportAddress]
|
||||||
whenIsJust userAddress $ addMailHeader "Reply-To" . renderAddress
|
whenIsJust userAddress (_mailFrom .=)
|
||||||
setSubjectI MsgMailSubjectSupport
|
replaceMailHeader "Auto-Submitted" $ Just "no"
|
||||||
|
setSubjectI $ maybe MsgMailSubjectSupport MsgMailSubjectSupportCustom jHelpSubject
|
||||||
setDate jRequestTime
|
setDate jRequestTime
|
||||||
rtime <- formatTimeMail SelFormatDateTime jRequestTime
|
rtime <- formatTimeMail SelFormatDateTime jRequestTime
|
||||||
addPart ($(ihamletFile "templates/mail/support.hamlet") :: HtmlUrlI18n UniWorXMessage (Route UniWorX))
|
addPart ($(ihamletFile "templates/mail/support.hamlet") :: HtmlUrlI18n UniWorXMessage (Route UniWorX))
|
||||||
|
|||||||
@ -22,6 +22,7 @@ dispatchNotificationCorrectionsAssigned nUser nSheet jRecipient = do
|
|||||||
]
|
]
|
||||||
return (course, sheet, nbrSubs)
|
return (course, sheet, nbrSubs)
|
||||||
when (nbrSubs > 0) . userMailT jRecipient $ do
|
when (nbrSubs > 0) . userMailT jRecipient $ do
|
||||||
|
replaceMailHeader "Auto-Submitted" $ Just "auto-generated"
|
||||||
setSubjectI $ MsgMailSubjectCorrectionsAssigned courseShorthand sheetName
|
setSubjectI $ MsgMailSubjectCorrectionsAssigned courseShorthand sheetName
|
||||||
|
|
||||||
MsgRenderer mr <- getMailMsgRenderer
|
MsgRenderer mr <- getMailMsgRenderer
|
||||||
|
|||||||
@ -19,6 +19,7 @@ dispatchNotificationCorrectionsNotDistributed nSheet jRecipient = do
|
|||||||
]
|
]
|
||||||
return (course, sheet, nbrSubs)
|
return (course, sheet, nbrSubs)
|
||||||
when (nbrSubs > 0) . userMailT jRecipient $ do
|
when (nbrSubs > 0) . userMailT jRecipient $ do
|
||||||
|
replaceMailHeader "Auto-Submitted" $ Just "auto-generated"
|
||||||
setSubjectI $ MsgMailSubjectSubmissionsUnassigned courseShorthand sheetName
|
setSubjectI $ MsgMailSubjectSubmissionsUnassigned courseShorthand sheetName
|
||||||
MsgRenderer mr <- getMailMsgRenderer
|
MsgRenderer mr <- getMailMsgRenderer
|
||||||
let termDesc = mr . ShortTermIdentifier $ unTermKey courseTerm
|
let termDesc = mr . ShortTermIdentifier $ unTermKey courseTerm
|
||||||
|
|||||||
@ -17,6 +17,7 @@ dispatchNotificationSheetActive nSheet jRecipient = userMailT jRecipient $ do
|
|||||||
sheet <- getJust nSheet
|
sheet <- getJust nSheet
|
||||||
course <- belongsToJust sheetCourse sheet
|
course <- belongsToJust sheetCourse sheet
|
||||||
return (course, sheet)
|
return (course, sheet)
|
||||||
|
replaceMailHeader "Auto-Submitted" $ Just "auto-generated"
|
||||||
setSubjectI $ MsgMailSubjectSheetActive courseShorthand sheetName
|
setSubjectI $ MsgMailSubjectSheetActive courseShorthand sheetName
|
||||||
|
|
||||||
MsgRenderer mr <- getMailMsgRenderer
|
MsgRenderer mr <- getMailMsgRenderer
|
||||||
|
|||||||
@ -20,6 +20,7 @@ dispatchNotificationSheetSoonInactive nSheet jRecipient = userMailT jRecipient $
|
|||||||
sheet <- getJust nSheet
|
sheet <- getJust nSheet
|
||||||
course <- belongsToJust sheetCourse sheet
|
course <- belongsToJust sheetCourse sheet
|
||||||
return (course, sheet)
|
return (course, sheet)
|
||||||
|
replaceMailHeader "Auto-Submitted" $ Just "auto-generated"
|
||||||
setSubjectI $ MsgMailSubjectSheetSoonInactive courseShorthand sheetName
|
setSubjectI $ MsgMailSubjectSheetSoonInactive courseShorthand sheetName
|
||||||
|
|
||||||
MsgRenderer mr <- getMailMsgRenderer
|
MsgRenderer mr <- getMailMsgRenderer
|
||||||
@ -45,6 +46,7 @@ dispatchNotificationSheetInactive nSheet jRecipient = userMailT jRecipient $ do
|
|||||||
-- E.distinctOn [E.don (subUser E.^. SubmissionUserUser)] -- Not necessary due to UniqueSubmisionUser
|
-- E.distinctOn [E.don (subUser E.^. SubmissionUserUser)] -- Not necessary due to UniqueSubmisionUser
|
||||||
return (E.countRows :: E.SqlExpr (E.Value Int64))
|
return (E.countRows :: E.SqlExpr (E.Value Int64))
|
||||||
return (course, sheet, nrSubs, nrSubmitters)
|
return (course, sheet, nrSubs, nrSubmitters)
|
||||||
|
replaceMailHeader "Auto-Submitted" $ Just "auto-generated"
|
||||||
setSubjectI $ MsgMailSubjectSheetInactive courseShorthand sheetName
|
setSubjectI $ MsgMailSubjectSheetInactive courseShorthand sheetName
|
||||||
|
|
||||||
MsgRenderer mr <- getMailMsgRenderer
|
MsgRenderer mr <- getMailMsgRenderer
|
||||||
|
|||||||
@ -22,6 +22,7 @@ dispatchNotificationSubmissionRated nSubmission jRecipient = userMailT jRecipien
|
|||||||
course <- belongsToJust sheetCourse sheet
|
course <- belongsToJust sheetCourse sheet
|
||||||
corrector <- traverse getJust submissionRatingBy
|
corrector <- traverse getJust submissionRatingBy
|
||||||
return (course, sheet, submission, corrector)
|
return (course, sheet, submission, corrector)
|
||||||
|
replaceMailHeader "Auto-Submitted" $ Just "auto-generated"
|
||||||
setSubjectI $ MsgMailSubjectSubmissionRated courseShorthand
|
setSubjectI $ MsgMailSubjectSubmissionRated courseShorthand
|
||||||
|
|
||||||
csid <- encrypt nSubmission
|
csid <- encrypt nSubmission
|
||||||
|
|||||||
@ -19,6 +19,7 @@ dispatchNotificationUserRightsUpdate nUser _originalRights jRecipient = userMai
|
|||||||
adminSchools <- getSchoolsOf nUser UserAdminSchool UserAdminUser
|
adminSchools <- getSchoolsOf nUser UserAdminSchool UserAdminUser
|
||||||
lecturerSchools <- getSchoolsOf nUser UserLecturerSchool UserLecturerUser
|
lecturerSchools <- getSchoolsOf nUser UserLecturerSchool UserLecturerUser
|
||||||
return (user,adminSchools,lecturerSchools)
|
return (user,adminSchools,lecturerSchools)
|
||||||
|
replaceMailHeader "Auto-Submitted" $ Just "auto-generated"
|
||||||
setSubjectI $ MsgMailSubjectUserRightsUpdate userDisplayName
|
setSubjectI $ MsgMailSubjectUserRightsUpdate userDisplayName
|
||||||
-- MsgRenderer mr <- getMailMsgRenderer
|
-- MsgRenderer mr <- getMailMsgRenderer
|
||||||
addAlternatives $ do
|
addAlternatives $ do
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import Utils.Lens
|
|||||||
dispatchJobSendTestEmail :: Email -> MailContext -> Handler ()
|
dispatchJobSendTestEmail :: Email -> MailContext -> Handler ()
|
||||||
dispatchJobSendTestEmail jEmail jMailContext = mailT jMailContext $ do
|
dispatchJobSendTestEmail jEmail jMailContext = mailT jMailContext $ do
|
||||||
_mailTo .= [Address Nothing jEmail]
|
_mailTo .= [Address Nothing jEmail]
|
||||||
|
replaceMailHeader "Auto-Submitted" $ Just "auto-generated"
|
||||||
setSubjectI MsgMailTestSubject
|
setSubjectI MsgMailTestSubject
|
||||||
now <- liftIO getCurrentTime
|
now <- liftIO getCurrentTime
|
||||||
nDT <- formatTimeMail SelFormatDateTime now
|
nDT <- formatTimeMail SelFormatDateTime now
|
||||||
|
|||||||
@ -17,7 +17,10 @@ data Job = JobSendNotification { jRecipient :: UserId, jNotification :: Notifica
|
|||||||
| JobQueueNotification { jNotification :: Notification }
|
| JobQueueNotification { jNotification :: Notification }
|
||||||
| JobHelpRequest { jSender :: Either (Maybe Address) UserId
|
| JobHelpRequest { jSender :: Either (Maybe Address) UserId
|
||||||
, jRequestTime :: UTCTime
|
, jRequestTime :: UTCTime
|
||||||
, jHelpRequest :: Text, jReferer :: Maybe Text }
|
, jHelpSubject :: Maybe Text
|
||||||
|
, jHelpRequest :: Text
|
||||||
|
, jReferer :: Maybe Text
|
||||||
|
}
|
||||||
| JobSetLogSettings { jInstance :: InstanceId, jLogSettings :: LogSettings }
|
| JobSetLogSettings { jInstance :: InstanceId, jLogSettings :: LogSettings }
|
||||||
| JobDistributeCorrections { jSheet :: SheetId }
|
| JobDistributeCorrections { jSheet :: SheetId }
|
||||||
deriving (Eq, Ord, Show, Read, Generic, Typeable)
|
deriving (Eq, Ord, Show, Read, Generic, Typeable)
|
||||||
|
|||||||
16
src/Mail.hs
16
src/Mail.hs
@ -27,7 +27,7 @@ module Mail
|
|||||||
, setSubjectI, setMailObjectId, setMailObjectId'
|
, setSubjectI, setMailObjectId, setMailObjectId'
|
||||||
, setDate, setDateCurrent
|
, setDate, setDateCurrent
|
||||||
, setMailSmtpData
|
, setMailSmtpData
|
||||||
, _mailFrom, _mailTo, _mailCc, _mailBcc, _mailHeaders, _mailParts
|
, _mailFrom, _mailTo, _mailCc, _mailBcc, _mailHeaders, _mailHeader, _mailParts
|
||||||
, _partType, _partEncoding, _partFilename, _partHeaders, _partContent
|
, _partType, _partEncoding, _partFilename, _partHeaders, _partContent
|
||||||
) where
|
) where
|
||||||
|
|
||||||
@ -99,9 +99,18 @@ import Data.Universe.Instances.Reverse.Hashable ()
|
|||||||
|
|
||||||
import GHC.Exts (IsList)
|
import GHC.Exts (IsList)
|
||||||
|
|
||||||
|
import Control.Monad.Trans.Maybe (MaybeT(..))
|
||||||
|
|
||||||
|
import Data.CaseInsensitive (CI)
|
||||||
|
import qualified Data.CaseInsensitive as CI
|
||||||
|
|
||||||
|
|
||||||
makeLenses_ ''Mail
|
makeLenses_ ''Mail
|
||||||
makeLenses_ ''Part
|
makeLenses_ ''Part
|
||||||
|
|
||||||
|
_mailHeader :: CI ByteString -> Traversal' Mail Text
|
||||||
|
_mailHeader hdrName = _mailHeaders . traverse . filtered (views _1 $ (== hdrName) . CI.mk) . _2
|
||||||
|
|
||||||
|
|
||||||
newtype MailT m a = MailT { _unMailT :: RWST MailContext MailSmtpData Mail m a }
|
newtype MailT m a = MailT { _unMailT :: RWST MailContext MailSmtpData Mail m a }
|
||||||
deriving newtype ( MonadTrans, Monad, Functor, MonadFail, Applicative, Alternative, MonadPlus
|
deriving newtype ( MonadTrans, Monad, Functor, MonadFail, Applicative, Alternative, MonadPlus
|
||||||
@ -443,7 +452,10 @@ setDate time = do
|
|||||||
|
|
||||||
setMailSmtpData :: (MonadHandler m, YesodMail (HandlerSite m)) => MailT m ()
|
setMailSmtpData :: (MonadHandler m, YesodMail (HandlerSite m)) => MailT m ()
|
||||||
setMailSmtpData = do
|
setMailSmtpData = do
|
||||||
Address _ from <- use _mailFrom
|
Just (Address _ from) <- runMaybeT $ asum
|
||||||
|
[ MaybeT . preuses (_mailHeader "Sender") $ fromString . unpack
|
||||||
|
, use _mailFrom
|
||||||
|
]
|
||||||
recps <- Set.fromList . map addressEmail . concat <$> forM [_mailTo, _mailCc, _mailBcc] use
|
recps <- Set.fromList . map addressEmail . concat <$> forM [_mailTo, _mailCc, _mailBcc] use
|
||||||
|
|
||||||
tell $ mempty { smtpRecipients = recps }
|
tell $ mempty { smtpRecipients = recps }
|
||||||
|
|||||||
@ -772,6 +772,20 @@ instance FromJSON AuthTagActive where
|
|||||||
derivePersistFieldJSON ''AuthTagActive
|
derivePersistFieldJSON ''AuthTagActive
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
data LecturerType = CourseLecturer | CourseAssistant
|
||||||
|
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable)
|
||||||
|
|
||||||
|
instance Universe LecturerType
|
||||||
|
instance Finite LecturerType
|
||||||
|
|
||||||
|
nullaryPathPiece ''LecturerType $ camelToPathPiece' 1
|
||||||
|
deriveJSON defaultOptions
|
||||||
|
{ constructorTagModifier = camelToPathPiece' 1
|
||||||
|
} ''LecturerType
|
||||||
|
derivePersistFieldJSON ''LecturerType
|
||||||
|
|
||||||
|
|
||||||
-- Type synonyms
|
-- Type synonyms
|
||||||
|
|
||||||
type Email = Text
|
type Email = Text
|
||||||
|
|||||||
33
src/Utils.hs
33
src/Utils.hs
@ -85,11 +85,6 @@ getMsgRenderer = do
|
|||||||
return $ MsgRenderer (mr . SomeMessage :: forall msg. RenderMessage site msg => msg -> Text)
|
return $ MsgRenderer (mr . SomeMessage :: forall msg. RenderMessage site msg => msg -> Text)
|
||||||
|
|
||||||
|
|
||||||
instance Monad FormResult where
|
|
||||||
FormMissing >>= _ = FormMissing
|
|
||||||
(FormFailure errs) >>= _ = FormFailure errs
|
|
||||||
(FormSuccess a) >>= f = f a
|
|
||||||
|
|
||||||
guardAuthResult :: MonadHandler m => AuthResult -> m ()
|
guardAuthResult :: MonadHandler m => AuthResult -> m ()
|
||||||
guardAuthResult AuthenticationRequired = notAuthenticated
|
guardAuthResult AuthenticationRequired = notAuthenticated
|
||||||
guardAuthResult (Unauthorized t) = permissionDenied t
|
guardAuthResult (Unauthorized t) = permissionDenied t
|
||||||
@ -145,8 +140,13 @@ hasTickmark :: Bool -> Markup
|
|||||||
hasTickmark True = [shamlet|<i .fas .fa-check>|]
|
hasTickmark True = [shamlet|<i .fas .fa-check>|]
|
||||||
hasTickmark False = mempty
|
hasTickmark False = mempty
|
||||||
|
|
||||||
|
isBad :: Bool -> Markup
|
||||||
|
-- ^ Display an icon that denotes that something™ is bad
|
||||||
|
isBad True = [shamlet|<i .fas .fa-bolt>|] -- or times?!
|
||||||
|
isBad False = mempty
|
||||||
|
|
||||||
isNew :: Bool -> Markup
|
isNew :: Bool -> Markup
|
||||||
isNew True = [shamlet|<i .fas .fa-exclamation>|]
|
isNew True = [shamlet|<i .fas .fa-seedling>|] -- was exclamation
|
||||||
isNew False = mempty
|
isNew False = mempty
|
||||||
|
|
||||||
|
|
||||||
@ -325,6 +325,14 @@ mergeAttrs = mergeAttrs' `on` sort
|
|||||||
mergeAttrs' xs1 [] = xs1
|
mergeAttrs' xs1 [] = xs1
|
||||||
|
|
||||||
|
|
||||||
|
-- | Copied form Util from package ghc
|
||||||
|
partitionWith :: (a -> Either b c) -> [a] -> ([b], [c])
|
||||||
|
-- ^ Uses a function to determine which of two output lists an input element should join
|
||||||
|
partitionWith _ [] = ([],[])
|
||||||
|
partitionWith f (x:xs) = case f x of
|
||||||
|
Left b -> (b:bs, cs)
|
||||||
|
Right c -> (bs, c:cs)
|
||||||
|
where (bs,cs) = partitionWith f xs
|
||||||
|
|
||||||
----------
|
----------
|
||||||
-- Sets --
|
-- Sets --
|
||||||
@ -447,6 +455,9 @@ instance Ord a => Ord (NTop (Maybe a)) where
|
|||||||
exceptTMaybe :: Monad m => ExceptT e m a -> MaybeT m a
|
exceptTMaybe :: Monad m => ExceptT e m a -> MaybeT m a
|
||||||
exceptTMaybe = MaybeT . fmap (either (const Nothing) Just) . runExceptT
|
exceptTMaybe = MaybeT . fmap (either (const Nothing) Just) . runExceptT
|
||||||
|
|
||||||
|
formResultToMaybe :: Alternative m => FormResult a -> m a
|
||||||
|
formResultToMaybe (FormSuccess x) = pure x
|
||||||
|
formResultToMaybe _ = empty
|
||||||
|
|
||||||
------------
|
------------
|
||||||
-- Either --
|
-- Either --
|
||||||
@ -532,10 +543,11 @@ ifM c m m' =
|
|||||||
do b <- c
|
do b <- c
|
||||||
if b then m else m'
|
if b then m else m'
|
||||||
|
|
||||||
-- | @ifNotM mc = ifM (not <$> mc)@
|
-- | @ifNotM mc = ifM (not <$> mc)@ from Agda.Utils.Monad
|
||||||
ifNotM :: Monad m => m Bool -> m a -> m a -> m a
|
ifNotM :: Monad m => m Bool -> m a -> m a -> m a
|
||||||
ifNotM c = flip $ ifM c
|
ifNotM c = flip $ ifM c
|
||||||
|
|
||||||
|
-- | Monadic boolean function, copied from Andreas Abel's utility function
|
||||||
and2M, or2M :: Monad m => m Bool -> m Bool -> m Bool
|
and2M, or2M :: Monad m => m Bool -> m Bool -> m Bool
|
||||||
and2M ma mb = ifM ma mb (return False)
|
and2M ma mb = ifM ma mb (return False)
|
||||||
or2M ma = ifM ma (return True)
|
or2M ma = ifM ma (return True)
|
||||||
@ -575,7 +587,8 @@ mconcatMapM f = foldM (\x my -> mappend x <$> my) mempty . map f . Fold.toList
|
|||||||
mconcatForM :: (Monoid b, Monad m, Foldable f) => f a -> (a -> m b) -> m b
|
mconcatForM :: (Monoid b, Monad m, Foldable f) => f a -> (a -> m b) -> m b
|
||||||
mconcatForM = flip mconcatMapM
|
mconcatForM = flip mconcatMapM
|
||||||
|
|
||||||
|
findM :: (Monad m, Foldable f) => (a -> MaybeT m b) -> f a -> m (Maybe b)
|
||||||
|
findM f = runMaybeT . Fold.foldr (\x as -> f x <|> as) mzero
|
||||||
|
|
||||||
-----------------
|
-----------------
|
||||||
-- Alternative --
|
-- Alternative --
|
||||||
@ -605,9 +618,9 @@ modifySessionJson (toPathPiece -> key) f = lookupSessionJson key >>= maybe (dele
|
|||||||
tellSessionJson :: (PathPiece k, FromJSON v, ToJSON v, MonadHandler m, Monoid v) => k -> v -> m ()
|
tellSessionJson :: (PathPiece k, FromJSON v, ToJSON v, MonadHandler m, Monoid v) => k -> v -> m ()
|
||||||
tellSessionJson key val = modifySessionJson key $ Just . (`mappend` val) . fromMaybe mempty
|
tellSessionJson key val = modifySessionJson key $ Just . (`mappend` val) . fromMaybe mempty
|
||||||
|
|
||||||
getSessionJson :: (PathPiece k, FromJSON v, MonadHandler m) => k -> m (Maybe v)
|
takeSessionJson :: (PathPiece k, FromJSON v, MonadHandler m) => k -> m (Maybe v)
|
||||||
-- ^ `lookupSessionJson` followed by `deleteSession`
|
-- ^ `lookupSessionJson` followed by `deleteSession`
|
||||||
getSessionJson key = lookupSessionJson key <* deleteSession (toPathPiece key)
|
takeSessionJson key = lookupSessionJson key <* deleteSession (toPathPiece key)
|
||||||
|
|
||||||
--------------------
|
--------------------
|
||||||
-- GET Parameters --
|
-- GET Parameters --
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
module Utils.Form where
|
module Utils.Form where
|
||||||
|
|
||||||
import ClassyPrelude.Yesod hiding (addMessage, cons, Proxy(..), identifyForm)
|
import ClassyPrelude.Yesod hiding (addMessage, cons, Proxy(..), identifyForm)
|
||||||
|
import Yesod.Core.Instances ()
|
||||||
import Settings
|
import Settings
|
||||||
|
|
||||||
import Utils.Parameters
|
import Utils.Parameters
|
||||||
@ -98,6 +99,11 @@ addAttrs attr valus fs = fs { fsAttrs = newAttrs $ fsAttrs fs }
|
|||||||
| attr==a = ( a, T.intercalate " " $ v : valus ) : t
|
| attr==a = ( a, T.intercalate " " $ v : valus ) : t
|
||||||
| otherwise = p : newAttrs t
|
| otherwise = p : newAttrs t
|
||||||
|
|
||||||
|
addPlaceholder :: Text -> FieldSettings site -> FieldSettings site
|
||||||
|
addPlaceholder placeholder fs = fs { fsAttrs = (placeholderAttr, placeholder) : filter ((/= placeholderAttr) . fst) (fsAttrs fs) }
|
||||||
|
where
|
||||||
|
placeholderAttr = "placeholder"
|
||||||
|
|
||||||
addClass :: Text -> FieldSettings site -> FieldSettings site
|
addClass :: Text -> FieldSettings site -> FieldSettings site
|
||||||
addClass = addAttr "class"
|
addClass = addAttr "class"
|
||||||
|
|
||||||
@ -107,6 +113,9 @@ addClasses = addAttrs "class"
|
|||||||
addName :: PathPiece p => p -> FieldSettings site -> FieldSettings site
|
addName :: PathPiece p => p -> FieldSettings site -> FieldSettings site
|
||||||
addName nm fs = fs { fsName = Just $ toPathPiece nm }
|
addName nm fs = fs { fsName = Just $ toPathPiece nm }
|
||||||
|
|
||||||
|
addId :: PathPiece p => p -> FieldSettings site -> FieldSettings site
|
||||||
|
addId fid fs = fs { fsId = Just $ toPathPiece fid }
|
||||||
|
|
||||||
addNameClass :: Text -> Text -> FieldSettings site -> FieldSettings site
|
addNameClass :: Text -> Text -> FieldSettings site -> FieldSettings site
|
||||||
addNameClass gName gClass fs = fs { fsName = Just gName, fsAttrs = ("class",gClass) : fsAttrs fs }
|
addNameClass gName gClass fs = fs { fsName = Just gName, fsAttrs = ("class",gClass) : fsAttrs fs }
|
||||||
|
|
||||||
@ -176,7 +185,10 @@ data FormIdentifier
|
|||||||
| FIDDBTable
|
| FIDDBTable
|
||||||
| FIDDelete
|
| FIDDelete
|
||||||
| FIDCourseRegister
|
| FIDCourseRegister
|
||||||
|
| FIDuserRights
|
||||||
|
| FIDcUserNote
|
||||||
| FIDAdminDemo
|
| FIDAdminDemo
|
||||||
|
| FIDUserDelete
|
||||||
deriving (Eq, Ord, Read, Show)
|
deriving (Eq, Ord, Read, Show)
|
||||||
|
|
||||||
instance PathPiece FormIdentifier where
|
instance PathPiece FormIdentifier where
|
||||||
@ -198,7 +210,7 @@ identifyForm' resLens identVal form fragment = do
|
|||||||
|]
|
|]
|
||||||
|
|
||||||
-- Check if we got its value back.
|
-- Check if we got its value back.
|
||||||
hasIdent <- (== Just identVal) <$> lookupGlobalPostParamForm PostFormIdentifier
|
hasIdent <- (== Just identVal) <$> lookupGlobalPostParamForm PostFormIdentifier
|
||||||
|
|
||||||
-- Run the form proper (with our hidden <input>). If the
|
-- Run the form proper (with our hidden <input>). If the
|
||||||
-- data is missing, then do not provide any params to the
|
-- data is missing, then do not provide any params to the
|
||||||
@ -210,7 +222,7 @@ identifyForm' resLens identVal form fragment = do
|
|||||||
|
|
||||||
identifyForm :: (Monad m, PathPiece ident, Eq ident) => ident -> (Html -> MForm m (FormResult a, widget)) -> (Html -> MForm m (FormResult a, widget))
|
identifyForm :: (Monad m, PathPiece ident, Eq ident) => ident -> (Html -> MForm m (FormResult a, widget)) -> (Html -> MForm m (FormResult a, widget))
|
||||||
identifyForm = identifyForm' id
|
identifyForm = identifyForm' id
|
||||||
|
|
||||||
|
|
||||||
{- Hinweise zur Erinnerung:
|
{- Hinweise zur Erinnerung:
|
||||||
- identForm primär, wenn es mehr als ein Formular pro Handler gibt
|
- identForm primär, wenn es mehr als ein Formular pro Handler gibt
|
||||||
@ -240,6 +252,7 @@ data ButtonMessage = MsgAmbiguousButtons
|
|||||||
| MsgMultipleButtonValues
|
| MsgMultipleButtonValues
|
||||||
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic, Typeable)
|
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic, Typeable)
|
||||||
|
|
||||||
|
-- | Default button for submitting. Required in Foundation for Login, other Buttons defined in Handler.Utils.Form
|
||||||
data ButtonSubmit = BtnSubmit
|
data ButtonSubmit = BtnSubmit
|
||||||
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic, Typeable)
|
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic, Typeable)
|
||||||
|
|
||||||
@ -303,6 +316,7 @@ combinedButtonFieldF :: forall m a.
|
|||||||
) => FieldSettings (HandlerSite m) -> AForm m [Maybe a]
|
) => FieldSettings (HandlerSite m) -> AForm m [Maybe a]
|
||||||
combinedButtonFieldF = combinedButtonField (universeF :: [a])
|
combinedButtonFieldF = combinedButtonField (universeF :: [a])
|
||||||
|
|
||||||
|
-- | Ensures that only a single button press is accepted at once
|
||||||
disambiguateButtons :: forall m a.
|
disambiguateButtons :: forall m a.
|
||||||
( MonadHandler m
|
( MonadHandler m
|
||||||
, RenderMessage (HandlerSite m) ButtonMessage
|
, RenderMessage (HandlerSite m) ButtonMessage
|
||||||
@ -341,6 +355,17 @@ submitButtonView = do
|
|||||||
fieldView bField btnId "" mempty (Right BtnSubmit) False
|
fieldView bField btnId "" mempty (Right BtnSubmit) False
|
||||||
|
|
||||||
|
|
||||||
|
buttonForm :: (Button site a, Finite a) => Html -> MForm (HandlerT site IO) (FormResult a, WidgetT site IO ())
|
||||||
|
buttonForm csrf = do
|
||||||
|
(res, ($ []) -> fViews) <- aFormToForm . disambiguateButtons $ combinedButtonFieldF ""
|
||||||
|
return (res, [whamlet|
|
||||||
|
$newline never
|
||||||
|
#{csrf}
|
||||||
|
$forall bView <- fViews
|
||||||
|
^{fvInput bView}
|
||||||
|
|])
|
||||||
|
|
||||||
|
|
||||||
-------------------
|
-------------------
|
||||||
-- Custom Fields --
|
-- Custom Fields --
|
||||||
-------------------
|
-------------------
|
||||||
|
|||||||
42
src/Utils/Modal.hs
Normal file
42
src/Utils/Modal.hs
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
module Utils.Modal
|
||||||
|
( Modal(..)
|
||||||
|
, customModal
|
||||||
|
, modal
|
||||||
|
) where
|
||||||
|
|
||||||
|
import ClassyPrelude.Yesod
|
||||||
|
|
||||||
|
import Control.Lens
|
||||||
|
import Control.Lens.Extras (is)
|
||||||
|
import Utils.Route
|
||||||
|
|
||||||
|
import Settings (widgetFile)
|
||||||
|
|
||||||
|
|
||||||
|
data Modal site = Modal
|
||||||
|
{ modalTriggerId
|
||||||
|
, modalId :: Maybe Text
|
||||||
|
, modalTrigger :: Maybe Text {- Dynamic URL -} -> Text {- TriggerId -} -> WidgetT site IO ()
|
||||||
|
, modalContent :: Either (SomeRoute site) (WidgetT site IO ())
|
||||||
|
}
|
||||||
|
|
||||||
|
customModal :: Modal site -> WidgetT site IO ()
|
||||||
|
customModal Modal{..} = do
|
||||||
|
let isDynamic = is _Left modalContent
|
||||||
|
modalId' <- maybe newIdent return modalId
|
||||||
|
triggerId' <- maybe newIdent return modalTriggerId
|
||||||
|
|
||||||
|
$(widgetFile "widgets/modal/modal")
|
||||||
|
|
||||||
|
route <- for (modalContent ^? _Left) toTextUrl
|
||||||
|
modalTrigger route triggerId'
|
||||||
|
|
||||||
|
-- | Create a link to a modal
|
||||||
|
modal :: WidgetT site IO () -- ^ Widget that represents the link
|
||||||
|
-> Either (SomeRoute site) (WidgetT site IO ()) -- ^ Modal contant: either dynamic link or static widget
|
||||||
|
-> WidgetT site IO () -- ^ result widget
|
||||||
|
modal modalTrigger' modalContent = customModal Modal{..}
|
||||||
|
where
|
||||||
|
modalTriggerId = Nothing
|
||||||
|
modalId = Nothing
|
||||||
|
modalTrigger mRoute triggerId = $(widgetFile "widgets/modal/trigger")
|
||||||
@ -24,7 +24,7 @@ projNI n i = do
|
|||||||
x <- newName "x"
|
x <- newName "x"
|
||||||
let rhs = varE x
|
let rhs = varE x
|
||||||
let pat = tupP $ replicate (pred i) wildP ++ varP x : replicate (n-i) wildP
|
let pat = tupP $ replicate (pred i) wildP ++ varP x : replicate (n-i) wildP
|
||||||
lamE [pat] rhs
|
lam1E pat rhs
|
||||||
|
|
||||||
|
|
||||||
-- | Generic projections N-tuples that are actually left-associative pairs
|
-- | Generic projections N-tuples that are actually left-associative pairs
|
||||||
@ -83,6 +83,14 @@ uncurryN n = do
|
|||||||
return $ LamE pat rhs
|
return $ LamE pat rhs
|
||||||
|
|
||||||
|
|
||||||
|
afterN :: Int -> ExpQ -- apply a function after another of arity N, i.e. $(afterN 1) = (.)
|
||||||
|
afterN n = do
|
||||||
|
f <- newName "f"
|
||||||
|
g <- newName "g"
|
||||||
|
--let rhs = [|$(curryN n) (g . ($(uncurryN n) f))|]
|
||||||
|
lamE [varP g, varP f] [|$(curryN n) $(varE g) . $(uncurryN n) $(varE f)|]
|
||||||
|
|
||||||
|
|
||||||
-- Special Show-Instances for Themes
|
-- Special Show-Instances for Themes
|
||||||
deriveShowWith :: (String -> String) -> Name -> Q [Dec]
|
deriveShowWith :: (String -> String) -> Name -> Q [Dec]
|
||||||
deriveShowWith = deriveSimpleWith ''Show 'show
|
deriveShowWith = deriveSimpleWith ''Show 'show
|
||||||
|
|||||||
@ -4,6 +4,7 @@ module Yesod.Core.Instances
|
|||||||
(
|
(
|
||||||
) where
|
) where
|
||||||
|
|
||||||
|
import Prelude (errorWithoutStackTrace)
|
||||||
import ClassyPrelude.Yesod
|
import ClassyPrelude.Yesod
|
||||||
|
|
||||||
import Utils (assertM')
|
import Utils (assertM')
|
||||||
@ -14,6 +15,12 @@ import Data.ByteString.Builder (toLazyByteString)
|
|||||||
import System.FilePath ((</>))
|
import System.FilePath ((</>))
|
||||||
|
|
||||||
import Data.Aeson
|
import Data.Aeson
|
||||||
|
|
||||||
|
import Control.Monad.Fix
|
||||||
|
import Control.Monad.Fail (MonadFail)
|
||||||
|
import qualified Control.Monad.Fail as MonadFail
|
||||||
|
import Control.Monad.Except (MonadError(..))
|
||||||
|
import Data.Functor.Extend
|
||||||
|
|
||||||
|
|
||||||
instance (RenderRoute site, ParseRoute site) => PathPiece (Route site) where
|
instance (RenderRoute site, ParseRoute site) => PathPiece (Route site) where
|
||||||
@ -39,3 +46,34 @@ instance (RenderRoute site, ParseRoute site) => FromJSON (Route site) where
|
|||||||
|
|
||||||
instance (RenderRoute site, ParseRoute site) => ToJSON (Route site) where
|
instance (RenderRoute site, ParseRoute site) => ToJSON (Route site) where
|
||||||
toJSON = String . toPathPiece
|
toJSON = String . toPathPiece
|
||||||
|
|
||||||
|
|
||||||
|
instance Monad FormResult where
|
||||||
|
(FormSuccess a) >>= f = f a
|
||||||
|
FormMissing >>= _ = FormMissing
|
||||||
|
(FormFailure errs) >>= _ = FormFailure errs
|
||||||
|
|
||||||
|
fail = MonadFail.fail
|
||||||
|
|
||||||
|
instance MonadFail FormResult where
|
||||||
|
fail _ = FormMissing
|
||||||
|
|
||||||
|
instance MonadError [Text] FormResult where
|
||||||
|
throwError = FormFailure
|
||||||
|
|
||||||
|
catchError a@(FormSuccess _) _ = a
|
||||||
|
catchError FormMissing _ = FormMissing
|
||||||
|
catchError (FormFailure errs) h = h errs
|
||||||
|
|
||||||
|
instance MonadPlus FormResult
|
||||||
|
|
||||||
|
instance MonadFix FormResult where
|
||||||
|
mfix f = let a = f (unSuccess a) in a
|
||||||
|
where unSuccess (FormSuccess x) = x
|
||||||
|
unSuccess FormMissing = errorWithoutStackTrace "mfix FormResult: FormMissing"
|
||||||
|
unSuccess (FormFailure _) = errorWithoutStackTrace "mfix FormResult: FormFailure"
|
||||||
|
|
||||||
|
instance Extend FormResult where
|
||||||
|
duplicated (FormSuccess x) = FormSuccess $ FormSuccess x
|
||||||
|
duplicated FormMissing = FormMissing
|
||||||
|
duplicated (FormFailure errs) = FormFailure errs
|
||||||
|
|||||||
@ -86,7 +86,7 @@ Handler.Utils.Table.Pagination.Types
|
|||||||
Handler.Utils.Table.Cells
|
Handler.Utils.Table.Cells
|
||||||
: extends dbTable with UniWorX specific functions, such as special courseCell
|
: extends dbTable with UniWorX specific functions, such as special courseCell
|
||||||
|
|
||||||
Handler.Utils.Templates
|
Utils.Modal
|
||||||
: Modals
|
: Modals
|
||||||
|
|
||||||
Handler.Utils.Zip
|
Handler.Utils.Zip
|
||||||
|
|||||||
@ -25,27 +25,26 @@
|
|||||||
color: var(--color-fontsec);
|
color: var(--color-fontsec);
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group__label {
|
.form-group-label {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
padding-top: 6px;
|
padding-top: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group__hint {
|
.form-group-label__hint {
|
||||||
margin-top: 7px;
|
margin-top: 7px;
|
||||||
color: var(--color-fontsec);
|
color: var(--color-fontsec);
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group--required {
|
.form-group--required {
|
||||||
|
.form-group-label__caption::after {
|
||||||
.form-group__label::after {
|
|
||||||
content: ' *';
|
content: ' *';
|
||||||
color: var(--color-error);
|
color: var(--color-error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group--optional {
|
.form-group--optional {
|
||||||
.form-group__label::after {
|
.form-group-label__caption::after {
|
||||||
content: '';
|
content: '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -66,6 +65,14 @@
|
|||||||
input, textarea {
|
input, textarea {
|
||||||
border-color: var(--color-error) !important;
|
border-color: var(--color-error) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-error {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-error {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
|
|||||||
@ -8,6 +8,9 @@
|
|||||||
var AUTOSUBMIT_BUTTON_SELECTOR = '[type="submit"][data-autosubmit]';
|
var AUTOSUBMIT_BUTTON_SELECTOR = '[type="submit"][data-autosubmit]';
|
||||||
var AJAX_SUBMIT_FLAG = 'ajaxSubmit';
|
var AJAX_SUBMIT_FLAG = 'ajaxSubmit';
|
||||||
|
|
||||||
|
var FORM_GROUP_CLASS = 'form-group';
|
||||||
|
var FORM_GROUP_WITH_ERRORS_CLASS = 'form-group--has-error';
|
||||||
|
|
||||||
function formValidator(inputs) {
|
function formValidator(inputs) {
|
||||||
var done = true;
|
var done = true;
|
||||||
inputs.forEach(function(inp) {
|
inputs.forEach(function(inp) {
|
||||||
@ -20,8 +23,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
window.utils.form = function(form, options) {
|
window.utils.form = function(form, options) {
|
||||||
|
options = options || {};
|
||||||
|
|
||||||
if (form.classList.contains(JS_INITIALIZED)) {
|
// dont initialize form if it is in a modal and is not forced
|
||||||
|
if (form.closest('.modal') && !options.force) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// dont initialize form if already initialized and should not be force-initialized
|
||||||
|
if (form.classList.contains(JS_INITIALIZED) && !options.force) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,6 +55,12 @@
|
|||||||
// inputs
|
// inputs
|
||||||
utilInstances.push(window.utils.setup('inputs', form, options));
|
utilInstances.push(window.utils.setup('inputs', form, options));
|
||||||
|
|
||||||
|
// form group errors
|
||||||
|
var formGroups = Array.from(form.querySelectorAll('.' + FORM_GROUP_CLASS));
|
||||||
|
formGroups.forEach(function(formGroup) {
|
||||||
|
utilInstances.push(window.utils.setup('errorRemover', formGroup, options));
|
||||||
|
});
|
||||||
|
|
||||||
form.classList.add(JS_INITIALIZED);
|
form.classList.add(JS_INITIALIZED);
|
||||||
|
|
||||||
function destroyUtils() {
|
function destroyUtils() {
|
||||||
@ -158,4 +174,29 @@
|
|||||||
destroy: function() {},
|
destroy: function() {},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// listens for focus events and removes any errors on an input
|
||||||
|
window.utils.errorRemover = function(formGroup, options) {
|
||||||
|
|
||||||
|
var inputElement = formGroup.querySelector('input:not([type="hidden"]), textarea, select');
|
||||||
|
if (!inputElement) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
inputElement.addEventListener('focus', focusListener);
|
||||||
|
|
||||||
|
function focusListener() {
|
||||||
|
var hasError = formGroup.classList.contains(FORM_GROUP_WITH_ERRORS_CLASS);
|
||||||
|
if (hasError) {
|
||||||
|
formGroup.classList.remove(FORM_GROUP_WITH_ERRORS_CLASS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
scope: formGroup,
|
||||||
|
destroy: function() {
|
||||||
|
inputElement.removeEventListener('focus', focusListener);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
})();
|
})();
|
||||||
|
|||||||
@ -3,37 +3,37 @@
|
|||||||
|
|
||||||
window.utils = window.utils || {};
|
window.utils = window.utils || {};
|
||||||
|
|
||||||
var JS_INITIALIZED_CLASS = 'js-initialized';
|
var JS_INITIALIZED_CLASS = 'js-inputs-initialized';
|
||||||
|
|
||||||
function isNotInitialized(element) {
|
|
||||||
return !element.classList.contains(JS_INITIALIZED_CLASS);
|
|
||||||
}
|
|
||||||
|
|
||||||
window.utils.inputs = function(wrapper, options) {
|
window.utils.inputs = function(wrapper, options) {
|
||||||
|
options = options || {};
|
||||||
var utilInstances = [];
|
var utilInstances = [];
|
||||||
|
|
||||||
|
if (wrapper.classList.contains(JS_INITIALIZED_CLASS) && !options.force) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// checkboxes
|
// checkboxes
|
||||||
var checkboxes = Array.from(wrapper.querySelectorAll('input[type="checkbox"]'));
|
var checkboxes = Array.from(wrapper.querySelectorAll('input[type="checkbox"]'));
|
||||||
checkboxes.filter(isNotInitialized).forEach(function(checkbox) {
|
checkboxes.forEach(function(checkbox) {
|
||||||
utilInstances.push(window.utils.setup('checkbox', checkbox));
|
utilInstances.push(window.utils.setup('checkbox', checkbox));
|
||||||
});
|
});
|
||||||
|
|
||||||
// radios
|
// radios
|
||||||
var radios = Array.from(wrapper.querySelectorAll('input[type="radio"]'));
|
var radios = Array.from(wrapper.querySelectorAll('input[type="radio"]'));
|
||||||
radios.filter(isNotInitialized).forEach(function(radio) {
|
radios.forEach(function(radio) {
|
||||||
utilInstances.push(window.utils.setup('radio', radio));
|
utilInstances.push(window.utils.setup('radio', radio));
|
||||||
});
|
});
|
||||||
|
|
||||||
// file-uploads
|
// file-uploads
|
||||||
var fileUploads = Array.from(wrapper.querySelectorAll('input[type="file"]'));
|
var fileUploads = Array.from(wrapper.querySelectorAll('input[type="file"]'));
|
||||||
fileUploads.filter(isNotInitialized).forEach(function(input) {
|
fileUploads.forEach(function(input) {
|
||||||
utilInstances.push(window.utils.setup('fileUpload', input, options));
|
utilInstances.push(window.utils.setup('fileUpload', input, options));
|
||||||
});
|
});
|
||||||
|
|
||||||
// file-checkboxes
|
// file-checkboxes
|
||||||
var fileCheckboxes = Array.from(wrapper.querySelectorAll('.file-checkbox'));
|
var fileCheckboxes = Array.from(wrapper.querySelectorAll('.file-checkbox'));
|
||||||
fileCheckboxes.filter(isNotInitialized).forEach(function(input) {
|
fileCheckboxes.forEach(function(input) {
|
||||||
utilInstances.push(window.utils.setup('fileCheckbox', input, options));
|
utilInstances.push(window.utils.setup('fileCheckbox', input, options));
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -45,6 +45,8 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
wrapper.classList.add(JS_INITIALIZED_CLASS);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
scope: wrapper,
|
scope: wrapper,
|
||||||
destroy: destroyUtils,
|
destroy: destroyUtils,
|
||||||
@ -74,7 +76,6 @@
|
|||||||
if (!i18n) {
|
if (!i18n) {
|
||||||
throw new Error('window.utils.fileUpload(input, options) needs to be passed i18n object via options');
|
throw new Error('window.utils.fileUpload(input, options) needs to be passed i18n object via options');
|
||||||
}
|
}
|
||||||
input.classList.add(JS_INITIALIZED_CLASS);
|
|
||||||
|
|
||||||
function renderFileList(files) {
|
function renderFileList(files) {
|
||||||
fileList.innerHTML = '';
|
fileList.innerHTML = '';
|
||||||
@ -166,8 +167,6 @@
|
|||||||
cont = cont.parentNode;
|
cont = cont.parentNode;
|
||||||
}
|
}
|
||||||
addListener(cont);
|
addListener(cont);
|
||||||
input.classList.add(JS_INITIALIZED_CLASS);
|
|
||||||
cont.classList.add(JS_INITIALIZED_CLASS);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setup();
|
setup();
|
||||||
@ -190,7 +189,6 @@
|
|||||||
labelEl.setAttribute('for', input.id);
|
labelEl.setAttribute('for', input.id);
|
||||||
wrapperEl.appendChild(input);
|
wrapperEl.appendChild(input);
|
||||||
wrapperEl.appendChild(labelEl);
|
wrapperEl.appendChild(labelEl);
|
||||||
input.classList.add(JS_INITIALIZED_CLASS);
|
|
||||||
|
|
||||||
if (siblingEl) {
|
if (siblingEl) {
|
||||||
parentEl.insertBefore(wrapperEl, siblingEl);
|
parentEl.insertBefore(wrapperEl, siblingEl);
|
||||||
@ -219,7 +217,6 @@
|
|||||||
wrapperEl.appendChild(siblingEl);
|
wrapperEl.appendChild(siblingEl);
|
||||||
}
|
}
|
||||||
|
|
||||||
input.classList.add(JS_INITIALIZED_CLASS);
|
|
||||||
parentEl.appendChild(wrapperEl);
|
parentEl.appendChild(wrapperEl);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -233,8 +230,6 @@
|
|||||||
window.utils.implicitSubmit = function(input, options) {
|
window.utils.implicitSubmit = function(input, options) {
|
||||||
var submit = options.submit;
|
var submit = options.submit;
|
||||||
|
|
||||||
console.log('implicitSubmit', input, submit);
|
|
||||||
|
|
||||||
if (!submit) {
|
if (!submit) {
|
||||||
throw new Error('window.utils.implicitSubmit(input, options) needs to be passed a submit element via options');
|
throw new Error('window.utils.implicitSubmit(input, options) needs to be passed a submit element via options');
|
||||||
}
|
}
|
||||||
@ -247,7 +242,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
input.addEventListener('keypress', doSubmit);
|
input.addEventListener('keypress', doSubmit);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
scope: input,
|
scope: input,
|
||||||
destroy: function() {
|
destroy: function() {
|
||||||
|
|||||||
@ -75,7 +75,7 @@
|
|||||||
function setupForm() {
|
function setupForm() {
|
||||||
var form = modalElement.querySelector('form');
|
var form = modalElement.querySelector('form');
|
||||||
if (form) {
|
if (form) {
|
||||||
utilInstances.push(window.utils.setup('form', form, { headers: MODAL_HEADERS }));
|
utilInstances.push(window.utils.setup('form', form, { headers: MODAL_HEADERS, force: true }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -37,6 +37,9 @@
|
|||||||
|
|
||||||
if (isAlreadySetup) {
|
if (isAlreadySetup) {
|
||||||
console.warn('Trying to setup a JS utility that\'s already been set up', { utility: utilName, scope, options });
|
console.warn('Trying to setup a JS utility that\'s already been set up', { utility: utilName, scope, options });
|
||||||
|
if (!options.force) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,17 +1,14 @@
|
|||||||
<div .container>
|
<section>
|
||||||
<h1>Uni2work - Admin Demopage
|
|
||||||
|
|
||||||
<p data-tooltip="Solch ein Tooltip kann mit dem <em>data-tooltip</em> Attribut erzeugt werden. Funktioniert aber nur in Block-Elementen die einen sinnvollen Wrapper haben.">
|
<p data-tooltip="Solch ein Tooltip kann mit dem <em>data-tooltip</em> Attribut erzeugt werden. Funktioniert aber nur in Block-Elementen die einen sinnvollen Wrapper haben.">
|
||||||
Diese interne Seite dient lediglich zum Testen diverser Funktionalitäten
|
Diese interne Seite dient lediglich zum Testen diverser Funktionalitäten
|
||||||
und zur Demonstration der verschiedenen Hilfsfunktionen/Module.
|
und zur Demonstration der verschiedenen Hilfsfunktionen/Module.
|
||||||
|
|
||||||
Der Handler sollte jeweils aktuelle Beispiele für alle möglichen Funktionalitäten enthalten, so dass man immer weiß, wo man nachschlagen kann.
|
Der Handler sollte jeweils aktuelle Beispiele für alle möglichen Funktionalitäten enthalten, so dass man immer weiß, wo man nachschlagen kann.
|
||||||
|
|
||||||
|
<section>
|
||||||
<div .container.js-show-hide>
|
|
||||||
<h2 .js-show-hide__toggle>Teilweise funktionierende Abschnitte
|
<h2 .js-show-hide__toggle>Teilweise funktionierende Abschnitte
|
||||||
|
|
||||||
<ul .js-show-hide__target>
|
<ul>
|
||||||
<li .list-group-item>
|
<li .list-group-item>
|
||||||
<a href=@{UsersR}>Benutzer Verwaltung
|
<a href=@{UsersR}>Benutzer Verwaltung
|
||||||
|
|
||||||
@ -22,7 +19,7 @@
|
|||||||
<li .list-group-item>
|
<li .list-group-item>
|
||||||
<a href=@{CourseNewR}>Kurse anlegen
|
<a href=@{CourseNewR}>Kurse anlegen
|
||||||
|
|
||||||
<div .container>
|
<section>
|
||||||
<h2>Funktionen zum Testen
|
<h2>Funktionen zum Testen
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
|
|||||||
@ -1,4 +1,11 @@
|
|||||||
<p>
|
<section>
|
||||||
$# Does not use link-email.hamlet, but should
|
|
||||||
^{mailtoHtml userEmail}
|
^{mailtoHtml userEmail}
|
||||||
^{form}
|
^{form}
|
||||||
|
<section>
|
||||||
|
^{userDataWidget}
|
||||||
|
<h3>
|
||||||
|
^{modal "Benutzer löschen" (Right deleteWidget)}
|
||||||
|
Achtung, dieser Link löscht momentan noch den kompletten Benutzer
|
||||||
|
unwiderruflich aus der Live-Datenbank mit
|
||||||
|
<code>DELETE CASCADE uid
|
||||||
|
\ Klausurdaten müssen jedoch langfristig gespeichert werden!
|
||||||
|
|||||||
7
templates/course-participants.hamlet
Normal file
7
templates/course-participants.hamlet
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
$# Shows all participants of a course, but no homework statistics
|
||||||
|
$# Should at some point allow email messaging
|
||||||
|
$#
|
||||||
|
$# participantTable : widget table
|
||||||
|
|
||||||
|
^{participantTable}
|
||||||
|
_{MsgCourseMembersCountOf (fromIntegral numParticipants) (courseCapacity course)}.
|
||||||
49
templates/course-user.hamlet
Normal file
49
templates/course-user.hamlet
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
<section>
|
||||||
|
<div .profile>
|
||||||
|
<dl .deflist.profile-dl>
|
||||||
|
<dt .deflist__dt> _{MsgEMail}
|
||||||
|
<dd .deflist__dd> #{mailtoHtml userEmail}
|
||||||
|
<dt .deflist__dt> _{MsgMatrikelNr}
|
||||||
|
<dd .deflist__dd>
|
||||||
|
$maybe matnr <- userMatrikelnummer
|
||||||
|
#{matnr}
|
||||||
|
$nothing
|
||||||
|
_{MsgNoMatrikelKnown}
|
||||||
|
<dt .deflist__dt>_{MsgRegisteredHeader}
|
||||||
|
<dd .deflist__dd>
|
||||||
|
<div .course__registration>
|
||||||
|
<a id="register-form">
|
||||||
|
<form method=post action=@{currentRoute}#register-form enctype=#{registerEnctype}>
|
||||||
|
^{registerView}
|
||||||
|
$maybe date <- mRegAt
|
||||||
|
_{MsgRegisteredSince date}
|
||||||
|
|
||||||
|
|
||||||
|
<dt .deflist__dt> _{MsgStudyTerms}
|
||||||
|
<dd .deflist__dd>
|
||||||
|
$if null studies
|
||||||
|
_{MsgNoStudyTermsKnown}
|
||||||
|
$else
|
||||||
|
<div .scrolltable>
|
||||||
|
<table .table.table--striped.table--hover.table--condensed>
|
||||||
|
<tr .table__row>
|
||||||
|
<th .table__th>_{MsgStudyTerm}
|
||||||
|
<th .table__th>_{MsgStudyFeatureDegree}
|
||||||
|
<th .table__th>_{MsgStudyFeatureType}
|
||||||
|
<th .table__th>_{MsgStudyFeatureAge}
|
||||||
|
<th .table__th>_{MsgStudyFeatureValid}
|
||||||
|
<th .table__th>_{MsgStudyFeatureUpdate}
|
||||||
|
$forall ((Entity _ StudyFeatures{..}), (Entity _ degree), (Entity _ field)) <- studies
|
||||||
|
$with _ <- notUsedT studyFeaturesUser
|
||||||
|
<tr.table__row>
|
||||||
|
<td .table__td>_{field}#{notUsedT studyFeaturesField}
|
||||||
|
<td .table__td>_{degree}#{notUsedT studyFeaturesDegree}
|
||||||
|
<td .table__td>_{studyFeaturesType}
|
||||||
|
<td .table__td>#{display studyFeaturesSemester}
|
||||||
|
<td .table__td>#{hasTickmark studyFeaturesValid}
|
||||||
|
<td .table__td>^{formatTimeW SelFormatDate studyFeaturesUpdated}
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<a id="note-form">
|
||||||
|
<form method=post action=@{currentRoute}#note-form enctype=#{noteEnctype}>
|
||||||
|
^{noteView}
|
||||||
@ -19,8 +19,18 @@
|
|||||||
<dd .deflist__dd>
|
<dd .deflist__dd>
|
||||||
<div>
|
<div>
|
||||||
<ul .list--inline .list--comma-separated>
|
<ul .list--inline .list--comma-separated>
|
||||||
$forall (E.Value displayname, E.Value surname, E.Value email) <- lecturers
|
$forall lect <- lecturers
|
||||||
<li>^{nameEmailWidget email displayname surname}
|
<li>^{nameEmailWidget' lect}
|
||||||
|
$with numassi <- length assistants
|
||||||
|
$if numassi > 1
|
||||||
|
<dt .deflist__dt>_{MsgAssistantsFor}
|
||||||
|
$else
|
||||||
|
<dt .deflist__dt>_{MsgAssistantFor}
|
||||||
|
<dd .deflist__dd>
|
||||||
|
<div>
|
||||||
|
<ul .list--inline .list--comma-separated>
|
||||||
|
$forall assi <- assistants
|
||||||
|
<li>^{nameEmailWidget' assi}
|
||||||
|
|
||||||
$maybe link <- courseLinkExternal course
|
$maybe link <- courseLinkExternal course
|
||||||
<dt .deflist__dt>Website
|
<dt .deflist__dt>Website
|
||||||
|
|||||||
@ -474,7 +474,7 @@ ul.list--inline {
|
|||||||
/* DEFINITION LIST */
|
/* DEFINITION LIST */
|
||||||
.deflist {
|
.deflist {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 100% ;
|
grid-template-columns: 100%;
|
||||||
}
|
}
|
||||||
.deflist__dt,
|
.deflist__dt,
|
||||||
.deflist__dd {
|
.deflist__dd {
|
||||||
@ -488,6 +488,10 @@ ul.list--inline {
|
|||||||
.deflist__dd {
|
.deflist__dd {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
|
|
||||||
|
> p {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
@ -507,7 +511,6 @@ ul.list--inline {
|
|||||||
|
|
||||||
.deflist__dt,
|
.deflist__dt,
|
||||||
.deflist__dd {
|
.deflist__dd {
|
||||||
border-bottom: 1px solid #d3d3d3;
|
|
||||||
padding: 12px 0;
|
padding: 12px 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
@ -527,17 +530,15 @@ ul.list--inline {
|
|||||||
}
|
}
|
||||||
|
|
||||||
section {
|
section {
|
||||||
padding-bottom: 20px;
|
padding-bottom: 30px;
|
||||||
margin-bottom: 20px;
|
|
||||||
border-bottom: 1px solid #d3d3d3;
|
border-bottom: 1px solid #d3d3d3;
|
||||||
|
|
||||||
+ section {
|
+ section {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
padding-top: 20px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
section {
|
&:last-child {
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,10 @@
|
|||||||
<div .container>
|
<div .container>
|
||||||
<h1>
|
<h1>
|
||||||
_{MsgUserAccountDeleted userDisplayName}
|
_{MsgUserAccountDeleted userDisplayName}
|
||||||
|
<div .container>
|
||||||
|
#{nameEmailHtml userEmail userDisplayName userSurname}
|
||||||
|
<div .container>
|
||||||
|
#{mailtoHtml userEmail}
|
||||||
<div .container>
|
<div .container>
|
||||||
#{display deletedSubmissions} Abgaben wurden unwiederruflich gelöscht.
|
#{display deletedSubmissions} Abgaben wurden unwiederruflich gelöscht.
|
||||||
$if groupSubmissions > 0
|
$if groupSubmissions > 0
|
||||||
@ -12,5 +16,3 @@
|
|||||||
$if deletedSubmissionGroups > 0
|
$if deletedSubmissionGroups > 0
|
||||||
<div .container>
|
<div .container>
|
||||||
#{display deletedSubmissionGroups} benannte Abgabengruppen wurden gelöscht, da diese dadurch leer wurden.
|
#{display deletedSubmissionGroups} benannte Abgabengruppen wurden gelöscht, da diese dadurch leer wurden.
|
||||||
<div .container>
|
|
||||||
Good Bye!
|
|
||||||
|
|||||||
@ -6,7 +6,6 @@
|
|||||||
<h4>
|
<h4>
|
||||||
aus UniWorX bekannt:
|
aus UniWorX bekannt:
|
||||||
<ul>
|
<ul>
|
||||||
<li> Studiengänge von Benutzern werden noch ignoriert
|
|
||||||
<li> Übungsgruppen
|
<li> Übungsgruppen
|
||||||
<li> Klausuren
|
<li> Klausuren
|
||||||
<li> Zentralanmeldungen
|
<li> Zentralanmeldungen
|
||||||
@ -14,7 +13,7 @@
|
|||||||
<h4>
|
<h4>
|
||||||
neue geplante Features:
|
neue geplante Features:
|
||||||
<ul>
|
<ul>
|
||||||
<li> Stundenplan/Kalender mit Veranstaltungen und Klausuren
|
<li> Stundenplan/Kalender mit allen Veranstaltungen und Klausuren
|
||||||
<li> Vollständige Vorlesungshomepages
|
<li> Vollständige Vorlesungshomepages
|
||||||
<li> Vollständige Internationalisierung deutsch/englisch/...
|
<li> Vollständige Internationalisierung deutsch/englisch/...
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,3 @@
|
|||||||
<p>
|
<p>
|
||||||
_{MsgHelpIntroduction}
|
_{MsgHelpIntroduction}
|
||||||
^{form}
|
^{formWidget}
|
||||||
|
|||||||
@ -1,5 +0,0 @@
|
|||||||
<div .container>
|
|
||||||
<h2>
|
|
||||||
Kurse mit offener Registrierung
|
|
||||||
<div .container>
|
|
||||||
^{courseTable}
|
|
||||||
3
templates/home/openCourses.hamlet
Normal file
3
templates/home/openCourses.hamlet
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<section>
|
||||||
|
<h2>_{MsgHomeOpenCourses}
|
||||||
|
^{courseTable}
|
||||||
3
templates/home/upcomingSheets.hamlet
Normal file
3
templates/home/upcomingSheets.hamlet
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<section>
|
||||||
|
<h2>_{MsgHomeUpcomingSheets}
|
||||||
|
^{sheetTable}
|
||||||
@ -1,17 +0,0 @@
|
|||||||
<div .container>
|
|
||||||
<h2>
|
|
||||||
Anstehende Übungsblätter
|
|
||||||
<div .container>
|
|
||||||
^{sheetTable}
|
|
||||||
|
|
||||||
<!--
|
|
||||||
<div .container>
|
|
||||||
<h1>
|
|
||||||
Anstehende Klausuren
|
|
||||||
TODO
|
|
||||||
|
|
||||||
<div .container>
|
|
||||||
<h1>
|
|
||||||
Anstehende Kursanmeldungen
|
|
||||||
TODO
|
|
||||||
-->
|
|
||||||
@ -1,47 +1,18 @@
|
|||||||
|
$newline text
|
||||||
|
<section>
|
||||||
|
UniWorX erfahrene Veranstalter finden
|
||||||
|
hier die wichtigsten Neuerungen.
|
||||||
|
|
||||||
UniWorX erfahrene Veranstalter finden
|
|
||||||
hier die wichtigsten Neuerungen.
|
|
||||||
<section>
|
<section>
|
||||||
<h2>Bekannte Probleme in Bearbeitung
|
<h2>Bekannte Probleme in Bearbeitung
|
||||||
|
|
||||||
<dl .deflist>
|
<dl .deflist>
|
||||||
|
<dt .deflist__dt>Derzeit keine bekannt.
|
||||||
|
|
||||||
|
|
||||||
$#
|
$#
|
||||||
$# MOVE ITEM TO SECTION "VERANSTALTUNGEN", once it is implemented:
|
$# MOVE ITEM TO SECTION "VERANSTALTUNGEN", once it is implemented:
|
||||||
$#
|
$#
|
||||||
<dt .deflist__dt> Kurs Assistenten
|
|
||||||
<dd .deflist__dd>
|
|
||||||
Momentan ist leider nur ein Dozent/Veranstalter pro Kurs erlaubt.
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<h4>Folgendes ist in Vorbereitung:
|
|
||||||
Kurs-Veranstalter dürfen <em>beliebige</em> Personen
|
|
||||||
ebenfalls zu Veranstaltern des Kurses machen.
|
|
||||||
|
|
||||||
Innerhalb des Kurses haben alle Kurs-Veranstalter die
|
|
||||||
gleichen Befugnisse und können insbesondere auch die
|
|
||||||
Liste der Veranstalter dieses Kurses bearbeiten.
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<h4>Unterschied zu UniWorX:
|
|
||||||
|
|
||||||
In Uni2work gibt es die Rollen "Dozent"
|
|
||||||
und "Veranstalter":
|
|
||||||
Dozenten dürfen im Wesentlichen neue Kurse erstellen.
|
|
||||||
Veranstalter haben vollen Zugriff auf einen speziellen Kurs.
|
|
||||||
|
|
||||||
Die Dozenten Berechtigung wird nach Instituten unterschieden.
|
|
||||||
|
|
||||||
<p>
|
|
||||||
In UniWorX gab es die Rolle "Assistent",
|
|
||||||
d.h. alle "Veranstalter" mussten auch "Dozent" sein;
|
|
||||||
eine Unterscheidung nach Instituten gab es nicht.
|
|
||||||
|
|
||||||
<dt .deflist__dt> Kurs Teilnehmer
|
|
||||||
<dd .deflist__dd>
|
|
||||||
Anzeige und Benachrichtigung angemeldeter
|
|
||||||
Kurs-Teilnehmer ist leider noch nicht fertig implementiert.
|
|
||||||
Voraussichtlich vor Start des Sommersemesters 2019 verfügbar.
|
|
||||||
|
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2>Veranstaltungen
|
<h2>Veranstaltungen
|
||||||
@ -74,6 +45,54 @@ hier die wichtigsten Neuerungen.
|
|||||||
<dt .deflist__dt> Kurs Passwort
|
<dt .deflist__dt> Kurs Passwort
|
||||||
<dd .deflist__dd> Die Anmeldung zum Kurs kann durch ein Passwort geschützt werden.
|
<dd .deflist__dd> Die Anmeldung zum Kurs kann durch ein Passwort geschützt werden.
|
||||||
|
|
||||||
|
<dt .deflist__dt> Kurs Assistenten
|
||||||
|
<dd .deflist__dd>
|
||||||
|
<p>
|
||||||
|
Kurs-Veranstalter dürfen <em>beliebige</em> Personen
|
||||||
|
ebenfalls zu Veranstaltern des Kurses machen.
|
||||||
|
|
||||||
|
Innerhalb des Kurses haben alle Kurs-Veranstalter die
|
||||||
|
gleichen Befugnisse und können insbesondere auch die
|
||||||
|
Liste der Veranstalter dieses Kurses bearbeiten.
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<h4>Unterschied zu UniWorX:
|
||||||
|
|
||||||
|
In Uni2work gibt es die Rollen "Dozent"
|
||||||
|
und "Assistent":
|
||||||
|
Dozenten dürfen im Wesentlichen neue Kurse erstellen.
|
||||||
|
Die Dozenten Berechtigung wird nach Instituten unterschieden.
|
||||||
|
|
||||||
|
Assistenten haben nur Zugriff auf einen speziellen Kurs,
|
||||||
|
aber innerhalb dieses Kurses die gleichen Rechte wie Dozenten.
|
||||||
|
|
||||||
|
<p>
|
||||||
|
In UniWorX gab es die Rolle "Assistent",
|
||||||
|
d.h. alle "Veranstalter" mussten auch "Dozent" sein;
|
||||||
|
eine Unterscheidung nach Instituten gab es nicht.
|
||||||
|
|
||||||
|
<dt .deflist__dt> Kurs Teilnehmer
|
||||||
|
<dd .deflist__dd>
|
||||||
|
<p>
|
||||||
|
Für die Teilnehmer eines Kurses werden nun Studiengangsinformationen angzeigt.
|
||||||
|
Studierende mit mehreren simultanen Studiengängen müssen bei der
|
||||||
|
Kursanmeldung ein Hauptfach auswählen, was die Notenmeldung beschleunigen kann.
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Falls Anstatt eines Studienganges oder eines Studienabschlusses nur eine
|
||||||
|
Nummer angzeigt wird, so hat Uni2work die Zuordnung dieser Schlüsselnummern
|
||||||
|
leider noch nicht erlernt. Dies muss leider sukzessive erfolgen, da wir
|
||||||
|
von der Studentenkanzelei keine aktuelle und vollständige Schlüsselzuordnung
|
||||||
|
bekommen können.
|
||||||
|
|
||||||
|
<dt .deflist__dt> Aus Studentensicht
|
||||||
|
<dd .deflist__dd>
|
||||||
|
<p>
|
||||||
|
UniWorX hatte spezielle Links "Aus Studentensicht", welche in Uni2work überflüssig geworden sind.
|
||||||
|
Stattdessen kann man sich in Uni2work #
|
||||||
|
<a href=@{AuthPredsR}>Berechtigungen hier temporär selbst entziehen
|
||||||
|
. Um die eigene Veranstaltung aus Sicht eines Teilnehmers zu sehen, deaktiviert man #
|
||||||
|
die Berechtigungsprüfungen "_{MsgAuthTagLecturer}" und/oder "_{MsgAuthTagCorrector}"
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2>Übungsbetrieb
|
<h2>Übungsbetrieb
|
||||||
@ -108,14 +127,18 @@ hier die wichtigsten Neuerungen.
|
|||||||
<dt .deflist__dt> Lösungshinweise
|
<dt .deflist__dt> Lösungshinweise
|
||||||
<dd .deflist__dd>
|
<dd .deflist__dd>
|
||||||
Zusätzlich zu Aufgabe und Lösung können Hinweise ab einem
|
Zusätzlich zu Aufgabe und Lösung können Hinweise ab einem
|
||||||
Datum vor Abgabfrist freigeschaltet werden,
|
Datum vor Ende des Abgabezeitraums freigeschaltet werden,
|
||||||
z.B. Lösungen zu Präsenzaufgaben.
|
z.B. Lösungen zu Präsenzaufgaben.
|
||||||
|
|
||||||
<dt .deflist__dt> Sichtbarkeit
|
<dt .deflist__dt> Sichtbarkeit
|
||||||
<dd .deflist__dd>
|
<dd .deflist__dd>
|
||||||
Übungsblätter können bis zu einem Datum vor den Teilnehmern versteckt werden.
|
|
||||||
<p>
|
<p>
|
||||||
Die Aufgabenstellung ist erst mit Eröffnung der Abgabe erhältlich,
|
Übungsblätter können bis zu einem Datum "Sichtabr ab" vor allen Teilnehmern versteckt werden.
|
||||||
|
Das kann nützlich sein, um Tutoren und Korrektoren ein provisorisches Übungsblatt verfügbar zu machen,
|
||||||
|
dessen Bewertungsmodalitäten und Fristen sich noch ändern können.
|
||||||
|
<p>
|
||||||
|
Erst wenn das Blatt sichtbar wird, sehen die Teilnehmer in Ihrer Übersichtsliste.
|
||||||
|
Alle Dateien zur Aufgabenstellung sind aber erst mit Beginn des Abgabezeitraums erhältlich,
|
||||||
so wie bisher in UniWorX auch.
|
so wie bisher in UniWorX auch.
|
||||||
|
|
||||||
<dt .deflist__dt> Zeitstempel
|
<dt .deflist__dt> Zeitstempel
|
||||||
@ -173,4 +196,4 @@ hier die wichtigsten Neuerungen.
|
|||||||
Planmäßige Wartungen werden ohne Ankündigung
|
Planmäßige Wartungen werden ohne Ankündigung
|
||||||
immer um 2:00h nachts durchgeführt.
|
immer um 2:00h nachts durchgeführt.
|
||||||
Es wird daher empfohlen, keine kritischen Abgabefristen
|
Es wird daher empfohlen, keine kritischen Abgabefristen
|
||||||
um oder kurz nach dieser Zeit einzustellen.
|
um oder kurz nach dieser Zeit einzustellen.
|
||||||
|
|||||||
@ -7,7 +7,7 @@
|
|||||||
<dt .deflist__dt> _{MsgMatrikelNr}
|
<dt .deflist__dt> _{MsgMatrikelNr}
|
||||||
<dd .deflist__dd> #{matnr}
|
<dd .deflist__dd> #{matnr}
|
||||||
<dt .deflist__dt> _{MsgEMail}
|
<dt .deflist__dt> _{MsgEMail}
|
||||||
<dd .deflist__dd> #{display userEmail}
|
<dd .deflist__dd> #{mailtoHtml userEmail}
|
||||||
<dt .deflist__dt> _{MsgIdent}
|
<dt .deflist__dt> _{MsgIdent}
|
||||||
<dd .deflist__dd> #{display userIdent}
|
<dd .deflist__dd> #{display userIdent}
|
||||||
<dt .deflist__dt> _{MsgLastLogin}
|
<dt .deflist__dt> _{MsgLastLogin}
|
||||||
@ -17,7 +17,7 @@
|
|||||||
$nothing
|
$nothing
|
||||||
_{MsgNever}
|
_{MsgNever}
|
||||||
$if not $ null admin_rights
|
$if not $ null admin_rights
|
||||||
<dt .deflist__dt> Administrator
|
<dt .deflist__dt>_{MsgAdminFor}
|
||||||
<dd .deflist__dd>
|
<dd .deflist__dd>
|
||||||
<ul .list-ul>
|
<ul .list-ul>
|
||||||
$forall (E.Value institute) <- admin_rights
|
$forall (E.Value institute) <- admin_rights
|
||||||
@ -25,7 +25,7 @@
|
|||||||
<a href=@{SchoolShowR $ SchoolKey institute}>
|
<a href=@{SchoolShowR $ SchoolKey institute}>
|
||||||
#{display institute}
|
#{display institute}
|
||||||
$if not $ null lecturer_rights
|
$if not $ null lecturer_rights
|
||||||
<dt .deflist__dt> Lehrberechtigt
|
<dt .deflist__dt>_{MsgLecturerFor}
|
||||||
<dd .deflist__dd>
|
<dd .deflist__dd>
|
||||||
<ul .list-ul>
|
<ul .list-ul>
|
||||||
$forall (E.Value institute) <- lecturer_rights
|
$forall (E.Value institute) <- lecturer_rights
|
||||||
@ -45,13 +45,12 @@
|
|||||||
<div .scrolltable>
|
<div .scrolltable>
|
||||||
<table .table.table--striped.table--hover.table--condensed>
|
<table .table.table--striped.table--hover.table--condensed>
|
||||||
<tr .table__row>
|
<tr .table__row>
|
||||||
<th .table__th> Studiengang
|
<th .table__th>_{MsgStudyTerm}
|
||||||
<th .table__th> Abschluss
|
<th .table__th>_{MsgStudyFeatureDegree}
|
||||||
<th .table__th> Studienart
|
<th .table__th>_{MsgStudyFeatureType}
|
||||||
<th .table__th> Semester
|
<th .table__th>_{MsgStudyFeatureAge}
|
||||||
<th .table__th> Aktiv
|
<th .table__th>_{MsgStudyFeatureValid}
|
||||||
<th .table__th> Update
|
<th .table__th>_{MsgStudyFeatureUpdate}
|
||||||
|
|
||||||
|
|
||||||
$forall ((Entity _ StudyFeatures{..}), (Entity _ degree), (Entity _ field)) <- studies
|
$forall ((Entity _ StudyFeatures{..}), (Entity _ degree), (Entity _ field)) <- studies
|
||||||
$with _ <- notUsedT studyFeaturesUser
|
$with _ <- notUsedT studyFeaturesUser
|
||||||
@ -115,20 +114,28 @@
|
|||||||
<a href=@{CorrectionsR}>Auflistung aller tatsächlich zugewiesenen Korrekturen
|
<a href=@{CorrectionsR}>Auflistung aller tatsächlich zugewiesenen Korrekturen
|
||||||
.
|
.
|
||||||
|
|
||||||
<h2>
|
|
||||||
^{modal "Alle Benutzerbezogenen Daten löschen" (Right delWdgt)}
|
<section>
|
||||||
<p>
|
<h2>_{MsgRemarks}
|
||||||
<h4>Hinweise:
|
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
Sichern Sie Ihre Daten! Während des Testbetriebs von Uni2work
|
Sichern Sie bitte Ihre Daten! Die Uni2work Datenbank wird täglich gesichert;
|
||||||
könnten Daten unabsichtlich unwidderuflich gelöscht werden.
|
dennoch können wir Probleme während des Testbetriebs noch nicht gänzlich ausschließen.
|
||||||
<li>
|
<li>
|
||||||
Nicht aufgeführt sind Zeitstempel mit Benutzerinformationen, z.B. bei der Editierung und Korrektur von Übungen, Übungsgruppenleiterschaft, Raumbuchungen, etc.
|
Nicht aufgeführt sind Zeitstempel mit Benutzerinformationen, z.B. bei der Editierung und Korrektur von Übungen, Übungsgruppenleiterschaft, Raumbuchungen, etc.
|
||||||
<li>
|
<li>
|
||||||
Benutzerdaten bleiben so lange gespeichert, bis ein Institutsadministrator über die Exmatrikulation informiert wurde. Dann wird der Account gelöscht.
|
Sie können die
|
||||||
Abgaben/Bonuspunkte werden unwiderruflich gelöscht.
|
<a href=@{HelpR}>
|
||||||
Klausurnoten verbleiben aus statistischen Gründen anonymisiert im System.
|
Löschung Ihre Daten über eine Supportanfrage beantragen
|
||||||
|
. Ihre Daten werden dann nach Ablauf einer Frist gelöscht.
|
||||||
|
Daten, welche keiner gesetzlichen Aufbewahrungsfrist unterliegen
|
||||||
|
(z.B. Klausurnoten) verbleiben im System bis zur Ablauf der Aufbewahrungsfrist.
|
||||||
|
<p>
|
||||||
|
Benutzerdaten bleiben prinzipiell so lange gespeichert,
|
||||||
|
bis ein Institutsadministrator über die Exmatrikulation informiert wurde.
|
||||||
|
Dann wird der Account mit einer angemessenen zeitverzögerung gelöscht.
|
||||||
|
Anonymisierte Klausurnoten verbleiben aus statistischen Gründen dauerhaft im System.
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
Bei gemeinsamen Gruppenabgaben wird nur die Zuordnung zu diesem Benutzer gelöscht.
|
Bei gemeinsamen Gruppenabgaben wird nur die Zuordnung zu diesem Benutzer gelöscht.
|
||||||
Die Abgabe selbst wird erst gelöscht, wenn alle Benutzer einer Abgabe deren Löschung veranlasst haben.
|
Die Abgabe selbst wird erst gelöscht, wenn alle Benutzer einer Abgabe deren Löschung veranlasst haben.
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
.table-filter {
|
.table-filter {
|
||||||
border-bottom: 1px solid #d3d3d3;
|
|
||||||
margin-bottom: 13px;
|
margin-bottom: 13px;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,7 +7,7 @@
|
|||||||
<h2>
|
<h2>
|
||||||
Bekannte Bugs
|
Bekannte Bugs
|
||||||
<h3>
|
<h3>
|
||||||
Stand: Februar 2019
|
Stand: März 2019
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
Login ist u.U. anders als im alten System, z.B. momentan geht nur <span style="font-family:monospace">@campus.lmu.de</span> aber nicht die Abkürzung <span style="font-family:monospace">@lmu.de</span>
|
Login ist u.U. anders als im alten System, z.B. momentan geht nur <span style="font-family:monospace">@campus.lmu.de</span> aber nicht die Abkürzung <span style="font-family:monospace">@lmu.de</span>
|
||||||
|
|||||||
@ -3,7 +3,7 @@ $newline never
|
|||||||
$case formLayout
|
$case formLayout
|
||||||
$of FormDBTablePagesize
|
$of FormDBTablePagesize
|
||||||
$forall view <- fieldViews
|
$forall view <- fieldViews
|
||||||
<label .form-group__label.label-pagesize for=#{fvId view}>#{fvLabel view}
|
<label .form-group-label.label-pagesize for=#{fvId view}>#{fvLabel view}
|
||||||
^{fvInput view}
|
^{fvInput view}
|
||||||
$of _
|
$of _
|
||||||
$forall view <- fieldViews
|
$forall view <- fieldViews
|
||||||
@ -13,10 +13,11 @@ $case formLayout
|
|||||||
$else
|
$else
|
||||||
<div .form-group :fvRequired view:.form-group--required :not $ fvRequired view:.form-group--optional :isJust $ fvErrors view:.form-group--has-error>
|
<div .form-group :fvRequired view:.form-group--required :not $ fvRequired view:.form-group--optional :isJust $ fvErrors view:.form-group--has-error>
|
||||||
$if not (Blaze.null $ fvLabel view)
|
$if not (Blaze.null $ fvLabel view)
|
||||||
<label .form-group__label for=#{fvId view}>
|
<label .form-group-label for=#{fvId view}>
|
||||||
#{fvLabel view}
|
<span .form-group-label__caption>
|
||||||
|
#{fvLabel view}
|
||||||
$maybe hint <- fvTooltip view
|
$maybe hint <- fvTooltip view
|
||||||
<div .form-group__hint>^{hint}
|
<div .form-group-label__hint>^{hint}
|
||||||
<div .form-group__input>
|
<div .form-group__input>
|
||||||
^{fvInput view}
|
^{fvInput view}
|
||||||
$maybe err <- fvErrors view
|
$maybe err <- fvErrors view
|
||||||
|
|||||||
@ -1,15 +1,21 @@
|
|||||||
<h2>
|
<h2>
|
||||||
Sind Sie sich absolut sicher, alle Ihre in Uni2work gespeicherten Daten zu löschen?
|
Sind Sie sich absolut sicher
|
||||||
|
Benutzer ^{nameEmailWidget userEmail userDisplayName userSurname} zu löschen?
|
||||||
<p>
|
<p>
|
||||||
Während der Testphase von Uni2work können Sie hiermit
|
Während der Testphase von Uni2work
|
||||||
Ihren Account bei Uni2work vollständig löschen.
|
werden Benutzer hiermit vollständig aus der Live-Datenbank mit
|
||||||
Mit Ihrem Campus-Account können Sie sich aber danach
|
<code>DELETE CASCADE uid
|
||||||
jederzeit erneut einloggen, wodurch wieder ein leerer Account erstellt wird.
|
gelöscht.
|
||||||
|
Klausurdaten müssen jedoch unbedingt 5 Jahre bis nach Exmatrikulation
|
||||||
|
aufbewahrt werden!
|
||||||
|
<p>
|
||||||
|
Benutzer können sich mit Ihrem Campus-Account
|
||||||
|
natürlich jederzeit erneut einloggen, wodurch wieder ein leerer Account erstellt wird.
|
||||||
<p>
|
<p>
|
||||||
Hochgeladene Hausaufgaben-Dateien werden unabhhängig vom Urherber nur dann gelöscht,
|
Hochgeladene Hausaufgaben-Dateien werden unabhhängig vom Urherber nur dann gelöscht,
|
||||||
wenn die Dateien ausschließlich Ihnen zugeordnet sind.
|
wenn die Dateien ausschließlich diesem Benutzer zugeordnet waren.
|
||||||
Dateien aus Gruppenabgaben werden also erst dann gelöscht,
|
Dateien aus Gruppenabgaben werden also erst dann gelöscht,
|
||||||
wenn alle Gruppenmitglieder Ihren Account gelöscht haben.
|
wenn die Accounts alle Gruppenmitglieder gelöscht wurden.
|
||||||
<p>
|
<p>
|
||||||
<em>Achtung:
|
<em>Achtung:
|
||||||
Auch abgegebene Hausübungen werden gelöscht!
|
Auch abgegebene Hausübungen werden gelöscht!
|
||||||
@ -18,9 +24,4 @@
|
|||||||
(Verbuchte Noten sollten dadurch nicht betroffen sein, aber in einem etwaigen
|
(Verbuchte Noten sollten dadurch nicht betroffen sein, aber in einem etwaigen
|
||||||
Streitfall konnen die per Uni2work verwalteten Hausaufgaben dann
|
Streitfall konnen die per Uni2work verwalteten Hausaufgaben dann
|
||||||
auch nicht mehr rekonstruiert/berücksichtigt werden.)
|
auch nicht mehr rekonstruiert/berücksichtigt werden.)
|
||||||
<p>
|
|
||||||
<em>Nach der Testphase von Uni2work wird das Löschen eines Accounts etwas
|
|
||||||
eingeschränkt werden, da z.B. Klausurnoten 5 Jahre bis nach Exmatrikulation
|
|
||||||
aufbewahrt werden müssen.
|
|
||||||
|
|
||||||
^{btnForm}
|
^{btnForm}
|
||||||
|
|||||||
@ -1,17 +1,19 @@
|
|||||||
$newline never
|
$newline never
|
||||||
$# Wrapper for all kinds of forms
|
$# Wrapper for all kinds of forms
|
||||||
<form ##{formId} method=#{decodeUtf8 (renderStdMethod formMethod)} action=#{fromMaybe "" formActionUrl} enctype=#{formEncoding} *{formAttrs}>
|
<section>
|
||||||
$# Distinguish different falvours of submit button layouts here:
|
<form ##{formId} method=#{decodeUtf8 (renderStdMethod formMethod)} action=#{fromMaybe "" formActionUrl} enctype=#{formEncoding} *{formAttrs}>
|
||||||
$case formSubmit
|
$# Distinguish different falvours of submit button layouts here:
|
||||||
$of FormNoSubmit
|
$case formSubmit
|
||||||
^{formWidget}
|
$of FormNoSubmit
|
||||||
$of FormSubmit
|
^{formWidget}
|
||||||
^{formWidget}
|
$of FormSubmit
|
||||||
^{submitButtonView}
|
^{formWidget}
|
||||||
$of FormDualSubmit
|
^{submitButtonView}
|
||||||
^{submitButtonView}
|
$of FormDualSubmit
|
||||||
^{formWidget}
|
^{submitButtonView}
|
||||||
^{submitButtonView}
|
^{formWidget}
|
||||||
$of FormAutoSubmit
|
^{submitButtonView}
|
||||||
<button type=submit data-autosubmit>
|
$of FormAutoSubmit
|
||||||
^{btnLabel BtnSubmit}
|
^{formWidget}
|
||||||
|
<button type=submit data-autosubmit>
|
||||||
|
^{btnLabel BtnSubmit}
|
||||||
|
|||||||
3
templates/widgets/massinput/list/cell.hamlet
Normal file
3
templates/widgets/massinput/list/cell.hamlet
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
$newline never
|
||||||
|
#{csrf}
|
||||||
|
^{fvInput}
|
||||||
@ -1,4 +1,5 @@
|
|||||||
<div .modal.js-modal #modal-#{modalId} data-trigger=#{triggerId} data-closeable :modalDynamic:data-dynamic>
|
$newline never
|
||||||
|
<div .modal.js-modal #modal-#{modalId'} data-trigger=#{triggerId'} data-closeable :isDynamic:data-dynamic>
|
||||||
$case modalContent
|
$case modalContent
|
||||||
$of Right content
|
$of Right content
|
||||||
<div .modal__content>
|
<div .modal__content>
|
||||||
|
|||||||
@ -1,8 +1,5 @@
|
|||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
var modalIdent = #{String modalId};
|
var modal = document.querySelector('#modal-' + #{String modalId'});
|
||||||
var selector = '#modal-' + modalIdent;
|
|
||||||
var modal = document.querySelector(selector);
|
|
||||||
|
|
||||||
if (modal) {
|
if (modal) {
|
||||||
window.utils.setup('modal', modal);
|
window.utils.setup('modal', modal);
|
||||||
}
|
}
|
||||||
|
|||||||
7
templates/widgets/modal/trigger.hamlet
Normal file
7
templates/widgets/modal/trigger.hamlet
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
$newline never
|
||||||
|
$maybe route <- mRoute
|
||||||
|
<a .modal__trigger href=#{route} ##{triggerId}>
|
||||||
|
<span .modal__trigger-label>^{modalTrigger'}
|
||||||
|
$nothing
|
||||||
|
<div .modal__trigger ##{triggerId}>
|
||||||
|
<span .modal__trigger-label>^{modalTrigger'}
|
||||||
3
templates/widgets/navbar/item.hamlet
Normal file
3
templates/widgets/navbar/item.hamlet
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<a .navbar__link-wrapper href=#{route} ##{menuIdent}>
|
||||||
|
<i .fas.fa-#{fromMaybe "none" menuItemIcon}>
|
||||||
|
<div .navbar__link-label>_{SomeMessage menuItemLabel}
|
||||||
@ -8,34 +8,31 @@ $newline never
|
|||||||
<li .navbar__list-item.navbar__list-item--favorite>
|
<li .navbar__list-item.navbar__list-item--favorite>
|
||||||
<a .navbar__link-wrapper href="#">
|
<a .navbar__link-wrapper href="#">
|
||||||
<i .fas.fa-star>
|
<i .fas.fa-star>
|
||||||
<div .navbar__link-label> Favorites
|
<div .navbar__link-label>_{MsgNavigationFavourites}
|
||||||
|
|
||||||
$forall (MenuItem{menuItemType, menuItemRoute, menuItemIcon, menuItemLabel, menuItemModal}, menuIdent, route) <- menuTypes
|
$forall (menuItem@MenuItem{menuItemType, menuItemRoute, menuItemModal}, menuIdent, _) <- menuTypes
|
||||||
$case menuItemType
|
$case menuItemType
|
||||||
$of NavbarAside
|
$of NavbarAside
|
||||||
<li .navbar__list-item :highlight (urlRoute menuItemRoute):.navbar__list-item--active>
|
<li .navbar__list-item :highlight (urlRoute menuItemRoute):.navbar__list-item--active>
|
||||||
$if menuItemModal
|
$if menuItemModal
|
||||||
<div .modal.js-modal #modal-#{menuIdent} data-trigger=#{menuIdent} data-closeable data-dynamic>
|
^{navbarModal (menuItem, menuIdent)}
|
||||||
<a .navbar__link-wrapper href=#{route} ##{menuIdent}>
|
$else
|
||||||
<i .fas.fa-#{fromMaybe "none" menuItemIcon}>
|
^{navbarItem (menuItem, menuIdent)}
|
||||||
<div .navbar__link-label>_{SomeMessage menuItemLabel}
|
|
||||||
$of _
|
$of _
|
||||||
|
|
||||||
<ul .navbar__list.list--inline>
|
<ul .navbar__list.list--inline>
|
||||||
$forall (MenuItem{menuItemType, menuItemRoute, menuItemIcon, menuItemLabel, menuItemModal}, menuIdent, route) <- menuTypes
|
$forall (menuItem@MenuItem{menuItemType, menuItemRoute, menuItemModal}, menuIdent, _) <- menuTypes
|
||||||
$case menuItemType
|
$case menuItemType
|
||||||
$of NavbarRight
|
$of NavbarRight
|
||||||
<li .navbar__list-item :highlight (urlRoute menuItemRoute):.navbar__list-item--active>
|
<li .navbar__list-item :highlight (urlRoute menuItemRoute):.navbar__list-item--active>
|
||||||
$if menuItemModal
|
$if menuItemModal
|
||||||
<div .modal.js-modal #modal-#{menuIdent} data-trigger=#{menuIdent} data-closeable data-dynamic>
|
^{navbarModal (menuItem, menuIdent)}
|
||||||
<a .navbar__link-wrapper href=#{route} ##{menuIdent}>
|
$else
|
||||||
<i .fas.fa-#{fromMaybe "none" menuItemIcon}>
|
^{navbarItem (menuItem, menuIdent)}
|
||||||
<div .navbar__link-label>_{SomeMessage menuItemLabel}
|
|
||||||
$of NavbarSecondary
|
$of NavbarSecondary
|
||||||
<li .navbar__list-item :highlight (urlRoute menuItemRoute):.navbar__list-item--active>
|
<li .navbar__list-item :highlight (urlRoute menuItemRoute):.navbar__list-item--active>
|
||||||
$if menuItemModal
|
$if menuItemModal
|
||||||
<div .modal.js-modal #modal-#{menuIdent} data-trigger=#{menuIdent} data-closeable data-dynamic>
|
^{navbarModal (menuItem, menuIdent)}
|
||||||
<a .navbar__link-wrapper href=#{route} ##{menuIdent}>
|
$else
|
||||||
<i .fas.fa-#{fromMaybe "none" menuItemIcon}>
|
^{navbarItem (menuItem, menuIdent)}
|
||||||
<div .navbar__link-label>_{SomeMessage menuItemLabel}
|
|
||||||
$of _
|
$of _
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
$# Display Rating, expects
|
$# Display Rating, expects
|
||||||
|
$# sub :: Submission
|
||||||
|
$# submissionRatingDone :: Submission -> Bool
|
||||||
$# submissionRatingPoints :: Maybe points
|
$# submissionRatingPoints :: Maybe points
|
||||||
|
|
||||||
$maybe points <- submissionRatingPoints
|
$if submissionRatingDone sub
|
||||||
$maybe grading <- preview _grading sheetType
|
$maybe (grading, points) <- mTuple (preview _grading sheetType) submissionRatingPoints
|
||||||
$case grading
|
$case grading
|
||||||
$of Points{..}
|
$of Points{..}
|
||||||
_{MsgAchievedOf points maxPoints}
|
_{MsgAchievedOf points maxPoints}
|
||||||
|
|||||||
2
test.sh
2
test.sh
@ -1,3 +1,3 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
exec -- stack build --test --fast --flag uniworx:dev --flag uniworx:library-only ${@}
|
exec -- stack build --test --coverage --fast --flag uniworx:dev --flag uniworx:library-only ${@}
|
||||||
|
|||||||
@ -390,8 +390,8 @@ fillDb = do
|
|||||||
insert_ $ CourseEdit jost now ffp
|
insert_ $ CourseEdit jost now ffp
|
||||||
void . insert $ DegreeCourse ffp sdBsc sdInf
|
void . insert $ DegreeCourse ffp sdBsc sdInf
|
||||||
void . insert $ DegreeCourse ffp sdMst sdInf
|
void . insert $ DegreeCourse ffp sdMst sdInf
|
||||||
void . insert $ Lecturer jost ffp
|
void . insert $ Lecturer jost ffp CourseLecturer
|
||||||
void . insert $ Lecturer gkleen ffp
|
void . insert $ Lecturer gkleen ffp CourseAssistant
|
||||||
adhoc <- insert $ Sheet ffp "AdHoc-Gruppen" Nothing NotGraded (Arbitrary 3) Nothing Nothing now now Nothing Nothing (Upload True) UserSubmissions False
|
adhoc <- insert $ Sheet ffp "AdHoc-Gruppen" Nothing NotGraded (Arbitrary 3) Nothing Nothing now now Nothing Nothing (Upload True) UserSubmissions False
|
||||||
insert_ $ SheetEdit gkleen now adhoc
|
insert_ $ SheetEdit gkleen now adhoc
|
||||||
feste <- insert $ Sheet ffp "Feste Gruppen" Nothing NotGraded RegisteredGroups Nothing Nothing now now Nothing Nothing (Upload True) UserSubmissions False
|
feste <- insert $ Sheet ffp "Feste Gruppen" Nothing NotGraded RegisteredGroups Nothing Nothing now now Nothing Nothing (Upload True) UserSubmissions False
|
||||||
@ -421,7 +421,7 @@ fillDb = do
|
|||||||
}
|
}
|
||||||
insert_ $ CourseEdit fhamann now eip
|
insert_ $ CourseEdit fhamann now eip
|
||||||
void . insert' $ DegreeCourse eip sdBsc sdInf
|
void . insert' $ DegreeCourse eip sdBsc sdInf
|
||||||
void . insert' $ Lecturer fhamann eip
|
void . insert' $ Lecturer fhamann eip CourseLecturer
|
||||||
-- interaction design
|
-- interaction design
|
||||||
ixd <- insert' Course
|
ixd <- insert' Course
|
||||||
{ courseName = "Interaction Design (User Experience Design I & II)"
|
{ courseName = "Interaction Design (User Experience Design I & II)"
|
||||||
@ -439,7 +439,7 @@ fillDb = do
|
|||||||
}
|
}
|
||||||
insert_ $ CourseEdit fhamann now ixd
|
insert_ $ CourseEdit fhamann now ixd
|
||||||
void . insert' $ DegreeCourse ixd sdBsc sdInf
|
void . insert' $ DegreeCourse ixd sdBsc sdInf
|
||||||
void . insert' $ Lecturer fhamann ixd
|
void . insert' $ Lecturer fhamann ixd CourseAssistant
|
||||||
-- concept development
|
-- concept development
|
||||||
ux3 <- insert' Course
|
ux3 <- insert' Course
|
||||||
{ courseName = "Concept Development (User Experience Design III)"
|
{ courseName = "Concept Development (User Experience Design III)"
|
||||||
@ -457,7 +457,7 @@ fillDb = do
|
|||||||
}
|
}
|
||||||
insert_ $ CourseEdit fhamann now ux3
|
insert_ $ CourseEdit fhamann now ux3
|
||||||
void . insert' $ DegreeCourse ux3 sdBsc sdInf
|
void . insert' $ DegreeCourse ux3 sdBsc sdInf
|
||||||
void . insert' $ Lecturer fhamann ux3
|
void . insert' $ Lecturer fhamann ux3 CourseAssistant
|
||||||
-- promo
|
-- promo
|
||||||
pmo <- insert' Course
|
pmo <- insert' Course
|
||||||
{ courseName = "Programmierung und Modellierung"
|
{ courseName = "Programmierung und Modellierung"
|
||||||
@ -475,7 +475,7 @@ fillDb = do
|
|||||||
}
|
}
|
||||||
insert_ $ CourseEdit jost now pmo
|
insert_ $ CourseEdit jost now pmo
|
||||||
void . insert $ DegreeCourse pmo sdBsc sdInf
|
void . insert $ DegreeCourse pmo sdBsc sdInf
|
||||||
void . insert $ Lecturer jost pmo
|
void . insert $ Lecturer jost pmo CourseAssistant
|
||||||
void . insertMany $ map (\(u,sf) -> CourseParticipant pmo u now sf)
|
void . insertMany $ map (\(u,sf) -> CourseParticipant pmo u now sf)
|
||||||
[(fhamann , Nothing)
|
[(fhamann , Nothing)
|
||||||
,(maxMuster , Just sfMMp)
|
,(maxMuster , Just sfMMp)
|
||||||
@ -534,5 +534,5 @@ fillDb = do
|
|||||||
insert_ $ CourseEdit gkleen now dbs
|
insert_ $ CourseEdit gkleen now dbs
|
||||||
void . insert' $ DegreeCourse dbs sdBsc sdInf
|
void . insert' $ DegreeCourse dbs sdBsc sdInf
|
||||||
void . insert' $ DegreeCourse dbs sdBsc sdMath
|
void . insert' $ DegreeCourse dbs sdBsc sdMath
|
||||||
void . insert' $ Lecturer gkleen dbs
|
void . insert' $ Lecturer gkleen dbs CourseLecturer
|
||||||
void . insert' $ Lecturer jost dbs
|
void . insert' $ Lecturer jost dbs CourseAssistant
|
||||||
|
|||||||
@ -140,6 +140,10 @@ instance Arbitrary AuthenticationMode where
|
|||||||
|
|
||||||
shrink AuthLDAP = []
|
shrink AuthLDAP = []
|
||||||
shrink (AuthPWHash _) = [AuthLDAP]
|
shrink (AuthPWHash _) = [AuthLDAP]
|
||||||
|
|
||||||
|
instance Arbitrary LecturerType where
|
||||||
|
arbitrary = genericArbitrary
|
||||||
|
shrink = genericShrink
|
||||||
|
|
||||||
|
|
||||||
spec :: Spec
|
spec :: Spec
|
||||||
@ -199,6 +203,8 @@ spec = do
|
|||||||
[ eqLaws, ordLaws, showReadLaws, boundedEnumLaws, finiteLaws, hashableLaws, jsonLaws, pathPieceLaws, jsonKeyLaws ]
|
[ eqLaws, ordLaws, showReadLaws, boundedEnumLaws, finiteLaws, hashableLaws, jsonLaws, pathPieceLaws, jsonKeyLaws ]
|
||||||
lawsCheckHspec (Proxy @AuthTagActive)
|
lawsCheckHspec (Proxy @AuthTagActive)
|
||||||
[ eqLaws, ordLaws, showReadLaws, jsonLaws, persistFieldLaws ]
|
[ eqLaws, ordLaws, showReadLaws, jsonLaws, persistFieldLaws ]
|
||||||
|
lawsCheckHspec (Proxy @LecturerType)
|
||||||
|
[ eqLaws, ordLaws, showReadLaws, boundedEnumLaws, finiteLaws, jsonLaws, pathPieceLaws, persistFieldLaws ]
|
||||||
|
|
||||||
describe "TermIdentifier" $ do
|
describe "TermIdentifier" $ do
|
||||||
it "has compatible encoding/decoding to/from Text" . property $
|
it "has compatible encoding/decoding to/from Text" . property $
|
||||||
|
|||||||
Reference in New Issue
Block a user