Merge branch 'master' into course-teaser

This commit is contained in:
Sarah Vaupel 2019-07-24 10:42:39 +02:00
commit f742450912
68 changed files with 1824 additions and 393 deletions

View File

@ -2,6 +2,104 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
## [4.2.0](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/compare/v4.1.2...v4.2.0) (2019-07-23)
### Bug Fixes
* **exam registration:** icons added to exam register message ([ce61528](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/ce61528))
* **exams:** change heading to rooms if no occurrence times are shown ([5cb9404](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/5cb9404))
* fix build ([caf4092](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/caf4092))
### Features
* **csv:** finish implementing csv import ([e35fed6](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/e35fed6))
* **csv:** implement csv import ([996bc2a](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/996bc2a))
* **exams:** allow assigning exam participants to occurrences ([e1996ac](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/e1996ac))
### [4.1.2](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/compare/v4.1.1...v4.1.2) (2019-07-17)
### Bug Fixes
* **corrections:** properly link corrector emails ([9385595](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/9385595))
### [4.1.1](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/compare/v4.1.0...v4.1.1) (2019-07-17)
### Bug Fixes
* **aform:** show info about required fields in all aforms ([63f6d01](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/63f6d01)), closes [#418](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/issues/418)
* **submissions:** only notify submittors if rating changes doneness ([4f1162c](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/4f1162c))
* **submissions:** only notify submittors if rating is done ([8e0c379](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/8e0c379))
* **submissions:** submitting produces an success alert now ([bf20d6f](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/bf20d6f)), closes [#286](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/issues/286)
## [4.1.0](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/compare/v4.0.1...v4.1.0) (2019-07-17)
### Features
* **exams:** allow forced deregistration ([1b532c4](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/1b532c4))
### [4.0.1](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/compare/v4.0.0...v4.0.1) (2019-07-16)
### Bug Fixes
* **exams:** fix caculation of maximum exercise points ([a9e74ca](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/a9e74ca))
## [4.0.0](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/compare/v3.0.0...v4.0.0) (2019-07-16)
### Features
* **csv:** add column explanations ([c8dca94](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/c8dca94))
### BREAKING CHANGES
* **csv:** CsvColumnsExplained now required
## [3.0.0](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/compare/v2.1.1...v3.0.0) (2019-07-16)
### Bug Fixes
* **course and exam registration:** distinguish registrations buttons ([ad825b6](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/ad825b6)), closes [#416](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/issues/416)
* **exam participant download:** fix icon not being shown ([a075b16](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/a075b16))
* **exams:** cleanup exam interface ([05e7b52](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/05e7b52))
* **sheet type info:** give better tooltips and name to sheet types ([9dbef1f](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/9dbef1f)), closes [#402](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/issues/402)
### Features
* **exams:** csv-export exercise data ([2218103](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/2218103))
* **exams:** filter on occurrence ([cf040ce](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/cf040ce))
* **exams:** introduce examOccurrenceName ([379a7ed](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/379a7ed))
* **exams:** show exam bonus in webinterface ([2b23600](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/2b23600))
* **sheetlist:** sort sheet file types in db by haskell Ord ([643cc41](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/643cc41))
### BREAKING CHANGES
* **exams:** examOccurrenceName
* **exams:** examStart and examPublishOccurrenceAssignments now optional
### [2.1.1](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/compare/v2.1.0...v2.1.1) (2019-07-10) ### [2.1.1](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/compare/v2.1.0...v2.1.1) (2019-07-10)

View File

@ -1,4 +1,5 @@
import { Utility } from '../../core/utility'; import { Utility } from '../../core/utility';
import './file-input.scss';
const FILE_INPUT_CLASS = 'file-input'; const FILE_INPUT_CLASS = 'file-input';
const FILE_INPUT_INITIALIZED_CLASS = 'file-input--initialized'; const FILE_INPUT_INITIALIZED_CLASS = 'file-input--initialized';

View File

@ -0,0 +1,3 @@
.file-input__list:empty {
display: none;
}

View File

@ -25,6 +25,11 @@
color: var(--color-fontsec); color: var(--color-fontsec);
} }
.form-section-legend {
color: var(--color-fontsec);
margin: 7px 0;
}
.form-group-label { .form-group-label {
font-weight: 600; font-weight: 600;
padding-top: 6px; padding-top: 6px;
@ -36,11 +41,9 @@
font-size: 0.9rem; font-size: 0.9rem;
} }
.form-group--required { .form-group--required .form-group-label__caption::after, .form-group__required-marker::before {
.form-group-label__caption::after { content: ' *';
content: ' *'; color: var(--color-error);
color: var(--color-error);
}
} }
.form-group--optional { .form-group--optional {

View File

@ -83,6 +83,10 @@
cursor: pointer; cursor: pointer;
} }
div.modal__trigger {
display: inline-block;
}
.modal__trigger-label { .modal__trigger-label {
font-style: italic; font-style: italic;
text-decoration: underline; text-decoration: underline;

View File

@ -5,6 +5,10 @@ BtnAbort: Abbrechen
BtnDelete: Löschen BtnDelete: Löschen
BtnRegister: Anmelden BtnRegister: Anmelden
BtnDeregister: Abmelden BtnDeregister: Abmelden
BtnCourseRegister: Zum Kurs anmelden
BtnCourseDeregister: Vom Kurs abmelden
BtnExamRegister: Anmelden zur Klausur
BtnExamDeregister: Von der Klausur abmelden
BtnHijack: Sitzung übernehmen BtnHijack: Sitzung übernehmen
BtnSave: Speichern BtnSave: Speichern
PressSaveToSave: Änderungen werden erst durch Drücken des Knopfes "Speichern" gespeichert. PressSaveToSave: Änderungen werden erst durch Drücken des Knopfes "Speichern" gespeichert.
@ -87,6 +91,7 @@ CourseDeregisterOk: Erfolgreich abgemeldet
CourseDeregisterLecturerTip: Wenn Sie den Teilnehmer vom Kurs abmelden kann es sein, dass sie Zugriff auf diese Daten verlieren CourseDeregisterLecturerTip: Wenn Sie den Teilnehmer vom Kurs abmelden kann es sein, dass sie Zugriff auf diese Daten verlieren
CourseStudyFeature: Assoziiertes Hauptfach CourseStudyFeature: Assoziiertes Hauptfach
CourseStudyFeatureUpdated: Assoziiertes Hauptfach geändert CourseStudyFeatureUpdated: Assoziiertes Hauptfach geändert
CourseStudyFeatureNone: Kein assoziiertes Hauptfach
CourseTutorial: Tutorium CourseTutorial: Tutorium
CourseStudyFeatureTooltip: Korrekte Angabe kann Notenweiterleitungen beschleunigen CourseStudyFeatureTooltip: Korrekte Angabe kann Notenweiterleitungen beschleunigen
CourseSecretWrong: Falsches Kennwort CourseSecretWrong: Falsches Kennwort
@ -435,7 +440,9 @@ HasCorrector: Korrektor zugeteilt
AssignedTime: Zuteilung AssignedTime: Zuteilung
AchievedBonusPoints: Erreichte Bonuspunkte AchievedBonusPoints: Erreichte Bonuspunkte
AchievedNormalPoints: Erreichte Punkte AchievedNormalPoints: Erreichte Punkte
AchievedPassPoints: Erreichte Punkte AchievedPoints: Erreichte Punkte
AchievedPassPoints: Erreichte Punkte zum Bestehen
AchievedPasses: Bestandene Blätter
AchievedOf achieved@Points possible@Points: #{achieved} von #{possible} AchievedOf achieved@Points possible@Points: #{achieved} von #{possible}
PassAchievedOf points@Points passingPoints@Points maxPoints@Points: #{points} von #{maxPoints} (Bestanden ab #{passingPoints}) PassAchievedOf points@Points passingPoints@Points maxPoints@Points: #{points} von #{maxPoints} (Bestanden ab #{passingPoints})
PassedResult: Ergebnis PassedResult: Ergebnis
@ -521,6 +528,7 @@ NotificationSettings: Erwünschte Benachrichtigungen
FormNotifications: Benachrichtigungen FormNotifications: Benachrichtigungen
FormBehaviour: Verhalten FormBehaviour: Verhalten
FormCosmetics: Oberfläche FormCosmetics: Oberfläche
FormFieldRequiredTip: Gekennzeichnete Pflichtfelder sind immer auszufüllen
ActiveAuthTags: Aktivierte Authorisierungsprädikate ActiveAuthTags: Aktivierte Authorisierungsprädikate
@ -580,6 +588,8 @@ SheetCorrectorSubmissionsTip: Abgabe erfolgt über ein Uni2work-externes Verfahr
SubmissionNoUploadExpected: Es ist keine Abgabe von Dateien vorgesehen. SubmissionNoUploadExpected: Es ist keine Abgabe von Dateien vorgesehen.
SubmissionReplace: Abgabe ersetzen SubmissionReplace: Abgabe ersetzen
SubmissionCreated: Abgabe erfolgreich angelegt
SubmissionUpdated: Abgabe erfolgreich ersetzt
AdminFeaturesHeading: Studiengänge AdminFeaturesHeading: Studiengänge
StudyTerms: Studiengänge StudyTerms: Studiengänge
@ -660,7 +670,7 @@ MailSubjectCorrectorInvitation tid@TermId ssh@SchoolId csh@CourseShorthand shn@S
MailSubjectTutorInvitation tid@TermId ssh@SchoolId csh@CourseShorthand tutn@TutorialName: [#{tid}-#{ssh}-#{csh}] Einladung zum Tutor für #{tutn} MailSubjectTutorInvitation tid@TermId ssh@SchoolId csh@CourseShorthand tutn@TutorialName: [#{tid}-#{ssh}-#{csh}] Einladung zum Tutor für #{tutn}
MailSubjectExamCorrectorInvitation tid@TermId ssh@SchoolId csh@CourseShorthand examn@ExamName: [#{tid}-#{ssh}-#{csh}] Einladung zum Korrektor für Klausur #{examn} MailSubjectExamCorrectorInvitation tid@TermId ssh@SchoolId csh@CourseShorthand examn@ExamName: [#{tid}-#{ssh}-#{csh}] Einladung zum Korrektor für #{examn}
MailSubjectSubmissionUserInvitation tid@TermId ssh@SchoolId csh@CourseShorthand shn@SheetName: [#{tid}-#{ssh}-#{csh}] Einladung zu einer Abgabe für #{shn} MailSubjectSubmissionUserInvitation tid@TermId ssh@SchoolId csh@CourseShorthand shn@SheetName: [#{tid}-#{ssh}-#{csh}] Einladung zu einer Abgabe für #{shn}
@ -678,10 +688,11 @@ SheetGradingPassBinary': Bestanden/Nicht bestanden
SheetTypeBonus grading@SheetGrading: Bonus SheetTypeBonus grading@SheetGrading: Bonus
SheetTypeNormal grading@SheetGrading: Normal SheetTypeNormal grading@SheetGrading: Normal
SheetTypeInformational grading@SheetGrading: Keine Wertung SheetTypeInformational grading@SheetGrading: Ohne Anrechung
SheetTypeNotGraded: Unbewertet SheetTypeNotGraded: Keine Korrektur
SheetTypeInfoNotGraded: Blätter ohne Wertung werden nirgends angerechnet, die Bewertung durch den Korrektor dient lediglich zur Information der Teilnehmer. SheetTypeInfoNotGraded: Keine Korrektur bedeutet, dass es gar kein Feedback gibt.
SheetTypeInfoBonus: Bonus Blätter zählen normal, erhöhen aber nicht die maximal erreichbare Punktzahl bzw. Anzahl zu bestehender Blätter. SheetTypeInfoBonus: Bonus Blätter zählen normal, erhöhen aber nicht die maximal erreichbare Punktzahl bzw. Anzahl zu bestehender Blätter.
SheetTypeInfoInformational: Blätter ohne Anrechnung werden nirgends angerechnet, die Bewertung durch den Korrektor dient lediglich zur Information der Teilnehmer.
SheetGradingBonusIncluded: Erzielte Bonuspunkte wurden hier bereits zu den erreichten normalen Punkten hinzugezählt. SheetGradingBonusIncluded: Erzielte Bonuspunkte wurden hier bereits zu den erreichten normalen Punkten hinzugezählt.
SummaryTitle: Zusammenfassung über SummaryTitle: Zusammenfassung über
SheetGradingSummaryTitle intgr@Integer: #{intgr} #{pluralDE intgr "Blatt" "Blätter"} SheetGradingSummaryTitle intgr@Integer: #{intgr} #{pluralDE intgr "Blatt" "Blätter"}
@ -689,8 +700,8 @@ SubmissionGradingSummaryTitle intgr@Integer: #{intgr} #{pluralDE intgr "Abgabe"
SheetTypeBonus': Bonus SheetTypeBonus': Bonus
SheetTypeNormal': Normal SheetTypeNormal': Normal
SheetTypeInformational': Keine Wertung SheetTypeInformational': Ohne Anrechung
SheetTypeNotGraded': Unbewertet SheetTypeNotGraded': Keine Korrektur
SheetGradingMaxPoints: Maximalpunktzahl SheetGradingMaxPoints: Maximalpunktzahl
SheetGradingPassingPoints: Notwendig zum Bestehen SheetGradingPassingPoints: Notwendig zum Bestehen
@ -927,6 +938,8 @@ CommTutorialHeading: Tutorium-Mitteilung
RecipientCustom: Weitere Empfänger RecipientCustom: Weitere Empfänger
RecipientToggleAll: Alle/Keine RecipientToggleAll: Alle/Keine
DBCsvImportActionToggleAll: Alle/Keine
RGCourseParticipants: Kursteilnehmer RGCourseParticipants: Kursteilnehmer
RGCourseLecturers: Kursverwalter RGCourseLecturers: Kursverwalter
RGCourseCorrectors: Korrektoren RGCourseCorrectors: Korrektoren
@ -960,9 +973,9 @@ TutorInvitationDeclined tutn@TutorialName: Sie haben die Einladung, Tutor für #
TutorInviteHeading tutn@TutorialName: Einladung zum Tutor für #{tutn} TutorInviteHeading tutn@TutorialName: Einladung zum Tutor für #{tutn}
TutorInviteExplanation: Sie wurden eingeladen, Tutor zu sein. TutorInviteExplanation: Sie wurden eingeladen, Tutor zu sein.
ExamCorrectorInvitationAccepted examn@ExamName: Sie wurden als Korrektor für Klausur #{examn} eingetragen ExamCorrectorInvitationAccepted examn@ExamName: Sie wurden als Korrektor für #{examn} eingetragen
ExamCorrectorInvitationDeclined examn@ExamName: Sie haben die Einladung, Korrektor für Klausur #{examn} zu werden, abgelehnt ExamCorrectorInvitationDeclined examn@ExamName: Sie haben die Einladung, Korrektor für #{examn} zu werden, abgelehnt
ExamCorrectorInviteHeading examn@ExamName: Einladung zum Korrektor für Klausur #{examn} ExamCorrectorInviteHeading examn@ExamName: Einladung zum Korrektor für #{examn}
ExamCorrectorInviteExplanation: Sie wurden eingeladen, Klausur-Korrektor zu sein. ExamCorrectorInviteExplanation: Sie wurden eingeladen, Klausur-Korrektor zu sein.
SubmissionUserInvitationAccepted shn@SheetName: Sie wurden als Mitabgebende(r) für eine Abgabe zu #{shn} eingetragen SubmissionUserInvitationAccepted shn@SheetName: Sie wurden als Mitabgebende(r) für eine Abgabe zu #{shn} eingetragen
@ -1056,7 +1069,7 @@ HealthWidgetMemcached: Memcached-Server liefert Widgets korrekt aus
CourseParticipants n@Int: Derzeit #{n} angemeldete Kursteilnehmer CourseParticipants n@Int: Derzeit #{n} angemeldete Kursteilnehmer
CourseParticipantsInvited n@Int: #{n} #{pluralDE n "Einladung" "Einladungen"} per E-Mail verschickt CourseParticipantsInvited n@Int: #{n} #{pluralDE n "Einladung" "Einladungen"} per E-Mail verschickt
CourseParticipantsAlreadyRegistered n@Int: #{n} Teilnehmer #{pluralDE n "ist" "sind"} bereits angemeldet CourseParticipantsAlreadyRegistered n@Int: #{n} Teilnehmer #{pluralDE n "ist" "sind"} bereits angemeldet
CourseParticipantsRegisteredWithoutField n@Int: #{n} Teilnehmer #{pluralDE n "wurde ohne assoziiertes Hauptfach" "wurden assoziierte Hauptfächer"} angemeldet, da #{pluralDE n "kein eindeutiges Hauptfach bestimmt werden konnte" "keine eindeutigen Hauptfächer bestimmt werden konnten"} CourseParticipantsRegisteredWithoutField n@Int: #{n} Teilnehmer #{pluralDE n "wurde ohne assoziiertes Hauptfach" "wurden ohne assoziierte Hauptfächer"} angemeldet, da #{pluralDE n "kein eindeutiges Hauptfach bestimmt werden konnte" "keine eindeutigen Hauptfächer bestimmt werden konnten"}
CourseParticipantsRegistered n@Int: #{n} Teilnehmer erfolgreich angemeldet CourseParticipantsRegistered n@Int: #{n} Teilnehmer erfolgreich angemeldet
CourseParticipantsRegisterHeading: Kursteilnehmer hinzufügen CourseParticipantsRegisterHeading: Kursteilnehmer hinzufügen
@ -1073,9 +1086,9 @@ ExamRegisterFrom: Anmeldung ab
ExamRegisterFromTip: Zeitpunkt ab dem sich Kursteilnehmer selbständig zur Klausur anmelden können; ohne Datum ist keine Anmeldung möglich ExamRegisterFromTip: Zeitpunkt ab dem sich Kursteilnehmer selbständig zur Klausur anmelden können; ohne Datum ist keine Anmeldung möglich
ExamRegisterTo: Anmeldung bis ExamRegisterTo: Anmeldung bis
ExamDeregisterUntil: Abmeldung bis ExamDeregisterUntil: Abmeldung bis
ExamPublishOccurrenceAssignments: Terminzuteilung den Teilnehmern mitteilen um ExamPublishOccurrenceAssignments: Termin- bzw. Raumzuteilung den Teilnehmern mitteilen um
ExamPublishOccurrenceAssignmentsTip: Ab diesem Zeitpunkt Teilnehmer einsehen zu welchen Teilprüfungen (Räumen) sie angemeldet sind ExamPublishOccurrenceAssignmentsTip: Ab diesem Zeitpunkt Teilnehmer einsehen zu welcher Teilprüfung bzw. welchen Raum sie angemeldet sind
ExamPublishOccurrenceAssignmentsParticipant: Terminzuteilung einsehbar ab ExamPublishOccurrenceAssignmentsParticipant: Termin- bzw. Raumzuteilung einsehbar ab
ExamFinished: Bewertung abgeschlossen ab ExamFinished: Bewertung abgeschlossen ab
ExamFinishedParticipant: Bewertung vorrausichtlich abgeschlossen ExamFinishedParticipant: Bewertung vorrausichtlich abgeschlossen
ExamFinishedTip: Zeitpunkt zu dem Klausurergebnisse den Teilnehmern gemeldet werden ExamFinishedTip: Zeitpunkt zu dem Klausurergebnisse den Teilnehmern gemeldet werden
@ -1086,7 +1099,7 @@ ExamShowGradesTip: Soll den Teilnehmern ihre genaue Note angezeigt werden, oder
ExamPublicStatistics: Statistik veröffentlichen ExamPublicStatistics: Statistik veröffentlichen
ExamPublicStatisticsTip: Soll die statistische Auswertung auch den Teilnehmer angezeigt werden, sobald diese ihre Noten einsehen können? ExamPublicStatisticsTip: Soll die statistische Auswertung auch den Teilnehmer angezeigt werden, sobald diese ihre Noten einsehen können?
ExamGradingRule: Notenberechnung ExamGradingRule: Notenberechnung
ExamGradingManual': Manuell ExamGradingManual': Keine automatische Berechnung
ExamGradingKey': Nach Schlüssel ExamGradingKey': Nach Schlüssel
ExamGradingKey: Notenschlüssel ExamGradingKey: Notenschlüssel
ExamGradingKeyTip: Die Grenzen beziehen sich auf die effektive Maximalpunktzahl, nachdem etwaige Bonuspunkte aus dem Übungsbetrieb angerechnet und die Ergebnise der Teilaufgaben mit ihrem Gewicht multipliziert wurden ExamGradingKeyTip: Die Grenzen beziehen sich auf die effektive Maximalpunktzahl, nachdem etwaige Bonuspunkte aus dem Übungsbetrieb angerechnet und die Ergebnise der Teilaufgaben mit ihrem Gewicht multipliziert wurden
@ -1096,7 +1109,7 @@ PointsMustBeMonotonic: Punktegrenzen müssen aufsteigend sein
GradingFrom: Ab GradingFrom: Ab
ExamNew: Neue Klausur ExamNew: Neue Klausur
ExamBonusRule: Klausurbonus aus Übungsbetrieb ExamBonusRule: Klausurbonus aus Übungsbetrieb
ExamNoBonus': Kein Bonus ExamNoBonus': Kein automatischer Bonus
ExamBonusPoints': Umrechnung von Übungspunkten ExamBonusPoints': Umrechnung von Übungspunkten
ExamEditHeading examn@ExamName: #{examn} bearbeiten ExamEditHeading examn@ExamName: #{examn} bearbeiten
@ -1105,15 +1118,19 @@ ExamBonusMaxPoints: Maximal erreichbare Klausur-Bonuspunkte
ExamBonusMaxPointsNonPositive: Maximaler Klausurbonus muss positiv und größer null sein ExamBonusMaxPointsNonPositive: Maximaler Klausurbonus muss positiv und größer null sein
ExamBonusOnlyPassed: Bonus nur nach Bestehen anrechnen ExamBonusOnlyPassed: Bonus nur nach Bestehen anrechnen
ExamOccurrenceRule: Automatische Terminzuteilung ExamOccurrenceRule: Automatische Termin- bzw. Raumzuteilung
ExamOccurrenceRuleParticipant: Terminzuteilung ExamOccurrenceRuleParticipant: Termin- bzw. Raumzuteilung
ExamRoomManual': Keine automatische Zuteilung ExamRoomManual': Keine automatische Zuteilung
ExamRoomSurname': Nach Nachname ExamRoomSurname': Nach Nachname
ExamRoomMatriculation': Nach Matrikelnummer ExamRoomMatriculation': Nach Matrikelnummer
ExamRoomRandom': Zufällig pro Teilnehmer ExamRoomRandom': Zufällig pro Teilnehmer
ExamOccurrence: Termin/Raum
ExamNoOccurrence: Kein Termin/Raum
ExamOccurrences: Prüfungen ExamOccurrences: Prüfungen
ExamRooms: Räume
ExamRoomAlreadyExists: Prüfung ist bereits eingetragen ExamRoomAlreadyExists: Prüfung ist bereits eingetragen
ExamRoomName: Interne Bezeichnung
ExamRoom: Raum ExamRoom: Raum
ExamRoomCapacity: Kapazität ExamRoomCapacity: Kapazität
ExamRoomCapacityNegative: Kapazität darf nicht negativ sein ExamRoomCapacityNegative: Kapazität darf nicht negativ sein
@ -1125,7 +1142,7 @@ ExamTimeTip: Nur zur Information der Studierenden, die tatsächliche Zeitangabe
ExamRoomRegistered: Zugeteilt ExamRoomRegistered: Zugeteilt
ExamFormTimes: Zeiten ExamFormTimes: Zeiten
ExamFormOccurrences: Prüfungstermine ExamFormOccurrences: Prüfungstermine/Räume
ExamFormAutomaticFunctions: Automatische Funktionen ExamFormAutomaticFunctions: Automatische Funktionen
ExamFormCorrection: Korrektur ExamFormCorrection: Korrektur
ExamFormParts: Teile ExamFormParts: Teile
@ -1142,8 +1159,8 @@ ExamPartWeight: Gewichtung
ExamPartResultPoints: Erreichte Punkte ExamPartResultPoints: Erreichte Punkte
ExamNameTaken exam@ExamName: Es existiert bereits eine Klausur mit Namen #{exam} ExamNameTaken exam@ExamName: Es existiert bereits eine Klausur mit Namen #{exam}
ExamCreated exam@ExamName: Klausur #{exam} erfolgreich angelegt ExamCreated exam@ExamName: #{exam} erfolgreich angelegt
ExamEdited exam@ExamName: Klausur #{exam} erfolgreich bearbeitet ExamEdited exam@ExamName: #{exam} erfolgreich bearbeitet
ExamNoShow: Nicht erschienen ExamNoShow: Nicht erschienen
ExamVoided: Entwertet ExamVoided: Entwertet
@ -1155,15 +1172,15 @@ ExamPassed: Bestanden
ExamNotPassed: Nicht bestanden ExamNotPassed: Nicht bestanden
ExamResult: Klausurergebnis ExamResult: Klausurergebnis
ExamRegisteredSuccess exam@ExamName: Erfolgreich zur Klausur #{exam} angemeldet ExamRegisteredSuccess exam@ExamName: Erfolgreich zur #{exam} angemeldet
ExamDeregisteredSuccess exam@ExamName: Erfolgreich von der Klausur #{exam} abgemeldet ExamDeregisteredSuccess exam@ExamName: Erfolgreich von der #{exam} abgemeldet
ExamRegistered: Angemeldet ExamRegistered: Angemeldet
ExamNotRegistered: Nicht angemeldet ExamNotRegistered: Nicht angemeldet
ExamRegistration: Anmeldung ExamRegistration: Anmeldung
ExamRegisterToMustBeAfterRegisterFrom: "Anmeldung ab" muss vor "Anmeldung bis" liegen ExamRegisterToMustBeAfterRegisterFrom: "Anmeldung ab" muss vor "Anmeldung bis" liegen
ExamDeregisterUntilMustBeAfterRegisterFrom: "Abmeldung bis" muss nach "Anmeldung bis" liegen ExamDeregisterUntilMustBeAfterRegisterFrom: "Abmeldung bis" muss nach "Anmeldung bis" liegen
ExamStartMustBeAfterPublishOccurrenceAssignments: Start muss nach Veröffentlichung der Terminzuordnung liegen ExamStartMustBeAfterPublishOccurrenceAssignments: Start muss nach Veröffentlichung der Termin- bzw. Raumzuordnung liegen
ExamEndMustBeAfterStart: Beginn der Klausur muss vor ihrem Ende liegen ExamEndMustBeAfterStart: Beginn der Klausur muss vor ihrem Ende liegen
ExamFinishedMustBeAfterEnd: "Bewertung abgeschlossen ab" muss nach Ende liegen ExamFinishedMustBeAfterEnd: "Bewertung abgeschlossen ab" muss nach Ende liegen
ExamFinishedMustBeAfterStart: "Bewertung abgeschlossen ab" muss nach Start liegen ExamFinishedMustBeAfterStart: "Bewertung abgeschlossen ab" muss nach Start liegen
@ -1171,10 +1188,20 @@ ExamClosedMustBeAfterFinished: "Noten stehen fest ab" muss nach "Bewertung abges
ExamClosedMustBeAfterStart: "Noten stehen fest ab" muss nach Start liegen ExamClosedMustBeAfterStart: "Noten stehen fest ab" muss nach Start liegen
ExamClosedMustBeAfterEnd: "Noten stehen fest ab" muss nach Ende liegen ExamClosedMustBeAfterEnd: "Noten stehen fest ab" muss nach Ende liegen
ExamOccurrenceEndMustBeAfterStart eoName@ExamOccurrenceName: Beginn des Termins #{eoName} muss vor seinem Ende liegen
ExamOccurrenceStartMustBeAfterExamStart eoName@ExamOccurrenceName: Beginn des Termins #{eoName} muss nach Beginn der Klausur liegen
ExamOccurrenceEndMustBeBeforeExamEnd eoName@ExamOccurrenceName: Ende des Termins #{eoName} muss vor Ende der Klausur liegen
ExamOccurrenceDuplicate eoRoom@Text eoRange@Text: Raum #{eoRoom}, Termin #{eoRange} kommt mehrfach mit der selben Beschreibung vor
ExamOccurrenceDuplicateName eoName@ExamOccurrenceName: Interne Terminbezeichnung #{eoName} kommt mehrfach vor
VersionHistory: Versionsgeschichte VersionHistory: Versionsgeschichte
KnownBugs: Bekannte Bugs KnownBugs: Bekannte Bugs
ExamUsersHeading: Klausurteilnehmer ExamUsersHeading: Klausurteilnehmer
ExamUserDeregister: Teilnehmer von Klausur abmelden
ExamUserAssignOccurrence: Termin/Raum zuweisen
ExamUsersDeregistered count@Int64: #{show count} Teilnehmer abgemeldet
ExamUsersOccurrenceUpdated count@Int64: Termin/Raum für #{show count} Teilnehmer gesetzt
CsvFile: CSV-Datei CsvFile: CSV-Datei
CsvModifyExisting: Existierende Einträge angleichen CsvModifyExisting: Existierende Einträge angleichen
@ -1182,3 +1209,48 @@ CsvAddNew: Neue Einträge einfügen
CsvDeleteMissing: Fehlende Einträge entfernen CsvDeleteMissing: Fehlende Einträge entfernen
BtnCsvExport: CSV-Datei exportieren BtnCsvExport: CSV-Datei exportieren
BtnCsvImport: CSV-Datei importieren BtnCsvImport: CSV-Datei importieren
BtnCsvImportConfirm: CSV-Import abschließen
CsvImportNotConfigured: CSV-Import nicht vorgesehen
CsvImportConfirmationHeading: CSV-Import abschließen
CsvImportConfirmationTip: Durch den CSV-Import würden die unten aufgeführten Änderungen vorgenommen. Bitte überprüfen Sie diese zunächst sorgfältig.
CsvImportUnnecessary: Durch den CSV-Import würden keine Änderungen vorgenommen werden
CsvImportSuccessful n@Int: CSV-Import erfolgreich, es #{pluralDE n "wurde eine Aktion" (mappend (mappend "wurden " (toMessage n)) " Aktionen")} durchgeführt
CsvImportAborted: CSV-Import abgebrochen
Proportion c@Text of@Text prop@Rational: #{c}/#{of} (#{rationalToFixed2 (100 * prop)}%)
CsvColumnsExplanationsLabel: Spalten
CsvColumnsExplanationsTip: Bedeutung der in der CSV-Datei enthaltenen Spalten
CsvColumnExamUserSurname: Nachname des Teilnehmers
CsvColumnExamUserName: Voller Name des Teilnehmers (inkl. Nachname)
CsvColumnExamUserMatriculation: Matrikelnummer des Teilnehmers
CsvColumnExamUserField: Hauptfach, mit dem der Teilnehmer seine Kursanmeldung assoziiert hat
CsvColumnExamUserDegree: Abschluss, den der Teilnehmer im assoziierten Hauptfach anstrebt
CsvColumnExamUserSemester: Fachsemester des Teilnehmers im assoziierten Hauptfach
CsvColumnExamUserOccurrence: Prüfungstermin/-Raum, zu dem der Teilnehmer angemeldet ist
CsvColumnExamUserExercisePoints: Anzahl von Punkten, die der Teilnehmer im Übungsbetrieb erreicht hat
CsvColumnExamUserExercisePointsMax: Maximale Anzahl von Punkten, die der Teilnehmer im Übungsbetrieb bis zu seinem Klausurtermin erreichen hätte können
CsvColumnExamUserExercisePasses: Anzahl von Übungsblättern, die der Teilnehmer bestanden hat
CsvColumnExamUserExercisePassesMax: Maximale Anzahl von Übungsblättern, die der Teilnehmer bis zu seinem Klausurtermin bestehen hätte können
Action: Aktion
DBCsvDuplicateKey: Zwei Zeilen der CSV-Dateien referenzieren den selben internen Datensatz und können daher nicht verarbeitet werden.
DBCsvDuplicateKeyTip: Entfernen Sie eine der unten aufgeführten Zeilen aus Ihren CSV-Dateien und versuchen Sie es erneut.
DBCsvKeyException: Für eine Zeile der CSV-Dateien konnte nicht festgestellt werden, ob sie zu einem bestehenden internen Datensatz korrespondieren.
DBCsvException: Bei der Berechnung der auszuführenden Aktionen für einen Datensatz ist ein Fehler aufgetreten.
ExamUserCsvCourseRegister: Benutzer zum Kurs und zur Klausur anmelden
ExamUserCsvRegister: Kursteilnehmer zur Klausur anmelden
ExamUserCsvAssignOccurrence: Teilnehmern einen anderen Termin/Raum zuweisen
ExamUserCsvDeregister: Teilnehmer von der Klausur abmelden
ExamUserCsvSetCourseField: Kurs-assoziiertes Hauptfach ändern
ExamUserCsvExceptionNoMatchingUser: Kursteilnehmer konnte nicht eindeutig identifiziert werden
ExamUserCsvExceptionNoMatchingStudyFeatures: Das angegebene Studienfach konnte keinem Hauptfach des Kursteilnehmers zugeordnet werden
ExamUserCsvExceptionNoMatchingOccurrence: Raum/Termin konnte nicht eindeutig identifiziert werden
TableHeadingFilter: Filter
TableHeadingCsvImport: CSV-Import
TableHeadingCsvExport: CSV-Export

View File

@ -8,8 +8,8 @@ Exam
registerFrom UTCTime Maybe registerFrom UTCTime Maybe
registerTo UTCTime Maybe registerTo UTCTime Maybe
deregisterUntil UTCTime Maybe deregisterUntil UTCTime Maybe
publishOccurrenceAssignments UTCTime publishOccurrenceAssignments UTCTime Maybe
start UTCTime start UTCTime Maybe
end UTCTime Maybe end UTCTime Maybe
finished UTCTime Maybe -- Grades shown to students, `ExamCorrector`s locked out finished UTCTime Maybe -- Grades shown to students, `ExamCorrector`s locked out
closed UTCTime Maybe -- Prüfungsamt hat Einsicht (notification) closed UTCTime Maybe -- Prüfungsamt hat Einsicht (notification)
@ -25,11 +25,13 @@ ExamPart
UniqueExamPart exam name UniqueExamPart exam name
ExamOccurrence ExamOccurrence
exam ExamId exam ExamId
name ExamOccurrenceName
room Text room Text
capacity Natural capacity Natural
start UTCTime start UTCTime
end UTCTime Maybe end UTCTime Maybe
description Html Maybe description Html Maybe
UniqueExamOccurrence exam name
ExamRegistration ExamRegistration
exam ExamId exam ExamId
user UserId user UserId

2
package-lock.json generated
View File

@ -1,6 +1,6 @@
{ {
"name": "uni2work", "name": "uni2work",
"version": "2.1.1", "version": "4.2.0",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {

View File

@ -1,6 +1,6 @@
{ {
"name": "uni2work", "name": "uni2work",
"version": "2.1.1", "version": "4.2.0",
"description": "", "description": "",
"keywords": [], "keywords": [],
"author": "", "author": "",
@ -20,7 +20,9 @@
"frontend:test:watch": "karma start --conf karma.conf.js --single-run false", "frontend:test:watch": "karma start --conf karma.conf.js --single-run false",
"frontend:build": "webpack", "frontend:build": "webpack",
"frontend:build:watch": "webpack --watch", "frontend:build:watch": "webpack --watch",
"release": "standard-version -a" "prerelease": "npm run test",
"release": "standard-version -a",
"postrelease": "git push --follow-tags origin master"
}, },
"husky": { "husky": {
"hooks": { "hooks": {

View File

@ -1,5 +1,5 @@
name: uniworx name: uniworx
version: 2.1.1 version: 4.2.0
dependencies: dependencies:
# Due to a bug in GHC 8.0.1, we block its usage # Due to a bug in GHC 8.0.1, we block its usage

2
routes
View File

@ -143,7 +143,7 @@
/show EShowR GET !time /show EShowR GET !time
/edit EEditR GET POST /edit EEditR GET POST
/corrector-invite ECInviteR GET POST /corrector-invite ECInviteR GET POST
/users EUsersR GET POST !timeANDcorrector /users EUsersR GET POST
/users/new EAddUserR GET POST /users/new EAddUserR GET POST
/users/invite EInviteR GET POST /users/invite EInviteR GET POST
/register ERegisterR POST !timeANDcourse-registered !timeANDexam-registered /register ERegisterR POST !timeANDcourse-registered !timeANDexam-registered

View File

@ -32,6 +32,7 @@ dummyLogin :: ( YesodAuth site
, YesodPersist site , YesodPersist site
, SqlBackendCanRead (YesodPersistBackend site) , SqlBackendCanRead (YesodPersistBackend site)
, RenderMessage site FormMessage , RenderMessage site FormMessage
, RenderMessage site AFormMessage
, RenderMessage site DummyMessage , RenderMessage site DummyMessage
, Button site ButtonSubmit , Button site ButtonSubmit
) => AuthPlugin site ) => AuthPlugin site

View File

@ -73,6 +73,7 @@ campusLogin :: forall site.
( YesodAuth site ( YesodAuth site
, RenderMessage site FormMessage , RenderMessage site FormMessage
, RenderMessage site CampusMessage , RenderMessage site CampusMessage
, RenderMessage site AFormMessage
, Button site ButtonSubmit , Button site ButtonSubmit
) => LdapConf -> LdapPool -> AuthPlugin site ) => LdapConf -> LdapPool -> AuthPlugin site
campusLogin conf@LdapConf{..} pool = AuthPlugin{..} campusLogin conf@LdapConf{..} pool = AuthPlugin{..}

View File

@ -40,6 +40,7 @@ hashLogin :: ( YesodAuth site
, SqlBackendCanRead (YesodPersistBackend site) , SqlBackendCanRead (YesodPersistBackend site)
, RenderMessage site FormMessage , RenderMessage site FormMessage
, RenderMessage site PWHashMessage , RenderMessage site PWHashMessage
, RenderMessage site AFormMessage
, Button site ButtonSubmit , Button site ButtonSubmit
) => PWHashAlgorithm -> AuthPlugin site ) => PWHashAlgorithm -> AuthPlugin site
hashLogin pwHashAlgo = AuthPlugin{..} hashLogin pwHashAlgo = AuthPlugin{..}

View File

@ -29,6 +29,8 @@ import Web.HttpApiData
import Data.Binary (Binary) import Data.Binary (Binary)
import qualified Data.Binary as Binary import qualified Data.Binary as Binary
import qualified Data.Csv as Csv
instance PersistField (CI Text) where instance PersistField (CI Text) where
toPersistValue ciText = PersistDbSpecific . Text.encodeUtf8 $ CI.original ciText toPersistValue ciText = PersistDbSpecific . Text.encodeUtf8 $ CI.original ciText
@ -86,11 +88,11 @@ instance (CI.FoldCase s, PathPiece s) => PathPiece (CI s) where
fromPathPiece = fmap CI.mk . fromPathPiece fromPathPiece = fmap CI.mk . fromPathPiece
toPathPiece = toPathPiece . CI.original toPathPiece = toPathPiece . CI.original
instance ToHttpApiData (CI Text) where instance ToHttpApiData s => ToHttpApiData (CI s) where
toUrlPiece = toUrlPiece . CI.original toUrlPiece = toUrlPiece . CI.original
toEncodedUrlPiece = toEncodedUrlPiece . CI.original toEncodedUrlPiece = toEncodedUrlPiece . CI.original
instance FromHttpApiData (CI Text) where instance (CI.FoldCase s, FromHttpApiData s) => FromHttpApiData (CI s) where
parseUrlPiece = fmap CI.mk . parseUrlPiece parseUrlPiece = fmap CI.mk . parseUrlPiece
instance (CI.FoldCase s, PathMultiPiece s) => PathMultiPiece (CI s) where instance (CI.FoldCase s, PathMultiPiece s) => PathMultiPiece (CI s) where
@ -101,3 +103,9 @@ instance (CI.FoldCase s, Binary s) => Binary (CI s) where
get = CI.mk <$> Binary.get get = CI.mk <$> Binary.get
put = Binary.put . CI.original put = Binary.put . CI.original
putList = Binary.putList . map CI.original putList = Binary.putList . map CI.original
instance Csv.ToField s => Csv.ToField (CI s) where
toField = Csv.toField . CI.original
instance (CI.FoldCase s, Csv.FromField s) => Csv.FromField (CI s) where
parseField = fmap CI.mk . Csv.parseField

View File

@ -9,5 +9,18 @@ import Data.Fixed
import Text.Blaze (ToMarkup(..)) import Text.Blaze (ToMarkup(..))
import qualified Data.Csv as Csv
import Data.Proxy (Proxy(..))
import Data.Scientific
instance HasResolution a => ToMarkup (Fixed a) where instance HasResolution a => ToMarkup (Fixed a) where
toMarkup = toMarkup . showFixed True toMarkup = toMarkup . showFixed True
instance HasResolution a => Csv.ToField (Fixed a) where
toField = Csv.toField . (realToFrac :: Fixed a -> Scientific)
instance HasResolution a => Csv.FromField (Fixed a) where
parseField = fmap (MkFixed . (round :: Scientific -> Integer) . (* fromInteger (resolution $ Proxy @a))) . Csv.parseField

View File

@ -3,12 +3,13 @@
module Data.UUID.Instances module Data.UUID.Instances
() where () where
import ClassyPrelude import ClassyPrelude.Yesod
import Data.UUID (UUID) import Data.UUID (UUID)
import qualified Data.UUID as UUID import qualified Data.UUID as UUID
import Database.Persist.Sql import Database.Persist.Sql
import Web.PathPieces
import Text.Blaze (ToMarkup(..))
instance PathPiece UUID where instance PathPiece UUID where
@ -25,3 +26,13 @@ instance PersistField UUID where
instance PersistFieldSql UUID where instance PersistFieldSql UUID where
sqlType _ = SqlOther "uuid" sqlType _ = SqlOther "uuid"
instance ToMarkup UUID where
toMarkup uuid = [shamlet|
$newline never
<span .uuid>
#{UUID.toText uuid}
|]
instance ToWidget site UUID where
toWidget = toWidget . toMarkup

View File

@ -4,20 +4,30 @@ module Database.Esqueleto.Utils
( true, false ( true, false
, isJust , isJust
, isInfixOf, hasInfix , isInfixOf, hasInfix
, or, and
, any, all , any, all
, SqlIn(..) , SqlIn(..)
, mkExactFilter, mkExactFilterWith , mkExactFilter, mkExactFilterWith
, mkContainsFilter, mkContainsFilterWith , mkContainsFilter, mkContainsFilterWith
, mkExistsFilter , mkExistsFilter
, anyFilter, allFilter , anyFilter, allFilter
, orderByOrd, orderByEnum
, lower, ciEq
) where ) where
import ClassyPrelude.Yesod hiding (isInfixOf, any, all, isJust)
import ClassyPrelude.Yesod hiding (isInfixOf, any, all, or, and, isJust)
import Data.Universe
import qualified Data.Set as Set import qualified Data.Set as Set
import qualified Data.List as List
import qualified Data.Foldable as F import qualified Data.Foldable as F
import qualified Database.Esqueleto as E import qualified Database.Esqueleto as E
import qualified Database.Esqueleto.Internal.Sql as E
import Database.Esqueleto.Utils.TH import Database.Esqueleto.Utils.TH
{-# ANN any ("HLint: ignore Use any" :: String) #-}
{-# ANN all ("HLint: ignore Use all" :: String) #-}
-- --
-- Description : Convenience for using `Esqueleto`, -- Description : Convenience for using `Esqueleto`,
@ -54,17 +64,19 @@ hasInfix :: ( E.Esqueleto query expr backend
=> expr (E.Value s2) -> expr (E.Value s1) -> expr (E.Value Bool) => expr (E.Value s2) -> expr (E.Value s1) -> expr (E.Value Bool)
hasInfix = flip isInfixOf hasInfix = flip isInfixOf
and, or :: Foldable f => f (E.SqlExpr (E.Value Bool)) -> E.SqlExpr (E.Value Bool)
and = F.foldr (E.&&.) true
or = F.foldr (E.||.) false
-- | Given a test and a set of values, check whether anyone succeeds the test -- | Given a test and a set of values, check whether anyone succeeds the test
-- WARNING: SQL leaves it explicitely unspecified whether `||` is short curcuited (i.e. lazily evaluated) -- WARNING: SQL leaves it explicitely unspecified whether `||` is short curcuited (i.e. lazily evaluated)
any :: Foldable f => any :: MonoFoldable f => (Element f -> E.SqlExpr (E.Value Bool)) -> f -> E.SqlExpr (E.Value Bool)
(a -> E.SqlExpr (E.Value Bool)) -> f a -> E.SqlExpr (E.Value Bool) any test = or . map test . otoList
any test = F.foldr (\needle acc -> acc E.||. test needle) false
-- | Given a test and a set of values, check whether all succeeds the test -- | Given a test and a set of values, check whether all succeeds the test
-- WARNING: SQL leaves it explicitely unspecified whether `&&` is short curcuited (i.e. lazily evaluated) -- WARNING: SQL leaves it explicitely unspecified whether `&&` is short curcuited (i.e. lazily evaluated)
all :: Foldable f => all :: MonoFoldable f => (Element f -> E.SqlExpr (E.Value Bool)) -> f -> E.SqlExpr (E.Value Bool)
(a -> E.SqlExpr (E.Value Bool)) -> f a -> E.SqlExpr (E.Value Bool) all test = and . map test . otoList
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
@ -132,7 +144,7 @@ mkExistsFilter :: PathPiece a
-> E.SqlExpr (E.Value Bool) -> E.SqlExpr (E.Value Bool)
mkExistsFilter query row criterias mkExistsFilter query row criterias
| Set.null criterias = true | Set.null criterias = true
| otherwise = any (E.exists . query row) criterias | otherwise = any (E.exists . query row) $ Set.toList criterias
-- | Combine several filters, using logical or -- | Combine several filters, using logical or
anyFilter :: (Foldable f) anyFilter :: (Foldable f)
@ -153,3 +165,18 @@ allFilter :: (Foldable f)
allFilter fltrs needle criterias = F.foldr aux true fltrs allFilter fltrs needle criterias = F.foldr aux true fltrs
where where
aux fltr acc = fltr needle criterias E.&&. acc aux fltr acc = fltr needle criterias E.&&. acc
orderByOrd :: (Ord a, Finite a, PersistField a) => E.SqlExpr (E.Value a) -> E.SqlExpr (E.Value Int)
orderByOrd = let sortUni = zip [1..] $ List.sort universeF in -- memoize this, might not work due to polymorphism
\x -> E.case_ [ (x E.==. E.val u, E.val i) | (i,u) <- sortUni ] (E.val (-1))
orderByEnum :: (Enum a, Finite a, PersistField a) => E.SqlExpr (E.Value a) -> E.SqlExpr (E.Value Int)
orderByEnum x = E.case_ [ (x E.==. E.val u, E.val $ fromEnum u) | u <- universeF ] (E.val (-1))
lower :: E.SqlString s => E.SqlExpr (E.Value s) -> E.SqlExpr (E.Value s)
lower = E.unsafeSqlFunction "LOWER"
ciEq :: E.SqlString s => E.SqlExpr (E.Value s) -> E.SqlExpr (E.Value s) -> E.SqlExpr (E.Value Bool)
ciEq a b = lower a E.==. lower b

View File

@ -294,6 +294,8 @@ embedRenderMessage ''UniWorX ''SubmissionModeDescr
in verbMap . splitCamel in verbMap . splitCamel
embedRenderMessage ''UniWorX ''UploadModeDescr id embedRenderMessage ''UniWorX ''UploadModeDescr id
embedRenderMessage ''UniWorX ''SecretJSONFieldException id embedRenderMessage ''UniWorX ''SecretJSONFieldException id
embedRenderMessage ''UniWorX ''AFormMessage $ concat . drop 2 . splitCamel
newtype SheetTypeHeader = SheetTypeHeader SheetType newtype SheetTypeHeader = SheetTypeHeader SheetType
embedRenderMessageVariant ''UniWorX ''SheetTypeHeader ("SheetType" <>) embedRenderMessageVariant ''UniWorX ''SheetTypeHeader ("SheetType" <>)
@ -677,7 +679,7 @@ tagAccessPredicate AuthTime = APDB $ \mAuthId route _ -> case route of
case subRoute of case subRoute of
EShowR -> guard visible EShowR -> guard visible
EUsersR -> guard $ examStart <= cTime EUsersR -> guard $ NTop examStart <= NTop (Just cTime)
&& NTop (Just cTime) <= NTop examFinished && NTop (Just cTime) <= NTop examFinished
ERegisterR ERegisterR
| not registered -> guard $ visible | not registered -> guard $ visible

View File

@ -168,7 +168,7 @@ colSubmittors = sortable (Just "submittors") (i18nCell MsgSubmissionUsers) $ \DB
colSMatrikel :: IsDBTable m a => Colonnade Sortable CorrectionTableData (DBCell m a) colSMatrikel :: IsDBTable m a => Colonnade Sortable CorrectionTableData (DBCell m a)
colSMatrikel = sortable Nothing (i18nCell MsgMatrikelNr) $ \DBRow{ dbrOutput=(_, _, _, _, _, users) } -> let colSMatrikel = sortable Nothing (i18nCell MsgMatrikelNr) $ \DBRow{ dbrOutput=(_, _, _, _, _, users) } -> let
protoCell = listCell (Map.toList users) $ \(userId, (User{..}, _)) -> anchorCellM (AdminUserR <$> encrypt userId) (maybe mempty toWidget userMatrikelnummer) protoCell = listCell (Map.toList users) $ \(userId, (User{..}, _)) -> anchorCellM (AdminUserR <$> encrypt userId) (fromMaybe mempty userMatrikelnummer)
in protoCell & cellAttrs <>~ [("class", "list--inline list--comma-separated")] in protoCell & cellAttrs <>~ [("class", "list--inline list--comma-separated")]
colRating :: forall m a. IsDBTable m (a, SheetTypeSummary) => Colonnade Sortable CorrectionTableData (DBCell m (a, SheetTypeSummary)) colRating :: forall m a. IsDBTable m (a, SheetTypeSummary) => Colonnade Sortable CorrectionTableData (DBCell m (a, SheetTypeSummary))

View File

@ -47,6 +47,21 @@ import Control.Monad.Except (MonadError(..))
import Generics.Deriving.Monoid (memptydefault, mappenddefault) import Generics.Deriving.Monoid (memptydefault, mappenddefault)
-- Dedicated CourseRegistrationButton
data ButtonCourseRegister = BtnCourseRegister | BtnCourseDeregister
deriving (Enum, Eq, Ord, Bounded, Read, Show, Generic, Typeable)
instance Universe ButtonCourseRegister
instance Finite ButtonCourseRegister
nullaryPathPiece ''ButtonCourseRegister $ camelToPathPiece' 1
embedRenderMessage ''UniWorX ''ButtonCourseRegister id
instance Button UniWorX ButtonCourseRegister where
btnClasses BtnCourseRegister = [BCIsButton, BCPrimary]
btnClasses BtnCourseDeregister = [BCIsButton, BCDanger]
btnLabel BtnCourseRegister = [whamlet|#{iconEnrol True} _{MsgBtnCourseRegister}|]
btnLabel BtnCourseDeregister = [whamlet|#{iconEnrol False} _{MsgBtnCourseDeregister}|]
-- 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, [Entity User]) type CourseTableData = DBRow (Entity Course, Int, Bool, Entity School, [Entity User])
@ -341,7 +356,7 @@ getCShowR tid ssh csh = do
mRegTo <- traverse (formatTime SelFormatDateTime) $ courseRegisterTo course mRegTo <- traverse (formatTime SelFormatDateTime) $ courseRegisterTo course
mDereg <- traverse (formatTime SelFormatDateTime) $ courseDeregisterUntil course mDereg <- traverse (formatTime SelFormatDateTime) $ courseDeregisterUntil course
mRegAt <- traverse (formatTime SelFormatDateTime) $ courseParticipantRegistration <$> registration mRegAt <- traverse (formatTime SelFormatDateTime) $ courseParticipantRegistration <$> registration
(regWidget, regEnctype) <- generateFormPost $ registerForm mbAid registration defSFid $ courseRegisterSecret course (regWidget, regEnctype) <- generateFormPost $ courseRegisterForm mbAid registration defSFid $ courseRegisterSecret course
let regForm = wrapForm regWidget def let regForm = wrapForm regWidget def
{ formAction = Just . SomeRoute $ CourseR tid ssh csh CRegisterR { formAction = Just . SomeRoute $ CourseR tid ssh csh CRegisterR
, formEncoding = regEnctype , formEncoding = regEnctype
@ -433,33 +448,34 @@ getCShowR tid ssh csh = do
guardM . hasReadAccessTo $ CExamR tid ssh csh examName EShowR guardM . hasReadAccessTo $ CExamR tid ssh csh examName EShowR
return r return r
dbtColonnade = dbColonnade $ mconcat dbtColonnade = dbColonnade $ mconcat
[ sortable (Just "name") (i18nCell MsgExamName) $ \DBRow{ dbrOutput = Entity _ Exam{..} } -> indicatorCell <> anchorCell (CExamR tid ssh csh examName EShowR) (toWidget examName) [ sortable (Just "name") (i18nCell MsgExamName) $ \DBRow{ dbrOutput = Entity _ Exam{..} } -> indicatorCell <> anchorCell (CExamR tid ssh csh examName EShowR) examName
, sortable (Just "register-from") (i18nCell MsgExamRegisterFrom) $ \DBRow { dbrOutput = Entity _ Exam{..} } -> maybe mempty dateTimeCell examRegisterFrom , sortable (Just "register-from") (i18nCell MsgExamRegisterFrom) $ \DBRow { dbrOutput = Entity _ Exam{..} } -> maybe mempty dateTimeCell examRegisterFrom
, sortable (Just "register-to") (i18nCell MsgExamRegisterTo) $ \DBRow { dbrOutput = Entity _ Exam{..} } -> maybe mempty dateTimeCell examRegisterTo , sortable (Just "register-to") (i18nCell MsgExamRegisterTo) $ \DBRow { dbrOutput = Entity _ Exam{..} } -> maybe mempty dateTimeCell examRegisterTo
, sortable (Just "time") (i18nCell MsgExamTime) $ \DBRow{ dbrOutput = Entity _ Exam{..} } -> cell $ do , sortable (Just "time") (i18nCell MsgExamTime) $ \DBRow{ dbrOutput = Entity _ Exam{..} } -> maybe mempty (cell . flip (formatTimeRangeW SelFormatDateTime) examEnd) examStart
startT <- formatTime SelFormatDateTime examStart , sortable (Just "registered") (i18nCell MsgExamRegistration ) $ \DBRow{ dbrOutput = Entity eId Exam{..} } -> sqlCell $ do
endT <- traverse (\examEnd' -> formatTime (bool SelFormatDateTime SelFormatTime $ ((==) `on` utctDay) examStart examEnd') examEnd') examEnd
[whamlet|
$newline never
#{startT}
$maybe endT' <- endT
\ #{endT'}
|]
, sortable Nothing mempty $ \DBRow{ dbrOutput = Entity eId Exam{..} } -> sqlCell $ do
mayRegister <- (== Authorized) <$> evalAccessDB (CExamR tid ssh csh examName ERegisterR) True mayRegister <- (== Authorized) <$> evalAccessDB (CExamR tid ssh csh examName ERegisterR) True
isRegistered <- case mbAid of isRegistered <- case mbAid of
Nothing -> return False Nothing -> return False
Just uid -> existsBy $ UniqueExamRegistration eId uid Just uid -> existsBy $ UniqueExamRegistration eId uid
if let label = bool MsgExamNotRegistered MsgExamRegistered isRegistered
| mayRegister -> do examUrl = CExamR tid ssh csh examName EShowR
(examRegisterForm, examRegisterEnctype) <- liftHandlerT . generateFormPost . buttonForm' $ bool [BtnRegister] [BtnDeregister] isRegistered if | mayRegister -> return $ simpleLinkI (SomeMessage label) examUrl
return $ wrapForm examRegisterForm def | otherwise -> return [whamlet|_{label}|]
{ formAction = Just . SomeRoute $ CExamR tid ssh csh examName ERegisterR -- , sortable Nothing mempty $ \DBRow{ dbrOutput = Entity eId Exam{..} } -> sqlCell $ do
, formEncoding = examRegisterEnctype -- mayRegister <- (== Authorized) <$> evalAccessDB (CExamR tid ssh csh examName ERegisterR) True
, formSubmit = FormNoSubmit -- isRegistered <- case mbAid of
} -- Nothing -> return False
| isRegistered -> return [whamlet|_{MsgExamRegistered}|] -- Just uid -> existsBy $ UniqueExamRegistration eId uid
| otherwise -> return mempty -- if
-- | mayRegister -> do
-- (examRegisterForm, examRegisterEnctype) <- liftHandlerT . generateFormPost . buttonForm' $ bool [BtnExamRegister] [BtnExamDeregister] isRegistered
-- return $ wrapForm examRegisterForm def
-- { formAction = Just . SomeRoute $ CExamR tid ssh csh examName ERegisterR
-- , formEncoding = examRegisterEnctype
-- , formSubmit = FormNoSubmit
-- }
-- | isRegistered -> return [whamlet|_{MsgExamRegistered}|]
-- | otherwise -> return mempty
] ]
dbtSorting = Map.fromList dbtSorting = Map.fromList
[ ("name", SortColumn $ \exam -> exam E.^. ExamName ) [ ("name", SortColumn $ \exam -> exam E.^. ExamName )
@ -467,6 +483,14 @@ getCShowR tid ssh csh = do
, ("register-from", SortColumn $ \exam -> exam E.^. ExamRegisterFrom ) , ("register-from", SortColumn $ \exam -> exam E.^. ExamRegisterFrom )
, ("register-to", SortColumn $ \exam -> exam E.^. ExamRegisterTo ) , ("register-to", SortColumn $ \exam -> exam E.^. ExamRegisterTo )
, ("visible", SortColumn $ \exam -> exam E.^. ExamVisibleFrom ) , ("visible", SortColumn $ \exam -> exam E.^. ExamVisibleFrom )
, ("registered", SortColumn $ \exam ->
case mbAid of
Nothing -> E.false
Just uid ->
E.exists $ E.from $ \reg -> do
E.where_ $ reg E.^. ExamRegistrationUser E.==. E.val uid
E.where_ $ reg E.^. ExamRegistrationExam E.==. exam E.^. ExamId
)
] ]
dbtFilter = Map.empty dbtFilter = Map.empty
dbtFilterUI = const mempty dbtFilterUI = const mempty
@ -489,9 +513,9 @@ getCShowR tid ssh csh = do
-- , maybe existing features if already registered -- , maybe existing features if already registered
-- , maybe some default study features -- , maybe some default study features
-- , maybe a course secret -- , maybe a course secret
registerForm :: Maybe UserId -> Maybe CourseParticipant -> Maybe StudyFeaturesId -> Maybe Text -> Form (Maybe StudyFeaturesId, Bool) courseRegisterForm :: Maybe UserId -> Maybe CourseParticipant -> Maybe StudyFeaturesId -> Maybe Text -> Form (Maybe StudyFeaturesId, Bool)
-- unfinished WIP: must take study features if registred and show as mforced field -- unfinished WIP: must take study features if registred and show as mforced field
registerForm loggedin participant defSFid msecret = identifyForm FIDcourseRegister $ \extra -> do courseRegisterForm loggedin participant defSFid msecret = identifyForm FIDcourseRegister $ \extra -> do
-- secret fields -- secret fields
(msecretRes', msecretView) <- case msecret of (msecretRes', msecretView) <- case msecret of
(Just _) | not isRegistered -> bimap Just Just <$> mreq textField (fslpI MsgCourseSecret "Code") Nothing (Just _) | not isRegistered -> bimap Just Just <$> mreq textField (fslpI MsgCourseSecret "Code") Nothing
@ -505,7 +529,7 @@ registerForm loggedin participant defSFid msecret = identifyForm FIDcourseRegist
_other -> mreq (studyFeaturesPrimaryFieldFor False [ ] loggedin) (fslI MsgCourseStudyFeature _other -> mreq (studyFeaturesPrimaryFieldFor False [ ] loggedin) (fslI MsgCourseStudyFeature
& setTooltip MsgCourseStudyFeatureTooltip) (Just defSFid) & setTooltip MsgCourseStudyFeatureTooltip) (Just defSFid)
-- button de-/register -- button de-/register
(btnRes, btnView) <- mreq (buttonField $ bool BtnRegister BtnDeregister isRegistered) "buttonField ignores settings anyway" Nothing (btnRes, btnView) <- mreq (buttonField $ bool BtnCourseRegister BtnCourseDeregister isRegistered) "buttonField ignores settings anyway" Nothing
let widget = $(widgetFile "widgets/register-form/register-form") let widget = $(widgetFile "widgets/register-form/register-form")
let msecretRes | Just res <- msecretRes' = Just <$> res let msecretRes | Just res <- msecretRes' = Just <$> res
@ -540,7 +564,7 @@ postCRegisterR tid ssh csh = do
registration <- getBy (UniqueParticipant aid cid) registration <- getBy (UniqueParticipant aid cid)
return (cid, course, entityVal <$> registration) return (cid, course, entityVal <$> registration)
let isRegistered = isJust registration let isRegistered = isJust registration
((regResult,_), _) <- runFormPost $ registerForm (Just aid) registration Nothing $ courseRegisterSecret course ((regResult,_), _) <- runFormPost $ courseRegisterForm (Just aid) registration Nothing $ courseRegisterSecret course
formResult regResult $ \(mbSfId,codeOk) -> if formResult regResult $ \(mbSfId,codeOk) -> if
| isRegistered -> do | isRegistered -> do
runDB $ deleteBy $ UniqueParticipant aid cid runDB $ deleteBy $ UniqueParticipant aid cid
@ -1072,7 +1096,7 @@ colUserComment tid ssh csh =
sortable (Just "note") (i18nCell MsgCourseUserNote) sortable (Just "note") (i18nCell MsgCourseUserNote)
$ \DBRow{ dbrOutput=(Entity uid _, _, mbNoteKey,_) } -> $ \DBRow{ dbrOutput=(Entity uid _, _, mbNoteKey,_) } ->
maybeEmpty mbNoteKey $ const $ maybeEmpty mbNoteKey $ const $
anchorCellM (courseLink <$> encrypt uid) (toWidget $ hasComment True) anchorCellM (courseLink <$> encrypt uid) (hasComment True)
where where
courseLink = CourseR tid ssh csh . CUserR courseLink = CourseR tid ssh csh . CUserR
@ -1386,8 +1410,8 @@ postCUserR tid ssh csh uCId = do
redirect $ currentRoute :#: registrationFieldFrag redirect $ currentRoute :#: registrationFieldFrag
let regButton let regButton
| Just _ <- mRegistration = BtnDeregister | Just _ <- mRegistration = BtnCourseDeregister
| otherwise = BtnRegister | otherwise = BtnCourseRegister
((regButtonRes, regButtonView), regButtonEnctype) <- runFormPost . identifyForm FIDcRegButton $ buttonForm' [regButton] ((regButtonRes, regButtonView), regButtonEnctype) <- runFormPost . identifyForm FIDcRegButton $ buttonForm' [regButton]
let registrationButtonFrag :: Text let registrationButtonFrag :: Text
@ -1401,7 +1425,7 @@ postCUserR tid ssh csh uCId = do
, formAnchor = Just registrationButtonFrag , formAnchor = Just registrationButtonFrag
} }
formResult regButtonRes $ \case formResult regButtonRes $ \case
BtnDeregister BtnCourseDeregister
| Just (Entity pId _) <- mRegistration | Just (Entity pId _) <- mRegistration
-> do -> do
runDB $ delete pId runDB $ delete pId
@ -1409,7 +1433,7 @@ postCUserR tid ssh csh uCId = do
redirect $ CourseR tid ssh csh CUsersR redirect $ CourseR tid ssh csh CUsersR
| otherwise | otherwise
-> invalidArgs ["User not registered"] -> invalidArgs ["User not registered"]
BtnRegister -> do BtnCourseRegister -> do
now <- liftIO getCurrentTime now <- liftIO getCurrentTime
let primaryField let primaryField
| [(Entity featId _, _, _)] <- filter (\(Entity _ StudyFeatures{..}, _, _) -> studyFeaturesType == FieldPrimary && studyFeaturesValid) studies | [(Entity featId _, _, _)] <- filter (\(Entity _ StudyFeatures{..}, _, _) -> studyFeaturesType == FieldPrimary && studyFeaturesValid) studies

View File

@ -22,6 +22,9 @@ import Data.Map ((!), (!?))
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 Data.Text as Text
import qualified Data.Text.Lens as Text
import Data.Aeson hiding (Result(..)) import Data.Aeson hiding (Result(..))
import Text.Hamlet (ihamlet) import Text.Hamlet (ihamlet)
import Text.Blaze.Html.Renderer.String (renderHtml) import Text.Blaze.Html.Renderer.String (renderHtml)
@ -29,11 +32,33 @@ import Text.Blaze.Html.Renderer.String (renderHtml)
import qualified Data.CaseInsensitive as CI import qualified Data.CaseInsensitive as CI
import qualified Control.Monad.State.Class as State import qualified Control.Monad.State.Class as State
import Control.Arrow (Kleisli(..))
import qualified Data.Csv as Csv import qualified Data.Csv as Csv
import qualified Data.Conduit.List as C import qualified Data.Conduit.List as C
import Numeric.Lens (integral)
import Database.Persist.Sql (deleteWhereCount, updateWhereCount)
-- Dedicated ExamRegistrationButton
data ButtonExamRegister = BtnExamRegister | BtnExamDeregister
deriving (Enum, Eq, Ord, Bounded, Read, Show, Generic, Typeable)
instance Universe ButtonExamRegister
instance Finite ButtonExamRegister
nullaryPathPiece ''ButtonExamRegister $ camelToPathPiece' 1
embedRenderMessage ''UniWorX ''ButtonExamRegister id
instance Button UniWorX ButtonExamRegister where
btnClasses BtnExamRegister = [BCIsButton, BCPrimary]
btnClasses BtnExamDeregister = [BCIsButton, BCDanger]
btnLabel BtnExamRegister = [whamlet|#{iconExamRegister True} _{MsgBtnExamRegister}|]
btnLabel BtnExamDeregister = [whamlet|#{iconExamRegister False} _{MsgBtnExamDeregister}|]
getCExamListR :: TermId -> SchoolId -> CourseShorthand -> Handler Html getCExamListR :: TermId -> SchoolId -> CourseShorthand -> Handler Html
getCExamListR tid ssh csh = do getCExamListR tid ssh csh = do
@ -52,19 +77,11 @@ getCExamListR tid ssh csh = do
guardM . hasReadAccessTo $ CExamR tid ssh csh examName EShowR guardM . hasReadAccessTo $ CExamR tid ssh csh examName EShowR
return x return x
dbtColonnade = dbColonnade . mconcat $ catMaybes dbtColonnade = dbColonnade . mconcat $ catMaybes
[ Just . sortable (Just "name") (i18nCell MsgExamName) $ \DBRow{ dbrOutput = Entity _ Exam{..} } -> anchorCell (CExamR tid ssh csh examName EShowR) $ toWidget examName [ Just . sortable (Just "name") (i18nCell MsgExamName) $ \DBRow{ dbrOutput = Entity _ Exam{..} } -> anchorCell (CExamR tid ssh csh examName EShowR) examName
, (<$ guard mayCreate) . sortable (Just "visible") (i18nCell MsgExamVisibleFrom) $ \DBRow { dbrOutput = Entity _ Exam{..} } -> maybe mempty (dateTimeCellVisible now) examVisibleFrom , (<$ guard mayCreate) . sortable (Just "visible") (i18nCell MsgExamVisibleFrom) $ \DBRow { dbrOutput = Entity _ Exam{..} } -> maybe mempty (dateTimeCellVisible now) examVisibleFrom
, Just . sortable (Just "register-from") (i18nCell MsgExamRegisterFrom) $ \DBRow { dbrOutput = Entity _ Exam{..} } -> maybe mempty dateTimeCell examRegisterFrom , Just . sortable (Just "register-from") (i18nCell MsgExamRegisterFrom) $ \DBRow { dbrOutput = Entity _ Exam{..} } -> maybe mempty dateTimeCell examRegisterFrom
, Just . sortable (Just "register-to") (i18nCell MsgExamRegisterTo) $ \DBRow { dbrOutput = Entity _ Exam{..} } -> maybe mempty dateTimeCell examRegisterTo , Just . sortable (Just "register-to") (i18nCell MsgExamRegisterTo) $ \DBRow { dbrOutput = Entity _ Exam{..} } -> maybe mempty dateTimeCell examRegisterTo
, Just . sortable (Just "time") (i18nCell MsgExamTime) $ \DBRow{ dbrOutput = Entity _ Exam{..} } -> cell $ do , Just . sortable (Just "time") (i18nCell MsgExamTime) $ \DBRow{ dbrOutput = Entity _ Exam{..} } -> maybe mempty (cell . flip (formatTimeRangeW SelFormatDateTime) examEnd) examStart
startT <- formatTime SelFormatDateTime examStart
endT <- traverse (\examEnd' -> formatTime (bool SelFormatDateTime SelFormatTime $ ((==) `on` utctDay) examStart examEnd') examEnd') examEnd
[whamlet|
$newline never
#{startT}
$maybe endT' <- endT
\ #{endT'}
|]
] ]
dbtSorting = Map.fromList dbtSorting = Map.fromList
[ ("name", SortColumn $ \exam -> exam E.^. ExamName ) [ ("name", SortColumn $ \exam -> exam E.^. ExamName )
@ -154,13 +171,13 @@ postECInviteR = invitationR examCorrectorInvitationConfig
data ExamForm = ExamForm data ExamForm = ExamForm
{ efName :: ExamName { efName :: ExamName
, efDescription :: Maybe Html , efDescription :: Maybe Html
, efStart :: UTCTime , efStart :: Maybe UTCTime
, efEnd :: Maybe UTCTime , efEnd :: Maybe UTCTime
, efVisibleFrom :: Maybe UTCTime , efVisibleFrom :: Maybe UTCTime
, efRegisterFrom :: Maybe UTCTime , efRegisterFrom :: Maybe UTCTime
, efRegisterTo :: Maybe UTCTime , efRegisterTo :: Maybe UTCTime
, efDeregisterUntil :: Maybe UTCTime , efDeregisterUntil :: Maybe UTCTime
, efPublishOccurrenceAssignments :: UTCTime , efPublishOccurrenceAssignments :: Maybe UTCTime
, efFinished :: Maybe UTCTime , efFinished :: Maybe UTCTime
, efClosed :: Maybe UTCTime , efClosed :: Maybe UTCTime
, efOccurrences :: Set ExamOccurrenceForm , efOccurrences :: Set ExamOccurrenceForm
@ -175,6 +192,7 @@ data ExamForm = ExamForm
data ExamOccurrenceForm = ExamOccurrenceForm data ExamOccurrenceForm = ExamOccurrenceForm
{ eofId :: Maybe CryptoUUIDExamOccurrence { eofId :: Maybe CryptoUUIDExamOccurrence
, eofName :: ExamOccurrenceName
, eofRoom :: Text , eofRoom :: Text
, eofCapacity :: Natural , eofCapacity :: Natural
, eofStart :: UTCTime , eofStart :: UTCTime
@ -189,6 +207,8 @@ data ExamPartForm = ExamPartForm
, epfWeight :: Rational , epfWeight :: Rational
} deriving (Read, Show, Eq, Ord, Generic, Typeable) } deriving (Read, Show, Eq, Ord, Generic, Typeable)
makeLenses_ ''ExamForm
deriveJSON defaultOptions deriveJSON defaultOptions
{ fieldLabelModifier = camelToPathPiece' 1 { fieldLabelModifier = camelToPathPiece' 1
} ''ExamPartForm } ''ExamPartForm
@ -206,13 +226,13 @@ examForm template html = do
<$> areq ciField (fslpI MsgExamName (mr MsgExamName) & setTooltip MsgExamNameTip) (efName <$> template) <$> areq ciField (fslpI MsgExamName (mr MsgExamName) & setTooltip MsgExamNameTip) (efName <$> template)
<*> (assertM (not . null . renderHtml) <$> aopt htmlField (fslpI MsgExamDescription "Html") (efDescription <$> template)) <*> (assertM (not . null . renderHtml) <$> aopt htmlField (fslpI MsgExamDescription "Html") (efDescription <$> template))
<* aformSection MsgExamFormTimes <* aformSection MsgExamFormTimes
<*> areq utcTimeField (fslpI MsgExamStart (mr MsgDate) & setTooltip MsgExamTimeTip) (efStart <$> template) <*> aopt utcTimeField (fslpI MsgExamStart (mr MsgDate) & setTooltip MsgExamTimeTip) (efStart <$> template)
<*> aopt utcTimeField (fslpI MsgExamEnd (mr MsgDate) & setTooltip MsgExamTimeTip) (efEnd <$> template) <*> aopt utcTimeField (fslpI MsgExamEnd (mr MsgDate) & setTooltip MsgExamTimeTip) (efEnd <$> template)
<*> aopt utcTimeField (fslpI MsgExamVisibleFrom (mr MsgDate) & setTooltip MsgExamVisibleFromTip) (efVisibleFrom <$> template) <*> aopt utcTimeField (fslpI MsgExamVisibleFrom (mr MsgDate) & setTooltip MsgExamVisibleFromTip) (efVisibleFrom <$> template)
<*> aopt utcTimeField (fslpI MsgExamRegisterFrom (mr MsgDate) & setTooltip MsgExamRegisterFromTip) (efRegisterFrom <$> template) <*> aopt utcTimeField (fslpI MsgExamRegisterFrom (mr MsgDate) & setTooltip MsgExamRegisterFromTip) (efRegisterFrom <$> template)
<*> aopt utcTimeField (fslpI MsgExamRegisterTo (mr MsgDate)) (efRegisterTo <$> template) <*> aopt utcTimeField (fslpI MsgExamRegisterTo (mr MsgDate)) (efRegisterTo <$> template)
<*> aopt utcTimeField (fslpI MsgExamDeregisterUntil (mr MsgDate)) (efDeregisterUntil <$> template) <*> aopt utcTimeField (fslpI MsgExamDeregisterUntil (mr MsgDate)) (efDeregisterUntil <$> template)
<*> areq utcTimeField (fslpI MsgExamPublishOccurrenceAssignments (mr MsgDate) & setTooltip MsgExamPublishOccurrenceAssignments) (efPublishOccurrenceAssignments <$> template) <*> aopt utcTimeField (fslpI MsgExamPublishOccurrenceAssignments (mr MsgDate) & setTooltip MsgExamPublishOccurrenceAssignmentsTip) (efPublishOccurrenceAssignments <$> template)
<*> aopt utcTimeField (fslpI MsgExamFinished (mr MsgDate) & setTooltip MsgExamFinishedTip) (efFinished <$> template) <*> aopt utcTimeField (fslpI MsgExamFinished (mr MsgDate) & setTooltip MsgExamFinishedTip) (efFinished <$> template)
<*> aopt utcTimeField (fslpI MsgExamClosed (mr MsgDate) & setTooltip MsgExamClosedTip) (efClosed <$> template) <*> aopt utcTimeField (fslpI MsgExamClosed (mr MsgDate) & setTooltip MsgExamClosedTip) (efClosed <$> template)
<* aformSection MsgExamFormOccurrences <* aformSection MsgExamFormOccurrences
@ -221,7 +241,7 @@ examForm template html = do
<*> (fromMaybe False <$> aopt checkBoxField (fslI MsgExamShowGrades & setTooltip MsgExamShowGradesTip) (Just . efShowGrades <$> template)) <*> (fromMaybe False <$> aopt checkBoxField (fslI MsgExamShowGrades & setTooltip MsgExamShowGradesTip) (Just . efShowGrades <$> template))
<*> (fromMaybe False <$> aopt checkBoxField (fslI MsgExamPublicStatistics & setTooltip MsgExamPublicStatisticsTip) (Just . efPublicStatistics <$> template)) <*> (fromMaybe False <$> aopt checkBoxField (fslI MsgExamPublicStatistics & setTooltip MsgExamPublicStatisticsTip) (Just . efPublicStatistics <$> template))
<*> examGradingRuleForm (efGradingRule <$> template) <*> examGradingRuleForm (efGradingRule <$> template)
<*> bonusRuleForm (efBonusRule <$> template) <*> examBonusRuleForm (efBonusRule <$> template)
<*> examOccurrenceRuleForm (efOccurrenceRule <$> template) <*> examOccurrenceRuleForm (efOccurrenceRule <$> template)
<* aformSection MsgExamFormCorrection <* aformSection MsgExamFormCorrection
<*> examCorrectorsForm (efCorrectors <$> template) <*> examCorrectorsForm (efCorrectors <$> template)
@ -272,7 +292,7 @@ examCorrectorsForm mPrev = wFormToAForm $ do
miLayout' :: MassInputLayout ListLength (Either UserEmail UserId) () miLayout' :: MassInputLayout ListLength (Either UserEmail UserId) ()
miLayout' lLength _ cellWdgts delButtons addWdgts = $(widgetFile "widgets/massinput/examCorrectors/layout") miLayout' lLength _ cellWdgts delButtons addWdgts = $(widgetFile "widgets/massinput/examCorrectors/layout")
fmap Set.fromList <$> massInputAccumW miAdd' miCell' miButtonAction' miLayout' ("correctors" :: Text) (fslI MsgExamCorrectors & setTooltip MsgMassInputTip) True (Set.toList <$> mPrev) fmap Set.fromList <$> massInputAccumW miAdd' miCell' miButtonAction' miLayout' ("correctors" :: Text) (fslI MsgExamCorrectors & setTooltip MsgMassInputTip) False (Set.toList <$> mPrev)
examOccurrenceForm :: Maybe (Set ExamOccurrenceForm) -> AForm Handler (Set ExamOccurrenceForm) examOccurrenceForm :: Maybe (Set ExamOccurrenceForm) -> AForm Handler (Set ExamOccurrenceForm)
examOccurrenceForm prev = wFormToAForm $ do examOccurrenceForm prev = wFormToAForm $ do
@ -281,11 +301,12 @@ examOccurrenceForm prev = wFormToAForm $ do
miButtonAction' :: forall p. PathPiece p => p -> Maybe (SomeRoute UniWorX) miButtonAction' :: forall p. PathPiece p => p -> Maybe (SomeRoute UniWorX)
miButtonAction' frag = Just . SomeRoute $ currentRoute :#: frag miButtonAction' frag = Just . SomeRoute $ currentRoute :#: frag
fmap (fmap Set.fromList) . massInputAccumEditW miAdd' miCell' miButtonAction' miLayout' miIdent' (fslI MsgExamOccurrences & setTooltip MsgMassInputTip) True $ Set.toList <$> prev fmap (fmap Set.fromList) . massInputAccumEditW miAdd' miCell' miButtonAction' miLayout' miIdent' (fslI MsgExamOccurrences & setTooltip MsgMassInputTip) False $ Set.toList <$> prev
where where
examOccurrenceForm' nudge mPrev csrf = do examOccurrenceForm' nudge mPrev csrf = do
(eofIdRes, eofIdView) <- mopt hiddenField ("" & addName (nudge "id")) (Just $ eofId =<< mPrev) (eofIdRes, eofIdView) <- mopt hiddenField ("" & addName (nudge "id")) (Just $ eofId =<< mPrev)
(eofRoomRes, eofRoomView) <- mpreq textField ("" & addName (nudge "name")) (eofRoom <$> mPrev) (eofNameRes, eofNameView) <- mpreq ciField ("" & addName (nudge "name")) (eofName <$> mPrev)
(eofRoomRes, eofRoomView) <- mpreq textField ("" & addName (nudge "room")) (eofRoom <$> mPrev)
(eofCapacityRes, eofCapacityView) <- mpreq (natFieldI MsgExamRoomCapacityNegative) ("" & addName (nudge "capacity")) (eofCapacity <$> mPrev) (eofCapacityRes, eofCapacityView) <- mpreq (natFieldI MsgExamRoomCapacityNegative) ("" & addName (nudge "capacity")) (eofCapacity <$> mPrev)
(eofStartRes, eofStartView) <- mpreq utcTimeField ("" & addName (nudge "start")) (eofStart <$> mPrev) (eofStartRes, eofStartView) <- mpreq utcTimeField ("" & addName (nudge "start")) (eofStart <$> mPrev)
(eofEndRes, eofEndView) <- mopt utcTimeField ("" & addName (nudge "end")) (eofEnd <$> mPrev) (eofEndRes, eofEndView) <- mopt utcTimeField ("" & addName (nudge "end")) (eofEnd <$> mPrev)
@ -293,6 +314,7 @@ examOccurrenceForm prev = wFormToAForm $ do
return ( ExamOccurrenceForm return ( ExamOccurrenceForm
<$> eofIdRes <$> eofIdRes
<*> eofNameRes
<*> eofRoomRes <*> eofRoomRes
<*> eofCapacityRes <*> eofCapacityRes
<*> eofStartRes <*> eofStartRes
@ -321,7 +343,7 @@ examPartsForm prev = wFormToAForm $ do
miButtonAction' :: forall p. PathPiece p => p -> Maybe (SomeRoute UniWorX) miButtonAction' :: forall p. PathPiece p => p -> Maybe (SomeRoute UniWorX)
miButtonAction' frag = Just . SomeRoute $ currentRoute :#: frag miButtonAction' frag = Just . SomeRoute $ currentRoute :#: frag
fmap (fmap Set.fromList) . massInputAccumEditW miAdd' miCell' miButtonAction' miLayout' miIdent' (fslI MsgExamParts & setTooltip MsgMassInputTip) True $ Set.toList <$> prev fmap (fmap Set.fromList) . massInputAccumEditW miAdd' miCell' miButtonAction' miLayout' miIdent' (fslI MsgExamParts & setTooltip MsgMassInputTip) False $ Set.toList <$> prev
where where
examPartForm' nudge mPrev csrf = do examPartForm' nudge mPrev csrf = do
(epfIdRes, epfIdView) <- mopt hiddenField ("" & addName (nudge "id")) (Just $ epfId =<< mPrev) (epfIdRes, epfIdView) <- mopt hiddenField ("" & addName (nudge "id")) (Just $ epfId =<< mPrev)
@ -381,6 +403,7 @@ examFormTemplate (Entity eId Exam{..}) = do
(Just -> eofId, ExamOccurrence{..}) <- occurrences' (Just -> eofId, ExamOccurrence{..}) <- occurrences'
return ExamOccurrenceForm return ExamOccurrenceForm
{ eofId { eofId
, eofName = examOccurrenceName
, eofRoom = examOccurrenceRoom , eofRoom = examOccurrenceRoom
, eofCapacity = examOccurrenceCapacity , eofCapacity = examOccurrenceCapacity
, eofStart = examOccurrenceStart , eofStart = examOccurrenceStart
@ -436,8 +459,8 @@ examTemplate cid = runMaybeT $ do
, efRegisterFrom = dateOffset <$> examRegisterFrom oldExam , efRegisterFrom = dateOffset <$> examRegisterFrom oldExam
, efRegisterTo = dateOffset <$> examRegisterTo oldExam , efRegisterTo = dateOffset <$> examRegisterTo oldExam
, efDeregisterUntil = dateOffset <$> examDeregisterUntil oldExam , efDeregisterUntil = dateOffset <$> examDeregisterUntil oldExam
, efPublishOccurrenceAssignments = dateOffset $ examPublishOccurrenceAssignments oldExam , efPublishOccurrenceAssignments = dateOffset <$> examPublishOccurrenceAssignments oldExam
, efStart = dateOffset $ examStart oldExam , efStart = dateOffset <$> examStart oldExam
, efEnd = dateOffset <$> examEnd oldExam , efEnd = dateOffset <$> examEnd oldExam
, efFinished = dateOffset <$> examFinished oldExam , efFinished = dateOffset <$> examFinished oldExam
, efClosed = dateOffset <$> examClosed oldExam , efClosed = dateOffset <$> examClosed oldExam
@ -456,14 +479,31 @@ validateExam = do
guardValidation MsgExamRegisterToMustBeAfterRegisterFrom $ NTop efRegisterTo >= NTop efRegisterFrom guardValidation MsgExamRegisterToMustBeAfterRegisterFrom $ NTop efRegisterTo >= NTop efRegisterFrom
guardValidation MsgExamDeregisterUntilMustBeAfterRegisterFrom $ NTop efDeregisterUntil >= NTop efRegisterFrom guardValidation MsgExamDeregisterUntilMustBeAfterRegisterFrom $ NTop efDeregisterUntil >= NTop efRegisterFrom
guardValidation MsgExamStartMustBeAfterPublishOccurrenceAssignments $ efStart >= efPublishOccurrenceAssignments guardValidation MsgExamStartMustBeAfterPublishOccurrenceAssignments . fromMaybe True $ (>=) <$> efStart <*> efPublishOccurrenceAssignments
guardValidation MsgExamEndMustBeAfterStart $ NTop efEnd >= NTop (Just efStart) guardValidation MsgExamEndMustBeAfterStart $ NTop efEnd >= NTop efStart
guardValidation MsgExamFinishedMustBeAfterEnd . fromMaybe True $ (>=) <$> efFinished <*> efEnd guardValidation MsgExamFinishedMustBeAfterEnd . fromMaybe True $ (>=) <$> efFinished <*> efEnd
guardValidation MsgExamFinishedMustBeAfterStart $ NTop efFinished >= NTop (Just efStart) guardValidation MsgExamFinishedMustBeAfterStart $ NTop efFinished >= NTop efStart
guardValidation MsgExamClosedMustBeAfterFinished . fromMaybe True $ (>=) <$> efClosed <*> efFinished guardValidation MsgExamClosedMustBeAfterFinished . fromMaybe True $ (>=) <$> efClosed <*> efFinished
guardValidation MsgExamClosedMustBeAfterStart $ NTop efClosed >= NTop (Just efStart) guardValidation MsgExamClosedMustBeAfterStart $ NTop efClosed >= NTop efStart
guardValidation MsgExamClosedMustBeAfterEnd . fromMaybe True $ (>=) <$> efClosed <*> efEnd guardValidation MsgExamClosedMustBeAfterEnd . fromMaybe True $ (>=) <$> efClosed <*> efEnd
forM_ efOccurrences $ \ExamOccurrenceForm{..} -> do
guardValidation (MsgExamOccurrenceEndMustBeAfterStart eofName) $ NTop eofEnd >= NTop (Just eofStart)
guardValidation (MsgExamOccurrenceStartMustBeAfterExamStart eofName) $ NTop (Just eofStart) >= NTop efStart
guardValidation (MsgExamOccurrenceEndMustBeBeforeExamEnd eofName) $ NTop eofEnd <= NTop efEnd
forM_ [ (a, b) | a <- Set.toAscList efOccurrences, b <- Set.toAscList efOccurrences, b > a ] $ \(a, b) -> do
eofRange' <- formatTimeRange SelFormatDateTime (eofStart a) (eofEnd a)
guardValidation (MsgExamOccurrenceDuplicate (eofRoom a) eofRange') $ any (\f -> f a b)
[ (/=) `on` eofRoom
, (/=) `on` eofStart
, (/=) `on` eofEnd
, (/=) `on` fmap renderHtml . eofDescription
]
guardValidation (MsgExamOccurrenceDuplicateName $ eofName a) $ ((/=) `on` eofName) a b
getCExamNewR, postCExamNewR :: TermId -> SchoolId -> CourseShorthand -> Handler Html getCExamNewR, postCExamNewR :: TermId -> SchoolId -> CourseShorthand -> Handler Html
getCExamNewR = postCExamNewR getCExamNewR = postCExamNewR
@ -510,6 +550,7 @@ postCExamNewR tid ssh csh = do
[ ExamOccurrence{..} [ ExamOccurrence{..}
| ExamOccurrenceForm{..} <- Set.toList efOccurrences | ExamOccurrenceForm{..} <- Set.toList efOccurrences
, let examOccurrenceExam = examid , let examOccurrenceExam = examid
examOccurrenceName = eofName
examOccurrenceRoom = eofRoom examOccurrenceRoom = eofRoom
examOccurrenceCapacity = eofCapacity examOccurrenceCapacity = eofCapacity
examOccurrenceStart = eofStart examOccurrenceStart = eofStart
@ -583,6 +624,7 @@ postEEditR tid ssh csh examn = do
ExamOccurrenceForm{ eofId = Nothing, .. } -> insert_ ExamOccurrenceForm{ eofId = Nothing, .. } -> insert_
ExamOccurrence ExamOccurrence
{ examOccurrenceExam = eId { examOccurrenceExam = eId
, examOccurrenceName = eofName
, examOccurrenceRoom = eofRoom , examOccurrenceRoom = eofRoom
, examOccurrenceCapacity = eofCapacity , examOccurrenceCapacity = eofCapacity
, examOccurrenceStart = eofStart , examOccurrenceStart = eofStart
@ -596,6 +638,7 @@ postEEditR tid ssh csh examn = do
guard $ examOccurrenceExam oldOcc == eId guard $ examOccurrenceExam oldOcc == eId
lift $ replace eofId' ExamOccurrence lift $ replace eofId' ExamOccurrence
{ examOccurrenceExam = eId { examOccurrenceExam = eId
, examOccurrenceName = eofName
, examOccurrenceRoom = eofRoom , examOccurrenceRoom = eofRoom
, examOccurrenceCapacity = eofCapacity , examOccurrenceCapacity = eofCapacity
, examOccurrenceStart = eofStart , examOccurrenceStart = eofStart
@ -661,7 +704,7 @@ getEShowR tid ssh csh examn = do
cTime <- liftIO getCurrentTime cTime <- liftIO getCurrentTime
mUid <- maybeAuthId mUid <- maybeAuthId
(Entity _ Exam{..}, parts, examVisible, (gradingVisible, gradingShown), (occurrenceAssignmentsVisible, occurrenceAssignmentsShown), results, result, occurrences, (registered, mayRegister)) <- runDB $ do (Entity _ Exam{..}, parts, examVisible, (gradingVisible, gradingShown), (occurrenceAssignmentsVisible, occurrenceAssignmentsShown), results, result, occurrences, (registered, mayRegister), occurrenceNamesShown) <- runDB $ do
exam@(Entity eId Exam{..}) <- fetchExam tid ssh csh examn exam@(Entity eId Exam{..}) <- fetchExam tid ssh csh examn
let examVisible = NTop (Just cTime) >= NTop examVisibleFrom let examVisible = NTop (Just cTime) >= NTop examVisibleFrom
@ -669,7 +712,7 @@ getEShowR tid ssh csh examn = do
let gradingVisible = NTop (Just cTime) >= NTop examFinished let gradingVisible = NTop (Just cTime) >= NTop examFinished
gradingShown <- or2M (return gradingVisible) . hasReadAccessTo $ CExamR tid ssh csh examn EEditR gradingShown <- or2M (return gradingVisible) . hasReadAccessTo $ CExamR tid ssh csh examn EEditR
let occurrenceAssignmentsVisible = cTime >= examPublishOccurrenceAssignments let occurrenceAssignmentsVisible = NTop (Just cTime) >= NTop examPublishOccurrenceAssignments
occurrenceAssignmentsShown <- or2M (return occurrenceAssignmentsVisible) . hasReadAccessTo $ CExamR tid ssh csh examn EEditR occurrenceAssignmentsShown <- or2M (return occurrenceAssignmentsVisible) . hasReadAccessTo $ CExamR tid ssh csh examn EEditR
parts <- selectList [ ExamPartExam ==. eId ] [ Asc ExamPartName ] parts <- selectList [ ExamPartExam ==. eId ] [ Asc ExamPartName ]
@ -701,13 +744,15 @@ getEShowR tid ssh csh examn = do
registered <- for mUid $ existsBy . UniqueExamRegistration eId registered <- for mUid $ existsBy . UniqueExamRegistration eId
mayRegister <- (== Authorized) <$> evalAccessDB (CExamR tid ssh csh examName ERegisterR) True mayRegister <- (== Authorized) <$> evalAccessDB (CExamR tid ssh csh examName ERegisterR) True
return (exam, parts, examVisible, (gradingVisible, gradingShown), (occurrenceAssignmentsVisible, occurrenceAssignmentsShown), results, result, occurrences, (registered, mayRegister)) occurrenceNamesShown <- hasReadAccessTo $ CExamR tid ssh csh examn EEditR
let examTimes = all (\(Entity _ ExamOccurrence{..}, _) -> examOccurrenceStart == examStart && examOccurrenceEnd == examEnd) occurrences return (exam, parts, examVisible, (gradingVisible, gradingShown), (occurrenceAssignmentsVisible, occurrenceAssignmentsShown), results, result, occurrences, (registered, mayRegister), occurrenceNamesShown)
let examTimes = all (\(Entity _ ExamOccurrence{..}, _) -> Just examOccurrenceStart == examStart && examOccurrenceEnd == examEnd) occurrences
registerWidget registerWidget
| Just isRegistered <- registered | Just isRegistered <- registered
, mayRegister = Just $ do , mayRegister = Just $ do
(examRegisterForm, examRegisterEnctype) <- liftHandlerT . generateFormPost . buttonForm' $ bool [BtnRegister] [BtnDeregister] isRegistered (examRegisterForm, examRegisterEnctype) <- liftHandlerT . generateFormPost . buttonForm' $ bool [BtnExamRegister] [BtnExamDeregister] isRegistered
[whamlet| [whamlet|
<p> <p>
$if isRegistered $if isRegistered
@ -761,12 +806,18 @@ queryStudyFeatures = $(sqlIJproj 3 1) . $(sqlLOJproj 2 2) . $(sqlLOJproj 3 3)
queryExamRegistration :: ExamUserTableExpr -> E.SqlExpr (Entity ExamRegistration) queryExamRegistration :: ExamUserTableExpr -> E.SqlExpr (Entity ExamRegistration)
queryExamRegistration = $(sqlIJproj 2 1) . $(sqlLOJproj 3 1) queryExamRegistration = $(sqlIJproj 2 1) . $(sqlLOJproj 3 1)
queryExamOccurrence :: ExamUserTableExpr -> E.SqlExpr (Maybe (Entity ExamOccurrence))
queryExamOccurrence = $(sqlLOJproj 3 2)
queryStudyDegree :: ExamUserTableExpr -> E.SqlExpr (Maybe (Entity StudyDegree)) queryStudyDegree :: ExamUserTableExpr -> E.SqlExpr (Maybe (Entity StudyDegree))
queryStudyDegree = $(sqlIJproj 3 2) . $(sqlLOJproj 2 2) . $(sqlLOJproj 3 3) queryStudyDegree = $(sqlIJproj 3 2) . $(sqlLOJproj 2 2) . $(sqlLOJproj 3 3)
queryStudyField :: ExamUserTableExpr -> E.SqlExpr (Maybe (Entity StudyTerms)) queryStudyField :: ExamUserTableExpr -> E.SqlExpr (Maybe (Entity StudyTerms))
queryStudyField = $(sqlIJproj 3 3) . $(sqlLOJproj 2 2) . $(sqlLOJproj 3 3) queryStudyField = $(sqlIJproj 3 3) . $(sqlLOJproj 2 2) . $(sqlLOJproj 3 3)
resultExamRegistration :: Lens' ExamUserTableData (Entity ExamRegistration)
resultExamRegistration = _dbrOutput . _1
resultUser :: Lens' ExamUserTableData (Entity User) resultUser :: Lens' ExamUserTableData (Entity User)
resultUser = _dbrOutput . _2 resultUser = _dbrOutput . _2
@ -783,18 +834,22 @@ resultExamOccurrence :: Traversal' ExamUserTableData (Entity ExamOccurrence)
resultExamOccurrence = _dbrOutput . _3 . _Just resultExamOccurrence = _dbrOutput . _3 . _Just
data ExamUserTableCsv = ExamUserTableCsv data ExamUserTableCsv = ExamUserTableCsv
{ csvUserSurname :: Text { csvEUserSurname :: Maybe Text
, csvUserName :: Text , csvEUserName :: Maybe Text
, csvUserMatriculation :: Maybe Text , csvEUserMatriculation :: Maybe Text
, csvUserField :: Maybe Text , csvEUserField :: Maybe Text
, csvUserDegree :: Maybe Text , csvEUserDegree :: Maybe Text
, csvUserSemester :: Maybe Int , csvEUserSemester :: Maybe Int
, csvUserRoom :: Maybe Text , csvEUserOccurrence :: Maybe (CI Text)
, csvEUserExercisePoints :: Maybe Points
, csvEUserExercisePasses :: Maybe Int
, csvEUserExercisePointsMax :: Maybe Points
, csvEUserExercisePassesMax :: Maybe Int
} }
deriving (Generic) deriving (Generic)
examUserTableCsvOptions :: Csv.Options examUserTableCsvOptions :: Csv.Options
examUserTableCsvOptions = Csv.defaultOptions { Csv.fieldLabelModifier = camelToPathPiece' 1 } examUserTableCsvOptions = Csv.defaultOptions { Csv.fieldLabelModifier = camelToPathPiece' 3 }
instance ToNamedRecord ExamUserTableCsv where instance ToNamedRecord ExamUserTableCsv where
toNamedRecord = Csv.genericToNamedRecord examUserTableCsvOptions toNamedRecord = Csv.genericToNamedRecord examUserTableCsvOptions
@ -805,75 +860,414 @@ instance FromNamedRecord ExamUserTableCsv where
instance DefaultOrdered ExamUserTableCsv where instance DefaultOrdered ExamUserTableCsv where
headerOrder = Csv.genericHeaderOrder examUserTableCsvOptions headerOrder = Csv.genericHeaderOrder examUserTableCsvOptions
instance CsvColumnsExplained ExamUserTableCsv where
csvColumnsExplanations = genericCsvColumnsExplanations examUserTableCsvOptions $ Map.fromList
[ ('csvEUserSurname , MsgCsvColumnExamUserSurname )
, ('csvEUserName , MsgCsvColumnExamUserName )
, ('csvEUserMatriculation , MsgCsvColumnExamUserMatriculation )
, ('csvEUserField , MsgCsvColumnExamUserField )
, ('csvEUserDegree , MsgCsvColumnExamUserDegree )
, ('csvEUserSemester , MsgCsvColumnExamUserSemester )
, ('csvEUserOccurrence , MsgCsvColumnExamUserOccurrence )
, ('csvEUserExercisePoints , MsgCsvColumnExamUserExercisePoints )
, ('csvEUserExercisePasses , MsgCsvColumnExamUserExercisePasses )
, ('csvEUserExercisePointsMax, MsgCsvColumnExamUserExercisePointsMax )
, ('csvEUserExercisePassesMax, MsgCsvColumnExamUserExercisePassesMax )
]
data ExamUserAction = ExamUserDeregister
| ExamUserAssignOccurrence
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic, Typeable)
instance Universe ExamUserAction
instance Finite ExamUserAction
nullaryPathPiece ''ExamUserAction $ camelToPathPiece' 2
embedRenderMessage ''UniWorX ''ExamUserAction id
data ExamUserActionData = ExamUserDeregisterData
| ExamUserAssignOccurrenceData (Maybe ExamOccurrenceId)
data ExamUserCsvActionClass
= ExamUserCsvCourseRegister
| ExamUserCsvRegister
| ExamUserCsvAssignOccurrence
| ExamUserCsvSetCourseField
| ExamUserCsvDeregister
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable)
embedRenderMessage ''UniWorX ''ExamUserCsvActionClass id
data ExamUserCsvAction
= ExamUserCsvCourseRegisterData
{ examUserCsvActUser :: UserId
, examUserCsvActCourseField :: Maybe StudyFeaturesId
, examUserCsvActOccurrence :: Maybe ExamOccurrenceId
}
| ExamUserCsvRegisterData
{ examUserCsvActUser :: UserId
, examUserCsvActOccurrence :: Maybe ExamOccurrenceId
}
| ExamUserCsvAssignOccurrenceData
{ examUserCsvActRegistration :: ExamRegistrationId
, examUserCsvActOccurrence :: Maybe ExamOccurrenceId
}
| ExamUserCsvSetCourseFieldData
{ examUserCsvActCourseParticipant :: CourseParticipantId
, examUserCsvActCourseField :: Maybe StudyFeaturesId
}
| ExamUserCsvDeregisterData
{ examUserCsvActRegistration :: ExamRegistrationId
}
deriving (Eq, Ord, Read, Show, Generic, Typeable)
deriveJSON defaultOptions
{ constructorTagModifier = over Text.packed $ Text.intercalate "-" . map Text.toLower . drop 3 . dropEnd 1 . splitCamel
, fieldLabelModifier = camelToPathPiece' 3
, sumEncoding = TaggedObject "action" "data"
} ''ExamUserCsvAction
data ExamUserCsvException
= ExamUserCsvExceptionNoMatchingUser
| ExamUserCsvExceptionNoMatchingStudyFeatures
| ExamUserCsvExceptionNoMatchingOccurrence
deriving (Show, Generic, Typeable)
instance Exception ExamUserCsvException
embedRenderMessage ''UniWorX ''ExamUserCsvException id
getEUsersR, postEUsersR :: TermId -> SchoolId -> CourseShorthand -> ExamName -> Handler Html getEUsersR, postEUsersR :: TermId -> SchoolId -> CourseShorthand -> ExamName -> Handler Html
getEUsersR = postEUsersR getEUsersR = postEUsersR
postEUsersR tid ssh csh examn = do postEUsersR tid ssh csh examn = do
Entity eid Exam{..} <- runDB $ fetchExam tid ssh csh examn (registrationResult, examUsersTable) <- runDB $ do
exam@(Entity eid Exam{..}) <- fetchExam tid ssh csh examn
bonus <- examBonus exam
let let
examUsersDBTable = DBTable{..} allBoni = (mappend <$> normalSummary <*> bonusSummary) $ fold bonus
where showPasses = numSheetsPasses allBoni /= 0
dbtSQLQuery ((examRegistration `E.InnerJoin` user) `E.LeftOuterJoin` occurrence `E.LeftOuterJoin` (courseParticipant `E.LeftOuterJoin` (studyFeatures `E.InnerJoin` studyDegree `E.InnerJoin` studyField))) = do showPoints = getSum (numSheetsPoints allBoni) /= 0
E.on $ studyField E.?. StudyTermsId E.==. studyFeatures E.?. StudyFeaturesField
E.on $ studyDegree E.?. StudyDegreeId E.==. studyFeatures E.?. StudyFeaturesDegree
E.on $ studyFeatures E.?. StudyFeaturesId E.==. E.joinV (courseParticipant E.?. CourseParticipantField)
E.on $ courseParticipant E.?. CourseParticipantCourse E.==. E.just (E.val examCourse)
E.&&. courseParticipant E.?. CourseParticipantUser E.==. E.just (user E.^. UserId)
E.on $ occurrence E.?. ExamOccurrenceExam E.==. E.just (E.val eid)
E.&&. occurrence E.?. ExamOccurrenceId E.==. examRegistration E.^. ExamRegistrationOccurrence
E.on $ examRegistration E.^. ExamRegistrationUser E.==. user E.^. UserId
E.where_ $ examRegistration E.^. ExamRegistrationExam E.==. E.val eid
return (examRegistration, user, occurrence, studyFeatures, studyDegree, studyField)
dbtRowKey = queryExamRegistration >>> (E.^. ExamRegistrationId)
dbtProj = return
dbtColonnade = dbColonnade $ mconcat
[ colUserNameLink (CourseR tid ssh csh . CUserR)
, colUserMatriclenr
, colField resultStudyField
, colDegreeShort resultStudyDegree
, colFeaturesSemester resultStudyFeatures
, sortable (Just "room") (i18nCell MsgExamRoom) (maybe mempty (cell . toWgt . examOccurrenceRoom . entityVal) . view _userTableOccurrence)
]
dbtSorting = Map.fromList
[ sortUserNameLink queryUser
, sortUserSurname queryUser
, sortUserDisplayName queryUser
, sortUserMatriclenr queryUser
, sortField queryStudyField
, sortDegreeShort queryStudyDegree
, sortFeaturesSemester queryStudyFeatures
]
dbtFilter = Map.fromList
[ fltrUserNameEmail queryUser
, fltrUserMatriclenr queryUser
, fltrField queryStudyField
, fltrDegree queryStudyDegree
, fltrFeaturesSemester queryStudyFeatures
]
dbtFilterUI mPrev = mconcat
[ fltrUserNameEmailUI mPrev
, fltrUserMatriclenrUI mPrev
, fltrFieldUI mPrev
, fltrDegreeUI mPrev
, fltrFeaturesSemesterUI mPrev
]
dbtStyle = def { dbsFilterLayout = defaultDBSFilterLayout }
dbtParams = def
dbtIdent :: Text
dbtIdent = "exam-users"
dbtCsvEncode :: DBTCsvEncode ExamUserTableData ExamUserTableCsv
dbtCsvEncode = DictJust . C.map $ ExamUserTableCsv
<$> view (resultUser . _entityVal . _userSurname)
<*> view (resultUser . _entityVal . _userDisplayName)
<*> view (resultUser . _entityVal . _userMatrikelnummer)
<*> preview (resultStudyField . _entityVal . to (\StudyTerms{..} -> studyTermsName <|> studyTermsShorthand <|> Just (tshow studyTermsKey)) . _Just)
<*> preview (resultStudyDegree . _entityVal . to (\StudyDegree{..} -> studyDegreeName <|> studyDegreeShorthand <|> Just (tshow studyDegreeKey)) . _Just)
<*> preview (resultStudyFeatures . _entityVal . _studyFeaturesSemester)
<*> preview (resultExamOccurrence . _entityVal . _examOccurrenceRoom)
dbtCsvDecode = Nothing
examUsersDBTableValidator = def let
((), examUsersTable) <- runDB $ dbTable examUsersDBTableValidator examUsersDBTable examUsersDBTable = DBTable{..}
where
dbtSQLQuery ((examRegistration `E.InnerJoin` user) `E.LeftOuterJoin` occurrence `E.LeftOuterJoin` (courseParticipant `E.LeftOuterJoin` (studyFeatures `E.InnerJoin` studyDegree `E.InnerJoin` studyField))) = do
E.on $ studyField E.?. StudyTermsId E.==. studyFeatures E.?. StudyFeaturesField
E.on $ studyDegree E.?. StudyDegreeId E.==. studyFeatures E.?. StudyFeaturesDegree
E.on $ studyFeatures E.?. StudyFeaturesId E.==. E.joinV (courseParticipant E.?. CourseParticipantField)
E.on $ courseParticipant E.?. CourseParticipantCourse E.==. E.just (E.val examCourse)
E.&&. courseParticipant E.?. CourseParticipantUser E.==. E.just (user E.^. UserId)
E.on $ occurrence E.?. ExamOccurrenceExam E.==. E.just (E.val eid)
E.&&. occurrence E.?. ExamOccurrenceId E.==. examRegistration E.^. ExamRegistrationOccurrence
E.on $ examRegistration E.^. ExamRegistrationUser E.==. user E.^. UserId
E.where_ $ examRegistration E.^. ExamRegistrationExam E.==. E.val eid
return (examRegistration, user, occurrence, studyFeatures, studyDegree, studyField)
dbtRowKey = queryExamRegistration >>> (E.^. ExamRegistrationId)
dbtProj = return
dbtColonnade = mconcat $ catMaybes
[ pure $ dbSelect (applying _2) id $ return . view (resultExamRegistration . _entityKey)
, pure $ colUserNameLink (CourseR tid ssh csh . CUserR)
, pure colUserMatriclenr
, pure $ colField resultStudyField
, pure $ colDegreeShort resultStudyDegree
, pure $ colFeaturesSemester resultStudyFeatures
, pure $ sortable (Just "occurrence") (i18nCell MsgExamOccurrence) $ maybe mempty (anchorCell' (\n -> CExamR tid ssh csh examn EShowR :#: [st|exam-occurrence__#{n}|]) id . examOccurrenceName . entityVal) . view _userTableOccurrence
, guardOn showPasses $ sortable Nothing (i18nCell MsgAchievedPasses) $ \(view $ resultUser . _entityKey -> uid) -> fromMaybe mempty $ do
SheetGradeSummary{achievedPasses} <- examBonusAchieved uid bonus
SheetGradeSummary{numSheetsPasses} <- examBonusPossible uid bonus
return $ propCell (getSum achievedPasses) (getSum numSheetsPasses)
, guardOn showPoints $ sortable Nothing (i18nCell MsgAchievedPoints) $ \(view $ resultUser . _entityKey -> uid) -> fromMaybe mempty $ do
SheetGradeSummary{achievedPoints} <- examBonusAchieved uid bonus
SheetGradeSummary{sumSheetsPoints} <- examBonusPossible uid bonus
return $ propCell (getSum achievedPoints) (getSum sumSheetsPoints)
]
dbtSorting = Map.fromList
[ sortUserNameLink queryUser
, sortUserSurname queryUser
, sortUserDisplayName queryUser
, sortUserMatriclenr queryUser
, sortField queryStudyField
, sortDegreeShort queryStudyDegree
, sortFeaturesSemester queryStudyFeatures
, ("occurrence", SortColumn $ queryExamOccurrence >>> (E.?. ExamOccurrenceName))
]
dbtFilter = Map.fromList
[ fltrUserNameEmail queryUser
, fltrUserMatriclenr queryUser
, fltrField queryStudyField
, fltrDegree queryStudyDegree
, fltrFeaturesSemester queryStudyFeatures
, ("occurrence", FilterColumn . E.mkContainsFilterWith Just $ queryExamOccurrence >>> (E.?. ExamOccurrenceName))
]
dbtFilterUI mPrev = mconcat
[ fltrUserNameEmailUI mPrev
, fltrUserMatriclenrUI mPrev
, fltrFieldUI mPrev
, fltrDegreeUI mPrev
, fltrFeaturesSemesterUI mPrev
, prismAForm (singletonFilter "occurrence") mPrev $ aopt textField (fslI MsgExamOccurrence)
]
dbtStyle = def { dbsFilterLayout = defaultDBSFilterLayout }
dbtParams = DBParamsForm
{ dbParamsFormMethod = POST
, dbParamsFormAction = Just . SomeRoute $ CExamR tid ssh csh examn EUsersR
, dbParamsFormAttrs = []
, dbParamsFormSubmit = FormSubmit
, dbParamsFormAdditional = \csrf -> do
let
actionMap :: Map ExamUserAction (AForm Handler ExamUserActionData)
actionMap = Map.fromList
[ ( ExamUserDeregister
, pure ExamUserDeregisterData
)
, ( ExamUserAssignOccurrence
, ExamUserAssignOccurrenceData
<$> aopt (examOccurrenceField eid) (fslI MsgExamOccurrence) (Just Nothing)
)
]
(res, formWgt) <- multiActionM actionMap (fslI MsgAction) Nothing csrf
let formRes = (, mempty) . First . Just <$> res
return (formRes, formWgt)
, dbParamsFormEvaluate = liftHandlerT . runFormPost
, dbParamsFormResult = id
, dbParamsFormIdent = def
}
dbtIdent :: Text
dbtIdent = "exam-users"
dbtCsvEncode :: DBTCsvEncode ExamUserTableData ExamUserTableCsv
dbtCsvEncode = DictJust . C.map $ ExamUserTableCsv
<$> view (resultUser . _entityVal . _userSurname . to Just)
<*> view (resultUser . _entityVal . _userDisplayName . to Just)
<*> view (resultUser . _entityVal . _userMatrikelnummer)
<*> preview (resultStudyField . _entityVal . to (\StudyTerms{..} -> studyTermsName <|> studyTermsShorthand <|> Just (tshow studyTermsKey)) . _Just)
<*> preview (resultStudyDegree . _entityVal . to (\StudyDegree{..} -> studyDegreeName <|> studyDegreeShorthand <|> Just (tshow studyDegreeKey)) . _Just)
<*> preview (resultStudyFeatures . _entityVal . _studyFeaturesSemester)
<*> preview (resultExamOccurrence . _entityVal . _examOccurrenceName)
<*> preview (resultUser . _entityKey . to (examBonusAchieved ?? bonus) . _Just . _achievedPoints . _Wrapped)
<*> preview (resultUser . _entityKey . to (examBonusAchieved ?? bonus) . _Just . _achievedPasses . _Wrapped . integral)
<*> preview (resultUser . _entityKey . to (examBonusPossible ?? bonus) . _Just . _sumSheetsPoints . _Wrapped)
<*> preview (resultUser . _entityKey . to (examBonusPossible ?? bonus) . _Just . _numSheetsPasses . _Wrapped . integral)
dbtCsvDecode = Just DBTCsvDecode
{ dbtCsvRowKey = \csv -> do
uid <- lift $ view _2 <$> guessUser csv
fmap E.Value . MaybeT . getKeyBy $ UniqueExamRegistration eid uid
, dbtCsvComputeActions = \case
DBCsvDiffMissing{dbCsvOldKey}
-> yield . ExamUserCsvDeregisterData $ E.unValue dbCsvOldKey
DBCsvDiffNew{dbCsvNewKey = Just _}
-> fail "An UniqueExamRegistration could be found, but the ExamRegistrationKey is not among the existing keys"
DBCsvDiffNew{dbCsvNewKey = Nothing, dbCsvNew} -> do
(isPart, uid) <- lift $ guessUser dbCsvNew
if
| isPart -> do
yieldM $ ExamUserCsvRegisterData <$> pure uid <*> lookupOccurrence dbCsvNew
newFeatures <- lift $ lookupStudyFeatures dbCsvNew
Just (Entity cpId CourseParticipant{ courseParticipantField = oldFeatures }) <- lift . getBy $ UniqueParticipant uid examCourse
when (newFeatures /= oldFeatures) $
yield $ ExamUserCsvSetCourseFieldData cpId newFeatures
| otherwise ->
yieldM $ ExamUserCsvCourseRegisterData uid <$> lookupStudyFeatures dbCsvNew <*> lookupOccurrence dbCsvNew
DBCsvDiffExisting{..} -> do
newOccurrence <- lift $ lookupOccurrence dbCsvNew
when (newOccurrence /= dbCsvOld ^? resultExamOccurrence . _entityKey) $
yield $ ExamUserCsvAssignOccurrenceData (E.unValue dbCsvOldKey) newOccurrence
newFeatures <- lift $ lookupStudyFeatures dbCsvNew
when (newFeatures /= dbCsvOld ^? resultStudyFeatures . _entityKey) $ do
Just (Entity cpId _) <- lift . getBy . flip UniqueParticipant examCourse $ dbCsvOld ^. resultUser . _entityKey
yield $ ExamUserCsvSetCourseFieldData cpId newFeatures
, dbtCsvClassifyAction = \case
ExamUserCsvCourseRegisterData{} -> ExamUserCsvCourseRegister
ExamUserCsvRegisterData{} -> ExamUserCsvRegister
ExamUserCsvDeregisterData{} -> ExamUserCsvDeregister
ExamUserCsvAssignOccurrenceData{} -> ExamUserCsvAssignOccurrence
ExamUserCsvSetCourseFieldData{} -> ExamUserCsvSetCourseField
, dbtCsvCoarsenActionClass = \case
ExamUserCsvCourseRegister -> DBCsvActionNew
ExamUserCsvRegister -> DBCsvActionNew
ExamUserCsvDeregister -> DBCsvActionMissing
_other -> DBCsvActionExisting
, dbtCsvExecuteActions = do
C.mapM_ $ \case
ExamUserCsvCourseRegisterData{..} -> do
now <- liftIO getCurrentTime
insert_ CourseParticipant
{ courseParticipantCourse = examCourse
, courseParticipantUser = examUserCsvActUser
, courseParticipantRegistration = now
, courseParticipantField = examUserCsvActCourseField
}
insert_ ExamRegistration
{ examRegistrationExam = eid
, examRegistrationUser = examUserCsvActUser
, examRegistrationOccurrence = examUserCsvActOccurrence
, examRegistrationTime = now
}
ExamUserCsvRegisterData{..} -> do
examRegistrationTime <- liftIO getCurrentTime
insert_ ExamRegistration
{ examRegistrationExam = eid
, examRegistrationUser = examUserCsvActUser
, examRegistrationOccurrence = examUserCsvActOccurrence
, ..
}
ExamUserCsvAssignOccurrenceData{..} ->
update examUserCsvActRegistration [ ExamRegistrationOccurrence =. examUserCsvActOccurrence ]
ExamUserCsvSetCourseFieldData{..} ->
update examUserCsvActCourseParticipant [ CourseParticipantField =. examUserCsvActCourseField ]
ExamUserCsvDeregisterData{..} -> delete examUserCsvActRegistration
return $ CExamR tid ssh csh examn EUsersR
, dbtCsvRenderKey = \(registeredUserName -> registeredUserName') -> \case
ExamUserCsvCourseRegisterData{..} -> do
(User{..}, occ) <- liftHandlerT . runDB $ (,) <$> getJust examUserCsvActUser <*> for examUserCsvActOccurrence getJust
[whamlet|
$newline never
^{nameWidget userDisplayName userSurname}
$maybe features <- examUserCsvActCourseField
, ^{studyFeaturesWidget features}
$nothing
, _{MsgCourseStudyFeatureNone}
$maybe ExamOccurrence{examOccurrenceName} <- occ
\ (#{examOccurrenceName})
$nothing
\ (_{MsgExamNoOccurrence})
|]
ExamUserCsvRegisterData{..} -> do
(User{..}, occ) <- liftHandlerT . runDB $ (,) <$> getJust examUserCsvActUser <*> for examUserCsvActOccurrence getJust
[whamlet|
$newline never
^{nameWidget userDisplayName userSurname}
$maybe ExamOccurrence{examOccurrenceName} <- occ
\ (#{examOccurrenceName})
$nothing
\ (_{MsgExamNoOccurrence})
|]
ExamUserCsvAssignOccurrenceData{..} -> do
occ <- for examUserCsvActOccurrence $ liftHandlerT . runDB . getJust
[whamlet|
$newline never
^{registeredUserName' examUserCsvActRegistration}
$maybe ExamOccurrence{examOccurrenceName} <- occ
\ (#{examOccurrenceName})
$nothing
\ (_{MsgExamNoOccurrence})
|]
ExamUserCsvSetCourseFieldData{..} -> do
User{..} <- liftHandlerT . runDB $ getJust . courseParticipantUser =<< getJust examUserCsvActCourseParticipant
[whamlet|
$newline never
^{nameWidget userDisplayName userSurname}
$maybe features <- examUserCsvActCourseField
, ^{studyFeaturesWidget features}
$nothing
, _{MsgCourseStudyFeatureNone}
|]
ExamUserCsvDeregisterData{..}
-> registeredUserName' examUserCsvActRegistration
, dbtCsvRenderActionClass = toWidget <=< ap getMessageRender . pure
, dbtCsvRenderException = ap getMessageRender . pure :: ExamUserCsvException -> DB Text
}
where
studyFeaturesWidget :: StudyFeaturesId -> Widget
studyFeaturesWidget featId = do
(StudyFeatures{studyFeaturesSemester}, (degree, terms)) <- liftHandlerT . runDB . ($ featId) . runKleisli $ Kleisli getJust >>> Kleisli return &&& Kleisli (getJust . studyFeaturesDegree) &&& Kleisli (getJust . studyFeaturesField)
[whamlet|
$newline never
_{StudyDegreeTerm degree terms}, _{MsgStudyFeatureAge} #{studyFeaturesSemester}
|]
registeredUserName :: Map (E.Value ExamRegistrationId) ExamUserTableData -> ExamRegistrationId -> Widget
registeredUserName existing (E.Value -> registration) = nameWidget userDisplayName userSurname
where
Entity _ User{..} = view resultUser $ existing ! registration
guessUser :: ExamUserTableCsv -> DB (Bool, UserId)
guessUser ExamUserTableCsv{..} = $cachedHereBinary (csvEUserMatriculation, csvEUserName, csvEUserSurname) $ do
users <- E.select . E.from $ \user -> do
E.where_ . E.and $ catMaybes
[ (user E.^. UserMatrikelnummer E.==.) . E.val . Just <$> csvEUserMatriculation
, (user E.^. UserDisplayName E.==.) . E.val <$> csvEUserName
, (user E.^. UserSurname E.==.) . E.val <$> csvEUserSurname
]
let isCourseParticipant = E.exists . E.from $ \courseParticipant ->
E.where_ $ courseParticipant E.^. CourseParticipantCourse E.==. E.val examCourse
E.&&. courseParticipant E.^. CourseParticipantUser E.==. user E.^. UserId
E.limit 2
return $ (isCourseParticipant, user E.^. UserId)
case users of
(filter . view $ _1 . _Value -> [(E.Value isPart, E.Value uid)])
-> return (isPart, uid)
[(E.Value isPart, E.Value uid)]
-> return (isPart, uid)
_other
-> throwM ExamUserCsvExceptionNoMatchingUser
lookupOccurrence :: ExamUserTableCsv -> DB (Maybe ExamOccurrenceId)
lookupOccurrence ExamUserTableCsv{..} = $cachedHereBinary (CI.foldedCase <$> csvEUserOccurrence) . for csvEUserOccurrence $ \occName -> do
occIds <- selectKeysList [ ExamOccurrenceName ==. occName, ExamOccurrenceExam ==. eid ] []
case occIds of
[occId] -> return occId
_other -> throwM ExamUserCsvExceptionNoMatchingOccurrence
lookupStudyFeatures :: ExamUserTableCsv -> DB (Maybe StudyFeaturesId)
lookupStudyFeatures csv@ExamUserTableCsv{..} = do
uid <- view _2 <$> guessUser csv
studyFeatures <- E.select . E.from $ \(studyFeatures `E.InnerJoin` studyDegree `E.InnerJoin` studyTerms) -> do
E.on $ studyTerms E.^. StudyTermsId E.==. studyFeatures E.^. StudyFeaturesField
E.on $ studyDegree E.^. StudyDegreeId E.==. studyFeatures E.^. StudyFeaturesDegree
E.where_ . E.and $ catMaybes
[ do
field <- csvEUserField
return . E.or $ catMaybes
[ Just $ studyTerms E.^. StudyTermsName `E.ciEq` E.just (E.val field)
, Just $ studyTerms E.^. StudyTermsShorthand `E.ciEq` E.just (E.val field)
, (studyTerms E.^. StudyTermsKey E.==.) . E.val <$> readMay field
]
, do
degree <- csvEUserDegree
return . E.or $ catMaybes
[ Just $ studyDegree E.^. StudyDegreeName `E.ciEq` E.just (E.val degree)
, Just $ studyDegree E.^. StudyDegreeShorthand `E.ciEq` E.just (E.val degree)
, (studyDegree E.^. StudyDegreeKey E.==.) . E.val <$> readMay degree
]
, (studyFeatures E.^. StudyFeaturesSemester E.==.) . E.val <$> csvEUserSemester
]
E.where_ $ studyFeatures E.^. StudyFeaturesUser E.==. E.val uid
E.&&. studyFeatures E.^. StudyFeaturesType E.==. E.val FieldPrimary
E.&&. studyFeatures E.^. StudyFeaturesValid E.==. E.val True
E.limit 2
return $ studyFeatures E.^. StudyFeaturesId
case studyFeatures of
[E.Value fid] -> return $ Just fid
_other
| is _Nothing csvEUserField
, is _Nothing csvEUserDegree
, is _Nothing csvEUserSemester
-> return Nothing
_other -> throwM ExamUserCsvExceptionNoMatchingStudyFeatures
examUsersDBTableValidator = def
postprocess :: FormResult (First ExamUserActionData, DBFormResult ExamRegistrationId Bool ExamUserTableData) -> FormResult (ExamUserActionData, Set ExamRegistrationId)
postprocess inp = do
(First (Just act), regMap) <- inp
let regSet = Map.keysSet . Map.filter id $ getDBFormResult (const False) regMap
return (act, regSet)
over _1 postprocess <$> dbTable examUsersDBTableValidator examUsersDBTable
formResult registrationResult $ \case
(ExamUserDeregisterData, selectedRegistrations) -> do
nrDel <- runDB $ deleteWhereCount
[ ExamRegistrationId <-. Set.toList selectedRegistrations
]
addMessageI Success $ MsgExamUsersDeregistered nrDel
redirect $ CExamR tid ssh csh examn EUsersR
(ExamUserAssignOccurrenceData occId, selectedRegistrations) -> do
nrUpdated <- runDB $ updateWhereCount
[ ExamRegistrationId <-. Set.toList selectedRegistrations
]
[ ExamRegistrationOccurrence =. occId
]
addMessageI Success $ MsgExamUsersOccurrenceUpdated nrUpdated
redirect $ CExamR tid ssh csh examn EUsersR
siteLayoutMsg (prependCourseTitle tid ssh csh MsgExamUsersHeading) $ do siteLayoutMsg (prependCourseTitle tid ssh csh MsgExamUsersHeading) $ do
setTitleI $ prependCourseTitle tid ssh csh MsgExamUsersHeading setTitleI $ prependCourseTitle tid ssh csh MsgExamUsersHeading
@ -897,18 +1291,26 @@ postERegisterR tid ssh csh examn = do
((btnResult, _), _) <- runFormPost buttonForm ((btnResult, _), _) <- runFormPost buttonForm
formResult btnResult $ \case formResult btnResult $ \case
BtnRegister -> do BtnExamRegister -> do
runDB $ do runDB $ do
now <- liftIO getCurrentTime now <- liftIO getCurrentTime
insert_ $ ExamRegistration eId uid Nothing now insert_ $ ExamRegistration eId uid Nothing now
audit' $ TransactionExamRegister (unTermKey tid) (unSchoolKey ssh) csh examn userIdent audit' $ TransactionExamRegister (unTermKey tid) (unSchoolKey ssh) csh examn userIdent
addMessageI Success $ MsgExamRegisteredSuccess examn addMessageWidget Success [whamlet|
<div>#{iconExamRegister True}
<div>&nbsp;
<div>_{MsgExamRegisteredSuccess examn}
|]
redirect $ CExamR tid ssh csh examn EShowR redirect $ CExamR tid ssh csh examn EShowR
BtnDeregister -> do BtnExamDeregister -> do
runDB $ do runDB $ do
deleteBy $ UniqueExamRegistration eId uid deleteBy $ UniqueExamRegistration eId uid
audit' $ TransactionExamDeregister (unTermKey tid) (unSchoolKey ssh) csh examn userIdent audit' $ TransactionExamDeregister (unTermKey tid) (unSchoolKey ssh) csh examn userIdent
addMessageI Success $ MsgExamDeregisteredSuccess examn addMessageWidget Info [whamlet|
<div>#{iconExamRegister False}
<div>&nbsp;
<div>_{MsgExamDeregisteredSuccess examn}
|] -- yes, it's a success message, but it should be visually different from a positive success, since most will just note the positive green color! See discussion on commit 5f4925a4
redirect $ CExamR tid ssh csh examn EShowR redirect $ CExamR tid ssh csh examn EShowR
invalidArgs ["Register/Deregister button required"] invalidArgs ["Register/Deregister button required"]

View File

@ -47,7 +47,7 @@ homeOpenCourses = do
let tid = courseTerm course let tid = courseTerm course
ssh = courseSchool course ssh = courseSchool course
csh = courseShorthand course csh = courseShorthand course
anchorCell (CourseR tid ssh csh CShowR) (toWidget csh) anchorCell (CourseR tid ssh csh CShowR) csh
, 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
] ]
@ -130,9 +130,9 @@ homeUpcomingSheets uid = do
, sortable (Just "school") (i18nCell MsgCourseSchool) $ \DBRow{ dbrOutput=(_,E.Value ssh,_,_,_,_) } -> , sortable (Just "school") (i18nCell MsgCourseSchool) $ \DBRow{ dbrOutput=(_,E.Value ssh,_,_,_,_) } ->
textCell $ toMessage ssh textCell $ toMessage ssh
, sortable (Just "course") (i18nCell MsgCourse) $ \DBRow{ dbrOutput=(E.Value tid, E.Value ssh, E.Value csh, _, _, _) } -> , sortable (Just "course") (i18nCell MsgCourse) $ \DBRow{ dbrOutput=(E.Value tid, E.Value ssh, E.Value csh, _, _, _) } ->
anchorCell (CourseR tid ssh csh CShowR) (toWidget csh) anchorCell (CourseR tid ssh csh CShowR) csh
, sortable (Just "sheet") (i18nCell MsgSheet) $ \DBRow{ dbrOutput=(E.Value tid, E.Value ssh, E.Value csh, E.Value shn, _, _) } -> , sortable (Just "sheet") (i18nCell MsgSheet) $ \DBRow{ dbrOutput=(E.Value tid, E.Value ssh, E.Value csh, E.Value shn, _, _) } ->
anchorCell (CSheetR tid ssh csh shn SShowR) (toWidget shn) anchorCell (CSheetR tid ssh csh shn SShowR) shn
, sortable (Just "deadline") (i18nCell MsgDeadline) $ \DBRow{ dbrOutput=(_, _, _, _, E.Value deadline, _) } -> , sortable (Just "deadline") (i18nCell MsgDeadline) $ \DBRow{ dbrOutput=(_, _, _, _, E.Value deadline, _) } ->
cell $ formatTime SelFormatDateTime deadline >>= toWidget cell $ formatTime SelFormatDateTime deadline >>= toWidget
, sortable (Just "done") (i18nCell MsgDone) $ \DBRow{ dbrOutput=(E.Value tid, E.Value ssh, E.Value csh, E.Value shn, _, E.Value mbsid) } -> , sortable (Just "done") (i18nCell MsgDone) $ \DBRow{ dbrOutput=(E.Value tid, E.Value ssh, E.Value csh, E.Value shn, _, E.Value mbsid) } ->
@ -142,7 +142,7 @@ homeUpcomingSheets uid = do
whenM (hasWriteAccessTo submitRoute) $ whenM (hasWriteAccessTo submitRoute) $
modal [whamlet|_{MsgMenuSubmissionNew}|] . Left $ SomeRoute submitRoute modal [whamlet|_{MsgMenuSubmissionNew}|] . Left $ SomeRoute submitRoute
(Just sid) -> anchorCellM (CSubmissionR tid ssh csh shn <$> encrypt sid <*> pure SubShowR) (Just sid) -> anchorCellM (CSubmissionR tid ssh csh shn <$> encrypt sid <*> pure SubShowR)
(toWidget $ hasTickmark True) (hasTickmark True)
] ]
let validator = def & defaultSorting [SortDescBy "done", SortAscBy "deadline"] let validator = def & defaultSorting [SortDescBy "done", SortAscBy "deadline"]
sheetTable <- liftHandlerT . runDB $ dbTableWidget' validator DBTable sheetTable <- liftHandlerT . runDB $ dbTableWidget' validator DBTable
@ -228,18 +228,10 @@ homeUpcomingExams uid = do
, sortable (Just "name") (i18nCell MsgExamName) $ \DBRow{ dbrOutput } -> do , sortable (Just "name") (i18nCell MsgExamName) $ \DBRow{ dbrOutput } -> do
let Entity _ Exam{..} = view lensExam dbrOutput let Entity _ Exam{..} = view lensExam dbrOutput
Entity _ Course{..} = view lensCourse dbrOutput Entity _ Course{..} = view lensCourse dbrOutput
indicatorCell <> anchorCell (CExamR courseTerm courseSchool courseShorthand examName EShowR) (toWidget examName) indicatorCell <> anchorCell (CExamR courseTerm courseSchool courseShorthand examName EShowR) examName
, sortable (Just "register-from") (i18nCell MsgExamRegisterFrom) $ \DBRow { dbrOutput = view lensExam -> Entity _ Exam{..} } -> maybe mempty dateTimeCell examRegisterFrom , sortable (Just "register-from") (i18nCell MsgExamRegisterFrom) $ \DBRow { dbrOutput = view lensExam -> Entity _ Exam{..} } -> maybe mempty dateTimeCell examRegisterFrom
, sortable (Just "register-to") (i18nCell MsgExamRegisterTo) $ \DBRow { dbrOutput = view lensExam -> Entity _ Exam{..} } -> maybe mempty dateTimeCell examRegisterTo , sortable (Just "register-to") (i18nCell MsgExamRegisterTo) $ \DBRow { dbrOutput = view lensExam -> Entity _ Exam{..} } -> maybe mempty dateTimeCell examRegisterTo
, sortable (Just "time") (i18nCell MsgExamTime) $ \DBRow{ dbrOutput = view lensExam -> Entity _ Exam{..} } -> cell $ do , sortable (Just "time") (i18nCell MsgExamTime) $ \DBRow{ dbrOutput = view lensExam -> Entity _ Exam{..} } -> maybe mempty (cell . flip (formatTimeRangeW SelFormatDateTime) examEnd) examStart
startT <- formatTime SelFormatDateTime examStart
endT <- traverse (\examEnd' -> formatTime (bool SelFormatDateTime SelFormatTime $ ((==) `on` utctDay) examStart examEnd') examEnd') examEnd
[whamlet|
$newline never
#{startT}
$maybe endT' <- endT
\ #{endT'}
|]
{- NOTE: We do not want thoughtless exam registrations, since many people click "register" and don't show up, causing logistic problems. {- NOTE: We do not want thoughtless exam registrations, since many people click "register" and don't show up, causing logistic problems.
Hence we force them here to click twice. Maybe add a captcha where users have to distinguish pictures showing pink elephants and course lecturers. Hence we force them here to click twice. Maybe add a captcha where users have to distinguish pictures showing pink elephants and course lecturers.
, sortable Nothing mempty $ \DBRow{ dbrOutput } -> sqlCell $ do , sortable Nothing mempty $ \DBRow{ dbrOutput } -> sqlCell $ do
@ -259,7 +251,7 @@ homeUpcomingExams uid = do
| otherwise -> return mempty | otherwise -> return mempty
-} -}
, sortable (Just "registered") (i18nCell MsgExamRegistration ) $ \DBRow{ dbrOutput } -> sqlCell $ do , sortable (Just "registered") (i18nCell MsgExamRegistration ) $ \DBRow{ dbrOutput } -> sqlCell $ do
let Entity eId Exam{..} = view lensExam dbrOutput let Entity eId Exam{..} = view lensExam dbrOutput
Entity _ Course{..} = view lensCourse dbrOutput Entity _ Course{..} = view lensCourse dbrOutput
mayRegister <- (== Authorized) <$> evalAccessDB (CExamR courseTerm courseSchool courseShorthand examName ERegisterR) True mayRegister <- (== Authorized) <$> evalAccessDB (CExamR courseTerm courseSchool courseShorthand examName ERegisterR) True
isRegistered <- existsBy $ UniqueExamRegistration eId uid isRegistered <- existsBy $ UniqueExamRegistration eId uid

View File

@ -29,6 +29,7 @@ import qualified Data.Conduit.List as C
-- import qualified Data.CaseInsensitive as CI -- import qualified Data.CaseInsensitive as CI
import qualified Database.Esqueleto as E import qualified Database.Esqueleto as E
import qualified Database.Esqueleto.Utils as E
-- import qualified Database.Esqueleto.Internal.Sql as E -- import qualified Database.Esqueleto.Internal.Sql as E
import Control.Monad.Writer (MonadWriter(..), execWriterT) import Control.Monad.Writer (MonadWriter(..), execWriterT)
@ -122,7 +123,7 @@ makeSheetForm msId template = identifyForm FIDsheet $ \html -> do
& setTooltip MsgSheetMarkingTip) (sfMarkingF <$> template) & setTooltip MsgSheetMarkingTip) (sfMarkingF <$> template)
<* aformSection MsgSheetFormType <* aformSection MsgSheetFormType
<*> sheetTypeAFormReq (fslI MsgSheetType <*> sheetTypeAFormReq (fslI MsgSheetType
& setTooltip (uniworxMessages [MsgSheetTypeInfoBonus,MsgSheetTypeInfoNotGraded])) & setTooltip (uniworxMessages [MsgSheetTypeInfoBonus, MsgSheetTypeInfoInformational, MsgSheetTypeInfoNotGraded]))
(sfType <$> template) (sfType <$> template)
<*> sheetGroupAFormReq (fslI MsgSheetGroup) (sfGrouping <$> template) <*> sheetGroupAFormReq (fslI MsgSheetGroup) (sfGrouping <$> template)
<*> submissionModeForm ((sfSubmissionMode <$> template) <|> pure (SubmissionMode False . Just $ UploadAny True defaultExtensionRestriction)) <*> submissionModeForm ((sfSubmissionMode <$> template) <|> pure (SubmissionMode False . Just $ UploadAny True defaultExtensionRestriction))
@ -206,7 +207,7 @@ getSheetListR tid ssh csh = do
sheetCol = widgetColonnade . mconcat $ sheetCol = widgetColonnade . mconcat $
[ -- dbRow , [ -- dbRow ,
sortable (Just "name") (i18nCell MsgSheet) sortable (Just "name") (i18nCell MsgSheet)
$ \DBRow{dbrOutput=(Entity _ Sheet{..}, _, _, _)} -> anchorCell (CSheetR tid ssh csh sheetName SShowR) (toWidget sheetName) $ \DBRow{dbrOutput=(Entity _ Sheet{..}, _, _, _)} -> anchorCell (CSheetR tid ssh csh sheetName SShowR) sheetName
, sortable (Just "last-edit") (i18nCell MsgLastEdit) , sortable (Just "last-edit") (i18nCell MsgLastEdit)
$ \DBRow{dbrOutput=(_, E.Value mEditTime, _, _)} -> foldMap dateTimeCell mEditTime $ \DBRow{dbrOutput=(_, E.Value mEditTime, _, _)} -> foldMap dateTimeCell mEditTime
, sortable (Just "visible-from") (i18nCell MsgAccessibleSince) , sortable (Just "visible-from") (i18nCell MsgAccessibleSince)
@ -393,7 +394,7 @@ getSShowR tid ssh csh shn = do
, dbtIdent = "files" :: Text , dbtIdent = "files" :: Text
, dbtSorting = Map.fromList , dbtSorting = Map.fromList
[ ( "type" [ ( "type"
, SortColumn $ \(sheetFile `E.InnerJoin` _file) -> sheetFile E.^. SheetFileType , SortColumn $ \(sheetFile `E.InnerJoin` _file) -> E.orderByEnum $ sheetFile E.^. SheetFileType
) )
, ( "path" , ( "path"
, SortColumn $ \(_sheetFile `E.InnerJoin` file) -> file E.^. FileTitle , SortColumn $ \(_sheetFile `E.InnerJoin` file) -> file E.^. FileTitle
@ -819,7 +820,10 @@ correctorForm shid = wFormToAForm $ do
postProcess' (Right sheetCorrectorUser, (sheetCorrectorState, sheetCorrectorLoad)) = Right SheetCorrector{..} postProcess' (Right sheetCorrectorUser, (sheetCorrectorState, sheetCorrectorLoad)) = Right SheetCorrector{..}
postProcess' (Left email, (state, load)) = Left (email, shid, (InvDBDataSheetCorrector load state, InvTokenDataSheetCorrector)) postProcess' (Left email, (state, load)) = Left (email, shid, (InvDBDataSheetCorrector load state, InvTokenDataSheetCorrector))
fmap postProcess <$> massInputW MassInput{..} (fslI MsgCorrectors & setTooltip MsgMassInputTip) True (Just . Map.fromList . zip [0..] $ Map.toList loads) filledData :: Maybe (Map ListPosition (Either UserEmail UserId, (CorrectorState, Load)))
filledData = Just . Map.fromList . zip [0..] $ Map.toList loads -- TODO orderBy Name?!
fmap postProcess <$> massInputW MassInput{..} (fslI MsgCorrectors & setTooltip MsgMassInputTip) True filledData
getSCorrR, postSCorrR :: TermId -> SchoolId -> CourseShorthand -> SheetName -> Handler Html getSCorrR, postSCorrR :: TermId -> SchoolId -> CourseShorthand -> SheetName -> Handler Html
postSCorrR = getSCorrR postSCorrR = getSCorrR

View File

@ -451,6 +451,8 @@ submissionHelper tid ssh csh shn mcid = do
deleteWhere [InvitationFor ==. invRef @SubmissionUser smid, InvitationEmail /<-. subEmails] deleteWhere [InvitationFor ==. invRef @SubmissionUser smid, InvitationEmail /<-. subEmails]
insertMany_ $ map (flip SubmissionUser smid) subUids insertMany_ $ map (flip SubmissionUser smid) subUids
sinkInvitationsF submissionUserInvitationConfig $ map (\lEmail -> (lEmail, smid, (InvDBDataSubmissionUser, InvTokenDataSubmissionUser))) subEmails sinkInvitationsF submissionUserInvitationConfig $ map (\lEmail -> (lEmail, smid, (InvDBDataSubmissionUser, InvTokenDataSubmissionUser))) subEmails
addMessageI Success $ if | Nothing <- msmid -> MsgSubmissionCreated
| otherwise -> MsgSubmissionUpdated
return smid return smid
cID <- encrypt smid cID <- encrypt smid
return $ Just cID return $ Just cID

View File

@ -165,7 +165,7 @@ postMessageListR = do
dbtColonnade = mconcat dbtColonnade = mconcat
[ dbSelect (applying _2) id $ \DBRow{ dbrOutput = (Entity smId _, _) } -> encrypt smId [ dbSelect (applying _2) id $ \DBRow{ dbrOutput = (Entity smId _, _) } -> encrypt smId
, dbRow , dbRow
, sortable Nothing (i18nCell MsgSystemMessageId) $ \DBRow{ dbrOutput = (Entity smId _, _) } -> anchorCellM' (encrypt smId) MessageR (toWidget . tshow . ciphertext) , sortable Nothing (i18nCell MsgSystemMessageId) $ \DBRow{ dbrOutput = (Entity smId _, _) } -> anchorCellM' (encrypt smId) MessageR ciphertext
, sortable (Just "from") (i18nCell MsgSystemMessageFrom) $ \DBRow{ dbrOutput = (Entity _ SystemMessage{..}, _) } -> cell $ maybe mempty (formatTimeW SelFormatDateTime) systemMessageFrom , sortable (Just "from") (i18nCell MsgSystemMessageFrom) $ \DBRow{ dbrOutput = (Entity _ SystemMessage{..}, _) } -> cell $ maybe mempty (formatTimeW SelFormatDateTime) systemMessageFrom
, sortable (Just "to") (i18nCell MsgSystemMessageTo) $ \DBRow{ dbrOutput = (Entity _ SystemMessage{..}, _) } -> cell $ maybe mempty (formatTimeW SelFormatDateTime) systemMessageTo , sortable (Just "to") (i18nCell MsgSystemMessageTo) $ \DBRow{ dbrOutput = (Entity _ SystemMessage{..}, _) } -> cell $ maybe mempty (formatTimeW SelFormatDateTime) systemMessageTo
, sortable (Just "authenticated") (i18nCell MsgSystemMessageAuthenticatedOnly) $ \DBRow{ dbrOutput = (Entity _ SystemMessage{..}, _) } -> tickmarkCell systemMessageAuthenticatedOnly , sortable (Just "authenticated") (i18nCell MsgSystemMessageAuthenticatedOnly) $ \DBRow{ dbrOutput = (Entity _ SystemMessage{..}, _) } -> tickmarkCell systemMessageAuthenticatedOnly

View File

@ -61,7 +61,7 @@ getCTutorialListR tid ssh csh = do
<li> <li>
^{nameEmailWidget' tutor} ^{nameEmailWidget' tutor}
|] |]
, sortable (Just "participants") (i18nCell MsgTutorialParticipants) $ \DBRow{ dbrOutput = (Entity _ Tutorial{..}, n) } -> anchorCell (CTutorialR tid ssh csh tutorialName TUsersR) . toWidget $ tshow n , sortable (Just "participants") (i18nCell MsgTutorialParticipants) $ \DBRow{ dbrOutput = (Entity _ Tutorial{..}, n) } -> anchorCell (CTutorialR tid ssh csh tutorialName TUsersR) $ tshow n
, sortable (Just "capacity") (i18nCell MsgTutorialCapacity) $ \DBRow{ dbrOutput = (Entity _ Tutorial{..}, _) } -> maybe mempty (textCell . tshow) tutorialCapacity , sortable (Just "capacity") (i18nCell MsgTutorialCapacity) $ \DBRow{ dbrOutput = (Entity _ Tutorial{..}, _) } -> maybe mempty (textCell . tshow) tutorialCapacity
, sortable (Just "room") (i18nCell MsgTutorialRoom) $ \DBRow{ dbrOutput = (Entity _ Tutorial{..}, _) } -> textCell tutorialRoom , sortable (Just "room") (i18nCell MsgTutorialRoom) $ \DBRow{ dbrOutput = (Entity _ Tutorial{..}, _) } -> textCell tutorialRoom
, sortable Nothing (i18nCell MsgTutorialTime) $ \DBRow{ dbrOutput = (Entity _ Tutorial{..}, _) } -> occurrencesCell tutorialTime , sortable Nothing (i18nCell MsgTutorialTime) $ \DBRow{ dbrOutput = (Entity _ Tutorial{..}, _) } -> occurrencesCell tutorialTime

View File

@ -10,9 +10,11 @@ module Handler.Utils.Csv
, ToNamedRecord(..), FromNamedRecord(..) , ToNamedRecord(..), FromNamedRecord(..)
, DefaultOrdered(..) , DefaultOrdered(..)
, ToField(..), FromField(..) , ToField(..), FromField(..)
, CsvRendered(..)
, toCsvRendered
) where ) where
import Import import Import hiding (Header)
import Data.Csv import Data.Csv
import Data.Csv.Conduit import Data.Csv.Conduit
@ -21,6 +23,8 @@ import qualified Data.Conduit.List as C
import qualified Data.Conduit.Combinators as C (sourceLazy) import qualified Data.Conduit.Combinators as C (sourceLazy)
import qualified Data.Map as Map import qualified Data.Map as Map
import qualified Data.Vector as Vector
import qualified Data.HashMap.Strict as HashMap
deriving instance Typeable CsvParseError deriving instance Typeable CsvParseError
@ -69,3 +73,31 @@ fileSourceCsv :: ( FromNamedRecord csv
=> FileInfo => FileInfo
-> Source m csv -> Source m csv
fileSourceCsv = (.| decodeCsv) . fileSource fileSourceCsv = (.| decodeCsv) . fileSource
data CsvRendered = CsvRendered
{ csvRenderedHeader :: Header
, csvRenderedData :: [NamedRecord]
} deriving (Eq, Read, Show, Generic, Typeable)
instance ToWidget UniWorX CsvRendered where
toWidget CsvRendered{..} = liftWidgetT $(widgetFile "widgets/csvRendered")
where
csvData = [ [ decodeUtf8 <$> HashMap.lookup columnKey row
| columnKey <- Vector.toList csvRenderedHeader
]
| row <- csvRenderedData
]
headers = decodeUtf8 <$> Vector.toList csvRenderedHeader
toCsvRendered :: forall mono.
( ToNamedRecord (Element mono)
, DefaultOrdered (Element mono)
, MonoFoldable mono
)
=> mono -> CsvRendered
toCsvRendered (otoList -> csvs) = CsvRendered{..}
where
csvRenderedHeader = headerOrder (error "not forced" :: Element mono)
csvRenderedData = map toNamedRecord csvs

View File

@ -3,10 +3,11 @@ module Handler.Utils.DateTime
, localTimeToUTC, TZ.LocalToUTCResult(..) , localTimeToUTC, TZ.LocalToUTCResult(..)
, toMidnight, beforeMidnight, toMidday, toMorning , toMidnight, beforeMidnight, toMidday, toMorning
, formatDiffDays , formatDiffDays
, formatTime, formatTime', formatTimeW , formatTime'
, formatTime, formatTimeW, formatTimeMail
, formatTimeRange, formatTimeRangeW, formatTimeRangeMail
, getTimeLocale, getDateTimeFormat , getTimeLocale, getDateTimeFormat
, validDateTimeFormats, dateTimeFormatOptions , validDateTimeFormats, dateTimeFormatOptions
, formatTimeMail
, addOneWeek, addWeeks , addOneWeek, addWeeks
, weeksToAdd , weeksToAdd
, setYear , setYear
@ -236,3 +237,44 @@ ceilingMinuteBy margin roundto utct = addUTCTime bonus utct
newMin = roundToNearestMultiple roundto $ oldMin + margin newMin = roundToNearestMultiple roundto $ oldMin + margin
newTime = oldTime { todMin = newMin, todSec = 0 } -- might be invalid, but correctly treated by `timeOfDayToTime` newTime = oldTime { todMin = newMin, todSec = 0 } -- might be invalid, but correctly treated by `timeOfDayToTime`
bonus = realToFrac $ timeOfDayToTime newTime - timeOfDayToTime oldTime bonus = realToFrac $ timeOfDayToTime newTime - timeOfDayToTime oldTime
formatTimeRange' :: ( HasLocalTime t, HasLocalTime t'
, Monad m
)
=> (forall t2. HasLocalTime t2 => SelDateTimeFormat -> t2 -> m Text) -- ^ @formatTime@
-> SelDateTimeFormat
-> t -- ^ Start
-> Maybe t' -- ^ End
-> m Text
formatTimeRange' cont proj startT endT = do
startT' <- cont proj startT
let
endProj = (/\ proj) $ if
| Just endT' <- endT
, ((==) `on` localDay) (toLocalTime startT) (toLocalTime endT')
-> SelFormatTime
| otherwise
-> SelFormatDateTime
endT' <- for endT $ cont endProj
return $ case endT' of
Nothing -> startT'
Just endT'' -> [st|#{startT'} #{endT''}|]
formatTimeRange :: ( HasLocalTime t, HasLocalTime t'
, MonadHandler m
, HandlerSite m ~ UniWorX
)
=> SelDateTimeFormat
-> t -- ^ Start
-> Maybe t' -- ^ End
-> m Text
formatTimeRange = formatTimeRange' formatTime
formatTimeRangeW :: (HasLocalTime t, HasLocalTime t') => SelDateTimeFormat -> t -> Maybe t' -> Widget
formatTimeRangeW s t t' = toWidget =<< formatTimeRange s t t'
formatTimeRangeMail :: (MonadMail m, HasLocalTime t, HasLocalTime t') => SelDateTimeFormat -> t -> Maybe t' -> m Text
formatTimeRangeMail = formatTimeRange' formatTimeMail

View File

@ -1,17 +1,23 @@
module Handler.Utils.Exam module Handler.Utils.Exam
( fetchExamAux ( fetchExamAux
, fetchExam, fetchExamId, fetchCourseIdExamId, fetchCourseIdExam , fetchExam, fetchExamId, fetchCourseIdExamId, fetchCourseIdExam
, examBonus, examBonusPossible, examBonusAchieved
) where ) where
import Import.NoFoundation import Import.NoFoundation
import Database.Persist.Sql (SqlBackendCanRead) import Database.Persist.Sql (SqlBackendCanRead)
import qualified Database.Esqueleto as E import qualified Database.Esqueleto as E
import qualified Database.Esqueleto.Utils as E
import qualified Database.Esqueleto.Internal.Sql as E import qualified Database.Esqueleto.Internal.Sql as E
import Database.Esqueleto.Utils.TH import Database.Esqueleto.Utils.TH
import Utils.Lens import Utils.Lens
import qualified Data.Conduit.List as C
import qualified Data.Map as Map
fetchExamAux :: ( SqlBackendCanRead backend fetchExamAux :: ( SqlBackendCanRead backend
, E.SqlSelect b a , E.SqlSelect b a
@ -45,3 +51,35 @@ fetchCourseIdExamId tid ssh cid examn = $(unValueN 2) <$> fetchExamAux (\tutoria
fetchCourseIdExam :: MonadHandler m => TermId -> SchoolId -> CourseShorthand -> ExamName -> ReaderT SqlBackend m (Key Course, Entity Exam) fetchCourseIdExam :: MonadHandler m => TermId -> SchoolId -> CourseShorthand -> ExamName -> ReaderT SqlBackend m (Key Course, Entity Exam)
fetchCourseIdExam tid ssh cid examn = over _1 E.unValue <$> fetchExamAux (\tutorial course -> (course E.^. CourseId, tutorial)) tid ssh cid examn fetchCourseIdExam tid ssh cid examn = over _1 E.unValue <$> fetchExamAux (\tutorial course -> (course E.^. CourseId, tutorial)) tid ssh cid examn
examBonus :: MonadHandler m => Entity Exam -> ReaderT SqlBackend m (Map UserId SheetTypeSummary)
examBonus (Entity eId Exam{..}) = runConduit $
let
rawData = E.selectSource . E.from $ \(((examRegistration `E.LeftOuterJoin` examOccurrence) `E.InnerJoin` sheet) `E.LeftOuterJoin` submission) -> E.distinctOnOrderBy [ E.asc $ examRegistration E.^. ExamRegistrationUser, E.asc $ sheet E.^. SheetId ] $ do
E.on $ submission E.?. SubmissionSheet E.==. E.just (sheet E.^. SheetId)
E.&&. E.exists (E.from $ \submissionUser -> E.where_ $ submissionUser E.^. SubmissionUserUser E.==. examRegistration E.^. ExamRegistrationUser
E.&&. E.just (submissionUser E.^. SubmissionUserSubmission) E.==. submission E.?. SubmissionId
)
E.on E.true
E.on $ examRegistration E.^. ExamRegistrationOccurrence E.==. examOccurrence E.?. ExamOccurrenceId
E.where_ $ sheet E.^. SheetCourse E.==. E.val examCourse
E.&&. examRegistration E.^. ExamRegistrationExam E.==. E.val eId
E.where_ $ E.case_
[ E.when_
( E.not_ . E.isNothing $ examRegistration E.^. ExamRegistrationOccurrence )
E.then_
( E.just (sheet E.^. SheetActiveTo) E.<=. examOccurrence E.?. ExamOccurrenceStart
E.&&. sheet E.^. SheetVisibleFrom E.<=. examOccurrence E.?. ExamOccurrenceStart
)
]
( E.else_ . E.not_ . E.isNothing $ sheet E.^. SheetVisibleFrom
)
return (examRegistration E.^. ExamRegistrationUser, sheet E.^. SheetType, submission)
accum = C.fold ?? Map.empty $ \acc (E.Value uid, E.Value sheetType, fmap entityVal -> sub) ->
Map.unionWith mappend acc . Map.singleton uid . sheetTypeSum sheetType . (>>= submissionRatingPoints) $ assertM submissionRatingDone sub
in rawData .| accum
examBonusPossible, examBonusAchieved :: UserId -> Map UserId SheetTypeSummary -> Maybe SheetGradeSummary
examBonusPossible uid bonusMap = normalSummary <$> Map.lookup uid bonusMap
examBonusAchieved uid bonusMap = (mappend <$> normalSummary <*> bonusSummary) <$> Map.lookup uid bonusMap

View File

@ -463,8 +463,8 @@ classifyBonusRule = \case
ExamNoBonus -> ExamNoBonus' ExamNoBonus -> ExamNoBonus'
ExamBonusPoints{} -> ExamBonusPoints' ExamBonusPoints{} -> ExamBonusPoints'
bonusRuleForm :: Maybe ExamBonusRule -> AForm Handler ExamBonusRule examBonusRuleForm :: Maybe ExamBonusRule -> AForm Handler ExamBonusRule
bonusRuleForm prev = multiActionA actions (fslI MsgExamBonusRule) $ classifyBonusRule <$> prev examBonusRuleForm prev = multiActionA actions (fslI MsgExamBonusRule) $ classifyBonusRule <$> prev
where where
actions :: Map ExamBonusRule' (AForm Handler ExamBonusRule) actions :: Map ExamBonusRule' (AForm Handler ExamBonusRule)
actions = Map.fromList actions = Map.fromList
@ -681,7 +681,7 @@ nullaryPathPiece ''SheetGrading' (camelToPathPiece . dropSuffix "'")
embedRenderMessage ''UniWorX ''SheetGrading' ("SheetGrading" <>) embedRenderMessage ''UniWorX ''SheetGrading' ("SheetGrading" <>)
data SheetType' = NotGraded' | Normal' | Bonus' | Informational' data SheetType' = Normal' | Bonus' | Informational' | NotGraded'
deriving (Eq, Ord, Read, Show, Enum, Bounded) deriving (Eq, Ord, Read, Show, Enum, Bounded)
instance Universe SheetType' instance Universe SheetType'
@ -913,6 +913,16 @@ optionsPersistCryptoId filts ords toDisplay = fmap mkOptionList $ do
, optionExternalValue = toPathPiece (cId :: CryptoID UUID (Key a)) , optionExternalValue = toPathPiece (cId :: CryptoID UUID (Key a))
}) cPairs }) cPairs
examOccurrenceField :: ( MonadHandler m
, HandlerSite m ~ UniWorX
)
=> ExamId
-> Field m ExamOccurrenceId
examOccurrenceField eid
= hoistField liftHandlerT . selectField . (fmap $ fmap entityKey)
$ optionsPersistCryptoId [ ExamOccurrenceExam ==. eid ] [ Asc ExamOccurrenceName ] examOccurrenceName
formResultModal :: (MonadHandler m, RedirectUrl (HandlerSite m) route) => FormResult a -> route -> (a -> WriterT [Message] m ()) -> m () formResultModal :: (MonadHandler m, RedirectUrl (HandlerSite m) route) => FormResult a -> route -> (a -> WriterT [Message] m ()) -> m ()
formResultModal res finalDest handler = maybeT_ $ do formResultModal res finalDest handler = maybeT_ $ do
messages <- case res of messages <- case res of

View File

@ -21,8 +21,6 @@ import Utils.Lens
import Handler.Utils.Form.MassInput.Liveliness import Handler.Utils.Form.MassInput.Liveliness
import Handler.Utils.Form.MassInput.TH import Handler.Utils.Form.MassInput.TH
import Algebra.Lattice hiding (join)
import Text.Blaze (Markup) import Text.Blaze (Markup)
import qualified Data.Text as Text import qualified Data.Text as Text

View File

@ -491,17 +491,20 @@ sinkSubmission userId mExists isUpdate = do
alreadySeen <- gets $ is (_Wrapped . _Just) . sinkSeenRating alreadySeen <- gets $ is (_Wrapped . _Just) . sinkSeenRating
when alreadySeen $ throwM DuplicateRating when alreadySeen $ throwM DuplicateRating
Submission{..} <- lift $ getJust submissionId submission <- lift $ getJust submissionId
now <- liftIO getCurrentTime now <- liftIO getCurrentTime
let let
rated = submissionRatingBy == Just userId -- FIXME: This behaviour is unintuitive and needs to be replaced with an "isDone"-Field in rating files rated = submissionRatingBy submission == Just userId -- FIXME: This behaviour is unintuitive and needs to be replaced with an "isDone"-Field in rating files
r' = let Rating'{..} = r r'@Rating'{..} = r
in Rating' { ratingTime = now <$ guard rated -- Ignore `ratingTime` from result @r@ of `parseRating` to ensure plausible timestamps (`parseRating` returns file modification time for consistency with `ratingFile`)
{ ratingTime = now <$ guard rated }
, .. submission' = submission
} { submissionRatingPoints = ratingPoints
let Rating'{..} = r' , submissionRatingComment = ratingComment
, submissionRatingTime = ratingTime
, submissionRatingBy = userId <$ guard rated -- This is never an update due to the definition of rated; this is done so idempotency of uploads is maintained (FIXME: when "isDone"-Field is introduced, set this to `Just userId`)
}
tellSt $ mempty{ sinkSeenRating = Last $ Just r' } tellSt $ mempty{ sinkSeenRating = Last $ Just r' }
unless isUpdate $ throwM RatingWithoutUpdate unless isUpdate $ throwM RatingWithoutUpdate
@ -510,25 +513,23 @@ sinkSubmission userId mExists isUpdate = do
-- --
-- 'fileModified' is simply stored and never inspected while -- 'fileModified' is simply stored and never inspected while
-- 'submissionChanged' is always set to @now@. -- 'submissionChanged' is always set to @now@.
let anyChanges = or $ let anyChanges = any (\f -> f submission submission') $
[ submissionRatingPoints /= ratingPoints [ (/=) `on` submissionRatingPoints
, submissionRatingComment /= ratingComment , (/=) `on` submissionRatingComment
, (/=) `on` submissionRatingDone
, (/=) `on` submissionRatingBy
] ]
when anyChanges $ do when anyChanges $ do
touchSubmission touchSubmission
Sheet{..} <- lift $ getJust submissionSheet Sheet{..} <- lift . getJust $ submissionSheet submission'
mapM_ throwM $ validateRating sheetType r' mapM_ throwM $ validateRating sheetType r'
when (isNothing submissionRatingTime) $ tellSt mempty { sinkSubmissionNotifyRating = Any True } when (submissionRatingDone submission' && not (submissionRatingDone submission)) $
lift $ update submissionId tellSt mempty { sinkSubmissionNotifyRating = Any True }
[ SubmissionRatingPoints =. ratingPoints lift $ replace submissionId submission'
, SubmissionRatingComment =. ratingComment
, SubmissionRatingTime =. ratingTime
, SubmissionRatingBy =. (userId <$ guard rated) -- This is never an update due to the definition of rated; this is done so idempotency of uploads is maintained (FIXME: when "isDone"-Field is introduced, set this to `Just userId`)
]
where where
a /~ b = not $ a ~~ b a /~ b = not $ a ~~ b

View File

@ -91,7 +91,7 @@ ifCell decision cTrue cFalse x
| otherwise = cFalse x | otherwise = cFalse x
linkEmptyCell :: IsDBTable m a => Route UniWorX -> Widget -> DBCell m a linkEmptyCell :: IsDBTable m a => Route UniWorX -> Widget -> DBCell m a
linkEmptyCell link wgt = linkEitherCell link (wgt,mempty) linkEmptyCell = anchorCell
msgCell :: (ToMessage t, IsDBTable m a) => t -> DBCell m a msgCell :: (ToMessage t, IsDBTable m a) => t -> DBCell m a
msgCell = textCell . toMessage msgCell = textCell . toMessage
@ -123,7 +123,7 @@ isNewCell = cell . toWidget . isNew
commentCell :: IsDBTable m a => Maybe (Route UniWorX) -> DBCell m a commentCell :: IsDBTable m a => Maybe (Route UniWorX) -> DBCell m a
commentCell Nothing = mempty commentCell Nothing = mempty
commentCell (Just link) = anchorCell link icon commentCell (Just link) = anchorCell link icon
where icon = toWidget $ hasComment True where icon = hasComment True
-- | whether something is visible or hidden -- | whether something is visible or hidden
isVisibleCell :: (IsDBTable m a) => Bool -> DBCell m a isVisibleCell :: (IsDBTable m a) => Bool -> DBCell m a
@ -134,11 +134,11 @@ isVisibleCell False = (cell . toWidget $ isVisible False) & addUrgencyClass
-- | for simple file downloads -- | for simple file downloads
fileCell :: IsDBTable m a => Route UniWorX -> DBCell m a fileCell :: IsDBTable m a => Route UniWorX -> DBCell m a
fileCell route = anchorCell route $ toWidget fileDownload fileCell route = anchorCell route fileDownload
-- | for zip-archive downloads -- | for zip-archive downloads
zipCell :: IsDBTable m a => Route UniWorX -> DBCell m a zipCell :: IsDBTable m a => Route UniWorX -> DBCell m a
zipCell route = anchorCell route $ toWidget zipDownload zipCell route = anchorCell route zipDownload
-- | Display an icon that opens a modal upon clicking -- | Display an icon that opens a modal upon clicking
modalCell :: (IsDBTable m a, ToWidget UniWorX w) => w -> DBCell m a modalCell :: (IsDBTable m a, ToWidget UniWorX w) => w -> DBCell m a
@ -214,6 +214,9 @@ maybeDateTimeCell = maybe mempty dateTimeCell
numCell :: (IsDBTable m a, Num b, ToMessage b) => b -> DBCell m a numCell :: (IsDBTable m a, Num b, ToMessage b) => b -> DBCell m a
numCell = textCell . toMessage numCell = textCell . toMessage
propCell :: (IsDBTable m a, Real b, ToMessage b) => b -> b -> DBCell m a
propCell curr max' = i18nCell $ MsgProportion (toMessage curr) (toMessage max') (toRational curr / toRational max')
int64Cell :: (IsDBTable m a) => Int64-> DBCell m a int64Cell :: (IsDBTable m a) => Int64-> DBCell m a
int64Cell = numCell int64Cell = numCell

View File

@ -6,7 +6,10 @@ module Handler.Utils.Table.Pagination
, FilterColumn(..), IsFilterColumn , FilterColumn(..), IsFilterColumn
, DBRow(..), _dbrOutput, _dbrIndex, _dbrCount , DBRow(..), _dbrOutput, _dbrIndex, _dbrCount
, DBStyle(..), defaultDBSFilterLayout, DBEmptyStyle(..) , DBStyle(..), defaultDBSFilterLayout, DBEmptyStyle(..)
, DBTCsvEncode, DBTCsvDecode , module Handler.Utils.Table.Pagination.CsvColumnExplanations
, DBCsvActionMode(..)
, DBCsvDiff(..), _DBCsvDiffNew, _DBCsvDiffExisting, _DBCsvDiffMissing, _dbCsvOldKey, _dbCsvOld, _dbCsvNewKey, _dbCsvNew
, DBTCsvEncode, DBTCsvDecode(..)
, DBTable(..), noCsvEncode, IsDBTable(..), DBCell(..) , DBTable(..), noCsvEncode, IsDBTable(..), DBCell(..)
, singletonFilter , singletonFilter
, DBParams(..) , DBParams(..)
@ -35,6 +38,7 @@ module Handler.Utils.Table.Pagination
) where ) where
import Handler.Utils.Table.Pagination.Types import Handler.Utils.Table.Pagination.Types
import Handler.Utils.Table.Pagination.CsvColumnExplanations
import Handler.Utils.Form import Handler.Utils.Form
import Handler.Utils.Csv import Handler.Utils.Csv
import Handler.Utils.ContentDisposition import Handler.Utils.ContentDisposition
@ -49,23 +53,28 @@ import qualified Database.Esqueleto.Internal.Language as E (From)
import qualified Network.Wai as Wai import qualified Network.Wai as Wai
import Control.Monad.RWS hiding ((<>), mapM_) import Control.Monad.RWS (RWST(..), execRWS)
import Control.Monad.Writer hiding ((<>), mapM_) import Control.Monad.Writer (WriterT(..))
import Control.Monad.Reader (ReaderT(..), mapReaderT) import Control.Monad.Reader (ReaderT(..), mapReaderT)
import Control.Monad.State (StateT(..), evalStateT)
import Control.Monad.Trans.Maybe import Control.Monad.Trans.Maybe
import Control.Monad.State.Class (modify)
import qualified Control.Monad.State.Class as State
import Data.Foldable (Foldable(foldMap)) import Data.Foldable (Foldable(foldMap))
import Data.Map (Map) import Data.Map (Map, (!))
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 Data.CaseInsensitive as CI import qualified Data.CaseInsensitive as CI
import Data.Csv (NamedRecord)
import Colonnade hiding (bool, fromMaybe, singleton) import Colonnade hiding (bool, fromMaybe, singleton)
import qualified Colonnade (singleton) import qualified Colonnade (singleton)
import Colonnade.Encode import Colonnade.Encode hiding (row)
import Text.Hamlet (hamletFile) import Text.Hamlet (hamletFile)
@ -96,6 +105,7 @@ import Data.Semigroup as Sem (Semigroup(..))
import qualified Data.Conduit.List as C import qualified Data.Conduit.List as C
import Handler.Utils.DateTime (formatTimeW) import Handler.Utils.DateTime (formatTimeW)
import qualified Control.Monad.Catch as Catch
#if MIN_VERSION_base(4,11,0) #if MIN_VERSION_base(4,11,0)
@ -272,7 +282,18 @@ piIsUnset PaginationInput{..} = and
] ]
data ButtonCsvMode = BtnCsvExport | BtnCsvImport data DBCsvActionMode = DBCsvActionNew | DBCsvActionExisting | DBCsvActionMissing
deriving (Read, Show, Eq, Ord, Enum, Bounded, Generic, Typeable)
instance Universe DBCsvActionMode
instance Finite DBCsvActionMode
nullaryPathPiece ''DBCsvActionMode $ camelToPathPiece' 3
deriveJSON defaultOptions
{ constructorTagModifier = camelToPathPiece' 3
} ''DBCsvActionMode
data ButtonCsvMode = BtnCsvExport | BtnCsvImport | BtnCsvImportConfirm
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic, Typeable) deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic, Typeable)
instance Universe ButtonCsvMode instance Universe ButtonCsvMode
instance Finite ButtonCsvMode instance Finite ButtonCsvMode
@ -288,21 +309,51 @@ instance Button UniWorX ButtonCsvMode where
#{iconCSV} #{iconCSV}
\ _{BtnCsvExport} \ _{BtnCsvExport}
|] |]
btnLabel BtnCsvImport btnLabel x = [whamlet|_{x}|]
= [whamlet|
$newline never
_{BtnCsvImport}
|]
data DBCsvMode = DBCsvNormal
| DBCsvExport
| DBCsvImport
{ _dbCsvFiles :: [FileInfo]
, _dbCsvModifyExisting, _dbCsvAddNew, _dbCsvDeleteMissing :: Bool
}
type DBTableKey k' = (ToJSON k', FromJSON k', Ord k', Binary k') data DBCsvMode
= DBCsvNormal
| DBCsvExport
| DBCsvImport
{ dbCsvFiles :: [FileInfo]
}
data DBCsvDiff r' csv k'
= DBCsvDiffNew
{ dbCsvNewKey :: Maybe k'
, dbCsvNew :: csv
}
| DBCsvDiffExisting
{ dbCsvOldKey :: k'
, dbCsvOld :: r'
, dbCsvNew :: csv
}
| DBCsvDiffMissing
{ dbCsvOldKey :: k'
, dbCsvOld :: r'
}
makeLenses_ ''DBCsvDiff
makePrisms ''DBCsvDiff
data DBCsvException k'
= DBCsvDuplicateKey
{ dbCsvDuplicateKey :: k'
, dbCsvDuplicateKeyRowA, dbCsvDuplicateKeyRowB :: NamedRecord
}
| DBCsvException
{ dbCsvExceptionRow :: NamedRecord
, dbCsvException :: Text
}
deriving (Show, Typeable)
makeLenses_ ''DBCsvException
instance (Typeable k', Show k') => Exception (DBCsvException k')
type DBTableKey k' = (Show k', ToJSON k', FromJSON k', Ord k', Binary k', Typeable k')
data DBRow r = forall k'. DBTableKey k' => DBRow data DBRow r = forall k'. DBTableKey k' => DBRow
{ dbrKey :: k' { dbrKey :: k'
, dbrOutput :: r , dbrOutput :: r
@ -444,9 +495,25 @@ instance PathPiece x => PathPiece (WithIdent x) where
(ident, (Text.stripSuffix sep -> Just rest)) <- return $ Text.breakOn sep txt (ident, (Text.stripSuffix sep -> Just rest)) <- return $ Text.breakOn sep txt
WithIdent <$> pure ident <*> fromPathPiece rest WithIdent <$> pure ident <*> fromPathPiece rest
type DBTCsvEncode r' csv = DictMaybe (ToNamedRecord csv, DefaultOrdered csv, CsvColumnsExplained csv) (Conduit r' (YesodDB UniWorX) csv)
type DBTCsvEncode r' csv = DictMaybe (ToNamedRecord csv, DefaultOrdered csv) (Conduit r' (YesodDB UniWorX) csv) data DBTCsvDecode r' k' csv = forall route csvAction csvActionClass csvException.
type DBTCsvDecode csv = DictMaybe (FromNamedRecord csv) (Sink csv (YesodDB UniWorX) ()) ( FromNamedRecord csv, ToNamedRecord csv, DefaultOrdered csv
, DBTableKey k'
, RedirectUrl UniWorX route
, Typeable csv
, Ord csvAction, FromJSON csvAction, ToJSON csvAction
, Ord csvActionClass
, Exception csvException
) => DBTCsvDecode
{ dbtCsvRowKey :: csv -> MaybeT (YesodDB UniWorX) k'
, dbtCsvComputeActions :: DBCsvDiff r' csv k' -> Source (YesodDB UniWorX) csvAction
, dbtCsvClassifyAction :: csvAction -> csvActionClass
, dbtCsvCoarsenActionClass :: csvActionClass -> DBCsvActionMode
, dbtCsvExecuteActions :: Sink csvAction (YesodDB UniWorX) route
, dbtCsvRenderKey :: Map k' r' -> csvAction -> Widget
, dbtCsvRenderActionClass :: csvActionClass -> Widget
, dbtCsvRenderException :: csvException -> YesodDB UniWorX Text
}
data DBTable m x = forall a r r' h i t k k' csv. data DBTable m x = forall a r r' h i t k k' csv.
( ToSortable h, Functor h ( ToSortable h, Functor h
@ -464,11 +531,11 @@ data DBTable m x = forall a r r' h i t k k' csv.
, dbtStyle :: DBStyle r' , dbtStyle :: DBStyle r'
, dbtParams :: DBParams m x , dbtParams :: DBParams m x
, dbtCsvEncode :: DBTCsvEncode r' csv , dbtCsvEncode :: DBTCsvEncode r' csv
, dbtCsvDecode :: DBTCsvDecode csv , dbtCsvDecode :: Maybe (DBTCsvDecode r' k' csv)
, dbtIdent :: i , dbtIdent :: i
} }
noCsvEncode :: DictMaybe (ToNamedRecord Void, DefaultOrdered Void) (Conduit r' (YesodDB UniWorX) Void) noCsvEncode :: DictMaybe (ToNamedRecord Void, DefaultOrdered Void, CsvColumnsExplained Void) (Conduit r' (YesodDB UniWorX) Void)
noCsvEncode = Nothing noCsvEncode = Nothing
class (MonadHandler m, HandlerSite m ~ UniWorX, Monoid' x, Monoid' (DBCell m x), Default (DBParams m x)) => IsDBTable (m :: * -> *) (x :: *) where class (MonadHandler m, HandlerSite m ~ UniWorX, Monoid' x, Monoid' (DBCell m x), Default (DBParams m x)) => IsDBTable (m :: * -> *) (x :: *) where
@ -760,9 +827,6 @@ dbTable PSValidator{..} dbtable@DBTable{ dbtIdent = dbtIdent'@(toPathPiece -> db
((csvExportRes, csvExportWdgt), csvExportEnctype) <- lift . runFormGet . identifyForm FIDDBTableCsvExport . set (mapped . mapped . _1 . mapped) DBCsvExport $ buttonForm' [BtnCsvExport] ((csvExportRes, csvExportWdgt), csvExportEnctype) <- lift . runFormGet . identifyForm FIDDBTableCsvExport . set (mapped . mapped . _1 . mapped) DBCsvExport $ buttonForm' [BtnCsvExport]
((csvImportRes, csvImportWdgt), csvImportEnctype) <- lift . runFormPost . identifyForm FIDDBTableCsvImport . renderAForm FormDBTableCsvImport $ DBCsvImport ((csvImportRes, csvImportWdgt), csvImportEnctype) <- lift . runFormPost . identifyForm FIDDBTableCsvImport . renderAForm FormDBTableCsvImport $ DBCsvImport
<$> areq fileFieldMultiple (fslI MsgCsvFile) Nothing <$> areq fileFieldMultiple (fslI MsgCsvFile) Nothing
<*> apopt checkBoxField (fslI MsgCsvModifyExisting) (Just True)
<*> apopt checkBoxField (fslI MsgCsvAddNew) (Just True)
<*> apopt checkBoxField (fslI MsgCsvDeleteMissing) (Just False)
let let
csvMode = asum csvMode = asum
@ -774,7 +838,7 @@ dbTable PSValidator{..} dbtable@DBTable{ dbtIdent = dbtIdent'@(toPathPiece -> db
{ formMethod = GET { formMethod = GET
, formAction = Just $ tblLink id , formAction = Just $ tblLink id
, formEncoding = csvExportEnctype , formEncoding = csvExportEnctype
, formAttrs = [("target", "_blank")] , formAttrs = [("target", "_blank"), ("class", "form--inline")]
, formSubmit = FormNoSubmit , formSubmit = FormNoSubmit
, formAnchor = Nothing :: Maybe Text , formAnchor = Nothing :: Maybe Text
} }
@ -786,6 +850,12 @@ dbTable PSValidator{..} dbtable@DBTable{ dbtIdent = dbtIdent'@(toPathPiece -> db
, formSubmit = FormSubmit , formSubmit = FormSubmit
, formAnchor = Nothing :: Maybe Text , formAnchor = Nothing :: Maybe Text
} }
csvColExplanations = case dbtCsvEncode of
(Just (Dict, _) :: DBTCsvEncode _ csv) -> assertM' (not . null) . Map.toList . csvColumnsExplanations $ Proxy @csv
Nothing -> Nothing
csvColExplanations' = case csvColExplanations of
Just csvColExplanations'' -> modal [whamlet|_{MsgCsvColumnsExplanationsLabel}|] $ Right $(widgetFile "table/csv-column-explanations")
Nothing -> mempty
rows' <- E.select . E.from $ \t -> do rows' <- E.select . E.from $ \t -> do
@ -824,13 +894,127 @@ dbTable PSValidator{..} dbtable@DBTable{ dbtIdent = dbtIdent'@(toPathPiece -> db
formResult csvMode $ \case formResult csvMode $ \case
DBCsvExport DBCsvExport
| Just (Dict, dbtCsvEncode') <- dbtCsvEncode | Just (Dict, dbtCsvEncode') <- dbtCsvEncode -> do
-> do setContentDisposition' . Just $ unpack dbtIdent <.> unpack extensionCsv
setContentDisposition' . Just $ unpack dbtIdent <.> unpack extensionCsv sendResponse <=< liftHandlerT . respondCsvDB $ C.sourceList rows .| dbtCsvEncode'
sendResponse <=< liftHandlerT . respondCsvDB $ C.sourceList rows .| dbtCsvEncode' DBCsvImport{..}
DBCsvImport{} | Just (DBTCsvDecode{ dbtCsvClassifyAction = dbtCsvClassifyAction :: csvAction -> csvActionClass
| Just (Dict, _dbtCsvDecode) <- dbtCsvDecode , ..
-> error "dbCsvImport" } :: DBTCsvDecode r' k' csv) <- dbtCsvDecode -> do
let existing = Map.fromList $ zip currentKeys rows
sourceDiff :: Source (StateT (Map k' csv) (YesodDB UniWorX)) (DBCsvDiff r' csv k')
sourceDiff = do
let
toDiff :: csv -> StateT (Map k' csv) (YesodDB UniWorX) (DBCsvDiff r' csv k')
toDiff row = do
rowKey <- lift $
handle (throwM . (DBCsvException (toNamedRecord row) :: Text -> DBCsvException k') <=< dbtCsvRenderException) . runMaybeT $ dbtCsvRowKey row
seenKeys <- State.get
(<* modify (maybe id (flip Map.insert row) rowKey)) $ if
| Just rowKey' <- rowKey
, Just oldRow <- Map.lookup rowKey' seenKeys
-> throwM $ DBCsvDuplicateKey rowKey' (toNamedRecord oldRow) (toNamedRecord row)
| Just rowKey' <- rowKey
, Just oldRow <- Map.lookup rowKey' existing
-> return $ DBCsvDiffExisting rowKey' oldRow row
| otherwise
-> return $ DBCsvDiffNew rowKey row
mapM_ fileSourceCsv dbCsvFiles .| C.mapM toDiff
seen <- State.get
forM_ (Map.toList existing) $ \(rowKey, oldRow) -> if
| Map.member rowKey seen -> return ()
| otherwise -> yield $ DBCsvDiffMissing rowKey oldRow
accActionMap :: Map csvActionClass (Set csvAction) -> csvAction -> Map csvActionClass (Set csvAction)
accActionMap acc csvAct = Map.insertWith Set.union (dbtCsvClassifyAction csvAct) (Set.singleton csvAct) acc
importCsv = do
let
dbtCsvComputeActions' :: Sink (DBCsvDiff r' csv k') (YesodDB UniWorX) (Map csvActionClass (Set csvAction))
dbtCsvComputeActions' = do
let innerAct = awaitForever $ \x
-> let doHandle
| Just inpCsv <- x ^? _dbCsvNew
= handle $ throwM . (DBCsvException (toNamedRecord inpCsv) :: Text -> DBCsvException k') <=< dbtCsvRenderException
| otherwise
= id
in yieldM . doHandle . runConduit $ dbtCsvComputeActions x .| C.fold accActionMap Map.empty
innerAct .| C.foldMap id
actionMap <- flip evalStateT Map.empty . runConduit $ sourceDiff .| transPipe lift dbtCsvComputeActions'
when (Map.null actionMap) $ do
addMessageI Info MsgCsvImportUnnecessary
redirect $ tblLink id
liftHandlerT . (>>= sendResponse) $
siteLayoutMsg MsgCsvImportConfirmationHeading $ do
setTitleI MsgCsvImportConfirmationHeading
let
precomputeIdents :: forall f m'. (Eq (Element f), MonoFoldable f, MonadHandler m') => f -> m' (Element f -> Text)
precomputeIdents = foldM (\f act -> (\id' x -> bool (f x) id' $ act == x) <$> newIdent) (\_ -> error "No id precomputed")
actionClassIdent <- precomputeIdents $ Map.keys actionMap
actionIdent <- precomputeIdents . Set.unions $ Map.elems actionMap
let defaultChecked actClass = case dbtCsvCoarsenActionClass actClass of
DBCsvActionMissing -> False
_other -> True
csvActionCheckBox :: [(Text, Text)] -> csvAction -> Widget
csvActionCheckBox vAttrs act = do
let
sJsonField :: Field (HandlerT UniWorX IO) csvAction
sJsonField = secretJsonField' $ \theId name attrs val _isReq ->
[whamlet|
$newline never
<input id=#{theId} *{attrs} type=checkbox name=#{name} value=#{either id id val} :defaultChecked (dbtCsvClassifyAction act):checked>
|]
fieldView sJsonField (actionIdent act) (toPathPiece PostDBCsvImportAction) vAttrs (Right act) False
(csvImportConfirmForm', csvImportConfirmEnctype) <- liftHandlerT . generateFormPost . identifyForm FIDDBTableCsvImportConfirm $ \csrf -> return (error "No meaningful FormResult", $(widgetFile "csv-import-confirmation"))
let csvImportConfirmForm = wrapForm' BtnCsvImportConfirm csvImportConfirmForm' FormSettings
{ formMethod = POST
, formAction = Just $ tblLink id
, formEncoding = csvImportConfirmEnctype
, formAttrs = []
, formSubmit = FormSubmit
, formAnchor = Nothing :: Maybe Text
}
$(widgetFile "csv-import-confirmation-wrapper")
let defaultHeaderOrder = headerOrder (error "not to be forced" :: csv)
catches importCsv
[ Catch.Handler $ \case
(DBCsvDuplicateKey{..} :: DBCsvException k')
-> liftHandlerT $ sendResponseStatus badRequest400 =<< do
mr <- getMessageRender
let offendingCsv = CsvRendered defaultHeaderOrder [ dbCsvDuplicateKeyRowA, dbCsvDuplicateKeyRowB ]
heading = ErrorResponseTitle $ InvalidArgs [mr MsgDBCsvDuplicateKey]
siteLayoutMsg heading $ do
setTitleI heading
[whamlet|
<p>_{MsgDBCsvDuplicateKey}
<p>_{MsgDBCsvDuplicateKeyTip}
^{offendingCsv}
|]
(DBCsvException{..} :: DBCsvException k')
-> liftHandlerT $ sendResponseStatus badRequest400 =<< do
mr <- getMessageRender
let offendingCsv = CsvRendered defaultHeaderOrder [ dbCsvExceptionRow ]
heading = ErrorResponseTitle $ InvalidArgs [mr MsgDBCsvException]
siteLayoutMsg heading $ do
setTitleI heading
[whamlet|
<p>_{MsgDBCsvException}
$if not (Text.null dbCsvException)
<p>#{dbCsvException}
^{ offendingCsv}
|]
]
_other -> return () _other -> return ()
let let
@ -903,7 +1087,7 @@ dbTable PSValidator{..} dbtable@DBTable{ dbtIdent = dbtIdent'@(toPathPiece -> db
csvWdgt = $(widgetFile "table/csv-transcode") csvWdgt = $(widgetFile "table/csv-transcode")
uiLayout table = dbsFilterLayout filterWdgt filterEnc (SomeRoute $ rawAction :#: wIdent "table-wrapper") $(widgetFile "table/layout") uiLayout table = csvWdgt <> dbsFilterLayout filterWdgt filterEnc (SomeRoute $ rawAction :#: wIdent "table-wrapper") $(widgetFile "table/layout")
dbInvalidateResult' = foldr (<=<) return . catMaybes $ dbInvalidateResult' = foldr (<=<) return . catMaybes $
[ do [ do
@ -912,6 +1096,22 @@ dbTable PSValidator{..} dbtable@DBTable{ dbtIdent = dbtIdent'@(toPathPiece -> db
return . dbInvalidateResult dbtParams . DBTIRowsMissing $ length previousKeys - length currentKeys return . dbInvalidateResult dbtParams . DBTIRowsMissing $ length previousKeys - length currentKeys
] ]
((csvImportConfirmRes, ()), _enctype) <- case dbtCsvDecode of
Just (DBTCsvDecode{dbtCsvExecuteActions} :: DBTCsvDecode r' k' csv) -> do
lift . runFormPost . identifyForm FIDDBTableCsvImportConfirm $ \_csrf -> do
acts <- globalPostParamFields PostDBCsvImportAction secretJsonField
return . (, ()) $ if
| null acts -> FormSuccess $ do
addMessageI Info MsgCsvImportAborted
redirect $ tblLink id
| otherwise -> FormSuccess $ do
finalDest <- runConduit $ C.sourceList acts .| dbtCsvExecuteActions
addMessageI Success . MsgCsvImportSuccessful $ length acts
E.transactionSave
redirect finalDest
_other -> return ((FormMissing, ()), mempty)
formResult csvImportConfirmRes id
dbInvalidateResult' <=< bool (dbHandler (Proxy @m) (Proxy @x) $ (\table -> $(widgetFile "table/layout-wrapper")) . uiLayout) (sendResponse <=< tblLayout . uiLayout <=< dbWidget (Proxy @m) (Proxy @x)) psShortcircuit <=< runDBTable dbtable paginationInput currentKeys . fmap swap $ runWriterT table' dbInvalidateResult' <=< bool (dbHandler (Proxy @m) (Proxy @x) $ (\table -> $(widgetFile "table/layout-wrapper")) . uiLayout) (sendResponse <=< tblLayout . uiLayout <=< dbWidget (Proxy @m) (Proxy @x)) psShortcircuit <=< runDBTable dbtable paginationInput currentKeys . fmap swap $ runWriterT table'
where where
tblLayout :: forall m'. (MonadHandler m', HandlerSite m' ~ UniWorX) => Widget -> m' Html tblLayout :: forall m'. (MonadHandler m', HandlerSite m' ~ UniWorX) => Widget -> m' Html
@ -993,43 +1193,47 @@ cellTooltip msg = cellContents.mapped %~ (<> tipWdgt)
-- | Always display widget; maybe a link if user is Authorized. -- | Always display widget; maybe a link if user is Authorized.
-- Also see variant `linkEmptyCell` -- Also see variant `linkEmptyCell`
anchorCell :: IsDBTable m a => Route UniWorX -> Widget -> DBCell m a anchorCell :: (HasRoute UniWorX url, ToWidget UniWorX wgt, IsDBTable m a) => url -> wgt -> DBCell m a
anchorCell = anchorCellM . return anchorCell = anchorCellM . return
{-# DEPRECATED anchorCell' "For compatibility with Colonnade; better use anchorCell instead." #-} anchorCell' :: ( IsDBTable m a
anchorCell' :: IsDBTable m a , ToWidget UniWorX wgt
=> (r -> Route UniWorX) , HasRoute UniWorX url
-> (r -> Widget) )
=> (r -> url)
-> (r -> wgt)
-> (r -> DBCell m a) -> (r -> DBCell m a)
anchorCell' mkRoute mkWidget val = anchorCell (mkRoute val) (mkWidget val) anchorCell' mkRoute mkWidget val = anchorCell (mkRoute val) (mkWidget val)
anchorCellM :: IsDBTable m a => WidgetT UniWorX IO (Route UniWorX) -> Widget -> DBCell m a anchorCellM :: (HasRoute UniWorX url, ToWidget UniWorX wgt, IsDBTable m a, HandlerSite m ~ UniWorX) => WidgetT UniWorX IO url -> wgt -> DBCell m a
anchorCellM routeM widget = anchorCellM' routeM id (const widget) anchorCellM routeM widget = anchorCellM' routeM id (const widget)
anchorCellM' :: IsDBTable m a => WidgetT UniWorX IO x -> (x -> Route UniWorX) -> (x -> Widget) -> DBCell m a anchorCellM' :: (HasRoute UniWorX url, ToWidget UniWorX wgt, IsDBTable m a, HandlerSite m ~ UniWorX) => WidgetT UniWorX IO x -> (x -> url) -> (x -> wgt) -> DBCell m a
anchorCellM' xM x2route x2widget = cell $ do anchorCellM' xM x2route x2widget = linkEitherCellM' xM x2route (x2widget, x2widget)
x <- xM
let route = x2route x
widget = x2widget x
authResult <- liftHandlerT $ isAuthorized route False
case authResult of
Authorized -> $(widgetFile "table/cell/link") -- show allowed link
_otherwise -> widget -- don't show prohibited link
-- | Variant of `anchorCell` that displays different widgets depending whether the route is authorized for current user -- | Variant of `anchorCell` that displays different widgets depending whether the route is authorized for current user
linkEitherCell :: IsDBTable m a => Route UniWorX -> (Widget, Widget) -> DBCell m a linkEitherCell :: (HasRoute UniWorX url, ToWidget UniWorX wgt, ToWidget UniWorX wgt', IsDBTable m a, HandlerSite m ~ UniWorX) => url -> (wgt, wgt') -> DBCell m a
linkEitherCell = linkEitherCellM . return linkEitherCell = linkEitherCellM . return
linkEitherCellM :: IsDBTable m a => WidgetT UniWorX IO (Route UniWorX) -> (Widget, Widget) -> DBCell m a linkEitherCellM :: (HasRoute UniWorX url, ToWidget UniWorX wgt, ToWidget UniWorX wgt', IsDBTable m a, HandlerSite m ~ UniWorX) => WidgetT UniWorX IO url -> (wgt, wgt') -> DBCell m a
linkEitherCellM routeM (widgetAuth,widgetUnauth) = linkEitherCellM' routeM id (const widgetAuth, const widgetUnauth) linkEitherCellM routeM (widgetAuth,widgetUnauth) = linkEitherCellM' routeM id (const widgetAuth, const widgetUnauth)
linkEitherCellM' :: IsDBTable m a => WidgetT UniWorX IO x -> (x -> Route UniWorX) -> (x -> Widget, x -> Widget) -> DBCell m a linkEitherCellM' :: forall m url wgt wgt' a x.
( HasRoute UniWorX url
, ToWidget UniWorX wgt
, ToWidget UniWorX wgt'
, IsDBTable m a
, HandlerSite m ~ UniWorX
)
=> WidgetT UniWorX IO x -> (x -> url) -> (x -> wgt, x -> wgt') -> DBCell m a
linkEitherCellM' xM x2route (x2widgetAuth,x2widgetUnauth) = cell $ do linkEitherCellM' xM x2route (x2widgetAuth,x2widgetUnauth) = cell $ do
x <- xM x <- xM
let route = x2route x let route = x2route x
widget = x2widgetAuth x widget, widgetUnauth :: WidgetT UniWorX IO ()
widgetUnauth = x2widgetUnauth x widget = toWidget $ x2widgetAuth x
authResult <- liftHandlerT $ isAuthorized route False widgetUnauth = toWidget $ x2widgetUnauth x
authResult <- liftHandlerT $ isAuthorized (urlRoute route) False
linkUrl <- toTextUrl route
case authResult of case authResult of
Authorized -> $(widgetFile "table/cell/link") -- show allowed link Authorized -> $(widgetFile "table/cell/link") -- show allowed link
_otherwise -> widgetUnauth -- show alternative widget _otherwise -> widgetUnauth -- show alternative widget

View File

@ -0,0 +1,70 @@
module Handler.Utils.Table.Pagination.CsvColumnExplanations
( CsvColumnsExplained(..)
, genericCsvColumnsExplanations
) where
import Import
import qualified Data.Csv as Csv
import GHC.Generics
import qualified GHC.Generics as Generics
import Language.Haskell.TH
-- import Language.Haskell.TH.Datatype
-- import Language.Haskell.TH.Lib
import qualified Data.Map as Map
import qualified Data.ByteString.Char8 as B8
class CsvColumnsExplained csv where
csvColumnsExplanations :: forall p. p csv -> Map Csv.Name Widget
csvColumnsExplanations _ = Map.empty
genericCsvColumnsExplanations :: forall msg p csv.
( Generic csv
, GCsvColumnsExplained (Rep csv)
, RenderMessage UniWorX msg
)
=> Csv.Options
-> Map Name msg
-> p csv
-> Map Csv.Name Widget
genericCsvColumnsExplanations opts msgMap' _ = Map.mapMaybe (fmap (toWidget <=< ap getMessageRender . pure) . flip Map.lookup msgMap) headerNames
where
msgMap :: Map String msg
msgMap = Map.mapKeys nameBase msgMap'
headerNames :: Map Csv.Name String
headerNames = gCsvColumnsExplanations opts $ Generics.from (error "proxy" :: csv)
class GCsvColumnsExplained a where
gCsvColumnsExplanations :: Csv.Options -> a p -> Map Csv.Name String
instance GCsvColumnsExplained U1 where
gCsvColumnsExplanations _ _ = Map.empty
instance (GCsvColumnsExplained a, GCsvColumnsExplained b) => GCsvColumnsExplained (a :*: b) where
gCsvColumnsExplanations opts _ = Map.unionWithKey (\h f1 f2 -> error $ "Column header " ++ B8.unpack h ++ " is produced by both " ++ f1 ++ " and " ++ f2 ++ "")
(gCsvColumnsExplanations opts (error "proxy" :: a p))
(gCsvColumnsExplanations opts (error "proxy" :: b p))
instance GCsvColumnsExplained a => GCsvColumnsExplained (M1 D c a) where
gCsvColumnsExplanations opts _ = gCsvColumnsExplanations opts (error "proxy" :: a p)
instance GCsvColumnsExplained a => GCsvColumnsExplained (M1 C c a) where
gCsvColumnsExplanations opts _ = gCsvColumnsExplanations opts (error "proxy" :: a p)
-- | Instance to ensure that you cannot derive DefaultOrdered for
-- constructors without selectors.
instance CsvColumnsExplained (M1 S ('MetaSel 'Nothing srcpk srcstr decstr) a ())
=> GCsvColumnsExplained (M1 S ('MetaSel 'Nothing srcpk srcstr decstr) a)
where
gCsvColumnsExplanations _ _ =
error "You cannot derive CsvColumnsExplanations for constructors without selectors."
instance Selector s => GCsvColumnsExplained (M1 S s a) where
gCsvColumnsExplanations (Csv.fieldLabelModifier -> f) m
| null name = error "Cannot derive CsvColumnsExplanations for constructors without selectors"
| otherwise = Map.singleton (B8.pack $ f name) name
where name = selName m

View File

@ -77,6 +77,8 @@ import Data.Aeson.Types as Import (FromJSON(..), ToJSON(..), FromJSONKey(..), To
import Data.Constraint as Import (Dict(..)) import Data.Constraint as Import (Dict(..))
import Data.Void as Import (Void) import Data.Void as Import (Void)
import Algebra.Lattice as Import hiding (meet, join)
import Language.Haskell.TH.Instances as Import () import Language.Haskell.TH.Instances as Import ()
import Data.List.NonEmpty.Instances as Import () import Data.List.NonEmpty.Instances as Import ()
import Data.NonNull.Instances as Import () import Data.NonNull.Instances as Import ()

View File

@ -7,8 +7,7 @@ module Jobs.Handler.SendNotification.SubmissionRated
import Import import Import
import Utils.Lens import Utils.Lens
import Handler.Utils.DateTime import Handler.Utils
import Handler.Utils.Mail
import Jobs.Handler.SendNotification.Utils import Jobs.Handler.SendNotification.Utils
import Text.Hamlet import Text.Hamlet
@ -23,6 +22,9 @@ 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)
whenIsJust corrector $ \corrector' ->
addMailHeader "Reply-To" . renderAddress $ userAddress corrector'
replaceMailHeader "Auto-Submitted" $ Just "auto-generated" replaceMailHeader "Auto-Submitted" $ Just "auto-generated"
setSubjectI $ MsgMailSubjectSubmissionRated courseShorthand setSubjectI $ MsgMailSubjectSubmissionRated courseShorthand
@ -45,7 +47,13 @@ dispatchNotificationSubmissionRated nSubmission jRecipient = userMailT jRecipien
, "submission-rating-points" Aeson..= (guard (sheetType /= NotGraded) *> submissionRatingPoints) , "submission-rating-points" Aeson..= (guard (sheetType /= NotGraded) *> submissionRatingPoints)
, "submission-rating-comment" Aeson..= submissionRatingComment , "submission-rating-comment" Aeson..= submissionRatingComment
, "submission-rating-time" Aeson..= submissionRatingTime , "submission-rating-time" Aeson..= submissionRatingTime
, "submission-rating-by" Aeson..= (userDisplayName <$> corrector) , (Aeson..=) "submission-rating-by" $ do
corrector' <- corrector
return $ Aeson.object
[ "display-name" Aeson..= userDisplayName corrector'
, "surname" Aeson..= userSurname corrector'
, "email" Aeson..= userEmail corrector'
]
, "submission-rating-passed" Aeson..= join (gradingPassed <$> sheetType ^? _grading <*> submissionRatingPoints) , "submission-rating-passed" Aeson..= join (gradingPassed <$> sheetType ^? _grading <*> submissionRatingPoints)
, "sheet-name" Aeson..= sheetName , "sheet-name" Aeson..= sheetName
, "sheet-type" Aeson..= sheetType , "sheet-type" Aeson..= sheetType

View File

@ -28,6 +28,8 @@ import Control.Monad.Trans.Reader (mapReaderT)
import Control.Monad.Except (MonadError(..)) import Control.Monad.Except (MonadError(..))
import Utils (exceptT) import Utils (exceptT)
import Numeric.Natural
-- Database versions must follow https://pvp.haskell.org: -- Database versions must follow https://pvp.haskell.org:
-- - Breaking changes are instances where manual migration is necessary (via customMigrations; i.e. changing a columns format) -- - Breaking changes are instances where manual migration is necessary (via customMigrations; i.e. changing a columns format)
-- - Non-breaking changes are instances where the automatic migration done by persistent is sufficient (i.e. adding a column or table) -- - Non-breaking changes are instances where the automatic migration done by persistent is sufficient (i.e. adding a column or table)
@ -57,7 +59,11 @@ share [mkPersist sqlSettings, mkMigrate "migrateDBVersioning"]
deriving Show Eq Ord deriving Show Eq Ord
|] |]
migrateAll :: (MonadLogger m, MonadBaseControl IO m, MonadIO m) => ReaderT SqlBackend m () migrateAll :: ( MonadLogger m
, MonadBaseControl IO m
, MonadIO m
)
=> ReaderT SqlBackend m ()
migrateAll = do migrateAll = do
$logDebugS "Migration" "Initial migration" $logDebugS "Migration" "Initial migration"
mapM_ ($logInfoS "Migration") =<< runMigrationSilent initialMigration mapM_ ($logInfoS "Migration") =<< runMigrationSilent initialMigration
@ -77,14 +83,19 @@ migrateAll = do
$logDebugS "Migration" "Persistent automatic migration" $logDebugS "Migration" "Persistent automatic migration"
mapM_ ($logInfoS "Migration") =<< runMigrationSilent migrateAll' mapM_ ($logInfoS "Migration") =<< runMigrationSilent migrateAll'
requiresMigration :: forall m. (MonadLogger m, MonadBaseControl IO m, MonadIO m) => ReaderT SqlBackend m Bool requiresMigration :: forall m.
( MonadLogger m
, MonadBaseControl IO m
, MonadIO m
)
=> ReaderT SqlBackend m Bool
requiresMigration = mapReaderT (exceptT return return) $ do requiresMigration = mapReaderT (exceptT return return) $ do
initial <- either id (map snd) <$> parseMigration initialMigration initial <- either id (map snd) <$> parseMigration initialMigration
when (not $ null initial) $ do when (not $ null initial) $ do
$logInfoS "Migration" $ intercalate "; " initial $logInfoS "Migration" $ intercalate "; " initial
throwError True throwError True
customs <- getMissingMigrations @_ @m customs <- mapReaderT lift $ getMissingMigrations @_ @m
when (not $ Map.null customs) $ do when (not $ Map.null customs) $ do
$logInfoS "Migration" . intercalate ", " . map tshow $ Map.keys customs $logInfoS "Migration" . intercalate ", " . map tshow $ Map.keys customs
throwError True throwError True
@ -123,7 +134,8 @@ getMissingMigrations = do
-} -}
customMigrations :: MonadIO m => Map (Key AppliedMigration) (ReaderT SqlBackend m ()) customMigrations :: ( MonadIO m
) => Map (Key AppliedMigration) (ReaderT SqlBackend m ())
customMigrations = Map.fromListWith (>>) customMigrations = Map.fromListWith (>>)
[ ( AppliedMigrationKey [migrationVersion|initial|] [version|0.0.0|] [ ( AppliedMigrationKey [migrationVersion|initial|] [version|0.0.0|]
, whenM (columnExists "user" "theme") $ do -- New theme format , whenM (columnExists "user" "theme") $ do -- New theme format
@ -292,6 +304,20 @@ customMigrations = Map.fromListWith (>>)
, whenM (tableExists "exam") $ -- Exams were an unused stub before , whenM (tableExists "exam") $ -- Exams were an unused stub before
tableDropEmpty "exam" tableDropEmpty "exam"
) )
, ( AppliedMigrationKey [migrationVersion|13.0.0|] [version|14.0.0|]
, whenM ((&&) <$> tableExists "exam_occurrence" <*> (not <$> columnExists "exam_occurrence" "name")) $ do
examOccurrences <- [sqlQQ| SELECT "id" FROM "exam_occurrence" ORDER BY "exam"; |]
[executeQQ|
ALTER TABLE "exam_occurrence" ADD COLUMN "name" citext DEFAULT null;
|]
forM_ (zip [0..] examOccurrences) $ \(n :: Natural, Single eoId :: Single ExamOccurrenceId) -> do
let name = [st|occ-#{tshow n}|]
[executeQQ| UPDATE "exam_occurrence" SET "name" = #{name} WHERE "id" = #{eoId} |]
[executeQQ|
ALTER TABLE "exam_occurrence" ALTER COLUMN "name" DROP DEFAULT;
ALTER TABLE "exam_occurrence" ALTER COLUMN "name" SET NOT NULL;
|]
)
] ]

View File

@ -13,8 +13,8 @@ import Data.Universe
data SheetType data SheetType
= Bonus { maxPoints :: Current.Points } -- Erhöht nicht das Maximum, wird gutgeschrieben = Normal { maxPoints :: Current.Points } -- Erhöht das Maximum, wird gutgeschrieben
| Normal { maxPoints :: Current.Points } -- Erhöht das Maximum, wird gutgeschrieben | Bonus { maxPoints :: Current.Points } -- Erhöht nicht das Maximum, wird gutgeschrieben
| Pass { maxPoints, passingPoints :: Current.Points } | Pass { maxPoints, passingPoints :: Current.Points }
| NotGraded | NotGraded
deriving (Show, Read, Eq) deriving (Show, Read, Eq)

View File

@ -16,19 +16,20 @@ import qualified Yesod.Auth.Util.PasswordStore as PWStore
type Count = Sum Integer type Count = Sum Integer
type Points = Centi type Points = Centi
type Email = Text type Email = Text
type SchoolName = CI Text type SchoolName = CI Text
type SchoolShorthand = CI Text type SchoolShorthand = CI Text
type CourseName = CI Text type CourseName = CI Text
type CourseShorthand = CI Text type CourseShorthand = CI Text
type SheetName = CI Text type SheetName = CI Text
type MaterialName = CI Text type MaterialName = CI Text
type UserEmail = CI Email type UserEmail = CI Email
type UserIdent = CI Text type UserIdent = CI Text
type TutorialName = CI Text type TutorialName = CI Text
type ExamName = CI Text type ExamName = CI Text
type ExamPartName = CI Text type ExamPartName = CI Text
type ExamOccurrenceName = CI Text
type PWHashAlgorithm = ByteString -> PWStore.Salt -> Int -> ByteString type PWHashAlgorithm = ByteString -> PWStore.Salt -> Int -> ByteString
type InstanceId = UUID type InstanceId = UUID

View File

@ -12,6 +12,7 @@ import Model.Types.Common
import Utils.Lens.TH import Utils.Lens.TH
import Control.Lens import Control.Lens
import Control.Lens.Extras (is)
import Generics.Deriving.Monoid (memptydefault, mappenddefault) import Generics.Deriving.Monoid (memptydefault, mappenddefault)
import Data.Set (Set) import Data.Set (Set)
@ -40,6 +41,7 @@ deriveJSON defaultOptions
derivePersistFieldJSON ''SheetGrading derivePersistFieldJSON ''SheetGrading
makeLenses_ ''SheetGrading makeLenses_ ''SheetGrading
makePrisms ''SheetGrading
_passingBound :: Fold SheetGrading (Either () Points) _passingBound :: Fold SheetGrading (Either () Points)
_passingBound = folding passPts _passingBound = folding passPts
@ -57,17 +59,22 @@ gradingPassed gr pts = either pBinary pPoints <$> gr ^? _passingBound
data SheetGradeSummary = SheetGradeSummary data SheetGradeSummary = SheetGradeSummary
{ numSheets :: Count -- Total number of sheets, includes all { numSheets :: Count -- Total number of sheets, includes all
, numSheetsPasses :: Count -- Number of sheets required to pass FKA: numGradePasses , numSheetsPasses :: Count -- Number of sheets admitting passing FKA: numGradePasses
, numSheetsPoints :: Count -- Number of sheets having points FKA: sumGradePointsd , numSheetsPoints :: Count -- Number of sheets having points FKA: sumGradePointsd
, numSheetsPassPoints :: Count -- Number of sheets where passing is by points
, sumSheetsPoints :: Sum Points -- Total of all points in all sheets , sumSheetsPoints :: Sum Points -- Total of all points in all sheets
, sumSheetsPassPoints :: Sum Points -- Achieved points within marked sheets where passing is by points
-- Marking dependend -- Marking dependend
, numMarked :: Count -- Number of already marked sheets , numMarked :: Count -- Number of already marked sheets
, numMarkedPasses :: Count -- Number of already marked sheets with passes , numMarkedPasses :: Count -- Number of already marked sheets with passes
, numMarkedPoints :: Count -- Number of already marked sheets with points , numMarkedPoints :: Count -- Number of already marked sheets with points
, numMarkedPassPoints :: Count -- Number of already marked sheets where passing is by points
, sumMarkedPoints :: Sum Points -- Achieveable points within marked sheets , sumMarkedPoints :: Sum Points -- Achieveable points within marked sheets
, sumMarkedPassPoints :: Sum Points -- Achieved points within marked sheets where passing is by points
-- --
, achievedPasses :: Count -- Achieved passes (within marked sheets) , achievedPasses :: Count -- Achieved passes (within marked sheets)
, achievedPoints :: Sum Points -- Achieved points (within marked sheets) , achievedPoints :: Sum Points -- Achieved points (within marked sheets)
, achievedPassPoints :: Sum Points -- Achieved points within marked sheets where passing is by points
} deriving (Generic, Read, Show, Eq) } deriving (Generic, Read, Show, Eq)
instance Monoid SheetGradeSummary where instance Monoid SheetGradeSummary where
@ -82,19 +89,24 @@ makeLenses_ ''SheetGradeSummary
sheetGradeSum :: SheetGrading -> Maybe Points -> SheetGradeSummary sheetGradeSum :: SheetGrading -> Maybe Points -> SheetGradeSummary
sheetGradeSum gr Nothing = mempty sheetGradeSum gr Nothing = mempty
{ numSheets = 1 { numSheets = 1
, numSheetsPasses = bool mempty 1 $ has _passingBound gr , numSheetsPasses = bool mempty 1 $ has _passingBound gr
, numSheetsPoints = bool mempty 1 $ has _maxPoints gr , numSheetsPoints = bool mempty 1 $ has _maxPoints gr
, numSheetsPassPoints = bool mempty 1 $ is _PassPoints gr
, sumSheetsPoints = maybe mempty Sum $ gr ^? _maxPoints , sumSheetsPoints = maybe mempty Sum $ gr ^? _maxPoints
, sumSheetsPassPoints = maybe mempty Sum . (<* guard (is _PassPoints gr)) $ gr ^? _maxPoints
} }
sheetGradeSum gr (Just p) = sheetGradeSum gr (Just p) =
let unmarked@SheetGradeSummary{..} = sheetGradeSum gr Nothing let unmarked@SheetGradeSummary{..} = sheetGradeSum gr Nothing
in unmarked in unmarked
{ numMarked = numSheets { numMarked = numSheets
, numMarkedPasses = numSheetsPasses , numMarkedPasses = numSheetsPasses
, numMarkedPoints = numSheetsPoints , numMarkedPoints = numSheetsPoints
, sumMarkedPoints = sumSheetsPoints , numMarkedPassPoints = numSheetsPassPoints
, sumMarkedPoints = sumSheetsPoints
, sumMarkedPassPoints = sumSheetsPassPoints
, achievedPasses = maybe mempty (bool 0 1) (gradingPassed gr p) , achievedPasses = maybe mempty (bool 0 1) (gradingPassed gr p)
, achievedPoints = bool mempty (Sum p) $ has _maxPoints gr , achievedPoints = bool mempty (Sum p) $ has _maxPoints gr
, achievedPassPoints = bool mempty (Sum p) $ is _PassPoints gr
} }

View File

@ -153,6 +153,22 @@ iconProblem = fontAwesomeIcon "bolt"
iconHint :: Markup iconHint :: Markup
iconHint = fontAwesomeIcon "life-ring" iconHint = fontAwesomeIcon "life-ring"
-- Icons for Course
iconCourse :: Markup
iconCourse = fontAwesomeIcon "graduation-cap"
iconExam :: Markup
iconExam = fontAwesomeIcon "file-invoice"
iconEnrol :: Bool -> Markup
iconEnrol True = fontAwesomeIcon "user-plus"
iconEnrol False = fontAwesomeIcon "user-slash"
iconExamRegister :: Bool -> Markup
iconExamRegister True = fontAwesomeIcon "calendar-check"
iconExamRegister False = fontAwesomeIcon "calendar-times"
-- Icons for SheetFileType -- Icons for SheetFileType
iconSolution :: Markup iconSolution :: Markup
iconSolution =fontAwesomeIcon "exclamation-circle" iconSolution =fontAwesomeIcon "exclamation-circle"
@ -170,7 +186,7 @@ iconCSV :: Markup
iconCSV = fontAwesomeIcon "file-csv" iconCSV = fontAwesomeIcon "file-csv"
-- Conditional icons -- Generic Conditional icons
isVisible :: Bool -> Markup isVisible :: Bool -> Markup
-- ^ Display an icon that denotes that something™ is visible or invisible -- ^ Display an icon that denotes that something™ is visible or invisible
@ -263,6 +279,9 @@ rationalToFixed = MkFixed . round . (* (fromIntegral $ resolution (Proxy :: HasR
rationalToFixed3 :: Rational -> Fixed E3 rationalToFixed3 :: Rational -> Fixed E3
rationalToFixed3 = rationalToFixed rationalToFixed3 = rationalToFixed
rationalToFixed2 :: Rational -> Fixed E2
rationalToFixed2 = rationalToFixed
-- | Convert `part` and `whole` into percentage including symbol -- | Convert `part` and `whole` into percentage including symbol
-- showing trailing zeroes and to decimal digits -- showing trailing zeroes and to decimal digits
textPercent :: Real a => a -> a -> Text textPercent :: Real a => a -> a -> Text
@ -693,6 +712,9 @@ assertM_ f x = guard . f =<< x
assertM' :: Alternative m => (a -> Bool) -> a -> m a assertM' :: Alternative m => (a -> Bool) -> a -> m a
assertM' f x = x <$ guard (f x) assertM' f x = x <$ guard (f x)
guardOn :: Alternative m => Bool -> a -> m a
guardOn b x = x <$ guard b
-- Some Utility Functions from Agda.Utils.Monad -- Some Utility Functions from Agda.Utils.Monad
-- | Monadic if-then-else. -- | Monadic if-then-else.
ifM :: Monad m => m Bool -> m a -> m a -> m a ifM :: Monad m => m Bool -> m a -> m a -> m a

View File

@ -35,6 +35,9 @@ import Utils.PathPiece
import Data.Time.Format.Instances () import Data.Time.Format.Instances ()
import Algebra.Lattice
import Algebra.Lattice.Ordered
-- $(timeLocaleMap _) :: [Lang] -> TimeLocale -- $(timeLocaleMap _) :: [Lang] -> TimeLocale
timeLocaleMap :: [(Lang, String)] -- ^ Languages and matching locales, first is taken as default timeLocaleMap :: [(Lang, String)] -- ^ Languages and matching locales, first is taken as default
@ -78,7 +81,7 @@ newtype DateTimeFormat = DateTimeFormat { unDateTimeFormat :: String }
instance Hashable DateTimeFormat instance Hashable DateTimeFormat
data SelDateTimeFormat = SelFormatDateTime | SelFormatDate | SelFormatTime data SelDateTimeFormat = SelFormatDate | SelFormatTime | SelFormatDateTime
deriving (Eq, Ord, Read, Show, Enum, Bounded, Data, Generic, Typeable) deriving (Eq, Ord, Read, Show, Enum, Bounded, Data, Generic, Typeable)
instance Universe SelDateTimeFormat instance Universe SelDateTimeFormat
@ -98,3 +101,19 @@ instance {-# OVERLAPPING #-} Default (SelDateTimeFormat -> DateTimeFormat) where
def SelFormatDateTime = "%c" def SelFormatDateTime = "%c"
def SelFormatDate = "%F" def SelFormatDate = "%F"
def SelFormatTime = "%T" def SelFormatTime = "%T"
instance JoinSemiLattice SelDateTimeFormat where
a \/ b = getOrdered $ ((\/) `on` Ordered) a b
instance MeetSemiLattice SelDateTimeFormat where
a /\ b = getOrdered $ ((/\) `on` Ordered) a b
instance Lattice SelDateTimeFormat
instance BoundedJoinSemiLattice SelDateTimeFormat where
bottom = SelFormatTime
instance BoundedMeetSemiLattice SelDateTimeFormat where
top = SelFormatDateTime
instance BoundedLattice SelDateTimeFormat

View File

@ -1,4 +1,5 @@
{-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE UndecidableInstances #-}
{-# OPTIONS_GHC -fno-warn-deprecations #-} {-# OPTIONS_GHC -fno-warn-deprecations #-}
module Utils.Form where module Utils.Form where
@ -198,6 +199,7 @@ data FormIdentifier
| FIDDBTable | FIDDBTable
| FIDDBTableCsvExport | FIDDBTableCsvExport
| FIDDBTableCsvImport | FIDDBTableCsvImport
| FIDDBTableCsvImportConfirm
| FIDDelete | FIDDelete
| FIDCourseRegister | FIDCourseRegister
| FIDuserRights | FIDuserRights
@ -566,7 +568,26 @@ data SecretJSONFieldException = SecretJSONFieldDecryptFailure
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable) deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable)
instance Exception SecretJSONFieldException instance Exception SecretJSONFieldException
secretJsonField :: ( ToJSON a, FromJSON a secretJsonField' :: ( ToJSON a, FromJSON a
, MonadHandler m
, MonadSecretBox (ExceptT EncodedSecretBoxException m)
, MonadSecretBox (WidgetT (HandlerSite m) IO)
, RenderMessage (HandlerSite m) FormMessage
, RenderMessage (HandlerSite m) SecretJSONFieldException
)
=> FieldViewFunc m Text -> Field m a
secretJsonField' fieldView' = Field{..}
where
fieldParse [v] [] = bimap (\_ -> SomeMessage SecretJSONFieldDecryptFailure) Just <$> runExceptT (encodedSecretBoxOpen v)
fieldParse [] [] = return $ Right Nothing
fieldParse _ _ = return . Left $ SomeMessage MsgValueRequired
fieldView theId name attrs val isReq = do
val' <- traverse (encodedSecretBox SecretBoxShort) val
fieldView' theId name attrs val' isReq
fieldEnctype = UrlEncoded
secretJsonField :: forall m a.
( ToJSON a, FromJSON a
, MonadHandler m , MonadHandler m
, MonadSecretBox (ExceptT EncodedSecretBoxException m) , MonadSecretBox (ExceptT EncodedSecretBoxException m)
, MonadSecretBox (WidgetT (HandlerSite m) IO) , MonadSecretBox (WidgetT (HandlerSite m) IO)
@ -574,17 +595,7 @@ secretJsonField :: ( ToJSON a, FromJSON a
, RenderMessage (HandlerSite m) SecretJSONFieldException , RenderMessage (HandlerSite m) SecretJSONFieldException
) )
=> Field m a => Field m a
secretJsonField = Field{..} secretJsonField = secretJsonField' $ fieldView (hiddenField :: Field m Text)
where
fieldParse [v] [] = bimap (\_ -> SomeMessage SecretJSONFieldDecryptFailure) Just <$> runExceptT (encodedSecretBoxOpen v)
fieldParse [] [] = return $ Right Nothing
fieldParse _ _ = return . Left $ SomeMessage MsgValueRequired
fieldView theId name attrs val _isReq = do
val' <- traverse (encodedSecretBox SecretBoxShort) val
[whamlet|
<input id=#{theId} name=#{name} *{attrs} type=hidden value=#{either id id val'}>
|]
fieldEnctype = UrlEncoded
htmlFieldSmall :: forall m. (Monad m, RenderMessage (HandlerSite m) FormMessage) => Field m Html htmlFieldSmall :: forall m. (Monad m, RenderMessage (HandlerSite m) FormMessage) => Field m Html
htmlFieldSmall = checkMMap sanitize (pack . renderHtml) textField htmlFieldSmall = checkMMap sanitize (pack . renderHtml) textField
@ -651,13 +662,16 @@ wrapForm' btn formWidget FormSettings{..} = do
-- | Use this type to pass information to the form template -- | Use this type to pass information to the form template
data FormLayout = FormStandard | FormDBTableFilter | FormDBTablePagesize | FormDBTableCsvImport data FormLayout = FormStandard | FormDBTableFilter | FormDBTablePagesize | FormDBTableCsvImport
renderAForm :: Monad m => FormLayout -> FormRender m a data AFormMessage = MsgAFormFieldRequiredTip
renderAForm :: (RenderMessage (HandlerSite m) AFormMessage, Monad m) => FormLayout -> FormRender m a
renderAForm formLayout aform fragment = do renderAForm formLayout aform fragment = do
(res, ($ []) -> fieldViews) <- aFormToForm aform (res, ($ []) -> fieldViews) <- aFormToForm aform
let widget = $(widgetFile "widgets/aform/aform") let formHasRequiredFields = any fvRequired fieldViews
widget = $(widgetFile "widgets/aform/aform")
return (res, widget) return (res, widget)
renderWForm :: MonadHandler m => FormLayout -> WForm m (FormResult a) -> -- Form a -- (Synonym unavailable here) renderWForm :: (RenderMessage (HandlerSite m) AFormMessage, MonadHandler m) => FormLayout -> WForm m (FormResult a) -> -- Form a -- (Synonym unavailable here)
(Markup -> MForm m (FormResult a, WidgetT (HandlerSite m) IO ())) (Markup -> MForm m (FormResult a, WidgetT (HandlerSite m) IO ()))
renderWForm formLayout = renderAForm formLayout . wFormToAForm renderWForm formLayout = renderAForm formLayout . wFormToAForm
@ -832,9 +846,18 @@ deriving newtype instance Monad m => Applicative (FormValidator r m)
deriving newtype instance Monad m => Monad (FormValidator r m) deriving newtype instance Monad m => Monad (FormValidator r m)
deriving newtype instance Monad m => MonadState r (FormValidator r m) deriving newtype instance Monad m => MonadState r (FormValidator r m)
deriving newtype instance MonadFix m => MonadFix (FormValidator r m) deriving newtype instance MonadFix m => MonadFix (FormValidator r m)
deriving newtype instance MonadResource m => MonadResource (FormValidator r m)
deriving newtype instance MonadThrow m => MonadThrow (FormValidator r m)
deriving newtype instance MonadIO m => MonadIO (FormValidator r m)
instance MonadBase b m => MonadBase b (FormValidator r m) where
liftBase = lift . liftBase
instance MonadTrans (FormValidator r) where instance MonadTrans (FormValidator r) where
lift = FormValidator . lift lift = FormValidator . lift
instance MonadHandler m => MonadHandler (FormValidator r m) where
type HandlerSite (FormValidator r m) = HandlerSite m
liftHandlerT = lift . liftHandlerT
validateForm :: MonadHandler m validateForm :: MonadHandler m
=> FormValidator a m () => FormValidator a m ()
-> (Markup -> MForm m (FormResult a, xml)) -> (Markup -> MForm m (FormResult a, xml))

View File

@ -6,7 +6,7 @@ module Utils.Parameters
, GlobalPostParam(..) , GlobalPostParam(..)
, lookupGlobalPostParam, hasGlobalPostParam, lookupGlobalPostParams , lookupGlobalPostParam, hasGlobalPostParam, lookupGlobalPostParams
, lookupGlobalPostParamForm, hasGlobalPostParamForm , lookupGlobalPostParamForm, hasGlobalPostParamForm
, globalPostParamField , globalPostParamField, globalPostParamFields
) where ) where
import ClassyPrelude.Yesod import ClassyPrelude.Yesod
@ -55,6 +55,7 @@ data GlobalPostParam = PostFormIdentifier
| PostDeleteTarget | PostDeleteTarget
| PostMassInputShape | PostMassInputShape
| PostBearer | PostBearer
| PostDBCsvImportAction
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic) deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic)
instance Universe GlobalPostParam instance Universe GlobalPostParam
@ -84,3 +85,9 @@ globalPostParamField ident Field{fieldParse} = runMaybeT $ do
ts <- fromMaybe [] . Map.lookup (toPathPiece ident) <$> MaybeT askParams ts <- fromMaybe [] . Map.lookup (toPathPiece ident) <$> MaybeT askParams
fs <- fromMaybe [] . Map.lookup (toPathPiece ident) <$> MaybeT askFiles fs <- fromMaybe [] . Map.lookup (toPathPiece ident) <$> MaybeT askFiles
MaybeT $ either (const Nothing) id <$> lift (fieldParse ts fs) MaybeT $ either (const Nothing) id <$> lift (fieldParse ts fs)
globalPostParamFields :: Monad m => GlobalPostParam -> Field m a -> MForm m [a]
globalPostParamFields ident Field{fieldParse} = fmap (fromMaybe []) . runMaybeT $ do
ts <- fromMaybe [] . Map.lookup (toPathPiece ident) <$> MaybeT askParams
fs <- fromMaybe [] . Map.lookup (toPathPiece ident) <$> MaybeT askFiles
forM ((Left <$> fs) ++ (Right <$> ts)) $ \inp -> MaybeT $ either (const Nothing) id <$> lift (either (\f -> fieldParse [] [f]) (\t -> fieldParse [t] []) inp)

View File

@ -3,10 +3,10 @@
<tr .table__row> <tr .table__row>
<th .table__th>_{MsgSubmission} <th .table__th>_{MsgSubmission}
<td .table__td>#{cid} <td .table__td>#{cid}
$maybe Entity _ User{userDisplayName} <- corrector $maybe Entity _ User{userDisplayName, userSurname, userEmail} <- corrector
<tr .table__row> <tr .table__row>
<th .table__th>_{MsgRatingBy} <th .table__th>_{MsgRatingBy}
<td .table__td>#{userDisplayName} <td .table__td>^{nameEmailWidget userEmail userDisplayName userSurname}
$maybe time <- submissionRatingTime $maybe time <- submissionRatingTime
<tr .table__row> <tr .table__row>
<th .table__th>_{MsgRatingTime} <th .table__th>_{MsgRatingTime}

View File

@ -0,0 +1,4 @@
<section>
<p>_{MsgCsvImportConfirmationTip}
<section>
^{csvImportConfirmForm}

View File

@ -0,0 +1,21 @@
$newline never
#{csrf}
<div .actions>
$forall actionClass <- sortOn dbtCsvCoarsenActionClass (Map.keys actionMap)
<div .action>
<input type=checkbox id=#{actionClassIdent actionClass} .action__checkbox :defaultChecked actionClass:checked>
<label .action__label for=#{actionClassIdent actionClass}>
^{dbtCsvRenderActionClass actionClass}
<fieldset .action__fieldset uw-interactive-fieldset .interactive-fieldset__target data-conditional-input=#{actionClassIdent actionClass}>
<div .action__checked-counter>
<div .action__toggle-all>
<input type=checkbox id=#{actionClassIdent actionClass}-toggle-all>
<label for=#{actionClassIdent actionClass}-toggle-all .action__option-label>
_{MsgDBCsvImportActionToggleAll}
<div .action__options>
$forall action <- Set.toList (actionMap ! actionClass)
<div .action__option>
^{csvActionCheckBox [] action}
<label .action__option-label for=#{actionIdent action}>
^{dbtCsvRenderKey existing action}

View File

@ -0,0 +1,81 @@
(function() {
var IMPORT_ACTIONS_SELECTOR = '.actions';
var IMPORT_ACTION_SELECTOR = '.action';
var IMPORT_ACTION_CHECKBOX_SELECTOR = '.action__checkbox ';
var IMPORT_ACTION_OPTIONS_SELECTOR = '.action__options';
var IMPORT_ACTION_TOGGLE_ALL_SELECTOR = '.action__toggle-all [type="checkbox"]';
var IMPORT_ACTION_CHECKED_COUNTER_SELECTOR = '.action__checked-counter';
var importActionsElement;
document.addEventListener('DOMContentLoaded', function() {
importActionsElement = document.querySelector(IMPORT_ACTIONS_SELECTOR);
setupActions();
});
function setupActions() {
var actionElements = Array.from(importActionsElement.querySelectorAll(IMPORT_ACTION_SELECTOR));
actionElements.forEach(function(element) {
setupAction(element);
});
}
function setupAction(actionElement) {
var actionCheckbox = actionElement.querySelector(IMPORT_ACTION_CHECKBOX_SELECTOR);
var actionOptions = actionElement.querySelector(IMPORT_ACTION_OPTIONS_SELECTOR);
if (actionOptions) {
var actionCheckboxes = Array.from(actionOptions.querySelectorAll('[type="checkbox"]'));
var toggleAllCheckbox = actionElement.querySelector(IMPORT_ACTION_TOGGLE_ALL_SELECTOR);
// setup action checkbox to toggle all child checkboxes if changed
actionCheckbox.addEventListener('change', function() {
actionCheckboxes.forEach(function(checkbox) {
checkbox.checked = actionCheckbox.checked;
});
updateCheckedCounter(actionElement, actionCheckboxes);
updateToggleAllCheckbox(toggleAllCheckbox, actionCheckboxes);
});
// update counter and toggle checkbox initially
updateCheckedCounter(actionElement, actionCheckboxes);
updateToggleAllCheckbox(toggleAllCheckbox, actionCheckboxes);
// register change listener for individual checkboxes
actionCheckboxes.forEach(function(checkbox) {
checkbox.addEventListener('change', function() {
updateCheckedCounter(actionElement, actionCheckboxes);
updateToggleAllCheckbox(toggleAllCheckbox, actionCheckboxes);
});
});
// register change listener for toggle all checkbox
if (toggleAllCheckbox) {
toggleAllCheckbox.addEventListener('change', function() {
actionCheckboxes.forEach(function(checkbox) {
checkbox.checked = toggleAllCheckbox.checked;
});
updateCheckedCounter(actionElement, actionCheckboxes);
});
}
}
}
// update checked state of toggle all checkbox based on all other checkboxes
function updateToggleAllCheckbox(toggleAllCheckbox, actionCheckboxes) {
var allChecked = actionCheckboxes.reduce(function(acc, checkbox) {
return acc && checkbox.checked;
}, true);
toggleAllCheckbox.checked = allChecked;
}
// update value of checked counter
function updateCheckedCounter(actionElement, actionCheckboxes) {
var checkedCounter = actionElement.querySelector(IMPORT_ACTION_CHECKED_COUNTER_SELECTOR);
var checkedCheckboxes = actionCheckboxes.reduce(function(acc, checkbox) { return checkbox.checked ? acc + 1 : acc; }, 0);
checkedCounter.innerHTML = checkedCheckboxes + '/' + actionCheckboxes.length;
}
})();

View File

@ -0,0 +1,52 @@
.action {
max-width: 800px;
padding: 3px 0;
&:not(:last-child) {
margin-bottom: 7px;
}
&:not(:first-child) {
margin-top: 7px;
}
}
.action__options {
max-height: 450px;
overflow-y: auto;
}
.action__option {
display: flex;
&:not(:last-child) {
margin-bottom: 10px;
}
}
.action__label,
.action__option-label {
margin-left: 15px;
vertical-align: top;
}
.action__fieldset {
margin: 7px 0 5px 9px;
padding: 5px 0 10px;
border-left: 1px solid #bcbcbc;
padding-left: 16px;
position: relative;
}
.action__toggle-all {
display: flex;
border-bottom: 1px solid #bcbcbc;
padding-bottom: 8px;
margin-bottom: 8px;
}
.action__checked-counter {
position: absolute;
right: 5px;
top: 5px;
}

View File

@ -329,7 +329,6 @@ input[type="button"].btn-info:hover,
.scrolltable { .scrolltable {
overflow: auto; overflow: auto;
box-shadow: 0 0 1px 1px var(--color-grey-light); box-shadow: 0 0 1px 1px var(--color-grey-light);
margin-bottom: 15px;
} }
@media (max-width: 425px) { @media (max-width: 425px) {
@ -637,3 +636,13 @@ section {
font-weight: var(--weight, 600); font-weight: var(--weight, 600);
background-color: rgba(var(--red), var(--green), 0, var(--opacity)); background-color: rgba(var(--red), var(--green), 0, var(--opacity));
} }
.uuid {
font-family: monospace;
}
.form--inline {
display: inline-block;
}

View File

@ -44,14 +44,14 @@ $maybe desc <- examDescription
$maybe deregUntil <- examDeregisterUntil $maybe deregUntil <- examDeregisterUntil
<dt .deflist__dt>_{MsgExamDeregisterUntil} <dt .deflist__dt>_{MsgExamDeregisterUntil}
<dd .deflist__dd>^{formatTimeW SelFormatDateTime deregUntil} <dd .deflist__dd>^{formatTimeW SelFormatDateTime deregUntil}
<dt .deflist__dt>_{MsgExamPublishOccurrenceAssignmentsParticipant} $maybe publishAssignments <- examPublishOccurrenceAssignments
<dd .deflist__dd>^{formatTimeW SelFormatDateTime examPublishOccurrenceAssignments} <dt .deflist__dt>_{MsgExamPublishOccurrenceAssignmentsParticipant}
<dd .deflist__dd>^{formatTimeW SelFormatDateTime publishAssignments}
$if examTimes $if examTimes
<dt .deflist__dt>_{MsgExamTime} <dt .deflist__dt>_{MsgExamTime}
<dd .deflist__dd> <dd .deflist__dd>
^{formatTimeW SelFormatDateTime examStart} $maybe start <- examStart
$maybe end <- examEnd ^{formatTimeRangeW SelFormatDateTime start examEnd}
\ ^{formatTimeW (bool SelFormatDateTime SelFormatTime ((on (==) utctDay) examStart end)) end}
$maybe finished <- examFinished $maybe finished <- examFinished
<dt .deflist__dt>_{MsgExamFinishedParticipant} <dt .deflist__dt>_{MsgExamFinishedParticipant}
<dd .deflist__dd>^{formatTimeW SelFormatDateTime finished} <dd .deflist__dd>^{formatTimeW SelFormatDateTime finished}
@ -89,10 +89,17 @@ $maybe desc <- examDescription
$if not (null occurrences) $if not (null occurrences)
<section> <section>
<h2> <h2>
_{MsgExamOccurrences} $if examTimes
_{MsgExamOccurrences}
$else
_{MsgExamRooms}
<table .table .table--striped .table--hover> <table .table .table--striped .table--hover>
<thead> <thead>
<tr .table__row .table__row--head> <tr .table__row .table__row--head>
$if occurrenceNamesShown
<th .table__th>
_{MsgExamRoomName}
^{isVisible False}
<th .table__th>_{MsgExamRoom} <th .table__th>_{MsgExamRoom}
$if not examTimes $if not examTimes
<th .table__th>_{MsgExamRoomTime} <th .table__th>_{MsgExamRoomTime}
@ -103,14 +110,14 @@ $if not (null occurrences)
$if not occurrenceAssignmentsVisible $if not occurrenceAssignmentsVisible
\ ^{isVisible False} \ ^{isVisible False}
<tbody> <tbody>
$forall (Entity _occId ExamOccurrence{examOccurrenceRoom, examOccurrenceStart, examOccurrenceEnd, examOccurrenceDescription}, registered) <- occurrences $forall (Entity _occId ExamOccurrence{examOccurrenceName, examOccurrenceRoom, examOccurrenceStart, examOccurrenceEnd, examOccurrenceDescription}, registered) <- occurrences
<tr .table__row :occurrenceAssignmentsShown && not registered:.occurrence--not-registered> <tr .table__row :occurrenceAssignmentsShown && not registered:.occurrence--not-registered>
$if occurrenceNamesShown
<td .table__td #exam-occurrence__#{examOccurrenceName}>#{examOccurrenceName}
<td .table__td>#{examOccurrenceRoom} <td .table__td>#{examOccurrenceRoom}
$if not examTimes $if not examTimes
<td .table__td> <td .table__td>
^{formatTimeW SelFormatDateTime examOccurrenceStart} ^{formatTimeRangeW SelFormatDateTime examOccurrenceStart examOccurrenceEnd}
$maybe end <- examOccurrenceEnd
\ ^{formatTimeW (bool SelFormatDateTime SelFormatTime ((on (==) utctDay) examStart end)) end}
<td .table__td> <td .table__td>
$maybe desc <- examOccurrenceDescription $maybe desc <- examOccurrenceDescription
#{desc} #{desc}

View File

@ -1,5 +1,10 @@
$newline never $newline never
<dl .deflist> <dl .deflist>
<dt .deflist__dt>23.07.2019
<dd .deflist__dd>
<ul>
<li>Import & Export von CSV-Dateien für Klausurteilnehmer
<dt .deflist__dt>26.06.2019 <dt .deflist__dt>26.06.2019
<dd .deflist__dd> <dd .deflist__dd>
<ul> <ul>

View File

@ -23,11 +23,11 @@ $newline never
<dd> <dd>
<a href=@{CSubmissionR tid ssh csh shn csid SubShowR}> <a href=@{CSubmissionR tid ssh csh shn csid SubShowR}>
#{csid} #{csid}
$maybe User{userDisplayName} <- corrector $maybe User{userDisplayName, userSurname, userEmail} <- corrector
<dt> <dt>
_{MsgRatingBy} _{MsgRatingBy}
<dd> <dd>
#{userDisplayName} #{nameEmailHtml userEmail userDisplayName userSurname}
$maybe time <- submissionRatingTime' $maybe time <- submissionRatingTime'
<dt> <dt>
_{MsgRatingTime} _{MsgRatingTime}

View File

@ -1,3 +1,3 @@
$newline never $newline never
<a href=@{route}> <a href=#{linkUrl}>
^{widget} ^{widget}

View File

@ -0,0 +1,7 @@
<h3>_{MsgCsvColumnsExplanationsTip}
<dl .deflist>
$forall (colName, colExplanation) <- csvColExplanations''
<dt .deflist__dt>#{decodeUtf8 colName}
<dd .deflist__dd>^{colExplanation}
<div>
^{csvExportWdgt'}

View File

@ -1,7 +1,14 @@
$newline never $newline never
$if is _Just dbtCsvDecode $if is _Just dbtCsvDecode
<div .csv-import> <div .csv-import>
^{csvImportWdgt'} <h3 .csv-import__toggle uw-show-hide data-show-hide-id=table-csv-import data-show-hide-collapsed>
_{MsgTableHeadingCsvImport}
<div .csv-import__content>
^{csvImportWdgt'}
$if is _Just dbtCsvEncode $if is _Just dbtCsvEncode
<div .csv-export> <div .csv-export>
^{csvExportWdgt'} <h3 .csv-import__toggle uw-show-hide data-show-hide-id=table-csv-export>
_{MsgTableHeadingCsvExport}
<div .csv-export__content>
^{csvExportWdgt'}
^{csvColExplanations'}

View File

@ -0,0 +1,21 @@
.csv-export {
margin-bottom: 13px;
.csv-export__content {
display: flex;
align-content: space-between;
align-items: center;
& > * {
margin-right: 10px;
&:last-child {
margin-right: 0;
}
}
}
}
.csv-import {
margin-bottom: 13px;
}

View File

@ -1,6 +1,7 @@
$newline never $newline never
<div .table-filter> <div .table-filter>
<h3 .table-filter__toggle uw-show-hide data-show-hide-id=table-filter data-show-hide-collapsed>Filter <h3 .table-filter__toggle uw-show-hide data-show-hide-id=table-filter data-show-hide-collapsed>
_{MsgTableHeadingFilter}
<div> <div>
^{filterForm} ^{filterForm}
^{scrolltable} ^{scrolltable}

View File

@ -5,7 +5,6 @@ $else
<div .table-header> <div .table-header>
<div .table__row-count> <div .table__row-count>
_{MsgRowCount rowCount} _{MsgRowCount rowCount}
^{csvWdgt}
^{table} ^{table}

View File

@ -3,6 +3,7 @@
display: flex; display: flex;
flex-flow: row-reverse; flex-flow: row-reverse;
justify-content: space-between; justify-content: space-between;
margin-bottom: 15px;
} }
/* TABLE FOOTER */ /* TABLE FOOTER */
@ -10,6 +11,7 @@
display: flex; display: flex;
flex-flow: row-reverse; flex-flow: row-reverse;
justify-content: space-between; justify-content: space-between;
margin-top: 15px;
} }
/* PAGINATION */ /* PAGINATION */

View File

@ -3,8 +3,9 @@ $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} <div>
^{fvInput view} <label .form-group-label.label-pagesize for=#{fvId view}>#{fvLabel view}
^{fvInput view}
$of _ $of _
$forall view <- fieldViews $forall view <- fieldViews
$if fvId view == idFormSectionNoinput $if fvId view == idFormSectionNoinput
@ -22,3 +23,7 @@ $case formLayout
^{fvInput view} ^{fvInput view}
$maybe err <- fvErrors view $maybe err <- fvErrors view
<div .form-error>#{err} <div .form-error>#{err}
$if formHasRequiredFields
<div .form-section-legend>
<span .form-group__required-marker>
_{MsgAFormFieldRequiredTip}

View File

@ -0,0 +1,14 @@
$newline never
<table .table .table--striped .table--hover>
<thead>
<tr .table__row .table__row--head>
$forall header <- headers
<th .table__th .table__th--csv>
#{header}
<tbody>
$forall row <- csvData
<tr .table__row>
$forall cell <- row
<td .table__td .table__td--csv>
$maybe cellText <- cell
#{cellText}

View File

@ -0,0 +1,3 @@
.table__td--csv, .table__th--csv {
font-family: monospace;
}

View File

@ -1,5 +1,6 @@
$newline never $newline never
<td>#{csrf}^{fvInput eofIdView}^{fvInput eofRoomView} <td>#{csrf}^{fvInput eofIdView}^{fvInput eofNameView}
<td>^{fvInput eofRoomView}
<td>^{fvInput eofCapacityView} <td>^{fvInput eofCapacityView}
<td>^{fvInput eofStartView} <td>^{fvInput eofStartView}
<td>^{fvInput eofEndView} <td>^{fvInput eofEndView}

View File

@ -1,6 +1,7 @@
$newline never $newline never
<table> <table>
<thead> <thead>
<th>_{MsgExamRoomName}
<th>_{MsgExamRoom} <th>_{MsgExamRoom}
<th>_{MsgExamRoomCapacity} <th>_{MsgExamRoomCapacity}
<th>_{MsgExamRoomStart} <th>_{MsgExamRoomStart}

View File

@ -1,5 +1,5 @@
$newline never $newline never
<div uw-modal data-modal-trigger=#{triggerId'} data-modal-closeable> <div .modal uw-modal data-modal-trigger=#{triggerId'} data-modal-closeable>
$case modalContent $case modalContent
$of Right content $of Right content
<div .modal__content> <div .modal__content>