Merge branch 'master' into changelog
This commit is contained in:
commit
0f02a00053
@ -1,5 +1,5 @@
|
|||||||
[Dolphin]
|
[Dolphin]
|
||||||
Timestamp=2018,3,14,10,57,55
|
Timestamp=2019,6,26,19,32,25
|
||||||
Version=4
|
Version=4
|
||||||
|
|
||||||
[Settings]
|
[Settings]
|
||||||
|
|||||||
6
.vscode/tasks.json
vendored
6
.vscode/tasks.json
vendored
@ -14,6 +14,7 @@
|
|||||||
"reveal": "always",
|
"reveal": "always",
|
||||||
"focus": false,
|
"focus": false,
|
||||||
"panel": "dedicated",
|
"panel": "dedicated",
|
||||||
|
"clear": true,
|
||||||
"showReuseMessage": false
|
"showReuseMessage": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -63,6 +64,11 @@
|
|||||||
"type": "npm",
|
"type": "npm",
|
||||||
"script": "frontend:lint",
|
"script": "frontend:lint",
|
||||||
"problemMatcher": []
|
"problemMatcher": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "npm",
|
||||||
|
"script": "lint",
|
||||||
|
"problemMatcher": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -1,5 +0,0 @@
|
|||||||
# Changelog
|
|
||||||
|
|
||||||
## Previously
|
|
||||||
|
|
||||||
A lot happened before we started maintaining this auto-generated changelog file. German speaking readers can take a look at [CHANGELOG_DE.md](CHANGELOG_DE.md) to travel further back in time.
|
|
||||||
103
CHANGELOG_DE.md
103
CHANGELOG_DE.md
@ -1,103 +0,0 @@
|
|||||||
# Changelog
|
|
||||||
|
|
||||||
### Version 07.06.2019
|
|
||||||
|
|
||||||
- Abgaben können bestimmte Dateinamen und Endungen erzwingen
|
|
||||||
- Übungsblätter bieten nun Zip-Archive für alle veröffentlichte Dateien, bzw. Dateigruppen an
|
|
||||||
|
|
||||||
### Version 20.05.2019
|
|
||||||
|
|
||||||
- Komplett überarbeitete Funktionalität zur automatischen Verteilung von Korrekturen
|
|
||||||
|
|
||||||
### Version 13.05.2019
|
|
||||||
|
|
||||||
- Kursverwalter können Teilnehmer hinzufügen
|
|
||||||
|
|
||||||
### Version 10.05.2019
|
|
||||||
|
|
||||||
- Besseres Interface zum Einstellen von Abgebenden
|
|
||||||
- Download von allen Dateien pro Kursmaterial/Übungsblatt
|
|
||||||
|
|
||||||
### Version 04.05.2019
|
|
||||||
|
|
||||||
- Kursmaterial
|
|
||||||
|
|
||||||
### Version 29.04.2019
|
|
||||||
|
|
||||||
- Tutorien
|
|
||||||
- Anzeige von Korrektoren auf den Kursseiten
|
|
||||||
|
|
||||||
### Version 20.04.2019
|
|
||||||
|
|
||||||
- Versand von Benachrichtigungen an Kursteilnehmer
|
|
||||||
- Eintragen von Korrektoren und Kursverwaltern auch ohne bestehenden Account
|
|
||||||
|
|
||||||
### Version 27.03.2019
|
|
||||||
|
|
||||||
- Kurse Veranstalter können nun mehrere Dozenten und Assistenten selbst eintragen
|
|
||||||
- Erfassung Studiengangsdaten
|
|
||||||
|
|
||||||
### Version 20.03.2019
|
|
||||||
|
|
||||||
- Kursanmeldung benötigen assoziertes Hauptfach (für Studierende mit mehreren Hauptfächern)
|
|
||||||
|
|
||||||
### Version 30.01.2019
|
|
||||||
|
|
||||||
- Designänderungen
|
|
||||||
|
|
||||||
### Version 16.01.2019
|
|
||||||
|
|
||||||
- Links für Bequemlichkeiten hinzugefügt (z.B. aktuelles Übungsblatt)
|
|
||||||
- Liste zugewiesener Abgaben lassen sich nun filtern
|
|
||||||
- Bugfix: Wenn zwischen Anzeige und Empfang eines Tabellen-Formulars Zeilen verschwinden wird nun eine sinnvolle Fehlermeldung angezeigt
|
|
||||||
|
|
||||||
### Version 30.11.2018
|
|
||||||
|
|
||||||
- Bugfix: Übungsblätter im "bestehen nach Punkten"-Modus werden wieder korrekt gespeichert
|
|
||||||
|
|
||||||
### Version 29.11.2018
|
|
||||||
|
|
||||||
- Bugfix: Formulare innerhalb von Tabellen funktionieren nun auch nach Javascript-Seitenwechsel oder Ändern der Sortierung
|
|
||||||
|
|
||||||
### Version 09.11.2018
|
|
||||||
|
|
||||||
- Bugfix: Zahlreiche Knöpfe/Formulare funktionieren wieder bei eingeschaltetem Javascript
|
|
||||||
- Verschiedene Verbesserungen für Korrektoren
|
|
||||||
|
|
||||||
### Version 19.10.2018
|
|
||||||
|
|
||||||
- Benutzer können sich in der Testphase komplett selbst löschen
|
|
||||||
- Hilfe Widget
|
|
||||||
- Benachrichtigungen per eMail für einige Ereignisse
|
|
||||||
|
|
||||||
### Version 18.09.2018
|
|
||||||
|
|
||||||
- Tooltips funktionieren auch ohne JavaScript
|
|
||||||
- Kurskürzel müssen nur innerhalb eines Instituts eindeutig sein
|
|
||||||
- User Data zeigt nun alle momentan gespeicherten Datensätze an
|
|
||||||
- Unterstützung von Tabellenzusammenfassungen, z.B. Punktsummen
|
|
||||||
- Intelligente Verteilung von Abgaben auf Korrektoren (z.B. bei Krankheit)
|
|
||||||
- Übungsblätter können Abgabe von Dateien verbieten und angeben ob ZIP-Archive entpackt werden sollen
|
|
||||||
|
|
||||||
### Version 06.08.2018
|
|
||||||
|
|
||||||
- Einführung einer Option, ob Dateien automatisch heruntergeladen werden sollen
|
|
||||||
|
|
||||||
### Version 01.08.2018
|
|
||||||
|
|
||||||
- Verbesserter Campus-Login
|
|
||||||
- (Ersatz einer C-Bibliothek mit undokumentierter Abhängigkeit durch selbst entwickelten Haskell-Code erlaubt nun auch Umlaute.)
|
|
||||||
|
|
||||||
### Version 31.07.2018
|
|
||||||
|
|
||||||
- Viele Verbesserung zur Anzeige von Korrekturen
|
|
||||||
- Kursliste über alle Semester hinweg (Top-Level-Navigation "Kurse"), wird in Zukunft Filter/Suchfunktion erhalten
|
|
||||||
|
|
||||||
### Version 10.07.2018
|
|
||||||
|
|
||||||
- Bugfixes, wählbares Format für Datum
|
|
||||||
|
|
||||||
### Version 03.07.2018
|
|
||||||
|
|
||||||
- Willkommen bei Uni2work aka "You-need-to-work!"
|
|
||||||
|
|
||||||
130
README.md
Normal file
130
README.md
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
# "Quick Start" Guide
|
||||||
|
|
||||||
|
The following description applies to Ubuntu and similar debian based Linux distributions.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
These are the things you need to do/install before you can get started working on Uni2work.
|
||||||
|
|
||||||
|
### Clone repository
|
||||||
|
Clone this repository and navigate into it
|
||||||
|
```sh
|
||||||
|
$ git clone https://gitlab.cip.ifi.lmu.de/jost/UniWorX.git && cd UniWorX
|
||||||
|
```
|
||||||
|
|
||||||
|
### `LDAP`
|
||||||
|
LDAP is needed to handle logins.
|
||||||
|
|
||||||
|
Install:
|
||||||
|
```sh
|
||||||
|
sudo apt-get install slapd ldap-utils
|
||||||
|
```
|
||||||
|
|
||||||
|
### `PostgreSQL`
|
||||||
|
PostgreSQL will serve as database for Uni2work.
|
||||||
|
|
||||||
|
Install:
|
||||||
|
```sh
|
||||||
|
$ sudo apt-get install postgresql
|
||||||
|
```
|
||||||
|
|
||||||
|
Switch to user *postgres* (got created during installation):
|
||||||
|
```sh
|
||||||
|
$ sudo -i -u postgres
|
||||||
|
```
|
||||||
|
|
||||||
|
Add new database user *uniworx*:
|
||||||
|
```sh
|
||||||
|
$ createuser --interactive
|
||||||
|
```
|
||||||
|
|
||||||
|
You'll get a prompt:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
Enter name of role to add: uniworx
|
||||||
|
Shall the new role be a superuser? (y/n) [not exactly sure. Guess not?]
|
||||||
|
Password: uniworx
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
Create database *uniworx*:
|
||||||
|
```sh
|
||||||
|
$ psql -c 'create database uniworx owner uniworx'
|
||||||
|
$ psql -c 'create database uniworx_test owner uniworx'
|
||||||
|
```
|
||||||
|
|
||||||
|
After you added the database switch back to your own user with `Ctrl + D`.
|
||||||
|
|
||||||
|
To properly access the database you now need to add a new linux user called *uniworx*. Enter "uniworx" as the password.
|
||||||
|
```sh
|
||||||
|
$ sudo adduser uniworx
|
||||||
|
```
|
||||||
|
|
||||||
|
### `Stack`
|
||||||
|
Stack is a toolbox for "Haskellers" to aid in developing Haskell projects.
|
||||||
|
|
||||||
|
Install:
|
||||||
|
```sh
|
||||||
|
$ curl -sSL https://get.haskellstack.org/ | sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Setup stack and install dependencies. This needs to be run from inside the directory you cloned this repository to:
|
||||||
|
```sh
|
||||||
|
$ stack setup
|
||||||
|
```
|
||||||
|
|
||||||
|
During this step or the next you might get an error that says something about missing C libraries for `ldap` and `lber`. You can install these using
|
||||||
|
```sh
|
||||||
|
$ sudo apt-get install libsasl2-dev libldap2-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
If you get an error that says *You need to install postgresql-server-dev-X.Y for building a server-side extension or libpq-dev for building a client-side application.*
|
||||||
|
Go ahead and install `libpq-dev` with
|
||||||
|
```sh
|
||||||
|
$ sudo apt-get install libpq-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Other packages you might need to install during this process:
|
||||||
|
```sh
|
||||||
|
$ sudo apt-get install pkg-config
|
||||||
|
$ sudo apt-get install libsodium-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Build the app:
|
||||||
|
```sh
|
||||||
|
$ stack build
|
||||||
|
```
|
||||||
|
|
||||||
|
This might take a few minutes... if not hours... be prepared.
|
||||||
|
|
||||||
|
install yesod:
|
||||||
|
```sh
|
||||||
|
$ stack install yesod-bin --install-ghc
|
||||||
|
```
|
||||||
|
|
||||||
|
### `Node` & `npm`
|
||||||
|
Node and Npm are needed to compile the frontend.
|
||||||
|
|
||||||
|
Install:
|
||||||
|
```sh
|
||||||
|
$ curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash -
|
||||||
|
$ sudo apt-get install -y nodejs
|
||||||
|
```
|
||||||
|
|
||||||
|
### Add dummy data to the database
|
||||||
|
After building the app you can prepare the database and add some dummy data:
|
||||||
|
```sh
|
||||||
|
$ ./db.sh -f
|
||||||
|
```
|
||||||
|
|
||||||
|
## Run Uni2work
|
||||||
|
```sh
|
||||||
|
$ npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
This will compile both frontend and backend and will start Uni2work in development mode (might take a few minutes the first time). It will keep running and will watch any file changes to automatically re-compile the application if necessary.
|
||||||
|
|
||||||
|
If you followed the steps above you should now be able to visit http://localhost:3000 and login as one of the accounts from the Development-Logins dropdown.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
Please see the [wiki](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/wikis/home) for more infos.
|
||||||
15
frontend/vendor/flatpickr.css
vendored
15
frontend/vendor/flatpickr.css
vendored
@ -1,3 +1,18 @@
|
|||||||
|
/*
|
||||||
|
custom code
|
||||||
|
hides the up/down arrows in time (number) inputs
|
||||||
|
*/
|
||||||
|
/* webkit */
|
||||||
|
.flatpickr-calendar input[type=number]::-webkit-inner-spin-button,
|
||||||
|
.flatpickr-calendar input[type=number]::-webkit-outer-spin-button {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
/* firefox */
|
||||||
|
.flatpickr-calendar input[type=number] {
|
||||||
|
-moz-appearance:textfield;
|
||||||
|
}
|
||||||
|
/* vendor code */
|
||||||
.flatpickr-calendar {
|
.flatpickr-calendar {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|||||||
@ -34,6 +34,12 @@ GenericShort: Kürzel
|
|||||||
GenericIsNew: Neu
|
GenericIsNew: Neu
|
||||||
GenericHasConflict: Konflikt
|
GenericHasConflict: Konflikt
|
||||||
GenericBack: Zurück
|
GenericBack: Zurück
|
||||||
|
GenericChange: Änderung
|
||||||
|
GenericNumChange: +/-
|
||||||
|
GenericMin: Min
|
||||||
|
GenericAvg: Avg
|
||||||
|
GenericMax: Max
|
||||||
|
GenericAll: Insgesamt
|
||||||
|
|
||||||
SummerTerm year@Integer: Sommersemester #{display year}
|
SummerTerm year@Integer: Sommersemester #{display year}
|
||||||
WinterTerm year@Integer: Wintersemester #{display year}/#{display $ succ year}
|
WinterTerm year@Integer: Wintersemester #{display year}/#{display $ succ year}
|
||||||
@ -197,6 +203,7 @@ SheetErrHintEarly: Hinweise dürfen erst nach Beginn des Abgabezeitraums herausg
|
|||||||
SheetErrSolutionEarly: Lösungen dürfen erst nach Ende der Abgabezeitraums herausgegeben werden
|
SheetErrSolutionEarly: Lösungen dürfen erst nach Ende der Abgabezeitraums herausgegeben werden
|
||||||
SheetNoCurrent: Es gibt momentan kein aktives Übungsblatt.
|
SheetNoCurrent: Es gibt momentan kein aktives Übungsblatt.
|
||||||
SheetNoOldUnassigned: Alle Abgaben inaktiver Blätter sind bereits einen Korrektor zugeteilt.
|
SheetNoOldUnassigned: Alle Abgaben inaktiver Blätter sind bereits einen Korrektor zugeteilt.
|
||||||
|
SheetsUnassignable name@Text: Momentan keine Abgaben zuteilbar für #{name}
|
||||||
|
|
||||||
Deadline: Abgabe
|
Deadline: Abgabe
|
||||||
Done: Eingereicht
|
Done: Eingereicht
|
||||||
@ -278,6 +285,7 @@ UnauthorizedCourseTime: Dieses Kurs erlaubt momentan keine Anmeldungen.
|
|||||||
UnauthorizedSheetTime: Dieses Übungsblatt ist momentan nicht freigegeben.
|
UnauthorizedSheetTime: Dieses Übungsblatt ist momentan nicht freigegeben.
|
||||||
UnauthorizedMaterialTime: Dieses Material ist momentan nicht freigegeben.
|
UnauthorizedMaterialTime: Dieses Material ist momentan nicht freigegeben.
|
||||||
UnauthorizedTutorialTime: Dieses Tutorium erlaubt momentan keine Anmeldungen.
|
UnauthorizedTutorialTime: Dieses Tutorium erlaubt momentan keine Anmeldungen.
|
||||||
|
UnauthorizedExamTime: Diese Klausur ist momentan nicht freigegeben.
|
||||||
UnauthorizedSubmissionOwner: Sie sind an dieser Abgabe nicht beteiligt.
|
UnauthorizedSubmissionOwner: Sie sind an dieser Abgabe nicht beteiligt.
|
||||||
UnauthorizedSubmissionRated: Diese Abgabe ist noch nicht korrigiert.
|
UnauthorizedSubmissionRated: Diese Abgabe ist noch nicht korrigiert.
|
||||||
UnauthorizedSubmissionCorrector: Sie sind nicht der Korrektor für diese Abgabe.
|
UnauthorizedSubmissionCorrector: Sie sind nicht der Korrektor für diese Abgabe.
|
||||||
@ -316,6 +324,7 @@ Correctors: Korrektoren
|
|||||||
CorState: Status
|
CorState: Status
|
||||||
CorByTut: Zuteilung nach Tutorium
|
CorByTut: Zuteilung nach Tutorium
|
||||||
CorProportion: Anteil
|
CorProportion: Anteil
|
||||||
|
CorDeficitProportion: Defizit Anteile
|
||||||
CorByProportionOnly proportion@Rational: #{display proportion} Anteile
|
CorByProportionOnly proportion@Rational: #{display proportion} Anteile
|
||||||
CorByProportionIncludingTutorial proportion@Rational: #{display proportion} Anteile - Tutorium
|
CorByProportionIncludingTutorial proportion@Rational: #{display proportion} Anteile - Tutorium
|
||||||
CorByProportionExcludingTutorial proportion@Rational: #{display proportion} Anteile + Tutorium
|
CorByProportionExcludingTutorial proportion@Rational: #{display proportion} Anteile + Tutorium
|
||||||
@ -391,6 +400,8 @@ UpdatedAssignedCorrectorSingle num@Int64: #{display num} Abgaben wurden dem neue
|
|||||||
NoCorrector: Kein Korrektor
|
NoCorrector: Kein Korrektor
|
||||||
RemovedCorrections num@Int64: Korrektur-Daten wurden von #{display num} Abgaben entfernt.
|
RemovedCorrections num@Int64: Korrektur-Daten wurden von #{display num} Abgaben entfernt.
|
||||||
UpdatedAssignedCorrectorsAuto num@Int64: #{display num} Abgaben wurden unter den Korrektoren aufgeteilt.
|
UpdatedAssignedCorrectorsAuto num@Int64: #{display num} Abgaben wurden unter den Korrektoren aufgeteilt.
|
||||||
|
UpdatedSheetCorrectorsAutoAssigned n@Int: #{display n} #{pluralDE n "Abgabe wurde einem Korrektor" "Abgaben wurden Korrektoren"} zugteilt.
|
||||||
|
UpdatedSheetCorrectorsAutoFailed n@Int: #{display n} #{pluralDE n "Abgabe konnte" "Abgaben konnten"} nicht automatisch zugewiesen werden.
|
||||||
CouldNotAssignCorrectorsAuto num@Int64: #{display num} Abgaben konnten nicht automatisch zugewiesen werden:
|
CouldNotAssignCorrectorsAuto num@Int64: #{display num} Abgaben konnten nicht automatisch zugewiesen werden:
|
||||||
SelfCorrectors num@Int64: #{display num} Abgaben wurden Abgebenden als eigenem Korrektor zugeteilt!
|
SelfCorrectors num@Int64: #{display num} Abgaben wurden Abgebenden als eigenem Korrektor zugeteilt!
|
||||||
|
|
||||||
@ -402,19 +413,22 @@ AssignSubmissionExceptionNoCorrectorsByProportion: Es sind keine Korrektoren mit
|
|||||||
AssignSubmissionExceptionSubmissionsNotFound n@Int: #{tshow n} Abgaben konnten nicht gefunden werden
|
AssignSubmissionExceptionSubmissionsNotFound n@Int: #{tshow n} Abgaben konnten nicht gefunden werden
|
||||||
NrSubmittorsTotal: Abgebende
|
NrSubmittorsTotal: Abgebende
|
||||||
NrSubmissionsTotal: Abgaben
|
NrSubmissionsTotal: Abgaben
|
||||||
|
NrSubmissionsTotalShort: Abg.
|
||||||
NrSubmissionsUnassigned: Ohne Korrektor
|
NrSubmissionsUnassigned: Ohne Korrektor
|
||||||
NoCorrectorAssigned: Ohne Korrektor
|
NoCorrectorAssigned: Ohne Korrektor
|
||||||
NrCorrectors: Korrektoren
|
NrCorrectors: Korrektoren
|
||||||
NrSubmissionsNewlyAssigned: Neu zugeteilt
|
NrSubmissionsNewlyAssigned: Neu zugeteilt
|
||||||
NrSubmissionsNotAssigned: Nicht zugeteilt
|
NrSubmissionsNotAssigned: Nicht zugeteilt
|
||||||
NrSubmissionsNotCorrected: Unkorrigiert
|
NrSubmissionsNotCorrected: Unkorrigiert
|
||||||
CorrectionTime: Korrekturdauer (Min/Avg/Max)
|
NrSubmissionsNotCorrectedShort: Unkg.
|
||||||
AssignSubmissionsRandomWarning: Die Zuteilungsvorschau kann geringfügig von der tatsächlichen Zuteilung abweichen, da die Zuteilung ein randomisierter Prozess ist. Mehrfaches neues Laden dieser Seite vor Betätigung des Zuteilungsknopfes kann dies sichtbar machen.
|
CorrectionTime: Korrekturdauer
|
||||||
|
AssignSubmissionsRandomWarning: Die Zuteilungsvorschau kann von der tatsächlichen Zuteilung abweichen, wenn mehrere Blätter auf einmal zugeteilt werden, da beim Ausgleich der Kontigente nur bereits zugeteilte Abgaben berücksichtigt werden. Da es ein randomisierte Prozess ist, kann es auch bei einzelnen Blättern gerinfgügige Abweichungen geben.
|
||||||
|
|
||||||
CorrectionsUploaded num@Int64: #{display num} Korrekturen wurden gespeichert:
|
CorrectionsUploaded num@Int64: #{display num} Korrekturen wurden gespeichert:
|
||||||
NoCorrectionsUploaded: In der hochgeladenen Datei wurden keine Korrekturen gefunden.
|
NoCorrectionsUploaded: In der hochgeladenen Datei wurden keine Korrekturen gefunden.
|
||||||
|
|
||||||
RatingBy: Korrigiert von
|
RatingBy: Korrigiert von
|
||||||
|
HasCorrector: Korrektor zugeteilt
|
||||||
AssignedTime: Zuteilung
|
AssignedTime: Zuteilung
|
||||||
AchievedBonusPoints: Erreichte Bonuspunkte
|
AchievedBonusPoints: Erreichte Bonuspunkte
|
||||||
AchievedNormalPoints: Erreichte Punkte
|
AchievedNormalPoints: Erreichte Punkte
|
||||||
@ -463,6 +477,7 @@ RatingNegative: Bewertungspunkte dürfen nicht negativ sein
|
|||||||
RatingExceedsMax: Bewertung übersteigt die erlaubte Maximalpunktzahl
|
RatingExceedsMax: Bewertung übersteigt die erlaubte Maximalpunktzahl
|
||||||
RatingNotExpected: Keine Bewertungen erlaubt
|
RatingNotExpected: Keine Bewertungen erlaubt
|
||||||
RatingBinaryExpected: Bewertung muss 0 (=durchgefallen) oder 1 (=bestanden) sein
|
RatingBinaryExpected: Bewertung muss 0 (=durchgefallen) oder 1 (=bestanden) sein
|
||||||
|
RatingPointsRequired: Bewertung erfordert für dieses Blatt eine Punktzahl
|
||||||
|
|
||||||
SubmissionSinkExceptionDuplicateFileTitle file@FilePath: Dateiname #{show file} kommt mehrfach im Zip-Archiv vor
|
SubmissionSinkExceptionDuplicateFileTitle file@FilePath: Dateiname #{show file} kommt mehrfach im Zip-Archiv vor
|
||||||
SubmissionSinkExceptionDuplicateRating: Mehr als eine Bewertung gefunden.
|
SubmissionSinkExceptionDuplicateRating: Mehr als eine Bewertung gefunden.
|
||||||
@ -638,6 +653,8 @@ MailSubjectCorrectorInvitation tid@TermId ssh@SchoolId csh@CourseShorthand shn@S
|
|||||||
|
|
||||||
MailSubjectTutorInvitation tid@TermId ssh@SchoolId csh@CourseShorthand tutn@TutorialName: [#{display tid}-#{display ssh}-#{csh}] Einladung zum Tutor für #{tutn}
|
MailSubjectTutorInvitation tid@TermId ssh@SchoolId csh@CourseShorthand tutn@TutorialName: [#{display tid}-#{display ssh}-#{csh}] Einladung zum Tutor für #{tutn}
|
||||||
|
|
||||||
|
MailSubjectExamCorrectorInvitation tid@TermId ssh@SchoolId csh@CourseShorthand examn@ExamName: [#{display tid}-#{display ssh}-#{csh}] Einladung zum Korrektor für Klausur #{examn}
|
||||||
|
|
||||||
MailSubjectSubmissionUserInvitation tid@TermId ssh@SchoolId csh@CourseShorthand shn@SheetName: [#{display tid}-#{display ssh}-#{csh}] Einladung zu einer Abgabe für #{shn}
|
MailSubjectSubmissionUserInvitation tid@TermId ssh@SchoolId csh@CourseShorthand shn@SheetName: [#{display tid}-#{display ssh}-#{csh}] Einladung zu einer Abgabe für #{shn}
|
||||||
|
|
||||||
SheetGrading: Bewertung
|
SheetGrading: Bewertung
|
||||||
@ -830,6 +847,7 @@ MenuCourseDelete: Kurs löschen
|
|||||||
MenuSubmissionNew: Abgabe anlegen
|
MenuSubmissionNew: Abgabe anlegen
|
||||||
MenuSubmissionOwn: Abgabe
|
MenuSubmissionOwn: Abgabe
|
||||||
MenuCorrectors: Korrektoren
|
MenuCorrectors: Korrektoren
|
||||||
|
MenuCorrectorsChange: Korrektoren ändern
|
||||||
MenuSheetEdit: Übungsblatt editieren
|
MenuSheetEdit: Übungsblatt editieren
|
||||||
MenuSheetDelete: Übungsblatt löschen
|
MenuSheetDelete: Übungsblatt löschen
|
||||||
MenuSheetClone: Als neues Übungsblatt klonen
|
MenuSheetClone: Als neues Übungsblatt klonen
|
||||||
@ -843,6 +861,9 @@ MenuAuthPreds: Authorisierungseinstellungen
|
|||||||
MenuTutorialDelete: Tutorium löschen
|
MenuTutorialDelete: Tutorium löschen
|
||||||
MenuTutorialEdit: Tutorium editieren
|
MenuTutorialEdit: Tutorium editieren
|
||||||
MenuTutorialComm: Mitteilung an Teilnehmer
|
MenuTutorialComm: Mitteilung an Teilnehmer
|
||||||
|
MenuExamList: Klausuren
|
||||||
|
MenuExamNew: Neue Klausur anlegen
|
||||||
|
MenuExamEdit: Bearbeiten
|
||||||
|
|
||||||
AuthPredsInfo: Um eigene Veranstaltungen aus Sicht der Teilnehmer anzusehen, können Veranstalter und Korrektoren hier die Prüfung ihrer erweiterten Berechtigungen temporär deaktivieren. Abgewählte Prädikate schlagen immer fehl. Abgewählte Prädikate werden also nicht geprüft um Zugriffe zu gewähren, welche andernfalls nicht erlaubt wären. Diese Einstellungen gelten nur temporär bis Ihre Sitzung abgelaufen ist, d.h. bis ihr Browser-Cookie abgelaufen ist. Durch Abwahl von Prädikaten kann man sich höchstens temporär aussperren.
|
AuthPredsInfo: Um eigene Veranstaltungen aus Sicht der Teilnehmer anzusehen, können Veranstalter und Korrektoren hier die Prüfung ihrer erweiterten Berechtigungen temporär deaktivieren. Abgewählte Prädikate schlagen immer fehl. Abgewählte Prädikate werden also nicht geprüft um Zugriffe zu gewähren, welche andernfalls nicht erlaubt wären. Diese Einstellungen gelten nur temporär bis Ihre Sitzung abgelaufen ist, d.h. bis ihr Browser-Cookie abgelaufen ist. Durch Abwahl von Prädikaten kann man sich höchstens temporär aussperren.
|
||||||
AuthPredsActive: Aktive Authorisierungsprädikate
|
AuthPredsActive: Aktive Authorisierungsprädikate
|
||||||
@ -859,6 +880,7 @@ AuthTagTutor: Nutzer ist Tutor
|
|||||||
AuthTagTime: Zeitliche Einschränkungen sind erfüllt
|
AuthTagTime: Zeitliche Einschränkungen sind erfüllt
|
||||||
AuthTagCourseRegistered: Nutzer ist Kursteilnehmer
|
AuthTagCourseRegistered: Nutzer ist Kursteilnehmer
|
||||||
AuthTagTutorialRegistered: Nutzer ist Tutoriumsteilnehmer
|
AuthTagTutorialRegistered: Nutzer ist Tutoriumsteilnehmer
|
||||||
|
AuthTagExamRegistered: Nutzer ist Klausurteilnehmer
|
||||||
AuthTagParticipant: Nutzer ist mit Kurs assoziiert
|
AuthTagParticipant: Nutzer ist mit Kurs assoziiert
|
||||||
AuthTagRegisterGroup: Nutzer ist nicht Mitglied eines anderen Tutoriums mit der selben Registrierungs-Gruppe
|
AuthTagRegisterGroup: Nutzer ist nicht Mitglied eines anderen Tutoriums mit der selben Registrierungs-Gruppe
|
||||||
AuthTagCapacity: Kapazität ist ausreichend
|
AuthTagCapacity: Kapazität ist ausreichend
|
||||||
@ -930,6 +952,11 @@ 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
|
||||||
|
ExamCorrectorInvitationDeclined examn@ExamName: Sie haben die Einladung, Korrektor für Klausur #{examn} zu werden, abgelehnt
|
||||||
|
ExamCorrectorInviteHeading examn@ExamName: Einladung zum Korrektor für Klausur #{examn}
|
||||||
|
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
|
||||||
SubmissionUserInvitationDeclined shn@SheetName: Sie haben die Einladung, Mitabgebende(r) für #{shn} zu werden, abgelehnt
|
SubmissionUserInvitationDeclined shn@SheetName: Sie haben die Einladung, Mitabgebende(r) für #{shn} zu werden, abgelehnt
|
||||||
SubmissionUserInviteHeading shn@SheetName: Einladung zu einer Abgabe für #{shn}
|
SubmissionUserInviteHeading shn@SheetName: Einladung zu einer Abgabe für #{shn}
|
||||||
@ -950,8 +977,8 @@ ScheduleRegular: Planmäßiger Termin
|
|||||||
ScheduleRegularKind: Plan
|
ScheduleRegularKind: Plan
|
||||||
WeekDay: Wochentag
|
WeekDay: Wochentag
|
||||||
Day: Tag
|
Day: Tag
|
||||||
OccurenceStart: Beginn
|
OccurrenceStart: Beginn
|
||||||
OccurenceEnd: Ende
|
OccurrenceEnd: Ende
|
||||||
ScheduleExists: Dieser Plan existiert bereits
|
ScheduleExists: Dieser Plan existiert bereits
|
||||||
|
|
||||||
ScheduleExceptions: Termin-Ausnahmen
|
ScheduleExceptions: Termin-Ausnahmen
|
||||||
@ -978,6 +1005,7 @@ TutorialsHeading: Tutorien
|
|||||||
TutorialEdit: Bearbeiten
|
TutorialEdit: Bearbeiten
|
||||||
TutorialDelete: Löschen
|
TutorialDelete: Löschen
|
||||||
|
|
||||||
|
CourseExams: Klausuren
|
||||||
CourseTutorials: Übungen
|
CourseTutorials: Übungen
|
||||||
|
|
||||||
ParticipantsN n@Int: #{tshow n} Teilnehmer
|
ParticipantsN n@Int: #{tshow n} Teilnehmer
|
||||||
@ -999,6 +1027,7 @@ TutorialNew: Neues Tutorium
|
|||||||
|
|
||||||
TutorialNameTaken tutn@TutorialName: Es existiert bereits anderes Tutorium mit Namen #{tutn}
|
TutorialNameTaken tutn@TutorialName: Es existiert bereits anderes Tutorium mit Namen #{tutn}
|
||||||
TutorialCreated tutn@TutorialName: Tutorium #{tutn} erfolgreich angelegt
|
TutorialCreated tutn@TutorialName: Tutorium #{tutn} erfolgreich angelegt
|
||||||
|
TutorialEdited tutn@TutorialName: Tutiorium #{tutn} erfolgreich bearbeitet
|
||||||
|
|
||||||
TutorialEditHeading tutn@TutorialName: #{tutn} bearbeiten
|
TutorialEditHeading tutn@TutorialName: #{tutn} bearbeiten
|
||||||
|
|
||||||
@ -1021,4 +1050,118 @@ CourseParticipantsInvited n@Int: #{tshow n} #{pluralDE n "Einladung" "Einladunge
|
|||||||
CourseParticipantsAlreadyRegistered n@Int: #{tshow n} Teilnehmer #{pluralDE n "ist" "sind"} bereits angemeldet
|
CourseParticipantsAlreadyRegistered n@Int: #{tshow n} Teilnehmer #{pluralDE n "ist" "sind"} bereits angemeldet
|
||||||
CourseParticipantsRegisteredWithoutField n@Int: #{tshow 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: #{tshow 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"}
|
||||||
CourseParticipantsRegistered n@Int: #{tshow n} Teilnehmer erfolgreich angemeldet
|
CourseParticipantsRegistered n@Int: #{tshow n} Teilnehmer erfolgreich angemeldet
|
||||||
CourseParticipantsRegisterHeading: Kursteilnehmer hinzufügen
|
CourseParticipantsRegisterHeading: Kursteilnehmer hinzufügen
|
||||||
|
|
||||||
|
ExamName: Name
|
||||||
|
ExamTime: Termin
|
||||||
|
ExamsHeading: Klausuren
|
||||||
|
ExamNameTip: Muss innerhalb der Veranstaltung eindeutig sein
|
||||||
|
ExamStart: Beginn
|
||||||
|
ExamEnd: Ende
|
||||||
|
ExamDescription: Beschreibung
|
||||||
|
ExamVisibleFrom: Sichtbar ab
|
||||||
|
ExamVisibleFromTip: Ohne Datum nie sichtbar und keine Anmeldung möglich
|
||||||
|
ExamRegisterFrom: Anmeldung ab
|
||||||
|
ExamRegisterFromTip: Zeitpunkt ab dem sich Kursteilnehmer selbständig zur Klausur anmelden können; ohne Datum ist keine Anmeldung möglich
|
||||||
|
ExamRegisterTo: Anmeldung bis
|
||||||
|
ExamDeregisterUntil: Abmeldung bis
|
||||||
|
ExamPublishOccurrenceAssignments: Terminzuteilung den Teilnehmern mitteilen um
|
||||||
|
ExamPublishOccurrenceAssignmentsTip: Ab diesem Zeitpunkt Teilnehmer einsehen zu welchen Teilprüfungen (Räumen) sie angemeldet sind
|
||||||
|
ExamPublishOccurrenceAssignmentsParticipant: Terminzuteilung einsehbar ab
|
||||||
|
ExamFinished: Bewertung abgeschlossen ab
|
||||||
|
ExamFinishedParticipant: Bewertung vorrausichtlich abgeschlossen
|
||||||
|
ExamFinishedTip: Zeitpunkt zu dem Klausurergebnisse den Teilnehmern gemeldet werden
|
||||||
|
ExamClosed: Noten stehen fest ab
|
||||||
|
ExamClosedTip: Zeitpunkt ab dem keine Änderungen an den Ergebnissen zulässig sind; Prüfungsämter bekommen Einsicht
|
||||||
|
ExamShowGrades: Noten anzeigen
|
||||||
|
ExamShowGradesTip: Soll den Teilnehmern ihre genaue Note angezeigt werden, oder sollen sie nur informiert werden, ob sie bestanden haben?
|
||||||
|
ExamPublicStatistics: Statistik veröffentlichen
|
||||||
|
ExamPublicStatisticsTip: Soll die statistische Auswertung auch den Teilnehmer angezeigt werden, sobald diese ihre Noten einsehen können?
|
||||||
|
ExamGradingRule: Notenberechnung
|
||||||
|
ExamGradingManual': Manuell
|
||||||
|
ExamGradingKey': Nach Schlü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
|
||||||
|
Points: Punkte
|
||||||
|
PointsMustBeNonNegative: Punktegrenzen dürfen nicht negativ sein
|
||||||
|
PointsMustBeMonotonic: Punktegrenzen müssen aufsteigend sein
|
||||||
|
GradingFrom: Ab
|
||||||
|
ExamNew: Neue Klausur
|
||||||
|
ExamBonusRule: Klausurbonus aus Übungsbetrieb
|
||||||
|
ExamNoBonus': Kein Bonus
|
||||||
|
ExamBonusPoints': Umrechnung von Übungspunkten
|
||||||
|
|
||||||
|
ExamEditHeading examn@ExamName: #{examn} bearbeiten
|
||||||
|
|
||||||
|
ExamBonusMaxPoints: Maximal erreichbare Klausur-Bonuspunkte
|
||||||
|
ExamBonusMaxPointsNonPositive: Maximaler Klausurbonus muss positiv und größer null sein
|
||||||
|
ExamBonusOnlyPassed: Bonus nur nach Bestehen anrechnen
|
||||||
|
|
||||||
|
ExamOccurrenceRule: Automatische Terminzuteilung
|
||||||
|
ExamOccurrenceRuleParticipant: Terminzuteilung
|
||||||
|
ExamRoomManual': Keine automatische Zuteilung
|
||||||
|
ExamRoomSurname': Nach Nachname
|
||||||
|
ExamRoomMatriculation': Nach Matrikelnummer
|
||||||
|
ExamRoomRandom': Zufällig pro Teilnehmer
|
||||||
|
|
||||||
|
ExamOccurrences: Prüfungen
|
||||||
|
ExamRoomAlreadyExists: Prüfung ist bereits eingetragen
|
||||||
|
ExamRoom: Raum
|
||||||
|
ExamRoomCapacity: Kapazität
|
||||||
|
ExamRoomCapacityNegative: Kapazität darf nicht negativ sein
|
||||||
|
ExamRoomTime: Termin
|
||||||
|
ExamRoomStart: Beginn
|
||||||
|
ExamRoomEnd: Ende
|
||||||
|
ExamRoomDescription: Beschreibung
|
||||||
|
ExamTimeTip: Nur zur Information der Studierenden, die tatsächliche Zeitangabe erfolgt pro Prüfung
|
||||||
|
ExamRoomRegistered: Zugeteilt
|
||||||
|
|
||||||
|
ExamFormTimes: Zeiten
|
||||||
|
ExamFormOccurrences: Prüfungstermine
|
||||||
|
ExamFormAutomaticFunctions: Automatische Funktionen
|
||||||
|
ExamFormCorrection: Korrektur
|
||||||
|
ExamFormParts: Teile
|
||||||
|
|
||||||
|
ExamCorrectors: Korrektoren
|
||||||
|
ExamCorrectorAlreadyAdded: Ein Korrektor mit dieser E-Mail ist bereits für diese Klausur eingetragen
|
||||||
|
|
||||||
|
ExamParts: Teilaufgaben
|
||||||
|
ExamPartWeightNegative: Gewicht aller Teilaufgaben muss größer oder gleich Null sein
|
||||||
|
ExamPartAlreadyExists: Teilaufgabe mit diesem Namen existiert bereits
|
||||||
|
ExamPartName: Name
|
||||||
|
ExamPartMaxPoints: Maximalpunktzahl
|
||||||
|
ExamPartWeight: Gewichtung
|
||||||
|
ExamPartResultPoints: Erreichte Punkte
|
||||||
|
|
||||||
|
ExamNameTaken exam@ExamName: Es existiert bereits eine Klausur mit Namen #{exam}
|
||||||
|
ExamCreated exam@ExamName: Klausur #{exam} erfolgreich angelegt
|
||||||
|
ExamEdited exam@ExamName: Klausur #{exam} erfolgreich bearbeitet
|
||||||
|
|
||||||
|
ExamNoShow: Nicht erschienen
|
||||||
|
ExamVoided: Entwertet
|
||||||
|
|
||||||
|
ExamBonusPoints possible@Points: Maximal #{showFixed True possible} Klausurpunkte
|
||||||
|
ExamBonusPointsPassed possible@Points: Maximal #{showFixed True possible} Klausurpunkte, falls die Klausur auch ohne Bonus bereits bestanden ist
|
||||||
|
|
||||||
|
ExamPassed: Bestanden
|
||||||
|
ExamNotPassed: Nicht bestanden
|
||||||
|
ExamResult: Klausurergebnis
|
||||||
|
|
||||||
|
ExamRegisteredSuccess exam@ExamName: Erfolgreich zur Klausur #{exam} angemeldet
|
||||||
|
ExamDeregisteredSuccess exam@ExamName: Erfolgreich von der Klausur #{exam} abgemeldet
|
||||||
|
ExamRegistered: Angemeldet
|
||||||
|
ExamNotRegistered: Nicht angemeldet
|
||||||
|
ExamRegistration: Anmeldung
|
||||||
|
|
||||||
|
ExamRegisterToMustBeAfterRegisterFrom: "Anmeldung ab" muss vor "Anmeldung bis" liegen
|
||||||
|
ExamDeregisterUntilMustBeAfterRegisterFrom: "Abmeldung bis" muss nach "Anmeldung bis" liegen
|
||||||
|
ExamStartMustBeAfterPublishOccurrenceAssignments: Start muss nach Veröffentlichung der Terminzuordnung liegen
|
||||||
|
ExamEndMustBeAfterStart: Beginn der Klausur muss vor ihrem Ende liegen
|
||||||
|
ExamFinishedMustBeAfterEnd: "Bewertung abgeschlossen ab" muss nach Ende liegen
|
||||||
|
ExamFinishedMustBeAfterStart: "Bewertung abgeschlossen ab" muss nach Start liegen
|
||||||
|
ExamClosedMustBeAfterFinished: "Noten stehen fest ab" muss nach "Bewertung abgeschlossen ab" liegen
|
||||||
|
ExamClosedMustBeAfterStart: "Noten stehen fest ab" muss nach Start liegen
|
||||||
|
ExamClosedMustBeAfterEnd: "Noten stehen fest ab" muss nach Ende liegen
|
||||||
|
|
||||||
|
VersionHistory: Versionsgeschichte
|
||||||
|
KnownBugs: Bekannte Bugs
|
||||||
75
models/exams
75
models/exams
@ -1,22 +1,55 @@
|
|||||||
-- EXAMS ARE TODO; THIS IS JUST AN UNUSED STUB
|
|
||||||
Exam
|
Exam
|
||||||
course CourseId
|
course CourseId
|
||||||
name Text
|
name ExamName
|
||||||
description Text
|
gradingRule ExamGradingRule
|
||||||
begin UTCTime
|
bonusRule ExamBonusRule
|
||||||
end UTCTime
|
occurrenceRule ExamOccurrenceRule
|
||||||
registrationBegin UTCTime
|
visibleFrom UTCTime Maybe
|
||||||
registrationEnd UTCTime
|
registerFrom UTCTime Maybe
|
||||||
deregistrationEnd UTCTime
|
registerTo UTCTime Maybe
|
||||||
ratingVisible Bool -- may participants see their own rating yet
|
deregisterUntil UTCTime Maybe
|
||||||
statisticsVisible Bool -- may participants view statistics over all participants (should not be allowed for 'small' courses)
|
publishOccurrenceAssignments UTCTime
|
||||||
--ExamEdit
|
start UTCTime
|
||||||
-- user UserId
|
end UTCTime Maybe
|
||||||
-- time UTCTime
|
finished UTCTime Maybe -- Grades shown to students, `ExamCorrector`s locked out
|
||||||
-- exam ExamId
|
closed UTCTime Maybe -- Prüfungsamt hat Einsicht (notification)
|
||||||
--ExamUser
|
publicStatistics Bool
|
||||||
-- user UserId
|
showGrades Bool
|
||||||
-- examId ExamId
|
description Html Maybe
|
||||||
-- -- CONTINUE HERE: Include rating in this table or separately?
|
UniqueExam course name
|
||||||
-- UniqueExamUser user examId
|
ExamPart
|
||||||
-- By default this file is used in Model.hs (which is imported by Foundation.hs)
|
exam ExamId
|
||||||
|
name (CI Text)
|
||||||
|
maxPoints Points Maybe
|
||||||
|
weight Rational
|
||||||
|
UniqueExamPart exam name
|
||||||
|
ExamOccurrence
|
||||||
|
exam ExamId
|
||||||
|
room Text
|
||||||
|
capacity Natural
|
||||||
|
start UTCTime
|
||||||
|
end UTCTime Maybe
|
||||||
|
description Html Maybe
|
||||||
|
ExamRegistration
|
||||||
|
exam ExamId
|
||||||
|
user UserId
|
||||||
|
occurrence ExamOccurrenceId Maybe
|
||||||
|
UniqueExamRegistration exam user
|
||||||
|
ExamPartResult
|
||||||
|
examPart ExamPartId
|
||||||
|
user UserId
|
||||||
|
result ExamResultPoints
|
||||||
|
UniqueExamPartResult examPart user
|
||||||
|
ExamResult
|
||||||
|
exam ExamId
|
||||||
|
user UserId
|
||||||
|
result ExamResultGrade
|
||||||
|
UniqueExamResult exam user
|
||||||
|
ExamCorrector
|
||||||
|
exam ExamId
|
||||||
|
user UserId
|
||||||
|
UniqueExamCorrector exam user
|
||||||
|
ExamPartCorrector
|
||||||
|
part ExamPartId
|
||||||
|
corrector ExamCorrector
|
||||||
|
UniqueExamPartCorrector part corrector
|
||||||
@ -4,7 +4,7 @@ Tutorial json
|
|||||||
type (CI Text) -- "Tutorium", "Zentralübung", ...
|
type (CI Text) -- "Tutorium", "Zentralübung", ...
|
||||||
capacity Int Maybe -- limit for enrolment in this tutorial
|
capacity Int Maybe -- limit for enrolment in this tutorial
|
||||||
room Text
|
room Text
|
||||||
time Occurences
|
time Occurrences
|
||||||
regGroup (CI Text) Maybe -- each participant may register for one tutorial per regGroup
|
regGroup (CI Text) Maybe -- each participant may register for one tutorial per regGroup
|
||||||
registerFrom UTCTime Maybe
|
registerFrom UTCTime Maybe
|
||||||
registerTo UTCTime Maybe
|
registerTo UTCTime Maybe
|
||||||
|
|||||||
11
routes
11
routes
@ -137,7 +137,16 @@
|
|||||||
/register TRegisterR POST !timeANDcapacityANDcourse-registeredANDregister-group !timeANDtutorial-registered
|
/register TRegisterR POST !timeANDcapacityANDcourse-registeredANDregister-group !timeANDtutorial-registered
|
||||||
/communication TCommR GET POST !tutor
|
/communication TCommR GET POST !tutor
|
||||||
/tutor-invite TInviteR GET POST
|
/tutor-invite TInviteR GET POST
|
||||||
|
/exams CExamListR GET !free
|
||||||
|
/exams/new CExamNewR GET POST
|
||||||
|
/exams/#ExamName ExamR:
|
||||||
|
/show EShowR GET !time
|
||||||
|
/edit EEditR GET POST
|
||||||
|
/corrector-invite ECInviteR GET POST
|
||||||
|
/users EUsersR GET POST !timeANDcorrector
|
||||||
|
/users/new EAddUserR GET POST
|
||||||
|
/users/invite EInviteR GET POST
|
||||||
|
/register ERegisterR POST !timeANDcourse-registered !timeANDexam-registered
|
||||||
|
|
||||||
/subs CorrectionsR GET POST !corrector !lecturer
|
/subs CorrectionsR GET POST !corrector !lecturer
|
||||||
/subs/upload CorrectionsUploadR GET POST !corrector !lecturer
|
/subs/upload CorrectionsUploadR GET POST !corrector !lecturer
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
{ nixpkgs ? import <nixpkgs>, compiler ? null }:
|
{ nixpkgs ? import <nixpkgs> }:
|
||||||
|
|
||||||
let
|
let
|
||||||
inherit (nixpkgs {}) pkgs;
|
inherit (nixpkgs {}) pkgs;
|
||||||
haskellPackages = if isNull compiler then pkgs.haskellPackages else pkgs.haskell.packages."${compiler}";
|
haskellPackages = import ./stackage.nix { inherit nixpkgs; };
|
||||||
|
|
||||||
drv = haskellPackages.callPackage ./uniworx.nix {};
|
drv = haskellPackages.callPackage ./uniworx.nix {};
|
||||||
|
|
||||||
@ -19,7 +19,7 @@ let
|
|||||||
'';
|
'';
|
||||||
|
|
||||||
override = oldAttrs: {
|
override = oldAttrs: {
|
||||||
nativeBuildInputs = oldAttrs.nativeBuildInputs ++ (with pkgs; [ postgresql pgadmin openldap ]) ++ (with haskellPackages; [ stack yesod-bin hlint cabal-install ]);
|
nativeBuildInputs = oldAttrs.nativeBuildInputs ++ (with pkgs; [ nodejs-8_x postgresql pgadmin openldap ]) ++ (with haskellPackages; [ stack yesod-bin hlint cabal-install ]);
|
||||||
shellHook = ''
|
shellHook = ''
|
||||||
export PROMPT_INFO="${oldAttrs.name}"
|
export PROMPT_INFO="${oldAttrs.name}"
|
||||||
|
|
||||||
|
|||||||
@ -113,6 +113,7 @@ import Handler.Material
|
|||||||
import Handler.CryptoIDDispatch
|
import Handler.CryptoIDDispatch
|
||||||
import Handler.SystemMessage
|
import Handler.SystemMessage
|
||||||
import Handler.Health
|
import Handler.Health
|
||||||
|
import Handler.Exam
|
||||||
|
|
||||||
|
|
||||||
-- This line actually creates our YesodDispatch instance. It is the second half
|
-- This line actually creates our YesodDispatch instance. It is the second half
|
||||||
|
|||||||
@ -43,6 +43,8 @@ decCryptoIDs [ ''SubmissionId
|
|||||||
, ''SystemMessageId
|
, ''SystemMessageId
|
||||||
, ''SystemMessageTranslationId
|
, ''SystemMessageTranslationId
|
||||||
, ''StudyFeaturesId
|
, ''StudyFeaturesId
|
||||||
|
, ''ExamOccurrenceId
|
||||||
|
, ''ExamPartId
|
||||||
]
|
]
|
||||||
|
|
||||||
instance {-# OVERLAPS #-} namespace ~ CryptoIDNamespace (CI FilePath) SubmissionId => PathPiece (E.CryptoID namespace (CI FilePath)) where
|
instance {-# OVERLAPS #-} namespace ~ CryptoIDNamespace (CI FilePath) SubmissionId => PathPiece (E.CryptoID namespace (CI FilePath)) where
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
module Database.Esqueleto.Utils
|
module Database.Esqueleto.Utils
|
||||||
( true, false
|
( true, false
|
||||||
|
, isJust
|
||||||
, isInfixOf, hasInfix
|
, isInfixOf, hasInfix
|
||||||
, any, all
|
, any, all
|
||||||
, SqlIn(..)
|
, SqlIn(..)
|
||||||
@ -11,7 +12,7 @@ module Database.Esqueleto.Utils
|
|||||||
, anyFilter, allFilter
|
, anyFilter, allFilter
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import ClassyPrelude.Yesod hiding (isInfixOf, any, all)
|
import ClassyPrelude.Yesod hiding (isInfixOf, any, all, isJust)
|
||||||
import qualified Data.Set as Set
|
import qualified Data.Set as Set
|
||||||
import qualified Data.Foldable as F
|
import qualified Data.Foldable as F
|
||||||
import qualified Database.Esqueleto as E
|
import qualified Database.Esqueleto as E
|
||||||
@ -34,6 +35,10 @@ true = E.val True
|
|||||||
false :: E.SqlExpr (E.Value Bool)
|
false :: E.SqlExpr (E.Value Bool)
|
||||||
false = E.val False
|
false = E.val False
|
||||||
|
|
||||||
|
-- | Negation of `isNothing` which is missing
|
||||||
|
isJust :: (E.Esqueleto query expr backend, PersistField typ) => expr (E.Value (Maybe typ)) -> expr (E.Value Bool)
|
||||||
|
isJust = E.not_ . E.isNothing
|
||||||
|
|
||||||
-- | Check if the first string is contained in the text derived from the second argument
|
-- | Check if the first string is contained in the text derived from the second argument
|
||||||
isInfixOf :: (E.Esqueleto query expr backend, E.SqlString s2) =>
|
isInfixOf :: (E.Esqueleto query expr backend, E.SqlString s2) =>
|
||||||
Text -> expr (E.Value s2) -> expr (E.Value Bool)
|
Text -> expr (E.Value s2) -> expr (E.Value Bool)
|
||||||
|
|||||||
@ -160,6 +160,7 @@ deriving instance Generic SheetR
|
|||||||
deriving instance Generic SubmissionR
|
deriving instance Generic SubmissionR
|
||||||
deriving instance Generic MaterialR
|
deriving instance Generic MaterialR
|
||||||
deriving instance Generic TutorialR
|
deriving instance Generic TutorialR
|
||||||
|
deriving instance Generic ExamR
|
||||||
deriving instance Generic (Route UniWorX)
|
deriving instance Generic (Route UniWorX)
|
||||||
|
|
||||||
-- | Convenient Type Synonyms:
|
-- | Convenient Type Synonyms:
|
||||||
@ -181,6 +182,10 @@ pattern CTutorialR :: TermId -> SchoolId -> CourseShorthand -> TutorialName -> T
|
|||||||
pattern CTutorialR tid ssh csh tnm ptn
|
pattern CTutorialR tid ssh csh tnm ptn
|
||||||
= CourseR tid ssh csh (TutorialR tnm ptn)
|
= CourseR tid ssh csh (TutorialR tnm ptn)
|
||||||
|
|
||||||
|
pattern CExamR :: TermId -> SchoolId -> CourseShorthand -> ExamName -> ExamR -> Route UniWorX
|
||||||
|
pattern CExamR tid ssh csh tnm ptn
|
||||||
|
= CourseR tid ssh csh (ExamR tnm ptn)
|
||||||
|
|
||||||
pattern CSubmissionR :: TermId -> SchoolId -> CourseShorthand -> SheetName -> CryptoFileNameSubmission -> SubmissionR -> Route UniWorX
|
pattern CSubmissionR :: TermId -> SchoolId -> CourseShorthand -> SheetName -> CryptoFileNameSubmission -> SubmissionR -> Route UniWorX
|
||||||
pattern CSubmissionR tid ssh csh shn cid ptn
|
pattern CSubmissionR tid ssh csh shn cid ptn
|
||||||
= CSheetR tid ssh csh shn (SubmissionR cid ptn)
|
= CSheetR tid ssh csh shn (SubmissionR cid ptn)
|
||||||
@ -206,6 +211,15 @@ noneOneMoreDE num noneText singularForm pluralForm
|
|||||||
| num == 1 = singularForm
|
| num == 1 = singularForm
|
||||||
| otherwise = pluralForm
|
| otherwise = pluralForm
|
||||||
|
|
||||||
|
noneMoreDE :: (Eq a, Num a)
|
||||||
|
=> a -- ^ Count
|
||||||
|
-> Text -- ^ None
|
||||||
|
-> Text -- ^ Some
|
||||||
|
-> Text
|
||||||
|
noneMoreDE num noneText someText
|
||||||
|
| num == 0 = noneText
|
||||||
|
| otherwise = someText
|
||||||
|
|
||||||
-- Convenience Type for Messages, since Yesod messages cannot deal with compound type identifiers
|
-- Convenience Type for Messages, since Yesod messages cannot deal with compound type identifiers
|
||||||
type IntMaybe = Maybe Int
|
type IntMaybe = Maybe Int
|
||||||
type TextList = [Text]
|
type TextList = [Text]
|
||||||
@ -318,6 +332,9 @@ instance RenderMessage UniWorX StudyDegreeTerm where
|
|||||||
mr :: RenderMessage UniWorX msg => msg -> Text
|
mr :: RenderMessage UniWorX msg => msg -> Text
|
||||||
mr = renderMessage foundation ls
|
mr = renderMessage foundation ls
|
||||||
|
|
||||||
|
instance RenderMessage UniWorX ExamGrade where
|
||||||
|
renderMessage _ _ = pack . (showFixed False :: Deci -> String) . fromRational . review numberGrade
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
newtype ErrorResponseTitle = ErrorResponseTitle ErrorResponse
|
newtype ErrorResponseTitle = ErrorResponseTitle ErrorResponse
|
||||||
@ -634,6 +651,30 @@ tagAccessPredicate AuthTutor = APDB $ \mAuthId route _ -> exceptT return return
|
|||||||
guardMExceptT (not $ Map.null resMap) (unauthorizedI MsgUnauthorizedTutor)
|
guardMExceptT (not $ Map.null resMap) (unauthorizedI MsgUnauthorizedTutor)
|
||||||
return Authorized
|
return Authorized
|
||||||
tagAccessPredicate AuthTime = APDB $ \mAuthId route _ -> case route of
|
tagAccessPredicate AuthTime = APDB $ \mAuthId route _ -> case route of
|
||||||
|
CExamR tid ssh csh examn subRoute -> maybeT (unauthorizedI MsgUnauthorizedExamTime) $ do
|
||||||
|
course <- $cachedHereBinary (tid, ssh, csh) . MaybeT . getKeyBy $ TermSchoolCourseShort tid ssh csh
|
||||||
|
Entity eId Exam{..} <- $cachedHereBinary (course, examn) . MaybeT . getBy $ UniqueExam course examn
|
||||||
|
cTime <- liftIO getCurrentTime
|
||||||
|
registered <- case mAuthId of
|
||||||
|
Just uid -> $cachedHereBinary (eId, uid) . lift . existsBy $ UniqueExamRegistration eId uid
|
||||||
|
Nothing -> return False
|
||||||
|
|
||||||
|
let visible = NTop examVisibleFrom <= NTop (Just cTime)
|
||||||
|
|
||||||
|
case subRoute of
|
||||||
|
EShowR -> guard visible
|
||||||
|
EUsersR -> guard $ examStart <= cTime
|
||||||
|
&& NTop (Just cTime) <= NTop examFinished
|
||||||
|
ERegisterR
|
||||||
|
| not registered -> guard $ visible
|
||||||
|
&& NTop examRegisterFrom <= NTop (Just cTime)
|
||||||
|
&& NTop (Just cTime) <= NTop examRegisterTo
|
||||||
|
| otherwise -> guard $ visible
|
||||||
|
&& NTop (Just cTime) <= NTop examDeregisterUntil
|
||||||
|
_ -> return ()
|
||||||
|
|
||||||
|
return Authorized
|
||||||
|
|
||||||
CTutorialR tid ssh csh tutn TRegisterR -> maybeT (unauthorizedI MsgUnauthorizedTutorialTime) $ do
|
CTutorialR tid ssh csh tutn TRegisterR -> maybeT (unauthorizedI MsgUnauthorizedTutorialTime) $ do
|
||||||
now <- liftIO getCurrentTime
|
now <- liftIO getCurrentTime
|
||||||
course <- $cachedHereBinary (tid, ssh, csh) . MaybeT . getKeyBy $ TermSchoolCourseShort tid ssh csh
|
course <- $cachedHereBinary (tid, ssh, csh) . MaybeT . getKeyBy $ TermSchoolCourseShort tid ssh csh
|
||||||
@ -757,6 +798,33 @@ tagAccessPredicate AuthTutorialRegistered = APDB $ \mAuthId route _ -> case rout
|
|||||||
guardMExceptT (c > 0) (unauthorizedI MsgUnauthorizedRegistered)
|
guardMExceptT (c > 0) (unauthorizedI MsgUnauthorizedRegistered)
|
||||||
return Authorized
|
return Authorized
|
||||||
r -> $unsupportedAuthPredicate AuthTutorialRegistered r
|
r -> $unsupportedAuthPredicate AuthTutorialRegistered r
|
||||||
|
tagAccessPredicate AuthExamRegistered = APDB $ \mAuthId route _ -> case route of
|
||||||
|
CExamR tid ssh csh examn _ -> exceptT return return $ do
|
||||||
|
authId <- maybeExceptT AuthenticationRequired $ return mAuthId
|
||||||
|
[E.Value c] <- $cachedHereBinary (authId, tid, ssh, csh, examn) . lift . E.select . E.from $ \(course `E.InnerJoin` exam `E.InnerJoin` examRegistration) -> do
|
||||||
|
E.on $ exam E.^. ExamId E.==. examRegistration E.^. ExamRegistrationExam
|
||||||
|
E.on $ course E.^. CourseId E.==. exam E.^. ExamCourse
|
||||||
|
E.where_ $ examRegistration E.^. ExamRegistrationUser E.==. E.val authId
|
||||||
|
E.&&. course E.^. CourseTerm E.==. E.val tid
|
||||||
|
E.&&. course E.^. CourseSchool E.==. E.val ssh
|
||||||
|
E.&&. course E.^. CourseShorthand E.==. E.val csh
|
||||||
|
E.&&. exam E.^. ExamName E.==. E.val examn
|
||||||
|
return (E.countRows :: E.SqlExpr (E.Value Int64))
|
||||||
|
guardMExceptT (c > 0) (unauthorizedI MsgUnauthorizedRegistered)
|
||||||
|
return Authorized
|
||||||
|
CourseR tid ssh csh _ -> exceptT return return $ do
|
||||||
|
authId <- maybeExceptT AuthenticationRequired $ return mAuthId
|
||||||
|
[E.Value c] <- $cachedHereBinary (authId, tid, ssh, csh) . lift . E.select . E.from $ \(course `E.InnerJoin` exam `E.InnerJoin` examRegistration) -> do
|
||||||
|
E.on $ exam E.^. ExamId E.==. examRegistration E.^. ExamRegistrationExam
|
||||||
|
E.on $ course E.^. CourseId E.==. exam E.^. ExamCourse
|
||||||
|
E.where_ $ examRegistration E.^. ExamRegistrationUser E.==. E.val authId
|
||||||
|
E.&&. course E.^. CourseTerm E.==. E.val tid
|
||||||
|
E.&&. course E.^. CourseSchool E.==. E.val ssh
|
||||||
|
E.&&. course E.^. CourseShorthand E.==. E.val csh
|
||||||
|
return (E.countRows :: E.SqlExpr (E.Value Int64))
|
||||||
|
guardMExceptT (c > 0) (unauthorizedI MsgUnauthorizedRegistered)
|
||||||
|
return Authorized
|
||||||
|
r -> $unsupportedAuthPredicate AuthExamRegistered r
|
||||||
tagAccessPredicate AuthParticipant = APDB $ \_ route _ -> case route of
|
tagAccessPredicate AuthParticipant = APDB $ \_ route _ -> case route of
|
||||||
CourseR tid ssh csh (CUserR cID) -> exceptT return return $ do
|
CourseR tid ssh csh (CUserR cID) -> exceptT return return $ do
|
||||||
let authorizedIfExists f = do
|
let authorizedIfExists f = do
|
||||||
@ -1427,7 +1495,7 @@ instance YesodBreadcrumbs UniWorX where
|
|||||||
breadcrumb (CourseR tid ssh csh CInviteR) = return ("Einladung", Just $ CourseR tid ssh csh CShowR)
|
breadcrumb (CourseR tid ssh csh CInviteR) = return ("Einladung", Just $ CourseR tid ssh csh CShowR)
|
||||||
breadcrumb (CourseR tid ssh csh (CUserR _)) = return ("Teilnehmer" , Just $ CourseR tid ssh csh CUsersR)
|
breadcrumb (CourseR tid ssh csh (CUserR _)) = return ("Teilnehmer" , Just $ CourseR tid ssh csh CUsersR)
|
||||||
breadcrumb (CourseR tid ssh csh CCorrectionsR) = return ("Abgaben" , Just $ CourseR tid ssh csh CShowR)
|
breadcrumb (CourseR tid ssh csh CCorrectionsR) = return ("Abgaben" , Just $ CourseR tid ssh csh CShowR)
|
||||||
breadcrumb (CourseR tid ssh csh CAssignR) = return ("Korrektur" , Just $ CourseR tid ssh csh CCorrectionsR)
|
breadcrumb (CourseR tid ssh csh CAssignR) = return ("Zuteilung Korrekturen" , Just $ CourseR tid ssh csh CCorrectionsR)
|
||||||
breadcrumb (CourseR tid ssh csh SheetListR) = return ("Übungen" , Just $ CourseR tid ssh csh CShowR)
|
breadcrumb (CourseR tid ssh csh SheetListR) = return ("Übungen" , Just $ CourseR tid ssh csh CShowR)
|
||||||
breadcrumb (CourseR tid ssh csh SheetNewR ) = return ("Neu", Just $ CourseR tid ssh csh SheetListR)
|
breadcrumb (CourseR tid ssh csh SheetNewR ) = return ("Neu", Just $ CourseR tid ssh csh SheetListR)
|
||||||
breadcrumb (CourseR tid ssh csh SheetCurrentR) = return ("Aktuelles Blatt", Just $ CourseR tid ssh csh SheetListR)
|
breadcrumb (CourseR tid ssh csh SheetCurrentR) = return ("Aktuelles Blatt", Just $ CourseR tid ssh csh SheetListR)
|
||||||
@ -1436,6 +1504,12 @@ instance YesodBreadcrumbs UniWorX where
|
|||||||
breadcrumb (CourseR tid ssh csh CTutorialListR) = return ("Tutorien", Just $ CourseR tid ssh csh CShowR)
|
breadcrumb (CourseR tid ssh csh CTutorialListR) = return ("Tutorien", Just $ CourseR tid ssh csh CShowR)
|
||||||
breadcrumb (CourseR tid ssh csh CTutorialNewR) = return ("Anlegen", Just $ CourseR tid ssh csh CTutorialListR)
|
breadcrumb (CourseR tid ssh csh CTutorialNewR) = return ("Anlegen", Just $ CourseR tid ssh csh CTutorialListR)
|
||||||
|
|
||||||
|
breadcrumb (CourseR tid ssh csh CExamListR) = return ("Klausuren", Just $ CourseR tid ssh csh CShowR)
|
||||||
|
breadcrumb (CourseR tid ssh csh CExamNewR) = return ("Anlegen", Just $ CourseR tid ssh csh CExamListR)
|
||||||
|
|
||||||
|
breadcrumb (CExamR tid ssh csh examn EShowR) = return (CI.original examn, Just $ CourseR tid ssh csh CExamListR)
|
||||||
|
breadcrumb (CExamR tid ssh csh examn EEditR) = return ("Bearbeiten", Just $ CExamR tid ssh csh examn EShowR)
|
||||||
|
|
||||||
breadcrumb (CTutorialR tid ssh csh tutn TUsersR) = return (CI.original tutn, Just $ CourseR tid ssh csh CTutorialListR)
|
breadcrumb (CTutorialR tid ssh csh tutn TUsersR) = return (CI.original tutn, Just $ CourseR tid ssh csh CTutorialListR)
|
||||||
breadcrumb (CTutorialR tid ssh csh tutn TEditR) = return ("Bearbeiten", Just $ CTutorialR tid ssh csh tutn TUsersR)
|
breadcrumb (CTutorialR tid ssh csh tutn TEditR) = return ("Bearbeiten", Just $ CTutorialR tid ssh csh tutn TUsersR)
|
||||||
breadcrumb (CTutorialR tid ssh csh tutn TDeleteR) = return ("Löschen", Just $ CTutorialR tid ssh csh tutn TUsersR)
|
breadcrumb (CTutorialR tid ssh csh tutn TDeleteR) = return ("Löschen", Just $ CTutorialR tid ssh csh tutn TUsersR)
|
||||||
@ -1445,7 +1519,7 @@ instance YesodBreadcrumbs UniWorX where
|
|||||||
breadcrumb (CSheetR tid ssh csh shn SEditR) = return ("Bearbeiten" , Just $ CSheetR tid ssh csh shn SShowR)
|
breadcrumb (CSheetR tid ssh csh shn SEditR) = return ("Bearbeiten" , Just $ CSheetR tid ssh csh shn SShowR)
|
||||||
breadcrumb (CSheetR tid ssh csh shn SDelR ) = return ("Löschen" , Just $ CSheetR tid ssh csh shn SShowR)
|
breadcrumb (CSheetR tid ssh csh shn SDelR ) = return ("Löschen" , Just $ CSheetR tid ssh csh shn SShowR)
|
||||||
breadcrumb (CSheetR tid ssh csh shn SSubsR) = return ("Abgaben" , Just $ CSheetR tid ssh csh shn SShowR)
|
breadcrumb (CSheetR tid ssh csh shn SSubsR) = return ("Abgaben" , Just $ CSheetR tid ssh csh shn SShowR)
|
||||||
breadcrumb (CSheetR tid ssh csh shn SAssignR) = return ("Zuteilen" , Just $ CSheetR tid ssh csh shn SSubsR)
|
breadcrumb (CSheetR tid ssh csh shn SAssignR) = return ("Zuteilung Korrekturen" , Just $ CSheetR tid ssh csh shn SSubsR)
|
||||||
breadcrumb (CSheetR tid ssh csh shn SubmissionNewR) = return ("Abgabe", Just $ CSheetR tid ssh csh shn SShowR)
|
breadcrumb (CSheetR tid ssh csh shn SubmissionNewR) = return ("Abgabe", Just $ CSheetR tid ssh csh shn SShowR)
|
||||||
breadcrumb (CSheetR tid ssh csh shn SubmissionOwnR) = return ("Abgabe", Just $ CSheetR tid ssh csh shn SShowR)
|
breadcrumb (CSheetR tid ssh csh shn SubmissionOwnR) = return ("Abgabe", Just $ CSheetR tid ssh csh shn SShowR)
|
||||||
breadcrumb (CSubmissionR tid ssh csh shn _ SubShowR) = return ("Abgabe", Just $ CSheetR tid ssh csh shn SShowR)
|
breadcrumb (CSubmissionR tid ssh csh shn _ SubShowR) = return ("Abgabe", Just $ CSheetR tid ssh csh shn SShowR)
|
||||||
@ -1864,7 +1938,7 @@ pageActions (CourseR tid ssh csh CShowR) =
|
|||||||
E.&&. course E.^. CourseSchool E.==. E.val ssh
|
E.&&. course E.^. CourseSchool E.==. E.val ssh
|
||||||
E.&&. course E.^. CourseShorthand E.==. E.val csh
|
E.&&. course E.^. CourseShorthand E.==. E.val csh
|
||||||
return $ sheet E.^. SheetName
|
return $ sheet E.^. SheetName
|
||||||
anyM sheetNames (sheetAccess . E.unValue)
|
anyM sheetNames $ sheetAccess . E.unValue
|
||||||
in runDB $ lecturerAccess `or2M` existsVisible
|
in runDB $ lecturerAccess `or2M` existsVisible
|
||||||
}
|
}
|
||||||
] ++ pageActions (CourseR tid ssh csh SheetListR) ++
|
] ++ pageActions (CourseR tid ssh csh SheetListR) ++
|
||||||
@ -1877,7 +1951,26 @@ pageActions (CourseR tid ssh csh CShowR) =
|
|||||||
, menuItemAccessCallback' = return True
|
, menuItemAccessCallback' = return True
|
||||||
}
|
}
|
||||||
, MenuItem
|
, MenuItem
|
||||||
{ menuItemType = PageActionSecondary
|
{ menuItemType = PageActionPrime
|
||||||
|
, menuItemLabel = MsgMenuExamList
|
||||||
|
, menuItemIcon = Nothing
|
||||||
|
, menuItemRoute = SomeRoute $ CourseR tid ssh csh CExamListR
|
||||||
|
, menuItemModal = False
|
||||||
|
, menuItemAccessCallback' =
|
||||||
|
let lecturerAccess = hasWriteAccessTo $ CourseR tid ssh csh CExamNewR
|
||||||
|
examAccess examn = hasReadAccessTo $ CExamR tid ssh csh examn EShowR
|
||||||
|
existsVisible = do
|
||||||
|
examNames <- E.select . E.from $ \(course `E.InnerJoin` exam) -> do
|
||||||
|
E.on $ course E.^. CourseId E.==. exam E.^. ExamCourse
|
||||||
|
E.where_ $ course E.^. CourseTerm E.==. E.val tid
|
||||||
|
E.&&. course E.^. CourseSchool E.==. E.val ssh
|
||||||
|
E.&&. course E.^. CourseShorthand E.==. E.val csh
|
||||||
|
return $ exam E.^. ExamName
|
||||||
|
anyM examNames $ examAccess . E.unValue
|
||||||
|
in runDB $ lecturerAccess `or2M` existsVisible
|
||||||
|
}
|
||||||
|
, MenuItem
|
||||||
|
{ menuItemType = PageActionSecondary
|
||||||
, menuItemLabel = MsgMenuCourseMembers
|
, menuItemLabel = MsgMenuCourseMembers
|
||||||
, menuItemIcon = Just "user-graduate"
|
, menuItemIcon = Just "user-graduate"
|
||||||
, menuItemRoute = SomeRoute $ CourseR tid ssh csh CUsersR
|
, menuItemRoute = SomeRoute $ CourseR tid ssh csh CUsersR
|
||||||
@ -2080,6 +2173,26 @@ pageActions (CTutorialR tid ssh csh tutn TUsersR) =
|
|||||||
, menuItemAccessCallback' = return True
|
, menuItemAccessCallback' = return True
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
pageActions (CourseR tid ssh csh CExamListR) =
|
||||||
|
[ MenuItem
|
||||||
|
{ menuItemType = PageActionPrime
|
||||||
|
, menuItemLabel = MsgMenuExamNew
|
||||||
|
, menuItemIcon = Nothing
|
||||||
|
, menuItemRoute = SomeRoute $ CourseR tid ssh csh CExamNewR
|
||||||
|
, menuItemModal = False
|
||||||
|
, menuItemAccessCallback' = return True
|
||||||
|
}
|
||||||
|
]
|
||||||
|
pageActions (CExamR tid ssh csh examn EShowR) =
|
||||||
|
[ MenuItem
|
||||||
|
{ menuItemType = PageActionPrime
|
||||||
|
, menuItemLabel = MsgMenuExamEdit
|
||||||
|
, menuItemIcon = Nothing
|
||||||
|
, menuItemRoute = SomeRoute $ CExamR tid ssh csh examn EEditR
|
||||||
|
, menuItemModal = False
|
||||||
|
, menuItemAccessCallback' = return True
|
||||||
|
}
|
||||||
|
]
|
||||||
pageActions (CSheetR tid ssh csh shn SShowR) =
|
pageActions (CSheetR tid ssh csh shn SShowR) =
|
||||||
[ MenuItem
|
[ MenuItem
|
||||||
{ menuItemType = PageActionPrime
|
{ menuItemType = PageActionPrime
|
||||||
|
|||||||
@ -165,7 +165,7 @@ postAdminTestR = do
|
|||||||
|
|
||||||
-- | Make a form for adding a point/line/plane/hyperplane/... (in this case: cell)
|
-- | Make a form for adding a point/line/plane/hyperplane/... (in this case: cell)
|
||||||
--
|
--
|
||||||
-- This /needs/ to replace all occurences of @mreq@ with @mpreq@ (no fields should be /actually/ required)
|
-- This /needs/ to replace all occurrences of @mreq@ with @mpreq@ (no fields should be /actually/ required)
|
||||||
mkAddForm :: ListPosition -- ^ Approximate position of the add-widget
|
mkAddForm :: ListPosition -- ^ Approximate position of the add-widget
|
||||||
-> Natural -- ^ Dimension Index, outermost dimension ist 0 i.e. if dimension is 3 hyperplane-adders get passed 0, planes get passed 1, lines get 2, and points get 3
|
-> Natural -- ^ Dimension Index, outermost dimension ist 0 i.e. if dimension is 3 hyperplane-adders get passed 0, planes get passed 1, lines get 2, and points get 3
|
||||||
-> (Text -> Text) -- ^ Nudge deterministic field ids so they're unique
|
-> (Text -> Text) -- ^ Nudge deterministic field ids so they're unique
|
||||||
|
|||||||
@ -334,10 +334,16 @@ makeCorrectionsTable whereClause dbtColonnade dbtFilterUI psValidator dbtProj' d
|
|||||||
| otherwise -> corrector E.?. UserEmail `E.in_` E.justList (E.valList . catMaybes $ Set.toList emails)
|
| otherwise -> corrector E.?. UserEmail `E.in_` E.justList (E.valList . catMaybes $ Set.toList emails)
|
||||||
E.||. (if Nothing `Set.member` emails then E.isNothing (corrector E.?. UserEmail) else E.val False)
|
E.||. (if Nothing `Set.member` emails then E.isNothing (corrector E.?. UserEmail) else E.val False)
|
||||||
)
|
)
|
||||||
|
, ( "isassigned"
|
||||||
|
, FilterColumn $ \((_ `E.InnerJoin` _ `E.InnerJoin` submission) `E.LeftOuterJoin` _ :: CorrectionTableExpr) criterion -> case getLast (criterion :: Last Bool) of
|
||||||
|
Nothing -> E.val True :: E.SqlExpr (E.Value Bool)
|
||||||
|
Just True -> E.isJust $ submission E.^. SubmissionRatingBy
|
||||||
|
Just False-> E.isNothing $ submission E.^. SubmissionRatingBy
|
||||||
|
)
|
||||||
, ( "israted"
|
, ( "israted"
|
||||||
, FilterColumn $ \((_ `E.InnerJoin` _ `E.InnerJoin` submission) `E.LeftOuterJoin` _ :: CorrectionTableExpr) criterion -> case getLast (criterion :: Last Bool) of
|
, FilterColumn $ \((_ `E.InnerJoin` _ `E.InnerJoin` submission) `E.LeftOuterJoin` _ :: CorrectionTableExpr) criterion -> case getLast (criterion :: Last Bool) of
|
||||||
Nothing -> E.val True :: E.SqlExpr (E.Value Bool)
|
Nothing -> E.val True :: E.SqlExpr (E.Value Bool)
|
||||||
Just True -> E.not_ . E.isNothing $ submission E.^. SubmissionRatingTime
|
Just True -> E.isJust $ submission E.^. SubmissionRatingTime
|
||||||
Just False-> E.isNothing $ submission E.^. SubmissionRatingTime
|
Just False-> E.isNothing $ submission E.^. SubmissionRatingTime
|
||||||
)
|
)
|
||||||
, ( "corrector-name-email" -- corrector filter does not work for text-filtering
|
, ( "corrector-name-email" -- corrector filter does not work for text-filtering
|
||||||
@ -636,10 +642,11 @@ postCCorrectionsR tid ssh csh = do
|
|||||||
filterUI = Just $ \mPrev -> mconcat
|
filterUI = Just $ \mPrev -> mconcat
|
||||||
[ prismAForm (singletonFilter "user-name-email") mPrev $ aopt textField (fslI MsgCourseMembers)
|
[ prismAForm (singletonFilter "user-name-email") mPrev $ aopt textField (fslI MsgCourseMembers)
|
||||||
, prismAForm (singletonFilter "user-matriclenumber") mPrev $ aopt textField (fslI MsgMatrikelNr)
|
, prismAForm (singletonFilter "user-matriclenumber") mPrev $ aopt textField (fslI MsgMatrikelNr)
|
||||||
, prismAForm (singletonFilter "corrector-name-email") mPrev $ aopt textField (fslI MsgCorrector)
|
-- "pseudonym" TODO DB only stores Word24
|
||||||
-- "pseudonym" TODO DB only stores Word24
|
|
||||||
, Map.singleton "sheet-search" . maybeToList <$> aopt textField (fslI MsgSheet) (Just <$> listToMaybe =<< ((Map.lookup "sheet-search" =<< mPrev) <|> (Map.lookup "sheet" =<< mPrev)))
|
, Map.singleton "sheet-search" . maybeToList <$> aopt textField (fslI MsgSheet) (Just <$> listToMaybe =<< ((Map.lookup "sheet-search" =<< mPrev) <|> (Map.lookup "sheet" =<< mPrev)))
|
||||||
, prismAForm (singletonFilter "israted" . maybePrism _PathPiece) mPrev $ aopt boolField (fslI MsgRatingTime)
|
, prismAForm (singletonFilter "corrector-name-email") mPrev $ aopt textField (fslI MsgCorrector)
|
||||||
|
, prismAForm (singletonFilter "isassigned" . maybePrism _PathPiece) mPrev $ aopt boolField (fslI MsgHasCorrector)
|
||||||
|
, prismAForm (singletonFilter "israted" . maybePrism _PathPiece) mPrev $ aopt boolField (fslI MsgRatingTime)
|
||||||
]
|
]
|
||||||
psValidator = def & defaultPagesize PagesizeAll -- Assisstant always want to see them all at once anyway
|
psValidator = def & defaultPagesize PagesizeAll -- Assisstant always want to see them all at once anyway
|
||||||
correctionsR whereClause colonnade filterUI psValidator $ Map.fromList
|
correctionsR whereClause colonnade filterUI psValidator $ Map.fromList
|
||||||
@ -669,8 +676,9 @@ postSSubsR tid ssh csh shn = do
|
|||||||
[ prismAForm (singletonFilter "user-name-email") mPrev $ aopt textField (fslI MsgCourseMembers)
|
[ prismAForm (singletonFilter "user-name-email") mPrev $ aopt textField (fslI MsgCourseMembers)
|
||||||
, prismAForm (singletonFilter "user-matriclenumber") mPrev $ aopt textField (fslI MsgMatrikelNr)
|
, prismAForm (singletonFilter "user-matriclenumber") mPrev $ aopt textField (fslI MsgMatrikelNr)
|
||||||
, prismAForm (singletonFilter "corrector-name-email") mPrev $ aopt textField (fslI MsgCorrector)
|
, prismAForm (singletonFilter "corrector-name-email") mPrev $ aopt textField (fslI MsgCorrector)
|
||||||
-- "pseudonym" TODO DB only stores Word24
|
, prismAForm (singletonFilter "isassigned" . maybePrism _PathPiece) mPrev $ aopt boolField (fslI MsgHasCorrector)
|
||||||
, prismAForm (singletonFilter "israted" . maybePrism _PathPiece) mPrev $ aopt boolField (fslI MsgRatingTime)
|
, prismAForm (singletonFilter "israted" . maybePrism _PathPiece) mPrev $ aopt boolField (fslI MsgRatingTime)
|
||||||
|
-- "pseudonym" TODO DB only stores Word24
|
||||||
]
|
]
|
||||||
psValidator = def & defaultPagesize PagesizeAll -- Assisstant always want to see them all at once anyway
|
psValidator = def & defaultPagesize PagesizeAll -- Assisstant always want to see them all at once anyway
|
||||||
correctionsR whereClause colonnade filterUI psValidator $ Map.fromList
|
correctionsR whereClause colonnade filterUI psValidator $ Map.fromList
|
||||||
@ -702,12 +710,12 @@ postCorrectionR tid ssh csh shn cid = do
|
|||||||
results <- runDB $ correctionData tid ssh csh shn sub
|
results <- runDB $ correctionData tid ssh csh shn sub
|
||||||
|
|
||||||
case results of
|
case results of
|
||||||
[(Entity _ Course{..}, Entity _ Sheet{..}, Entity _ Submission{..}, corrector)] -> do
|
[(Entity _ Course{..}, Entity _ Sheet{..}, Entity _ subm@Submission{..}, corrector)] -> do
|
||||||
let ratingComment = fmap Text.strip submissionRatingComment >>= (\c -> c <$ guard (not $ null c))
|
let ratingComment = fmap Text.strip submissionRatingComment >>= (\c -> c <$ guard (not $ null c))
|
||||||
pointsForm = case sheetType of
|
pointsForm = case sheetType of
|
||||||
NotGraded -> pure Nothing
|
NotGraded -> pure Nothing
|
||||||
_otherwise -> aopt (pointsFieldMax $ preview (_grading . _maxPoints) sheetType)
|
_otherwise -> aopt (pointsFieldMax $ preview (_grading . _maxPoints) sheetType)
|
||||||
(fslpI MsgRatingPoints "Punktezahl")
|
(fslpI MsgRatingPoints "Punktezahl" & setTooltip sheetType)
|
||||||
(Just submissionRatingPoints)
|
(Just submissionRatingPoints)
|
||||||
|
|
||||||
((corrResult, corrForm'), corrEncoding) <- runFormPost . identifyForm FIDcorrection . renderAForm FormStandard $ (,,)
|
((corrResult, corrForm'), corrEncoding) <- runFormPost . identifyForm FIDcorrection . renderAForm FormStandard $ (,,)
|
||||||
@ -726,22 +734,20 @@ postCorrectionR tid ssh csh shn cid = do
|
|||||||
, formEncoding = uploadEncoding
|
, formEncoding = uploadEncoding
|
||||||
}
|
}
|
||||||
|
|
||||||
case corrResult of
|
formResult corrResult $ \(rated, ratingPoints', ratingComment') -> do
|
||||||
FormMissing -> return ()
|
uid <- liftHandlerT requireAuthId
|
||||||
FormFailure errs -> mapM_ (addMessage Error . toHtml) errs
|
now <- liftIO getCurrentTime
|
||||||
FormSuccess (rated, ratingPoints', ratingComment') -> do
|
|
||||||
uid <- liftHandlerT requireAuthId
|
|
||||||
now <- liftIO getCurrentTime
|
|
||||||
|
|
||||||
if
|
if
|
||||||
| errs <- validateRating sheetType Rating'
|
| errs <- validateRating sheetType Rating'
|
||||||
{ ratingPoints = ratingPoints'
|
{ ratingPoints = ratingPoints'
|
||||||
, ratingComment = ratingComment'
|
, ratingComment = ratingComment'
|
||||||
, ratingTime = (now <$ guard rated)
|
, ratingTime = (now <$ guard rated)
|
||||||
}
|
}
|
||||||
, not $ null errs
|
, not $ null errs
|
||||||
-> mapM_ (addMessageI Error) errs
|
-> mapM_ (addMessageI Error) errs
|
||||||
| otherwise -> runDBJobs $ do
|
| otherwise -> do
|
||||||
|
runDBJobs $ do
|
||||||
update sub [ SubmissionRatingBy =. Just uid
|
update sub [ SubmissionRatingBy =. Just uid
|
||||||
, SubmissionRatingTime =. (now <$ guard rated)
|
, SubmissionRatingTime =. (now <$ guard rated)
|
||||||
, SubmissionRatingPoints =. ratingPoints'
|
, SubmissionRatingPoints =. ratingPoints'
|
||||||
@ -753,25 +759,29 @@ postCorrectionR tid ssh csh shn cid = do
|
|||||||
when (rated && isNothing submissionRatingTime) $ do
|
when (rated && isNothing submissionRatingTime) $ do
|
||||||
$logDebugS "CorrectionR" [st|Rated #{tshow sub}|]
|
$logDebugS "CorrectionR" [st|Rated #{tshow sub}|]
|
||||||
queueDBJob . JobQueueNotification $ NotificationSubmissionRated sub
|
queueDBJob . JobQueueNotification $ NotificationSubmissionRated sub
|
||||||
|
|
||||||
redirect $ CSubmissionR tid ssh csh shn cid CorrectionR
|
|
||||||
|
|
||||||
case uploadResult of
|
|
||||||
FormMissing -> return ()
|
|
||||||
FormFailure errs -> mapM_ (addMessage Error . toHtml) errs
|
|
||||||
FormSuccess fileUploads -> do
|
|
||||||
uid <- requireAuthId
|
|
||||||
|
|
||||||
res <- msgSubmissionErrors . runDBJobs . runConduit $ transPipe (lift . lift) fileUploads .| extractRatingsMsg .| sinkSubmission uid (Right sub) True
|
|
||||||
case res of
|
|
||||||
Nothing -> return () -- ErrorMessages are already added by msgSubmissionErrors
|
|
||||||
(Just _) -> do
|
|
||||||
addMessageI Success MsgRatingFilesUpdated
|
|
||||||
redirect $ CSubmissionR tid ssh csh shn cid CorrectionR
|
redirect $ CSubmissionR tid ssh csh shn cid CorrectionR
|
||||||
|
|
||||||
|
formResult uploadResult $ \fileUploads -> do
|
||||||
|
uid <- requireAuthId
|
||||||
|
|
||||||
|
res <- msgSubmissionErrors . runDBJobs . runConduit $ transPipe (lift . lift) fileUploads .| extractRatingsMsg .| sinkSubmission uid (Right sub) True
|
||||||
|
case res of
|
||||||
|
Nothing -> return () -- ErrorMessages are already added by msgSubmissionErrors
|
||||||
|
(Just _) -> do
|
||||||
|
addMessageI Success MsgRatingFilesUpdated
|
||||||
|
redirect $ CSubmissionR tid ssh csh shn cid CorrectionR
|
||||||
|
|
||||||
mr <- getMessageRender
|
mr <- getMessageRender
|
||||||
let sheetTypeDesc = mr sheetType
|
let sheetTypeDesc = mr sheetType
|
||||||
defaultLayout $ do
|
heading = MsgCorrectionHead tid ssh csh shn cid
|
||||||
|
headingWgt = [whamlet|
|
||||||
|
$newline never
|
||||||
|
_{heading}
|
||||||
|
$if not (submissionRatingDone subm)
|
||||||
|
\ ^{isVisibleWidget False}
|
||||||
|
|]
|
||||||
|
siteLayout headingWgt $ do
|
||||||
|
setTitleI heading
|
||||||
let userCorrection = $(widgetFile "correction-user")
|
let userCorrection = $(widgetFile "correction-user")
|
||||||
$(widgetFile "correction")
|
$(widgetFile "correction")
|
||||||
_ -> notFound
|
_ -> notFound
|
||||||
@ -1042,11 +1052,8 @@ data SubAssignInfo = SubAssignInfo { saiName :: SheetName, saiSubmissionNr, saiC
|
|||||||
getCAssignR, postCAssignR :: TermId -> SchoolId -> CourseShorthand -> Handler Html
|
getCAssignR, postCAssignR :: TermId -> SchoolId -> CourseShorthand -> Handler Html
|
||||||
getCAssignR = postCAssignR
|
getCAssignR = postCAssignR
|
||||||
postCAssignR tid ssh csh = do
|
postCAssignR tid ssh csh = do
|
||||||
(shids,cid) <- runDB $ do
|
cid <- runDB $ getKeyBy404 $ TermSchoolCourseShort tid ssh csh
|
||||||
cid <- getKeyBy404 $ TermSchoolCourseShort tid ssh csh
|
assignHandler tid ssh csh cid []
|
||||||
shids <- selectKeysList [SheetCourse ==. cid] [Asc SheetActiveTo]
|
|
||||||
return (shids,cid)
|
|
||||||
assignHandler tid ssh csh cid shids
|
|
||||||
|
|
||||||
getSAssignR, postSAssignR :: TermId -> SchoolId -> CourseShorthand -> SheetName -> Handler Html
|
getSAssignR, postSAssignR :: TermId -> SchoolId -> CourseShorthand -> SheetName -> Handler Html
|
||||||
getSAssignR = postSAssignR
|
getSAssignR = postSAssignR
|
||||||
@ -1054,51 +1061,13 @@ postSAssignR tid ssh csh shn = do
|
|||||||
(shid,cid) <- runDB $ fetchSheetIdCourseId tid ssh csh shn
|
(shid,cid) <- runDB $ fetchSheetIdCourseId tid ssh csh shn
|
||||||
assignHandler tid ssh csh cid [shid]
|
assignHandler tid ssh csh cid [shid]
|
||||||
|
|
||||||
-- DEPRECATED assignHandler', delete me soonish
|
{- TODO: Feature:
|
||||||
assignHandler' :: TermId -> SchoolId -> CourseShorthand -> CourseId -> [SheetId] -> Handler Html
|
make distivt buttons for each sheet, so that users see which sheet will be assigned.
|
||||||
assignHandler' tid ssh csh _cid rawSids = do
|
Currently this information is available within the page heading!
|
||||||
-- gather data
|
|
||||||
openSubs <- runDB $ (\f -> foldM f Map.empty rawSids) $
|
|
||||||
\acc sid -> maybeT (return acc) $ do
|
|
||||||
Just Sheet{sheetName=saiName} <- lift $ get sid
|
|
||||||
guardM $ lift $ hasWriteAccessTo $ CSheetR tid ssh csh saiName SAssignR -- we must check, whether the submission is already closed and thus assignable
|
|
||||||
saiUnassignedNr <- lift $ count [SubmissionSheet ==. sid, SubmissionRatingBy ==. Nothing]
|
|
||||||
guard $ 0 < saiUnassignedNr -- only consider sheets with unassigned submissions
|
|
||||||
saiSubmissionNr <- lift $ count [SubmissionSheet ==. sid]
|
|
||||||
saiCorrectorNr <- lift $ count [SheetCorrectorSheet ==. sid, SheetCorrectorState ==. CorrectorNormal]
|
|
||||||
-- guard $ saiCorrectorNr > 0 -- COMMENTED OUT BECAUSE we should show sheets without possible correctors to inform the user about these problematic sheets
|
|
||||||
return $ Map.insert sid SubAssignInfo{..} acc
|
|
||||||
let sids = Map.keys openSubs
|
|
||||||
linkBack <- simpleLinkI (SomeMessage MsgGenericBack) <$> case sids of
|
|
||||||
[sid] -> do Sheet{sheetName} <- runDB $ getJust sid
|
|
||||||
return $ CSheetR tid ssh csh sheetName SSubsR
|
|
||||||
_ -> return $ CourseR tid ssh csh CCorrectionsR
|
|
||||||
-- process form
|
|
||||||
currentRoute <- getCurrentRoute
|
|
||||||
((btnResult, btnWdgt), btnEnctype) <- runFormPost $ identifyForm FIDAssignSubmissions buttonForm
|
|
||||||
assignmentStatus <- fmap (fromMaybe Map.empty) . formResultMaybe btnResult $ \BtnSubmissionsAssign ->
|
|
||||||
-- Assign submissions
|
|
||||||
fmap Just . runDB $ (\f -> foldM f Map.empty sids) $
|
|
||||||
\acc sid -> flip (Map.insert sid) acc <$> assignSubmissions sid Nothing
|
|
||||||
-- Too much important information for an alert message. Display proper info page instead
|
|
||||||
let btnForm = wrapForm btnWdgt def
|
|
||||||
{ formAction = SomeRoute <$> currentRoute
|
|
||||||
, formEncoding = btnEnctype
|
|
||||||
, formSubmit = FormNoSubmit
|
|
||||||
}
|
|
||||||
headingShort = MsgMenuCorrectionsAssign
|
|
||||||
headingLong = prependCourseTitle tid ssh csh MsgMenuCorrectionsAssign
|
|
||||||
siteLayoutMsg headingShort $ do
|
|
||||||
setTitleI headingLong
|
|
||||||
$(widgetFile "corrections-assign")
|
|
||||||
if null sids || not (null assignmentStatus)
|
|
||||||
then linkBack -- TODO: convenience link might be unnecessary: either via breadcrumb or closing model. Remove, if modal works fine. Otherwise change to PrimaryAction?
|
|
||||||
else btnForm
|
|
||||||
|
|
||||||
|
Stub:
|
||||||
{- TODO: make buttons for each sheet, so that users see which sheet is assigned
|
|
||||||
data ButtonCorrectionsAssign = BtnCorrectionsAssignAll | BtnCorrectionsAssignSheet SheetName
|
data ButtonCorrectionsAssign = BtnCorrectionsAssignAll | BtnCorrectionsAssignSheet SheetName
|
||||||
deriving (Enum, Eq, Ord, Bounded, Read, Show, Generic, Typeable)
|
deriving (Enum, Eq, Ord, Bounded, Read, Show, Generic, Typeable)
|
||||||
instance Button UniWorX ButtonCorrectionsAssign
|
instance Button UniWorX ButtonCorrectionsAssign
|
||||||
-- Are those needed any more?
|
-- Are those needed any more?
|
||||||
instance Universe ButtonCorrectionsAssign
|
instance Universe ButtonCorrectionsAssign
|
||||||
@ -1116,7 +1085,7 @@ assignHandler tid ssh csh cid assignSids = do
|
|||||||
(btnWdgt, btnResult) <- runButtonForm FIDAssignSubmissions
|
(btnWdgt, btnResult) <- runButtonForm FIDAssignSubmissions
|
||||||
|
|
||||||
-- gather data
|
-- gather data
|
||||||
(nrParticipants, groupsPossible, infoMap, correctorMap, assignment) <- runDB $ do
|
(assignSheetNames, nrParticipants, groupsPossible, infoMap, correctorMap, assignment) <- runDB $ do
|
||||||
-- cid <- getKeyBy404 $ TermSchoolCourseShort tid ssh csh
|
-- cid <- getKeyBy404 $ TermSchoolCourseShort tid ssh csh
|
||||||
nrParticipants <- count [CourseParticipantCourse ==. cid]
|
nrParticipants <- count [CourseParticipantCourse ==. cid]
|
||||||
|
|
||||||
@ -1127,32 +1096,40 @@ assignHandler tid ssh csh cid assignSids = do
|
|||||||
groupsPossible =
|
groupsPossible =
|
||||||
let foldFun (Entity _ Sheet{sheetGrouping=sgr}) acc = acc || sgr /= NoGroups
|
let foldFun (Entity _ Sheet{sheetGrouping=sgr}) acc = acc || sgr /= NoGroups
|
||||||
in List.foldr foldFun False sheetList
|
in List.foldr foldFun False sheetList
|
||||||
|
assignSheetNames = fmap sheetName $ mapMaybe (\sid -> Map.lookup sid sheets) assignSids
|
||||||
|
|
||||||
-- plan or assign unassigned submissions for given sheets
|
-- plan or assign unassigned submissions for given sheets
|
||||||
let buildA :: (Map SheetName ((Set SubmissionId, Set SubmissionId), Map (Maybe UserId) Int)) -> SheetId -> DB (Map SheetName ((Set SubmissionId, Set SubmissionId), Map (Maybe UserId) Int))
|
let buildA :: (Map SheetName ((Set SubmissionId, Set SubmissionId), Map (Maybe UserId) Int, Map UserId Rational)) -> SheetId -> DB (Map SheetName ((Set SubmissionId, Set SubmissionId), Map (Maybe UserId) Int, Map UserId Rational))
|
||||||
buildA acc sid = maybeT (return acc) $ do
|
buildA acc sid = maybeT (return acc) $ do
|
||||||
let shn = sheetName $ sheets ! sid
|
let shn = sheetName $ sheets ! sid
|
||||||
-- is sheet closed?
|
-- is sheet closed?
|
||||||
guardM $ lift $ hasWriteAccessTo $ CSheetR tid ssh csh shn SAssignR -- we must check, whether the submission is already closed and thus assignable
|
guardM $ lift $ hasWriteAccessTo $ CSheetR tid ssh csh shn SAssignR -- we must check, whether the submission is already closed and thus assignable
|
||||||
-- has at least one submisison?
|
-- ask for assignment plan
|
||||||
[E.Value hasSubmission] <- lift $ E.select $ return $ E.exists $ E.from $ \submission -> do
|
let ignoreExceptions :: AssignSubmissionException -> DB (Map SubmissionId (Maybe UserId), Map UserId Rational) -- silently ignore errors, since we check all sheets and only care about sheets with unassigned corrections
|
||||||
E.where_ $ submission E.^. SubmissionSheet E.==. E.val sid
|
ignoreExceptions NoCorrectors = return mempty
|
||||||
E.where_ $ E.isNothing $ submission E.^. SubmissionRatingBy
|
ignoreExceptions NoCorrectorsByProportion = return mempty
|
||||||
guard hasSubmission
|
ignoreExceptions (SubmissionsNotFound _sids_not_found) = return mempty -- cannot happen, since last argument to planSubmissions is Nothing
|
||||||
-- has at least one active corrector?
|
(plan,deficit) <- lift $ handle ignoreExceptions $ planSubmissions sid Nothing
|
||||||
[E.Value hasCorrector] <- lift $ E.select $ return $ E.exists $ E.from $ \corrector -> do
|
guard $ not $ null plan -- only proceed if there is a plan for this sheet
|
||||||
E.where_ $ corrector E.^. SheetCorrectorSheet E.==. E.val sid
|
-- implement assignment plan
|
||||||
E.where_ $ corrector E.^. SheetCorrectorState E.==. E.val CorrectorNormal
|
|
||||||
-- E.where_ $ corrector E.^. SheetCorrectorLoad E./=. E.val (Load {byTutorial = Nothing, byProportion = 0})
|
|
||||||
guard hasCorrector
|
|
||||||
-- TODO: Refactor guards above! We already have these informations, but forcing the maps inside the DB acces might not be a good idea
|
|
||||||
-- TODO: Maybe refactor planSubmissions instead to not throw exceptions, but signal "ok" or "not possible" instead!
|
|
||||||
plan <- lift $ planSubmissions sid Nothing
|
|
||||||
status <- lift $ case btnResult of
|
status <- lift $ case btnResult of
|
||||||
Nothing -> return (Set.empty, Set.empty)
|
Nothing -> return (Set.empty, Set.empty)
|
||||||
(Just BtnSubmissionsAssign) -> writeSubmissionPlan plan -- TODO: this comes to late!!
|
(Just BtnSubmissionsAssign) -> do
|
||||||
return $ Map.insert shn (status, countMapElems plan) acc
|
status@(sub_ok,sub_fail) <- writeSubmissionPlan plan
|
||||||
assignment <- foldM buildA Map.empty assignSids
|
let nr_ok = olength sub_ok
|
||||||
|
nr_fail = olength sub_fail
|
||||||
|
alert_ok = toMaybe (nr_ok > 0) $ SomeMessage $ MsgUpdatedSheetCorrectorsAutoAssigned nr_ok
|
||||||
|
alert_fail = toMaybe (nr_fail > 0) $ SomeMessage $ MsgUpdatedSheetCorrectorsAutoFailed nr_fail
|
||||||
|
msg_status = bool Success Error $ nr_fail > 0
|
||||||
|
msg_header = SomeMessage $ shn <> ":"
|
||||||
|
when (nr_ok > 0 || nr_fail > 0) $
|
||||||
|
addMessageI msg_status $ UniWorXMessages $ msg_header : catMaybes [alert_ok, alert_fail]
|
||||||
|
return status
|
||||||
|
return $ Map.insert shn (status, countMapElems plan, deficit) acc
|
||||||
|
assignSids' <- if null assignSids -- assignAll; we distinguish assignSids' here avoid useless Alerts
|
||||||
|
then selectKeysList [SheetCourse ==. cid] [Asc SheetActiveTo]
|
||||||
|
else return assignSids
|
||||||
|
assignment <- foldM buildA Map.empty assignSids'
|
||||||
|
|
||||||
correctors <- E.select . E.from $ \(corrector `E.InnerJoin` user) -> do
|
correctors <- E.select . E.from $ \(corrector `E.InnerJoin` user) -> do
|
||||||
E.on $ corrector E.^. SheetCorrectorUser E.==. user E.^. UserId
|
E.on $ corrector E.^. SheetCorrectorUser E.==. user E.^. UserId
|
||||||
@ -1164,6 +1141,12 @@ assignHandler tid ssh csh cid assignSids = do
|
|||||||
in Map.insertWith (\(usr, ma) (_, mb) -> (usr, Map.union ma mb)) uid (user, Map.singleton shn sheetcorr) acc
|
in Map.insertWith (\(usr, ma) (_, mb) -> (usr, Map.union ma mb)) uid (user, Map.singleton shn sheetcorr) acc
|
||||||
)
|
)
|
||||||
|
|
||||||
|
-- -- lecturerNames :: Map UserId User
|
||||||
|
-- lecturerNames <- fmap entities2map $ E.select $ E.from $ \(user `E.InnerJoin` lecturer) -> do
|
||||||
|
-- E.on $ user E.^. UserId E.==. lecturer E.^. LecturerUser
|
||||||
|
-- E.where_ $ lecturer E.^. LecturerCourse E.==. E.val cid
|
||||||
|
-- return user
|
||||||
|
|
||||||
submissions <- E.select . E.from $ \submission -> do
|
submissions <- E.select . E.from $ \submission -> do
|
||||||
E.where_ $ submission E.^. SubmissionSheet `E.in_` E.valList sheetIds
|
E.where_ $ submission E.^. SubmissionSheet `E.in_` E.valList sheetIds
|
||||||
let numSubmittors = E.sub_select . E.from $ \subUser -> do
|
let numSubmittors = E.sub_select . E.from $ \subUser -> do
|
||||||
@ -1196,34 +1179,69 @@ assignHandler tid ssh csh cid assignSids = do
|
|||||||
}
|
}
|
||||||
in Map.insertWith (Map.unionWith (<>)) shnm cinf m
|
in Map.insertWith (Map.unionWith (<>)) shnm cinf m
|
||||||
|
|
||||||
return (nrParticipants, groupsPossible, infoMap, correctorMap, assignment)
|
return (assignSheetNames, nrParticipants, groupsPossible, infoMap, correctorMap, assignment)
|
||||||
|
|
||||||
let -- infoMap :: Map SheetName (Map (Maybe UserId) CorrectionInfo) -- repeated here for easier reference
|
let -- infoMap :: Map SheetName (Map (Maybe UserId) CorrectionInfo) -- repeated here for easier reference
|
||||||
-- create aggregate maps
|
-- create aggregate maps
|
||||||
|
|
||||||
|
-- Always iterate over sheetList for consistent sorting!
|
||||||
|
sheetList :: [(SheetName, CorrectionInfo)]
|
||||||
|
sheetList = Map.toDescList sheetMap -- newest Sheet first, except for CorrectionSheetTable
|
||||||
|
|
||||||
sheetMap :: Map SheetName CorrectionInfo
|
sheetMap :: Map SheetName CorrectionInfo
|
||||||
sheetMap = Map.map fold infoMap
|
sheetMap = Map.map fold infoMap
|
||||||
|
|
||||||
|
sheetLoad :: Map SheetName Load
|
||||||
|
sheetLoad = -- Map.unionsWith (<>) ((Map.filter () ) . snd) <$> Map.elems correctorMap)
|
||||||
|
let buildSL acc (_user,mSnSc) = Map.foldlWithKey buildL acc mSnSc
|
||||||
|
buildL acc s SheetCorrector{sheetCorrectorLoad=l, sheetCorrectorState=CorrectorNormal}
|
||||||
|
= Map.insertWith (<>) s l acc
|
||||||
|
buildL acc _ _ = acc
|
||||||
|
in Map.foldl buildSL Map.empty correctorMap
|
||||||
|
|
||||||
|
deficitMap :: Map UserId Rational
|
||||||
|
deficitMap = foldMap (view _3) assignment
|
||||||
|
|
||||||
corrMap :: Map (Maybe UserId) CorrectionInfo
|
corrMap :: Map (Maybe UserId) CorrectionInfo
|
||||||
corrMap = Map.unionsWith (<>) $ Map.elems infoMap
|
corrMap = Map.unionsWith (<>) $ Map.elems infoMap
|
||||||
sheetNames = Map.keys infoMap
|
|
||||||
|
corrInfos :: [CorrectionInfo]
|
||||||
|
corrInfos = sortBy (compare `on` (byName . ciCorrector) ) $ Map.elems corrMap
|
||||||
|
where byName Nothing = Nothing
|
||||||
|
byName (Just uid) = Map.lookup uid correctorMap
|
||||||
|
corrMapSum :: CorrectionInfo
|
||||||
|
corrMapSum = fold corrMap
|
||||||
|
|
||||||
let -- whamlet convenience functions
|
let -- whamlet convenience functions
|
||||||
-- avoid nestes hamelt $maybe with duplicated $nothing
|
-- avoid nestes hamlet $maybe with duplicated $nothing
|
||||||
getCorrector :: Maybe UserId -> (Widget,Map SheetName SheetCorrector)
|
getCorrector :: Maybe UserId -> (Widget,Map SheetName SheetCorrector)
|
||||||
getCorrector (Just uid)
|
getCorrector (Just uid)
|
||||||
| Just (User{..},loadMap) <- Map.lookup uid correctorMap
|
| Just (User{..},loadMap) <- Map.lookup uid correctorMap
|
||||||
= (nameEmailWidget userEmail userDisplayName userSurname, loadMap)
|
= (nameEmailWidget userEmail userDisplayName userSurname, loadMap)
|
||||||
|
-- | Just (User{..} ) <- Map.lookup uid lecturerNames
|
||||||
|
-- = (nameEmailWidget userEmail userDisplayName userSurname, mempty) -- lecturers may also correct in rare cases
|
||||||
getCorrector _ = ([whamlet|_{MsgNoCorrectorAssigned}|], mempty)
|
getCorrector _ = ([whamlet|_{MsgNoCorrectorAssigned}|], mempty)
|
||||||
-- avoid nestes hamelt $maybe with duplicated $nothing
|
-- avoid nestes hamlet $maybe with duplicated $nothing
|
||||||
getCorrSheetStatus :: Maybe UserId -> SheetName -> Maybe CorrectionInfo
|
getCorrSheetStatus :: Maybe UserId -> SheetName -> Maybe CorrectionInfo
|
||||||
getCorrSheetStatus corr shn
|
getCorrSheetStatus corr shn
|
||||||
| (Just smap) <- Map.lookup shn infoMap
|
| (Just smap) <- Map.lookup shn infoMap
|
||||||
= Map.lookup corr smap
|
= Map.lookup corr smap
|
||||||
getCorrSheetStatus _ _ = Nothing
|
getCorrSheetStatus _ _ = Nothing
|
||||||
-- avoid nestes hamelt $maybe with duplicated $nothing
|
-- avoid nestes hamlet $maybe with duplicated $nothing
|
||||||
getCorrNewAssignment :: Maybe UserId -> SheetName -> Maybe Int
|
getCorrNewAssignment :: Maybe UserId -> SheetName -> Maybe Int
|
||||||
getCorrNewAssignment corr shn
|
getCorrNewAssignment corr shn
|
||||||
| (Just (_,cass)) <- Map.lookup shn assignment
|
| (Just (_,cass,_)) <- Map.lookup shn assignment
|
||||||
= Map.lookup corr cass
|
= Map.lookup corr cass
|
||||||
getCorrNewAssignment _ _ = Nothing
|
getCorrNewAssignment _ _ = Nothing
|
||||||
|
-- avoid nestes hamlet $maybe with duplicated $nothing
|
||||||
|
getCorrDeficit :: Maybe UserId -> Maybe Rational
|
||||||
|
getCorrDeficit (Just uid) = Map.lookup uid deficitMap
|
||||||
|
getCorrDeficit _ = Nothing
|
||||||
|
|
||||||
|
getLoadSum :: SheetName -> Text
|
||||||
|
getLoadSum shn | (Just load) <- Map.lookup shn sheetLoad
|
||||||
|
= showCompactCorrectorLoad load CorrectorNormal
|
||||||
|
getLoadSum _ = mempty
|
||||||
|
|
||||||
showDiffDays :: Maybe NominalDiffTime -> Text
|
showDiffDays :: Maybe NominalDiffTime -> Text
|
||||||
showDiffDays = foldMap formatDiffDays
|
showDiffDays = foldMap formatDiffDays
|
||||||
@ -1231,11 +1249,17 @@ assignHandler tid ssh csh cid assignSids = do
|
|||||||
showAvgsDays Nothing _ = mempty
|
showAvgsDays Nothing _ = mempty
|
||||||
showAvgsDays (Just dt) n = formatDiffDays $ dt / fromIntegral n
|
showAvgsDays (Just dt) n = formatDiffDays $ dt / fromIntegral n
|
||||||
heat :: Integer -> Integer -> Double
|
heat :: Integer -> Integer -> Double
|
||||||
heat full achieved = roundToDigits 3 $ cutOffPercent 0.4 (fromIntegral full) (fromIntegral achieved)
|
heat = heat' 0.3
|
||||||
|
heat' :: Double -> Integer -> Integer -> Double
|
||||||
|
heat' cut full achieved = roundToDigits 3 $ cutOffPercent cut (fromIntegral full^2) (fromIntegral achieved^2)
|
||||||
let headingShort
|
let headingShort
|
||||||
| 0 < Map.size assignment = MsgMenuCorrectionsAssignSheet $ Text.intercalate ", " $ fmap CI.original $ Map.keys assignment
|
| 0 < Map.size assignment = MsgMenuCorrectionsAssignSheet $ Text.intercalate ", " $ fmap CI.original $ Map.keys assignment
|
||||||
| otherwise = MsgMenuCorrectionsAssign
|
| otherwise = MsgMenuCorrectionsAssign
|
||||||
headingLong = prependCourseTitle tid ssh csh MsgMenuCorrectionsAssign
|
headingLong = prependCourseTitle tid ssh csh MsgMenuCorrectionsAssign
|
||||||
|
|
||||||
|
unassignableSheets = filter (\shn -> Map.notMember shn assignment) assignSheetNames
|
||||||
|
unless (null unassignableSheets) $ addMessageI Warning $ MsgSheetsUnassignable $ Text.intercalate ", " $ fmap CI.original unassignableSheets
|
||||||
|
|
||||||
siteLayoutMsg headingShort $ do
|
siteLayoutMsg headingShort $ do
|
||||||
setTitleI headingLong
|
setTitleI headingLong
|
||||||
$(widgetFile "corrections-overview")
|
$(widgetFile "corrections-overview")
|
||||||
|
|||||||
@ -360,7 +360,7 @@ getCShowR tid ssh csh = do
|
|||||||
^{nameEmailWidget' tutor}
|
^{nameEmailWidget' tutor}
|
||||||
|]
|
|]
|
||||||
, 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{..} } -> occurencesCell tutorialTime
|
, sortable Nothing (i18nCell MsgTutorialTime) $ \DBRow{ dbrOutput = Entity _ Tutorial{..} } -> occurrencesCell tutorialTime
|
||||||
, sortable (Just "register-from") (i18nCell MsgTutorialRegisterFrom) $ \DBRow{ dbrOutput = Entity _ Tutorial{..} } -> maybeDateTimeCell tutorialRegisterFrom
|
, sortable (Just "register-from") (i18nCell MsgTutorialRegisterFrom) $ \DBRow{ dbrOutput = Entity _ Tutorial{..} } -> maybeDateTimeCell tutorialRegisterFrom
|
||||||
, sortable (Just "register-to") (i18nCell MsgTutorialRegisterTo) $ \DBRow{ dbrOutput = Entity _ Tutorial{..} } -> maybeDateTimeCell tutorialRegisterTo
|
, sortable (Just "register-to") (i18nCell MsgTutorialRegisterTo) $ \DBRow{ dbrOutput = Entity _ Tutorial{..} } -> maybeDateTimeCell tutorialRegisterTo
|
||||||
, sortable (Just "deregister-until") (i18nCell MsgTutorialDeregisterUntil) $ \DBRow{ dbrOutput = Entity _ Tutorial{..} } -> maybeDateTimeCell tutorialDeregisterUntil
|
, sortable (Just "deregister-until") (i18nCell MsgTutorialDeregisterUntil) $ \DBRow{ dbrOutput = Entity _ Tutorial{..} } -> maybeDateTimeCell tutorialDeregisterUntil
|
||||||
@ -407,6 +407,63 @@ getCShowR tid ssh csh = do
|
|||||||
& defaultSorting [SortAscBy "type", SortAscBy "name"]
|
& defaultSorting [SortAscBy "type", SortAscBy "name"]
|
||||||
(Any hasTutorials, tutorialTable) <- runDB $ dbTable tutorialDBTableValidator tutorialDBTable
|
(Any hasTutorials, tutorialTable) <- runDB $ dbTable tutorialDBTableValidator tutorialDBTable
|
||||||
|
|
||||||
|
let
|
||||||
|
examDBTable = DBTable{..}
|
||||||
|
where
|
||||||
|
dbtSQLQuery exam = do
|
||||||
|
E.where_ $ exam E.^. ExamCourse E.==. E.val cid
|
||||||
|
return exam
|
||||||
|
dbtRowKey = (E.^. ExamId)
|
||||||
|
dbtProj r@DBRow{ dbrOutput = Entity _ Exam{..} } = do
|
||||||
|
guardM . hasReadAccessTo $ CExamR tid ssh csh examName EShowR
|
||||||
|
return r
|
||||||
|
dbtColonnade = dbColonnade $ mconcat
|
||||||
|
[ sortable (Just "name") (i18nCell MsgExamName) $ \DBRow{ dbrOutput = Entity _ Exam{..} } -> indicatorCell <> anchorCell (CExamR tid ssh csh examName EShowR) (toWidget examName)
|
||||||
|
, 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 "time") (i18nCell MsgExamTime) $ \DBRow{ dbrOutput = Entity _ Exam{..} } -> cell $ do
|
||||||
|
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'}
|
||||||
|
|]
|
||||||
|
, sortable Nothing mempty $ \DBRow{ dbrOutput = Entity eId Exam{..} } -> sqlCell $ do
|
||||||
|
mayRegister <- (== Authorized) <$> evalAccessDB (CExamR tid ssh csh examName ERegisterR) True
|
||||||
|
isRegistered <- case mbAid of
|
||||||
|
Nothing -> return False
|
||||||
|
Just uid -> existsBy $ UniqueExamRegistration eId uid
|
||||||
|
if
|
||||||
|
| mayRegister -> do
|
||||||
|
(examRegisterForm, examRegisterEnctype) <- liftHandlerT . generateFormPost . buttonForm' $ bool [BtnRegister] [BtnDeregister] 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
|
||||||
|
[ ("name", SortColumn $ \exam -> exam E.^. ExamName )
|
||||||
|
, ("time", SortColumn $ \exam -> exam E.^. ExamStart )
|
||||||
|
, ("register-from", SortColumn $ \exam -> exam E.^. ExamRegisterFrom )
|
||||||
|
, ("register-to", SortColumn $ \exam -> exam E.^. ExamRegisterTo )
|
||||||
|
, ("visible", SortColumn $ \exam -> exam E.^. ExamVisibleFrom )
|
||||||
|
]
|
||||||
|
dbtFilter = Map.empty
|
||||||
|
dbtFilterUI = const mempty
|
||||||
|
dbtStyle = def
|
||||||
|
dbtParams = def
|
||||||
|
dbtIdent :: Text
|
||||||
|
dbtIdent = "exams"
|
||||||
|
|
||||||
|
examDBTableValidator = def
|
||||||
|
& defaultSorting [SortAscBy "time"]
|
||||||
|
(Any hasExams, examTable) <- runDB $ dbTable examDBTableValidator examDBTable
|
||||||
|
|
||||||
siteLayout (toWgt $ courseName course) $ do
|
siteLayout (toWgt $ courseName course) $ do
|
||||||
setTitleI $ prependCourseTitle tid ssh csh (""::Text)
|
setTitleI $ prependCourseTitle tid ssh csh (""::Text)
|
||||||
$(widgetFile "course")
|
$(widgetFile "course")
|
||||||
|
|||||||
764
src/Handler/Exam.hs
Normal file
764
src/Handler/Exam.hs
Normal file
@ -0,0 +1,764 @@
|
|||||||
|
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
||||||
|
|
||||||
|
module Handler.Exam where
|
||||||
|
|
||||||
|
import Import
|
||||||
|
|
||||||
|
import Handler.Utils
|
||||||
|
import Handler.Utils.Exam
|
||||||
|
import Handler.Utils.Invitations
|
||||||
|
import Handler.Utils.Table.Cells
|
||||||
|
import Jobs.Queue
|
||||||
|
|
||||||
|
import Utils.Lens hiding (parts)
|
||||||
|
|
||||||
|
import qualified Database.Esqueleto as E
|
||||||
|
import qualified Database.Esqueleto.Utils as E
|
||||||
|
|
||||||
|
import Data.Map ((!), (!?))
|
||||||
|
import qualified Data.Map as Map
|
||||||
|
import qualified Data.Set as Set
|
||||||
|
|
||||||
|
import Data.Aeson hiding (Result(..))
|
||||||
|
import Text.Hamlet (ihamlet)
|
||||||
|
import Text.Blaze.Html.Renderer.String (renderHtml)
|
||||||
|
|
||||||
|
import qualified Data.CaseInsensitive as CI
|
||||||
|
|
||||||
|
import qualified Control.Monad.State.Class as State
|
||||||
|
|
||||||
|
|
||||||
|
getCExamListR :: TermId -> SchoolId -> CourseShorthand -> Handler Html
|
||||||
|
getCExamListR tid ssh csh = do
|
||||||
|
Entity cid Course{..} <- runDB . getBy404 $ TermSchoolCourseShort tid ssh csh
|
||||||
|
now <- liftIO getCurrentTime
|
||||||
|
mayCreate <- hasWriteAccessTo $ CourseR tid ssh csh CExamNewR
|
||||||
|
|
||||||
|
let
|
||||||
|
examDBTable = DBTable{..}
|
||||||
|
where
|
||||||
|
dbtSQLQuery exam = do
|
||||||
|
E.where_ $ exam E.^. ExamCourse E.==. E.val cid
|
||||||
|
return exam
|
||||||
|
dbtRowKey = (E.^. ExamId)
|
||||||
|
dbtProj x@DBRow{ dbrOutput = Entity _ Exam{..} } = do
|
||||||
|
guardM . hasReadAccessTo $ CExamR tid ssh csh examName EShowR
|
||||||
|
return x
|
||||||
|
dbtColonnade = dbColonnade . mconcat $ catMaybes
|
||||||
|
[ Just . sortable (Just "name") (i18nCell MsgExamName) $ \DBRow{ dbrOutput = Entity _ Exam{..} } -> anchorCell (CExamR tid ssh csh examName EShowR) $ toWidget examName
|
||||||
|
, (<$ 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-to") (i18nCell MsgExamRegisterTo) $ \DBRow { dbrOutput = Entity _ Exam{..} } -> maybe mempty dateTimeCell examRegisterTo
|
||||||
|
, Just . sortable (Just "time") (i18nCell MsgExamTime) $ \DBRow{ dbrOutput = Entity _ Exam{..} } -> cell $ do
|
||||||
|
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
|
||||||
|
[ ("name", SortColumn $ \exam -> exam E.^. ExamName )
|
||||||
|
, ("time", SortColumn $ \exam -> exam E.^. ExamStart )
|
||||||
|
, ("register-from", SortColumn $ \exam -> exam E.^. ExamRegisterFrom )
|
||||||
|
, ("register-to", SortColumn $ \exam -> exam E.^. ExamRegisterTo )
|
||||||
|
, ("visible", SortColumn $ \exam -> exam E.^. ExamVisibleFrom )
|
||||||
|
]
|
||||||
|
dbtFilter = Map.empty
|
||||||
|
dbtFilterUI = const mempty
|
||||||
|
dbtStyle = def
|
||||||
|
dbtParams = def
|
||||||
|
dbtIdent :: Text
|
||||||
|
dbtIdent = "exams"
|
||||||
|
|
||||||
|
examDBTableValidator = def
|
||||||
|
& defaultSorting [SortAscBy "time"]
|
||||||
|
((), examTable) <- runDB $ dbTable examDBTableValidator examDBTable
|
||||||
|
|
||||||
|
siteLayoutMsg (prependCourseTitle tid ssh csh MsgExamsHeading) $ do
|
||||||
|
setTitleI $ prependCourseTitle tid ssh csh MsgExamsHeading
|
||||||
|
$(widgetFile "exam-list")
|
||||||
|
|
||||||
|
|
||||||
|
instance IsInvitableJunction ExamCorrector where
|
||||||
|
type InvitationFor ExamCorrector = Exam
|
||||||
|
data InvitableJunction ExamCorrector = JunctionExamCorrector
|
||||||
|
deriving (Eq, Ord, Read, Show, Generic, Typeable)
|
||||||
|
data InvitationDBData ExamCorrector = InvDBDataExamCorrector
|
||||||
|
deriving (Eq, Ord, Read, Show, Generic, Typeable)
|
||||||
|
data InvitationTokenData ExamCorrector = InvTokenDataExamCorrector
|
||||||
|
deriving (Eq, Ord, Read, Show, Generic, Typeable)
|
||||||
|
|
||||||
|
_InvitableJunction = iso
|
||||||
|
(\ExamCorrector{..} -> (examCorrectorUser, examCorrectorExam, JunctionExamCorrector))
|
||||||
|
(\(examCorrectorUser, examCorrectorExam, JunctionExamCorrector) -> ExamCorrector{..})
|
||||||
|
|
||||||
|
instance ToJSON (InvitableJunction ExamCorrector) where
|
||||||
|
toJSON = genericToJSON defaultOptions { fieldLabelModifier = camelToPathPiece' 1 }
|
||||||
|
toEncoding = genericToEncoding defaultOptions { fieldLabelModifier = camelToPathPiece' 1 }
|
||||||
|
instance FromJSON (InvitableJunction ExamCorrector) where
|
||||||
|
parseJSON = genericParseJSON defaultOptions { fieldLabelModifier = camelToPathPiece' 1 }
|
||||||
|
|
||||||
|
instance ToJSON (InvitationDBData ExamCorrector) where
|
||||||
|
toJSON = genericToJSON defaultOptions { fieldLabelModifier = camelToPathPiece' 4 }
|
||||||
|
toEncoding = genericToEncoding defaultOptions { fieldLabelModifier = camelToPathPiece' 4 }
|
||||||
|
instance FromJSON (InvitationDBData ExamCorrector) where
|
||||||
|
parseJSON = genericParseJSON defaultOptions { fieldLabelModifier = camelToPathPiece' 4 }
|
||||||
|
|
||||||
|
instance ToJSON (InvitationTokenData ExamCorrector) where
|
||||||
|
toJSON = genericToJSON defaultOptions { constructorTagModifier = camelToPathPiece' 4 }
|
||||||
|
toEncoding = genericToEncoding defaultOptions { constructorTagModifier = camelToPathPiece' 4 }
|
||||||
|
instance FromJSON (InvitationTokenData ExamCorrector) where
|
||||||
|
parseJSON = genericParseJSON defaultOptions { constructorTagModifier = camelToPathPiece' 4 }
|
||||||
|
|
||||||
|
examCorrectorInvitationConfig :: InvitationConfig ExamCorrector
|
||||||
|
examCorrectorInvitationConfig = InvitationConfig{..}
|
||||||
|
where
|
||||||
|
invitationRoute (Entity _ Exam{..}) _ = do
|
||||||
|
Course{..} <- get404 examCourse
|
||||||
|
return $ CExamR courseTerm courseSchool courseShorthand examName ECInviteR
|
||||||
|
invitationResolveFor = do
|
||||||
|
Just (CExamR tid csh ssh examn ECInviteR) <- getCurrentRoute
|
||||||
|
fetchExamId tid csh ssh examn
|
||||||
|
invitationSubject Exam{..} _ = do
|
||||||
|
Course{..} <- get404 examCourse
|
||||||
|
return . SomeMessage $ MsgMailSubjectExamCorrectorInvitation courseTerm courseSchool courseShorthand examName
|
||||||
|
invitationHeading Exam{..} _ = return . SomeMessage $ MsgExamCorrectorInviteHeading examName
|
||||||
|
invitationExplanation _ _ = [ihamlet|_{SomeMessage MsgExamCorrectorInviteExplanation}|]
|
||||||
|
invitationTokenConfig _ _ = do
|
||||||
|
itAuthority <- liftHandlerT requireAuthId
|
||||||
|
return $ InvitationTokenConfig itAuthority Nothing Nothing Nothing
|
||||||
|
invitationRestriction _ _ = return Authorized
|
||||||
|
invitationForm _ _ _ = pure JunctionExamCorrector
|
||||||
|
invitationSuccessMsg Exam{..} _ = return . SomeMessage $ MsgExamCorrectorInvitationAccepted examName
|
||||||
|
invitationUltDest Exam{..} _ = do
|
||||||
|
Course{..} <- get404 examCourse
|
||||||
|
return . SomeRoute $ CourseR courseTerm courseSchool courseShorthand CExamListR
|
||||||
|
|
||||||
|
getECInviteR, postECInviteR :: TermId -> SchoolId -> CourseShorthand -> ExamName -> Handler Html
|
||||||
|
getECInviteR = postECInviteR
|
||||||
|
postECInviteR = invitationR examCorrectorInvitationConfig
|
||||||
|
|
||||||
|
|
||||||
|
data ExamForm = ExamForm
|
||||||
|
{ efName :: ExamName
|
||||||
|
, efDescription :: Maybe Html
|
||||||
|
, efStart :: UTCTime
|
||||||
|
, efEnd :: Maybe UTCTime
|
||||||
|
, efVisibleFrom :: Maybe UTCTime
|
||||||
|
, efRegisterFrom :: Maybe UTCTime
|
||||||
|
, efRegisterTo :: Maybe UTCTime
|
||||||
|
, efDeregisterUntil :: Maybe UTCTime
|
||||||
|
, efPublishOccurrenceAssignments :: UTCTime
|
||||||
|
, efFinished :: Maybe UTCTime
|
||||||
|
, efClosed :: Maybe UTCTime
|
||||||
|
, efOccurrences :: Set ExamOccurrenceForm
|
||||||
|
, efShowGrades :: Bool
|
||||||
|
, efPublicStatistics :: Bool
|
||||||
|
, efGradingRule :: ExamGradingRule
|
||||||
|
, efBonusRule :: ExamBonusRule
|
||||||
|
, efOccurrenceRule :: ExamOccurrenceRule
|
||||||
|
, efCorrectors :: Set (Either UserEmail UserId)
|
||||||
|
, efExamParts :: Set ExamPartForm
|
||||||
|
}
|
||||||
|
|
||||||
|
data ExamOccurrenceForm = ExamOccurrenceForm
|
||||||
|
{ eofId :: Maybe CryptoUUIDExamOccurrence
|
||||||
|
, eofRoom :: Text
|
||||||
|
, eofCapacity :: Natural
|
||||||
|
, eofStart :: UTCTime
|
||||||
|
, eofEnd :: Maybe UTCTime
|
||||||
|
, eofDescription :: Maybe Html
|
||||||
|
} deriving (Read, Show, Eq, Ord, Generic, Typeable)
|
||||||
|
|
||||||
|
data ExamPartForm = ExamPartForm
|
||||||
|
{ epfId :: Maybe CryptoUUIDExamPart
|
||||||
|
, epfName :: ExamPartName
|
||||||
|
, epfMaxPoints :: Maybe Points
|
||||||
|
, epfWeight :: Rational
|
||||||
|
} deriving (Read, Show, Eq, Ord, Generic, Typeable)
|
||||||
|
|
||||||
|
deriveJSON defaultOptions
|
||||||
|
{ fieldLabelModifier = camelToPathPiece' 1
|
||||||
|
} ''ExamPartForm
|
||||||
|
|
||||||
|
deriveJSON defaultOptions
|
||||||
|
{ fieldLabelModifier = camelToPathPiece' 1
|
||||||
|
} ''ExamOccurrenceForm
|
||||||
|
|
||||||
|
|
||||||
|
examForm :: Maybe ExamForm -> Form ExamForm
|
||||||
|
examForm template html = do
|
||||||
|
MsgRenderer mr <- getMsgRenderer
|
||||||
|
|
||||||
|
flip (renderAForm FormStandard) html $ ExamForm
|
||||||
|
<$> areq ciField (fslpI MsgExamName (mr MsgExamName) & setTooltip MsgExamNameTip) (efName <$> template)
|
||||||
|
<*> (assertM (not . null . renderHtml) <$> aopt htmlField (fslpI MsgExamDescription "Html") (efDescription <$> template))
|
||||||
|
<* aformSection MsgExamFormTimes
|
||||||
|
<*> areq utcTimeField (fslpI MsgExamStart (mr MsgDate) & setTooltip MsgExamTimeTip) (efStart <$> template)
|
||||||
|
<*> aopt utcTimeField (fslpI MsgExamEnd (mr MsgDate) & setTooltip MsgExamTimeTip) (efEnd <$> template)
|
||||||
|
<*> aopt utcTimeField (fslpI MsgExamVisibleFrom (mr MsgDate) & setTooltip MsgExamVisibleFromTip) (efVisibleFrom <$> template)
|
||||||
|
<*> aopt utcTimeField (fslpI MsgExamRegisterFrom (mr MsgDate) & setTooltip MsgExamRegisterFromTip) (efRegisterFrom <$> template)
|
||||||
|
<*> aopt utcTimeField (fslpI MsgExamRegisterTo (mr MsgDate)) (efRegisterTo <$> template)
|
||||||
|
<*> aopt utcTimeField (fslpI MsgExamDeregisterUntil (mr MsgDate)) (efDeregisterUntil <$> template)
|
||||||
|
<*> areq utcTimeField (fslpI MsgExamPublishOccurrenceAssignments (mr MsgDate) & setTooltip MsgExamPublishOccurrenceAssignments) (efPublishOccurrenceAssignments <$> template)
|
||||||
|
<*> aopt utcTimeField (fslpI MsgExamFinished (mr MsgDate) & setTooltip MsgExamFinishedTip) (efFinished <$> template)
|
||||||
|
<*> aopt utcTimeField (fslpI MsgExamClosed (mr MsgDate) & setTooltip MsgExamClosedTip) (efClosed <$> template)
|
||||||
|
<* aformSection MsgExamFormOccurrences
|
||||||
|
<*> examOccurrenceForm (efOccurrences <$> template)
|
||||||
|
<* aformSection MsgExamFormAutomaticFunctions
|
||||||
|
<*> (fromMaybe False <$> aopt checkBoxField (fslI MsgExamShowGrades & setTooltip MsgExamShowGradesTip) (Just . efShowGrades <$> template))
|
||||||
|
<*> (fromMaybe False <$> aopt checkBoxField (fslI MsgExamPublicStatistics & setTooltip MsgExamPublicStatisticsTip) (Just . efPublicStatistics <$> template))
|
||||||
|
<*> examGradingRuleForm (efGradingRule <$> template)
|
||||||
|
<*> bonusRuleForm (efBonusRule <$> template)
|
||||||
|
<*> examOccurrenceRuleForm (efOccurrenceRule <$> template)
|
||||||
|
<* aformSection MsgExamFormCorrection
|
||||||
|
<*> examCorrectorsForm (efCorrectors <$> template)
|
||||||
|
<* aformSection MsgExamFormParts
|
||||||
|
<*> examPartsForm (efExamParts <$> template)
|
||||||
|
|
||||||
|
examCorrectorsForm :: Maybe (Set (Either UserEmail UserId)) -> AForm Handler (Set (Either UserEmail UserId))
|
||||||
|
examCorrectorsForm mPrev = wFormToAForm $ do
|
||||||
|
MsgRenderer mr <- getMsgRenderer
|
||||||
|
Just currentRoute <- getCurrentRoute
|
||||||
|
uid <- liftHandlerT requireAuthId
|
||||||
|
|
||||||
|
let
|
||||||
|
miButtonAction' :: forall p. PathPiece p => p -> Maybe (SomeRoute UniWorX)
|
||||||
|
miButtonAction' frag = Just . SomeRoute $ currentRoute :#: frag
|
||||||
|
|
||||||
|
miAdd' :: (Text -> Text) -> FieldView UniWorX -> Form ([Either UserEmail UserId] -> FormResult [Either UserEmail UserId])
|
||||||
|
miAdd' nudge submitView csrf = do
|
||||||
|
(addRes, addView) <- mpreq (multiUserField False $ Just corrUserSuggestions) ("" & addName (nudge "email")) Nothing
|
||||||
|
let
|
||||||
|
addRes'
|
||||||
|
| otherwise
|
||||||
|
= addRes <&> \newDat oldDat -> if
|
||||||
|
| existing <- newDat `Set.intersection` Set.fromList oldDat
|
||||||
|
, not $ Set.null existing
|
||||||
|
-> FormFailure [mr MsgExamCorrectorAlreadyAdded]
|
||||||
|
| otherwise
|
||||||
|
-> FormSuccess $ Set.toList newDat
|
||||||
|
return (addRes', $(widgetFile "widgets/massinput/examCorrectors/add"))
|
||||||
|
|
||||||
|
corrUserSuggestions :: E.SqlQuery (E.SqlExpr (Entity User))
|
||||||
|
corrUserSuggestions = E.from $ \(lecturer `E.InnerJoin` course `E.InnerJoin` exam `E.InnerJoin` corrector `E.InnerJoin` corrUser) -> do
|
||||||
|
E.on $ corrUser E.^. UserId E.==. corrector E.^. ExamCorrectorUser
|
||||||
|
E.on $ corrector E.^. ExamCorrectorExam E.==. exam E.^. ExamId
|
||||||
|
E.on $ exam E.^. ExamCourse E.==. course E.^. CourseId
|
||||||
|
E.on $ course E.^. CourseId E.==. lecturer E.^. LecturerCourse
|
||||||
|
E.where_ $ lecturer E.^. LecturerUser E.==. E.val uid
|
||||||
|
return corrUser
|
||||||
|
|
||||||
|
|
||||||
|
miCell' :: Either UserEmail UserId -> Widget
|
||||||
|
miCell' (Left email) =
|
||||||
|
$(widgetFile "widgets/massinput/examCorrectors/cellInvitation")
|
||||||
|
miCell' (Right userId) = do
|
||||||
|
User{..} <- liftHandlerT . runDB $ get404 userId
|
||||||
|
$(widgetFile "widgets/massinput/examCorrectors/cellKnown")
|
||||||
|
|
||||||
|
miLayout' :: MassInputLayout ListLength (Either UserEmail UserId) ()
|
||||||
|
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)
|
||||||
|
|
||||||
|
examOccurrenceForm :: Maybe (Set ExamOccurrenceForm) -> AForm Handler (Set ExamOccurrenceForm)
|
||||||
|
examOccurrenceForm prev = wFormToAForm $ do
|
||||||
|
Just currentRoute <- getCurrentRoute
|
||||||
|
let
|
||||||
|
miButtonAction' :: forall p. PathPiece p => p -> Maybe (SomeRoute UniWorX)
|
||||||
|
miButtonAction' frag = Just . SomeRoute $ currentRoute :#: frag
|
||||||
|
|
||||||
|
fmap (fmap Set.fromList) . massInputAccumEditW miAdd' miCell' miButtonAction' miLayout' miIdent' (fslI MsgExamOccurrences & setTooltip MsgMassInputTip) True $ Set.toList <$> prev
|
||||||
|
where
|
||||||
|
examOccurrenceForm' nudge mPrev csrf = do
|
||||||
|
(eofIdRes, eofIdView) <- mopt hiddenField ("" & addName (nudge "id")) (Just $ eofId =<< mPrev)
|
||||||
|
(eofRoomRes, eofRoomView) <- mpreq textField ("" & addName (nudge "name")) (eofRoom <$> mPrev)
|
||||||
|
(eofCapacityRes, eofCapacityView) <- mpreq (natFieldI MsgExamRoomCapacityNegative) ("" & addName (nudge "capacity")) (eofCapacity <$> mPrev)
|
||||||
|
(eofStartRes, eofStartView) <- mpreq utcTimeField ("" & addName (nudge "start")) (eofStart <$> mPrev)
|
||||||
|
(eofEndRes, eofEndView) <- mopt utcTimeField ("" & addName (nudge "end")) (eofEnd <$> mPrev)
|
||||||
|
(eofDescRes, eofDescView) <- mopt htmlFieldSmall ("" & addName (nudge "description")) (eofDescription <$> mPrev)
|
||||||
|
|
||||||
|
return ( ExamOccurrenceForm
|
||||||
|
<$> eofIdRes
|
||||||
|
<*> eofRoomRes
|
||||||
|
<*> eofCapacityRes
|
||||||
|
<*> eofStartRes
|
||||||
|
<*> eofEndRes
|
||||||
|
<*> (assertM (not . null . renderHtml) <$> eofDescRes)
|
||||||
|
, $(widgetFile "widgets/massinput/examRooms/form")
|
||||||
|
)
|
||||||
|
|
||||||
|
miAdd' nudge submitView csrf = do
|
||||||
|
MsgRenderer mr <- getMsgRenderer
|
||||||
|
(res, formWidget) <- examOccurrenceForm' nudge Nothing csrf
|
||||||
|
let
|
||||||
|
addRes = res <&> \newDat (Set.fromList -> oldDat) -> if
|
||||||
|
| newDat `Set.member` oldDat -> FormFailure [mr MsgExamRoomAlreadyExists]
|
||||||
|
| otherwise -> FormSuccess $ pure newDat
|
||||||
|
return (addRes, $(widgetFile "widgets/massinput/examRooms/add"))
|
||||||
|
miCell' nudge dat = examOccurrenceForm' nudge (Just dat)
|
||||||
|
miLayout' lLength _ cellWdgts delButtons addWdgts = $(widgetFile "widgets/massinput/examRooms/layout")
|
||||||
|
miIdent' :: Text
|
||||||
|
miIdent' = "exam-occurrences"
|
||||||
|
|
||||||
|
examPartsForm :: Maybe (Set ExamPartForm) -> AForm Handler (Set ExamPartForm)
|
||||||
|
examPartsForm prev = wFormToAForm $ do
|
||||||
|
Just currentRoute <- getCurrentRoute
|
||||||
|
let
|
||||||
|
miButtonAction' :: forall p. PathPiece p => p -> Maybe (SomeRoute UniWorX)
|
||||||
|
miButtonAction' frag = Just . SomeRoute $ currentRoute :#: frag
|
||||||
|
|
||||||
|
fmap (fmap Set.fromList) . massInputAccumEditW miAdd' miCell' miButtonAction' miLayout' miIdent' (fslI MsgExamParts & setTooltip MsgMassInputTip) True $ Set.toList <$> prev
|
||||||
|
where
|
||||||
|
examPartForm' nudge mPrev csrf = do
|
||||||
|
(epfIdRes, epfIdView) <- mopt hiddenField ("" & addName (nudge "id")) (Just $ epfId =<< mPrev)
|
||||||
|
(epfNameRes, epfNameView) <- mpreq ciField ("" & addName (nudge "name")) (epfName <$> mPrev)
|
||||||
|
(epfMaxPointsRes, epfMaxPointsView) <- mopt pointsField ("" & addName (nudge "max-points")) (epfMaxPoints <$> mPrev)
|
||||||
|
(epfWeightRes, epfWeightView) <- mpreq (checkBool (>= 0) MsgExamPartWeightNegative rationalField) ("" & addName (nudge "weight")) (epfWeight <$> mPrev <|> Just 1)
|
||||||
|
|
||||||
|
return ( ExamPartForm
|
||||||
|
<$> epfIdRes
|
||||||
|
<*> epfNameRes
|
||||||
|
<*> epfMaxPointsRes
|
||||||
|
<*> epfWeightRes
|
||||||
|
, $(widgetFile "widgets/massinput/examParts/form")
|
||||||
|
)
|
||||||
|
|
||||||
|
miAdd' nudge submitView csrf = do
|
||||||
|
MsgRenderer mr <- getMsgRenderer
|
||||||
|
(res, formWidget) <- examPartForm' nudge Nothing csrf
|
||||||
|
let
|
||||||
|
addRes = res <&> \newDat (Set.fromList -> oldDat) -> if
|
||||||
|
| any (((==) `on` epfName) newDat) oldDat -> FormFailure [mr MsgExamPartAlreadyExists]
|
||||||
|
| otherwise -> FormSuccess $ pure newDat
|
||||||
|
return (addRes, $(widgetFile "widgets/massinput/examParts/add"))
|
||||||
|
miCell' nudge dat = examPartForm' nudge (Just dat)
|
||||||
|
miLayout' lLength _ cellWdgts delButtons addWdgts = $(widgetFile "widgets/massinput/examParts/layout")
|
||||||
|
miIdent' :: Text
|
||||||
|
miIdent' = "exam-parts"
|
||||||
|
|
||||||
|
examFormTemplate :: Entity Exam -> DB ExamForm
|
||||||
|
examFormTemplate (Entity eId Exam{..}) = do
|
||||||
|
parts <- selectList [ ExamPartExam ==. eId ] []
|
||||||
|
occurrences <- selectList [ ExamOccurrenceExam ==. eId ] []
|
||||||
|
correctors <- selectList [ ExamCorrectorExam ==. eId ] []
|
||||||
|
invitations <- map (\(email, InvDBDataExamCorrector) -> email) <$> sourceInvitationsList eId
|
||||||
|
|
||||||
|
parts' <- forM parts $ \(Entity pid part) -> (,) <$> encrypt pid <*> pure part
|
||||||
|
occurrences' <- forM occurrences $ \(Entity oid occ) -> (,) <$> encrypt oid <*> pure occ
|
||||||
|
|
||||||
|
return ExamForm
|
||||||
|
{ efName = examName
|
||||||
|
, efGradingRule = examGradingRule
|
||||||
|
, efBonusRule = examBonusRule
|
||||||
|
, efOccurrenceRule = examOccurrenceRule
|
||||||
|
, efVisibleFrom = examVisibleFrom
|
||||||
|
, efRegisterFrom = examRegisterFrom
|
||||||
|
, efRegisterTo = examRegisterTo
|
||||||
|
, efDeregisterUntil = examDeregisterUntil
|
||||||
|
, efPublishOccurrenceAssignments = examPublishOccurrenceAssignments
|
||||||
|
, efStart = examStart
|
||||||
|
, efEnd = examEnd
|
||||||
|
, efFinished = examFinished
|
||||||
|
, efClosed = examClosed
|
||||||
|
, efShowGrades = examShowGrades
|
||||||
|
, efPublicStatistics = examPublicStatistics
|
||||||
|
, efDescription = examDescription
|
||||||
|
, efOccurrences = Set.fromList $ do
|
||||||
|
(Just -> eofId, ExamOccurrence{..}) <- occurrences'
|
||||||
|
return ExamOccurrenceForm
|
||||||
|
{ eofId
|
||||||
|
, eofRoom = examOccurrenceRoom
|
||||||
|
, eofCapacity = examOccurrenceCapacity
|
||||||
|
, eofStart = examOccurrenceStart
|
||||||
|
, eofEnd = examOccurrenceEnd
|
||||||
|
, eofDescription = examOccurrenceDescription
|
||||||
|
}
|
||||||
|
, efExamParts = Set.fromList $ do
|
||||||
|
(Just -> epfId, ExamPart{..}) <- parts'
|
||||||
|
return ExamPartForm
|
||||||
|
{ epfId
|
||||||
|
, epfName = examPartName
|
||||||
|
, epfMaxPoints = examPartMaxPoints
|
||||||
|
, epfWeight = examPartWeight
|
||||||
|
}
|
||||||
|
, efCorrectors = Set.unions
|
||||||
|
[ Set.fromList $ map Left invitations
|
||||||
|
, Set.fromList . map Right $ do
|
||||||
|
Entity _ ExamCorrector{..} <- correctors
|
||||||
|
return examCorrectorUser
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
examTemplate :: CourseId -> DB (Maybe ExamForm)
|
||||||
|
examTemplate cid = runMaybeT $ do
|
||||||
|
newCourse <- MaybeT $ get cid
|
||||||
|
|
||||||
|
[(Entity _ oldCourse, Entity _ oldExam)] <- lift . E.select . E.from $ \(course `E.InnerJoin` exam) -> do
|
||||||
|
E.on $ course E.^. CourseId E.==. exam E.^. ExamCourse
|
||||||
|
E.where_ $ ( course E.^. CourseShorthand E.==. E.val (courseShorthand newCourse)
|
||||||
|
E.||. course E.^. CourseName E.==. E.val (courseName newCourse)
|
||||||
|
)
|
||||||
|
E.&&. course E.^. CourseSchool E.==. E.val (courseSchool newCourse)
|
||||||
|
E.where_ . E.not_ . E.exists . E.from $ \exam' -> do
|
||||||
|
E.where_ $ exam' E.^. ExamCourse E.==. E.val cid
|
||||||
|
E.where_ $ exam E.^. ExamName E.==. exam' E.^. ExamName
|
||||||
|
E.where_ . E.not_ . E.isNothing $ exam E.^. ExamVisibleFrom
|
||||||
|
E.limit 1
|
||||||
|
E.orderBy [ E.desc $ course E.^. CourseTerm, E.asc $ exam E.^. ExamVisibleFrom ]
|
||||||
|
return (course, exam)
|
||||||
|
|
||||||
|
oldTerm <- MaybeT . get $ courseTerm oldCourse
|
||||||
|
newTerm <- MaybeT . get $ courseTerm newCourse
|
||||||
|
|
||||||
|
let
|
||||||
|
dateOffset = over _utctDay . addDays $ (diffDays `on` termLectureEnd) newTerm oldTerm
|
||||||
|
|
||||||
|
return ExamForm
|
||||||
|
{ efName = examName oldExam
|
||||||
|
, efGradingRule = examGradingRule oldExam
|
||||||
|
, efBonusRule = examBonusRule oldExam
|
||||||
|
, efOccurrenceRule = examOccurrenceRule oldExam
|
||||||
|
, efVisibleFrom = dateOffset <$> examVisibleFrom oldExam
|
||||||
|
, efRegisterFrom = dateOffset <$> examRegisterFrom oldExam
|
||||||
|
, efRegisterTo = dateOffset <$> examRegisterTo oldExam
|
||||||
|
, efDeregisterUntil = dateOffset <$> examDeregisterUntil oldExam
|
||||||
|
, efPublishOccurrenceAssignments = dateOffset $ examPublishOccurrenceAssignments oldExam
|
||||||
|
, efStart = dateOffset $ examStart oldExam
|
||||||
|
, efEnd = dateOffset <$> examEnd oldExam
|
||||||
|
, efFinished = dateOffset <$> examFinished oldExam
|
||||||
|
, efClosed = dateOffset <$> examClosed oldExam
|
||||||
|
, efShowGrades = examShowGrades oldExam
|
||||||
|
, efPublicStatistics = examPublicStatistics oldExam
|
||||||
|
, efDescription = examDescription oldExam
|
||||||
|
, efOccurrences = Set.empty
|
||||||
|
, efExamParts = Set.empty
|
||||||
|
, efCorrectors = Set.empty
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
validateExam :: (MonadHandler m, HandlerSite m ~ UniWorX) => FormValidator ExamForm m ()
|
||||||
|
validateExam = do
|
||||||
|
ExamForm{..} <- State.get
|
||||||
|
|
||||||
|
guardValidation MsgExamRegisterToMustBeAfterRegisterFrom $ NTop efRegisterTo >= NTop efRegisterFrom
|
||||||
|
guardValidation MsgExamDeregisterUntilMustBeAfterRegisterFrom $ NTop efDeregisterUntil >= NTop efRegisterFrom
|
||||||
|
guardValidation MsgExamStartMustBeAfterPublishOccurrenceAssignments $ efStart >= efPublishOccurrenceAssignments
|
||||||
|
guardValidation MsgExamEndMustBeAfterStart $ NTop efEnd >= NTop (Just efStart)
|
||||||
|
guardValidation MsgExamFinishedMustBeAfterEnd . fromMaybe True $ (>=) <$> efFinished <*> efEnd
|
||||||
|
guardValidation MsgExamFinishedMustBeAfterStart $ NTop efFinished >= NTop (Just efStart)
|
||||||
|
guardValidation MsgExamClosedMustBeAfterFinished . fromMaybe True $ (>=) <$> efClosed <*> efFinished
|
||||||
|
guardValidation MsgExamClosedMustBeAfterStart $ NTop efClosed >= NTop (Just efStart)
|
||||||
|
guardValidation MsgExamClosedMustBeAfterEnd . fromMaybe True $ (>=) <$> efClosed <*> efEnd
|
||||||
|
|
||||||
|
|
||||||
|
getCExamNewR, postCExamNewR :: TermId -> SchoolId -> CourseShorthand -> Handler Html
|
||||||
|
getCExamNewR = postCExamNewR
|
||||||
|
postCExamNewR tid ssh csh = do
|
||||||
|
(cid, template) <- runDB $ do
|
||||||
|
cid <- getKeyBy404 $ TermSchoolCourseShort tid ssh csh
|
||||||
|
template <- examTemplate cid
|
||||||
|
return (cid, template)
|
||||||
|
|
||||||
|
((newExamResult, newExamWidget), newExamEnctype) <- runFormPost . validateForm validateExam $ examForm template
|
||||||
|
|
||||||
|
formResult newExamResult $ \ExamForm{..} -> do
|
||||||
|
insertRes <- runDBJobs $ do
|
||||||
|
insertRes <- insertUnique Exam
|
||||||
|
{ examName = efName
|
||||||
|
, examCourse = cid
|
||||||
|
, examGradingRule = efGradingRule
|
||||||
|
, examBonusRule = efBonusRule
|
||||||
|
, examOccurrenceRule = efOccurrenceRule
|
||||||
|
, examVisibleFrom = efVisibleFrom
|
||||||
|
, examRegisterFrom = efRegisterFrom
|
||||||
|
, examRegisterTo = efRegisterTo
|
||||||
|
, examDeregisterUntil = efDeregisterUntil
|
||||||
|
, examPublishOccurrenceAssignments = efPublishOccurrenceAssignments
|
||||||
|
, examStart = efStart
|
||||||
|
, examEnd = efEnd
|
||||||
|
, examFinished = efFinished
|
||||||
|
, examClosed = efClosed
|
||||||
|
, examShowGrades = efShowGrades
|
||||||
|
, examPublicStatistics = efPublicStatistics
|
||||||
|
, examDescription = efDescription
|
||||||
|
}
|
||||||
|
whenIsJust insertRes $ \examid -> do
|
||||||
|
insertMany_
|
||||||
|
[ ExamPart{..}
|
||||||
|
| ExamPartForm{..} <- Set.toList efExamParts
|
||||||
|
, let examPartExam = examid
|
||||||
|
examPartName = epfName
|
||||||
|
examPartMaxPoints = epfMaxPoints
|
||||||
|
examPartWeight = epfWeight
|
||||||
|
]
|
||||||
|
|
||||||
|
insertMany_
|
||||||
|
[ ExamOccurrence{..}
|
||||||
|
| ExamOccurrenceForm{..} <- Set.toList efOccurrences
|
||||||
|
, let examOccurrenceExam = examid
|
||||||
|
examOccurrenceRoom = eofRoom
|
||||||
|
examOccurrenceCapacity = eofCapacity
|
||||||
|
examOccurrenceStart = eofStart
|
||||||
|
examOccurrenceEnd = eofEnd
|
||||||
|
examOccurrenceDescription = eofDescription
|
||||||
|
]
|
||||||
|
|
||||||
|
let (invites, adds) = partitionEithers $ Set.toList efCorrectors
|
||||||
|
insertMany_ [ ExamCorrector{..}
|
||||||
|
| examCorrectorUser <- adds
|
||||||
|
, let examCorrectorExam = examid
|
||||||
|
]
|
||||||
|
sinkInvitationsF examCorrectorInvitationConfig $ map (, examid, (InvDBDataExamCorrector, InvTokenDataExamCorrector)) invites
|
||||||
|
return insertRes
|
||||||
|
case insertRes of
|
||||||
|
Nothing -> addMessageI Error $ MsgExamNameTaken efName
|
||||||
|
Just _ -> do
|
||||||
|
addMessageI Success $ MsgExamCreated efName
|
||||||
|
redirect $ CourseR tid ssh csh CExamListR
|
||||||
|
|
||||||
|
let heading = prependCourseTitle tid ssh csh MsgExamNew
|
||||||
|
|
||||||
|
siteLayoutMsg heading $ do
|
||||||
|
setTitleI heading
|
||||||
|
let
|
||||||
|
newExamForm = wrapForm newExamWidget def
|
||||||
|
{ formMethod = POST
|
||||||
|
, formAction = Just . SomeRoute $ CourseR tid ssh csh CExamNewR
|
||||||
|
, formEncoding = newExamEnctype
|
||||||
|
}
|
||||||
|
$(widgetFile "exam-new")
|
||||||
|
|
||||||
|
getEEditR, postEEditR :: TermId -> SchoolId -> CourseShorthand -> ExamName -> Handler Html
|
||||||
|
getEEditR = postEEditR
|
||||||
|
postEEditR tid ssh csh examn = do
|
||||||
|
(cid, eId, template) <- runDB $ do
|
||||||
|
(cid, exam@(Entity eId _)) <- fetchCourseIdExam tid ssh csh examn
|
||||||
|
|
||||||
|
template <- examFormTemplate exam
|
||||||
|
|
||||||
|
return (cid, eId, template)
|
||||||
|
|
||||||
|
((editExamResult, editExamWidget), editExamEnctype) <- runFormPost . validateForm validateExam . examForm $ Just template
|
||||||
|
|
||||||
|
formResult editExamResult $ \ExamForm{..} -> do
|
||||||
|
insertRes <- runDBJobs $ do
|
||||||
|
insertRes <- myReplaceUnique eId Exam
|
||||||
|
{ examCourse = cid
|
||||||
|
, examName = efName
|
||||||
|
, examGradingRule = efGradingRule
|
||||||
|
, examBonusRule = efBonusRule
|
||||||
|
, examOccurrenceRule = efOccurrenceRule
|
||||||
|
, examVisibleFrom = efVisibleFrom
|
||||||
|
, examRegisterFrom = efRegisterFrom
|
||||||
|
, examRegisterTo = efRegisterTo
|
||||||
|
, examDeregisterUntil = efDeregisterUntil
|
||||||
|
, examPublishOccurrenceAssignments = efPublishOccurrenceAssignments
|
||||||
|
, examStart = efStart
|
||||||
|
, examEnd = efEnd
|
||||||
|
, examFinished = efFinished
|
||||||
|
, examClosed = efClosed
|
||||||
|
, examPublicStatistics = efPublicStatistics
|
||||||
|
, examShowGrades = efShowGrades
|
||||||
|
, examDescription = efDescription
|
||||||
|
}
|
||||||
|
|
||||||
|
when (is _Nothing insertRes) $ do
|
||||||
|
occIds <- fmap catMaybes . forM (Set.toList efOccurrences) $ traverse decrypt . eofId
|
||||||
|
deleteWhere [ ExamOccurrenceExam ==. eId, ExamOccurrenceId /<-. occIds ]
|
||||||
|
forM_ (Set.toList efOccurrences) $ \case
|
||||||
|
ExamOccurrenceForm{ eofId = Nothing, .. } -> insert_
|
||||||
|
ExamOccurrence
|
||||||
|
{ examOccurrenceExam = eId
|
||||||
|
, examOccurrenceRoom = eofRoom
|
||||||
|
, examOccurrenceCapacity = eofCapacity
|
||||||
|
, examOccurrenceStart = eofStart
|
||||||
|
, examOccurrenceEnd = eofEnd
|
||||||
|
, examOccurrenceDescription = eofDescription
|
||||||
|
}
|
||||||
|
ExamOccurrenceForm{ .. } -> void . runMaybeT $ do
|
||||||
|
cID <- hoistMaybe eofId
|
||||||
|
eofId' <- decrypt cID
|
||||||
|
oldOcc <- MaybeT $ get eofId'
|
||||||
|
guard $ examOccurrenceExam oldOcc == eId
|
||||||
|
lift $ replace eofId' ExamOccurrence
|
||||||
|
{ examOccurrenceExam = eId
|
||||||
|
, examOccurrenceRoom = eofRoom
|
||||||
|
, examOccurrenceCapacity = eofCapacity
|
||||||
|
, examOccurrenceStart = eofStart
|
||||||
|
, examOccurrenceEnd = eofEnd
|
||||||
|
, examOccurrenceDescription = eofDescription
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pIds <- fmap catMaybes . forM (Set.toList efExamParts) $ traverse decrypt . epfId
|
||||||
|
deleteWhere [ ExamPartExam ==. eId, ExamPartId /<-. pIds ]
|
||||||
|
forM_ (Set.toList efExamParts) $ \case
|
||||||
|
ExamPartForm{ epfId = Nothing, .. } -> insert_
|
||||||
|
ExamPart
|
||||||
|
{ examPartExam = eId
|
||||||
|
, examPartName = epfName
|
||||||
|
, examPartMaxPoints = epfMaxPoints
|
||||||
|
, examPartWeight = epfWeight
|
||||||
|
}
|
||||||
|
ExamPartForm{ .. } -> void . runMaybeT $ do
|
||||||
|
cID <- hoistMaybe epfId
|
||||||
|
epfId' <- decrypt cID
|
||||||
|
oldPart <- MaybeT $ get epfId'
|
||||||
|
guard $ examPartExam oldPart == eId
|
||||||
|
lift $ replace epfId' ExamPart
|
||||||
|
{ examPartExam = eId
|
||||||
|
, examPartName = epfName
|
||||||
|
, examPartMaxPoints = epfMaxPoints
|
||||||
|
, examPartWeight = epfWeight
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
let (invites, adds) = partitionEithers $ Set.toList efCorrectors
|
||||||
|
|
||||||
|
deleteWhere [ ExamCorrectorExam ==. eId ]
|
||||||
|
insertMany_ $ map (ExamCorrector eId) adds
|
||||||
|
|
||||||
|
deleteWhere [ InvitationFor ==. invRef @ExamCorrector eId, InvitationEmail /<-. invites ]
|
||||||
|
sinkInvitationsF examCorrectorInvitationConfig $ map (, eId, (InvDBDataExamCorrector, InvTokenDataExamCorrector)) invites
|
||||||
|
|
||||||
|
return insertRes
|
||||||
|
|
||||||
|
case insertRes of
|
||||||
|
Just _ -> addMessageI Error $ MsgExamNameTaken efName
|
||||||
|
Nothing -> do
|
||||||
|
addMessageI Success $ MsgExamEdited efName
|
||||||
|
redirect $ CExamR tid ssh csh efName EShowR
|
||||||
|
|
||||||
|
let heading = prependCourseTitle tid ssh csh . MsgExamEditHeading $ efName template
|
||||||
|
|
||||||
|
siteLayoutMsg heading $ do
|
||||||
|
setTitleI heading
|
||||||
|
let
|
||||||
|
editExamForm = wrapForm editExamWidget def
|
||||||
|
{ formMethod = POST
|
||||||
|
, formAction = Just . SomeRoute $ CExamR tid ssh csh examn EEditR
|
||||||
|
, formEncoding = editExamEnctype
|
||||||
|
}
|
||||||
|
$(widgetFile "exam-edit")
|
||||||
|
|
||||||
|
|
||||||
|
getEShowR :: TermId -> SchoolId -> CourseShorthand -> ExamName -> Handler Html
|
||||||
|
getEShowR tid ssh csh examn = do
|
||||||
|
cTime <- liftIO getCurrentTime
|
||||||
|
mUid <- maybeAuthId
|
||||||
|
|
||||||
|
(Entity _ Exam{..}, parts, examVisible, (gradingVisible, gradingShown), (occurrenceAssignmentsVisible, occurrenceAssignmentsShown), results, result, occurrences, (registered, mayRegister)) <- runDB $ do
|
||||||
|
exam@(Entity eId Exam{..}) <- fetchExam tid ssh csh examn
|
||||||
|
|
||||||
|
let examVisible = NTop (Just cTime) >= NTop examVisibleFrom
|
||||||
|
|
||||||
|
let gradingVisible = NTop (Just cTime) >= NTop examFinished
|
||||||
|
gradingShown <- or2M (return gradingVisible) . hasReadAccessTo $ CExamR tid ssh csh examn EEditR
|
||||||
|
|
||||||
|
let occurrenceAssignmentsVisible = cTime >= examPublishOccurrenceAssignments
|
||||||
|
occurrenceAssignmentsShown <- or2M (return occurrenceAssignmentsVisible) . hasReadAccessTo $ CExamR tid ssh csh examn EEditR
|
||||||
|
|
||||||
|
parts <- selectList [ ExamPartExam ==. eId ] [ Asc ExamPartName ]
|
||||||
|
|
||||||
|
resultsRaw <- for mUid $ \uid ->
|
||||||
|
E.select . E.from $ \examPartResult -> do
|
||||||
|
E.where_ $ examPartResult E.^. ExamPartResultUser E.==. E.val uid
|
||||||
|
E.&&. examPartResult E.^. ExamPartResultExamPart `E.in_` E.valList (map entityKey parts)
|
||||||
|
return examPartResult
|
||||||
|
let results = maybe Map.empty (\rs -> Map.fromList [ (examPartResultExamPart, res) | res@(Entity _ ExamPartResult{..}) <- rs ]) resultsRaw
|
||||||
|
|
||||||
|
result <- fmap join . for mUid $ getBy . UniqueExamResult eId
|
||||||
|
|
||||||
|
occurrencesRaw <- E.select . E.from $ \examOccurrence -> do
|
||||||
|
E.where_ $ examOccurrence E.^. ExamOccurrenceExam E.==. E.val eId
|
||||||
|
let
|
||||||
|
registered
|
||||||
|
| Just uid <- mUid
|
||||||
|
= E.exists . E.from $ \examRegistration -> do
|
||||||
|
E.where_ $ examRegistration E.^. ExamRegistrationExam E.==. E.val eId
|
||||||
|
E.&&. examRegistration E.^. ExamRegistrationUser E.==. E.val uid
|
||||||
|
E.&&. examRegistration E.^. ExamRegistrationOccurrence E.==. E.just (examOccurrence E.^. ExamOccurrenceId)
|
||||||
|
| otherwise = E.false
|
||||||
|
E.orderBy [E.desc registered, E.asc $ examOccurrence E.^. ExamOccurrenceStart, E.asc $ examOccurrence E.^. ExamOccurrenceRoom]
|
||||||
|
return (examOccurrence, registered)
|
||||||
|
|
||||||
|
let occurrences = map (over _2 E.unValue) occurrencesRaw
|
||||||
|
|
||||||
|
registered <- for mUid $ existsBy . UniqueExamRegistration eId
|
||||||
|
mayRegister <- (== Authorized) <$> evalAccessDB (CExamR tid ssh csh examName ERegisterR) True
|
||||||
|
|
||||||
|
return (exam, parts, examVisible, (gradingVisible, gradingShown), (occurrenceAssignmentsVisible, occurrenceAssignmentsShown), results, result, occurrences, (registered, mayRegister))
|
||||||
|
|
||||||
|
let examTimes = all (\(Entity _ ExamOccurrence{..}, _) -> examOccurrenceStart == examStart && examOccurrenceEnd == examEnd) occurrences
|
||||||
|
registerWidget
|
||||||
|
| Just isRegistered <- registered
|
||||||
|
, mayRegister = Just $ do
|
||||||
|
(examRegisterForm, examRegisterEnctype) <- liftHandlerT . generateFormPost . buttonForm' $ bool [BtnRegister] [BtnDeregister] isRegistered
|
||||||
|
[whamlet|
|
||||||
|
<p>
|
||||||
|
$if isRegistered
|
||||||
|
_{MsgExamRegistered}
|
||||||
|
$else
|
||||||
|
_{MsgExamNotRegistered}
|
||||||
|
|]
|
||||||
|
wrapForm examRegisterForm def
|
||||||
|
{ formAction = Just . SomeRoute $ CExamR tid ssh csh examName ERegisterR
|
||||||
|
, formEncoding = examRegisterEnctype
|
||||||
|
, formSubmit = FormNoSubmit
|
||||||
|
}
|
||||||
|
| fromMaybe False registered = Just [whamlet|_{MsgExamRegistered}|]
|
||||||
|
| otherwise = Nothing
|
||||||
|
|
||||||
|
let heading = prependCourseTitle tid ssh csh $ CI.original examName
|
||||||
|
|
||||||
|
siteLayoutMsg heading $ do
|
||||||
|
setTitleI heading
|
||||||
|
let
|
||||||
|
gradingKeyW :: [Points] -> Widget
|
||||||
|
gradingKeyW bounds
|
||||||
|
= let boundWidgets :: [Widget]
|
||||||
|
boundWidgets = toWidget . (pack :: String -> Text) . showFixed True <$> 0 : bounds
|
||||||
|
grades :: [ExamGrade]
|
||||||
|
grades = universeF
|
||||||
|
in $(widgetFile "widgets/gradingKey")
|
||||||
|
|
||||||
|
examBonusW :: ExamBonusRule -> Widget
|
||||||
|
examBonusW bonusRule = $(widgetFile "widgets/bonusRule")
|
||||||
|
$(widgetFile "exam-show")
|
||||||
|
|
||||||
|
getEUsersR, postEUsersR :: TermId -> SchoolId -> CourseShorthand -> ExamName -> Handler Html
|
||||||
|
getEUsersR = postEUsersR
|
||||||
|
postEUsersR = error "postEUsersR"
|
||||||
|
|
||||||
|
getEAddUserR, postEAddUserR :: TermId -> SchoolId -> CourseShorthand -> ExamName -> Handler Html
|
||||||
|
getEAddUserR = postEAddUserR
|
||||||
|
postEAddUserR = error "postEAddUserR"
|
||||||
|
|
||||||
|
getEInviteR, postEInviteR :: TermId -> SchoolId -> CourseShorthand -> ExamName -> Handler Html
|
||||||
|
getEInviteR = postEInviteR
|
||||||
|
postEInviteR = error "postEInviteR"
|
||||||
|
|
||||||
|
postERegisterR :: TermId -> SchoolId -> CourseShorthand -> ExamName -> Handler Html
|
||||||
|
postERegisterR tid ssh csh examn = do
|
||||||
|
uid <- requireAuthId
|
||||||
|
|
||||||
|
Entity eId Exam{..} <- runDB $ fetchExam tid ssh csh examn
|
||||||
|
|
||||||
|
((btnResult, _), _) <- runFormPost buttonForm
|
||||||
|
|
||||||
|
formResult btnResult $ \case
|
||||||
|
BtnRegister -> do
|
||||||
|
runDB . void . insert $ ExamRegistration eId uid Nothing
|
||||||
|
addMessageI Success $ MsgExamRegisteredSuccess examn
|
||||||
|
redirect $ CExamR tid ssh csh examn EShowR
|
||||||
|
BtnDeregister -> do
|
||||||
|
runDB . deleteBy $ UniqueExamRegistration eId uid
|
||||||
|
addMessageI Success $ MsgExamDeregisteredSuccess examn
|
||||||
|
redirect $ CExamR tid ssh csh examn EShowR
|
||||||
|
|
||||||
|
invalidArgs ["Register/Deregister button required"]
|
||||||
@ -63,7 +63,7 @@ getHealthR = do
|
|||||||
<dd .deflist__dd>#{boolSymbol passed}
|
<dd .deflist__dd>#{boolSymbol passed}
|
||||||
$of HealthLDAPAdmins (Just found)
|
$of HealthLDAPAdmins (Just found)
|
||||||
<dt .deflist__dt>_{MsgHealthLDAPAdmins}
|
<dt .deflist__dt>_{MsgHealthLDAPAdmins}
|
||||||
<dd .deflist__dd>#{textPercent found}
|
<dd .deflist__dd>#{textPercent found 1}
|
||||||
$of HealthSMTPConnect (Just passed)
|
$of HealthSMTPConnect (Just passed)
|
||||||
<dt .deflist__dt>_{MsgHealthSMTPConnect}
|
<dt .deflist__dt>_{MsgHealthSMTPConnect}
|
||||||
<dd .deflist__dd>#{boolSymbol passed}
|
<dd .deflist__dd>#{boolSymbol passed}
|
||||||
@ -80,7 +80,7 @@ getInstanceR = do
|
|||||||
instanceInfo@(clusterId, instanceId) <- getsYesod $ (,) <$> appClusterID <*> appInstanceID
|
instanceInfo@(clusterId, instanceId) <- getsYesod $ (,) <$> appClusterID <*> appInstanceID
|
||||||
|
|
||||||
setWeakEtagHashable (clusterId, instanceId)
|
setWeakEtagHashable (clusterId, instanceId)
|
||||||
|
|
||||||
selectRep $ do
|
selectRep $ do
|
||||||
provideRep $
|
provideRep $
|
||||||
siteLayoutMsg MsgInstanceIdentification $ do
|
siteLayoutMsg MsgInstanceIdentification $ do
|
||||||
|
|||||||
@ -66,6 +66,6 @@ postHelpR = do
|
|||||||
let formWidget = wrapForm formWidget' def
|
let formWidget = wrapForm formWidget' def
|
||||||
{ formAction = Just $ SomeRoute HelpR
|
{ formAction = Just $ SomeRoute HelpR
|
||||||
, formEncoding = formEnctype
|
, formEncoding = formEnctype
|
||||||
, formAttrs = [ ("data-ajax-submit", "") | isModal ]
|
, formAttrs = [ asyncSubmitAttr | isModal ]
|
||||||
}
|
}
|
||||||
$(widgetFile "help")
|
$(widgetFile "help")
|
||||||
|
|||||||
@ -31,12 +31,13 @@ getDataProtR = -- do
|
|||||||
-- | Allgemeine Informationen
|
-- | Allgemeine Informationen
|
||||||
getInfoR :: Handler Html
|
getInfoR :: Handler Html
|
||||||
getInfoR = do
|
getInfoR = do
|
||||||
let infoHeading = [whamlet|Re-Implementierung von <a href="https://uniworx.ifi.lmu.de/">UniWorX</a>|]
|
siteLayoutMsg MsgInfoHeading $ do
|
||||||
siteLayout infoHeading $ do
|
setTitleI MsgInfoHeading
|
||||||
let features = $(widgetFile "featureList")
|
let features = $(i18nWidgetFile "featureList")
|
||||||
|
changeLog = $(i18nWidgetFile "changelog")
|
||||||
|
knownBugs = $(i18nWidgetFile "knownBugs")
|
||||||
gitInfo :: Text
|
gitInfo :: Text
|
||||||
gitInfo = $gitDescribe <> " (" <> $gitCommitDate <> ")"
|
gitInfo = $gitDescribe <> " (" <> $gitCommitDate <> ")"
|
||||||
changeLog <- withUrlRenderer $(textFile "CHANGELOG_DE.md")
|
|
||||||
$(widgetFile "versionHistory")
|
$(widgetFile "versionHistory")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -147,7 +147,7 @@ postProfileR = do
|
|||||||
|
|
||||||
siteLayout [whamlet|_{MsgProfileFor} ^{nameWidget userDisplayName userSurname}|] $ do
|
siteLayout [whamlet|_{MsgProfileFor} ^{nameWidget userDisplayName userSurname}|] $ do
|
||||||
setTitle . toHtml $ "Profil " <> userIdent
|
setTitle . toHtml $ "Profil " <> userIdent
|
||||||
let settingsForm =
|
let settingsForm =
|
||||||
wrapForm formWidget FormSettings
|
wrapForm formWidget FormSettings
|
||||||
{ formMethod = POST
|
{ formMethod = POST
|
||||||
, formAction = Just . SomeRoute $ ProfileR :#: ProfileSettings
|
, formAction = Just . SomeRoute $ ProfileR :#: ProfileSettings
|
||||||
@ -593,7 +593,7 @@ postUserNotificationR cID = do
|
|||||||
let formWidget = wrapForm nsInnerWdgt def
|
let formWidget = wrapForm nsInnerWdgt def
|
||||||
{ formAction = Just . SomeRoute $ UserNotificationR cID
|
{ formAction = Just . SomeRoute $ UserNotificationR cID
|
||||||
, formEncoding = nsEnc
|
, formEncoding = nsEnc
|
||||||
, formAttrs = [ ("data-ajax-submit", "") | isModal ]
|
, formAttrs = [ asyncSubmitAttr | isModal ]
|
||||||
}
|
}
|
||||||
|
|
||||||
formResultModal nsRes (UserNotificationR cID, [ (toPathPiece GetBearer, toPathPiece jwt) | Just jwt <- pure mJwt ]) $ \ns -> do
|
formResultModal nsRes (UserNotificationR cID, [ (toPathPiece GetBearer, toPathPiece jwt) | Just jwt <- pure mJwt ]) $ \ns -> do
|
||||||
|
|||||||
@ -257,9 +257,7 @@ getSheetListR tid ssh csh = do
|
|||||||
(Just (Entity _ Submission{submissionRatingPoints=Just sPoints})) ->
|
(Just (Entity _ Submission{submissionRatingPoints=Just sPoints})) ->
|
||||||
case preview (_grading . _maxPoints) sType of
|
case preview (_grading . _maxPoints) sType of
|
||||||
Just maxPoints
|
Just maxPoints
|
||||||
| maxPoints /= 0 ->
|
| maxPoints /= 0 -> textCell $ textPercent sPoints maxPoints
|
||||||
let percent = sPoints / maxPoints
|
|
||||||
in textCell $ textPercent $ realToFrac percent
|
|
||||||
_other -> mempty
|
_other -> mempty
|
||||||
_other -> mempty
|
_other -> mempty
|
||||||
]
|
]
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import Handler.Utils.Tutorial
|
|||||||
import Handler.Utils.Table.Cells
|
import Handler.Utils.Table.Cells
|
||||||
import Handler.Utils.Delete
|
import Handler.Utils.Delete
|
||||||
import Handler.Utils.Communication
|
import Handler.Utils.Communication
|
||||||
import Handler.Utils.Form.Occurences
|
import Handler.Utils.Form.Occurrences
|
||||||
import Handler.Utils.Invitations
|
import Handler.Utils.Invitations
|
||||||
import Jobs.Queue
|
import Jobs.Queue
|
||||||
|
|
||||||
@ -64,7 +64,7 @@ getCTutorialListR tid ssh csh = do
|
|||||||
, 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) . toWidget $ 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{..}, _) } -> occurencesCell tutorialTime
|
, sortable Nothing (i18nCell MsgTutorialTime) $ \DBRow{ dbrOutput = (Entity _ Tutorial{..}, _) } -> occurrencesCell tutorialTime
|
||||||
, sortable (Just "register-group") (i18nCell MsgTutorialRegGroup) $ \DBRow{ dbrOutput = (Entity _ Tutorial{..}, _) } -> maybe mempty (textCell . CI.original) tutorialRegGroup
|
, sortable (Just "register-group") (i18nCell MsgTutorialRegGroup) $ \DBRow{ dbrOutput = (Entity _ Tutorial{..}, _) } -> maybe mempty (textCell . CI.original) tutorialRegGroup
|
||||||
, sortable (Just "register-from") (i18nCell MsgTutorialRegisterFrom) $ \DBRow{ dbrOutput = (Entity _ Tutorial{..}, _) } -> maybeDateTimeCell tutorialRegisterFrom
|
, sortable (Just "register-from") (i18nCell MsgTutorialRegisterFrom) $ \DBRow{ dbrOutput = (Entity _ Tutorial{..}, _) } -> maybeDateTimeCell tutorialRegisterFrom
|
||||||
, sortable (Just "register-to") (i18nCell MsgTutorialRegisterTo) $ \DBRow{ dbrOutput = (Entity _ Tutorial{..}, _) } -> maybeDateTimeCell tutorialRegisterTo
|
, sortable (Just "register-to") (i18nCell MsgTutorialRegisterTo) $ \DBRow{ dbrOutput = (Entity _ Tutorial{..}, _) } -> maybeDateTimeCell tutorialRegisterTo
|
||||||
@ -275,7 +275,7 @@ data TutorialForm = TutorialForm
|
|||||||
, tfType :: CI Text
|
, tfType :: CI Text
|
||||||
, tfCapacity :: Maybe Int
|
, tfCapacity :: Maybe Int
|
||||||
, tfRoom :: Text
|
, tfRoom :: Text
|
||||||
, tfTime :: Occurences
|
, tfTime :: Occurrences
|
||||||
, tfRegGroup :: Maybe (CI Text)
|
, tfRegGroup :: Maybe (CI Text)
|
||||||
, tfRegisterFrom :: Maybe UTCTime
|
, tfRegisterFrom :: Maybe UTCTime
|
||||||
, tfRegisterTo :: Maybe UTCTime
|
, tfRegisterTo :: Maybe UTCTime
|
||||||
@ -322,7 +322,7 @@ tutorialForm cid template html = do
|
|||||||
<*> areq (ciField & addDatalist tutTypeDatalist) (fslpI MsgTutorialType $ mr MsgTutorialType) (tfType <$> template)
|
<*> areq (ciField & addDatalist tutTypeDatalist) (fslpI MsgTutorialType $ mr MsgTutorialType) (tfType <$> template)
|
||||||
<*> aopt (natFieldI MsgTutorialCapacityNonPositive) (fslpI MsgTutorialCapacity (mr MsgTutorialCapacity) & setTooltip MsgTutorialCapacityTip) (tfCapacity <$> template)
|
<*> aopt (natFieldI MsgTutorialCapacityNonPositive) (fslpI MsgTutorialCapacity (mr MsgTutorialCapacity) & setTooltip MsgTutorialCapacityTip) (tfCapacity <$> template)
|
||||||
<*> areq textField (fslpI MsgTutorialRoom $ mr MsgTutorialRoomPlaceholder) (tfRoom <$> template)
|
<*> areq textField (fslpI MsgTutorialRoom $ mr MsgTutorialRoomPlaceholder) (tfRoom <$> template)
|
||||||
<*> occurencesAForm ("occurences" :: Text) (tfTime <$> template)
|
<*> occurrencesAForm ("occurrences" :: Text) (tfTime <$> template)
|
||||||
<*> fmap (assertM (not . Text.null . CI.original) . fmap (CI.map Text.strip)) (aopt ciField (fslI MsgTutorialRegGroup & setTooltip MsgTutorialRegGroupTip) ((tfRegGroup <$> template) <|> Just (Just "tutorial")))
|
<*> fmap (assertM (not . Text.null . CI.original) . fmap (CI.map Text.strip)) (aopt ciField (fslI MsgTutorialRegGroup & setTooltip MsgTutorialRegGroupTip) ((tfRegGroup <$> template) <|> Just (Just "tutorial")))
|
||||||
<*> aopt utcTimeField (fslpI MsgRegisterFrom (mr MsgDate)
|
<*> aopt utcTimeField (fslpI MsgRegisterFrom (mr MsgDate)
|
||||||
& setTooltip MsgCourseRegisterFromTip
|
& setTooltip MsgCourseRegisterFromTip
|
||||||
@ -456,7 +456,7 @@ postTEditR tid ssh csh tutn = do
|
|||||||
case insertRes of
|
case insertRes of
|
||||||
Just _ -> addMessageI Error $ MsgTutorialNameTaken tfName
|
Just _ -> addMessageI Error $ MsgTutorialNameTaken tfName
|
||||||
Nothing -> do
|
Nothing -> do
|
||||||
addMessageI Success $ MsgTutorialCreated tfName
|
addMessageI Success $ MsgTutorialEdited tfName
|
||||||
redirect $ CourseR tid ssh csh CTutorialListR
|
redirect $ CourseR tid ssh csh CTutorialListR
|
||||||
|
|
||||||
let heading = prependCourseTitle tid ssh csh . MsgTutorialEditHeading $ tfName template
|
let heading = prependCourseTitle tid ssh csh . MsgTutorialEditHeading $ tfName template
|
||||||
|
|||||||
@ -30,6 +30,8 @@ instance Semigroup CorrectionInfo where
|
|||||||
mergeWith prj f = on f prj corrA corrB
|
mergeWith prj f = on f prj corrA corrB
|
||||||
|
|
||||||
keepEqual (Just x) (Just y) | x==y = Just x
|
keepEqual (Just x) (Just y) | x==y = Just x
|
||||||
|
keepEqual Nothing other = other
|
||||||
|
keepEqual other Nothing = other
|
||||||
keepEqual _ _ = Nothing
|
keepEqual _ _ = Nothing
|
||||||
|
|
||||||
instance Monoid CorrectionInfo where
|
instance Monoid CorrectionInfo where
|
||||||
|
|||||||
47
src/Handler/Utils/Exam.hs
Normal file
47
src/Handler/Utils/Exam.hs
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
module Handler.Utils.Exam
|
||||||
|
( fetchExamAux
|
||||||
|
, fetchExam, fetchExamId, fetchCourseIdExamId, fetchCourseIdExam
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Import.NoFoundation
|
||||||
|
|
||||||
|
import Database.Persist.Sql (SqlBackendCanRead)
|
||||||
|
import qualified Database.Esqueleto as E
|
||||||
|
import qualified Database.Esqueleto.Internal.Sql as E
|
||||||
|
import Database.Esqueleto.Utils.TH
|
||||||
|
|
||||||
|
import Utils.Lens
|
||||||
|
|
||||||
|
|
||||||
|
fetchExamAux :: ( SqlBackendCanRead backend
|
||||||
|
, E.SqlSelect b a
|
||||||
|
, MonadHandler m
|
||||||
|
, Typeable a
|
||||||
|
)
|
||||||
|
=> (E.SqlExpr (Entity Exam) -> E.SqlExpr (Entity Course) -> b)
|
||||||
|
-> TermId -> SchoolId -> CourseShorthand -> ExamName -> ReaderT backend m a
|
||||||
|
fetchExamAux prj tid ssh csh examn =
|
||||||
|
let cachId = encodeUtf8 $ tshow (tid, ssh, csh, examn)
|
||||||
|
in cachedBy cachId $ do
|
||||||
|
tutList <- E.select . E.from $ \(course `E.InnerJoin` tut) -> do
|
||||||
|
E.on $ course E.^. CourseId E.==. tut E.^. ExamCourse
|
||||||
|
E.where_ $ course E.^. CourseTerm E.==. E.val tid
|
||||||
|
E.&&. course E.^. CourseSchool E.==. E.val ssh
|
||||||
|
E.&&. course E.^. CourseShorthand E.==. E.val csh
|
||||||
|
E.&&. tut E.^. ExamName E.==. E.val examn
|
||||||
|
return $ prj tut course
|
||||||
|
case tutList of
|
||||||
|
[tut] -> return tut
|
||||||
|
_other -> notFound
|
||||||
|
|
||||||
|
fetchExam :: MonadHandler m => TermId -> SchoolId -> CourseShorthand -> ExamName -> ReaderT SqlBackend m (Entity Exam)
|
||||||
|
fetchExam = fetchExamAux const
|
||||||
|
|
||||||
|
fetchExamId :: MonadHandler m => TermId -> SchoolId -> CourseShorthand -> ExamName -> ReaderT SqlBackend m (Key Exam)
|
||||||
|
fetchExamId tid ssh cid examn = E.unValue <$> fetchExamAux (\tutorial _ -> tutorial E.^. ExamId) tid ssh cid examn
|
||||||
|
|
||||||
|
fetchCourseIdExamId :: MonadHandler m => TermId -> SchoolId -> CourseShorthand -> ExamName -> ReaderT SqlBackend m (Key Course, Key Exam)
|
||||||
|
fetchCourseIdExamId tid ssh cid examn = $(unValueN 2) <$> fetchExamAux (\tutorial course -> (course E.^. CourseId, tutorial E.^. ExamId)) tid ssh cid examn
|
||||||
|
|
||||||
|
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
|
||||||
@ -19,7 +19,6 @@ import qualified Data.CaseInsensitive as CI
|
|||||||
-- import Yesod.Core
|
-- import Yesod.Core
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
-- import Yesod.Form.Types
|
-- import Yesod.Form.Types
|
||||||
import Yesod.Form.Functions (parseHelper)
|
|
||||||
import Yesod.Form.Bootstrap3
|
import Yesod.Form.Bootstrap3
|
||||||
|
|
||||||
import Handler.Utils.Zip
|
import Handler.Utils.Zip
|
||||||
@ -38,8 +37,6 @@ import Control.Monad.Trans.Except (throwE, runExceptT)
|
|||||||
import Control.Monad.Writer.Class
|
import Control.Monad.Writer.Class
|
||||||
import Control.Monad.Error.Class (MonadError(..))
|
import Control.Monad.Error.Class (MonadError(..))
|
||||||
|
|
||||||
import Data.Scientific (Scientific)
|
|
||||||
import Text.Read (readMaybe)
|
|
||||||
import Data.Either (partitionEithers)
|
import Data.Either (partitionEithers)
|
||||||
|
|
||||||
import Utils.Lens
|
import Utils.Lens
|
||||||
@ -56,6 +53,9 @@ import Yesod.Core.Types (FileInfo(..))
|
|||||||
import System.FilePath (isExtensionOf)
|
import System.FilePath (isExtensionOf)
|
||||||
import Data.Text.Lens (unpacked)
|
import Data.Text.Lens (unpacked)
|
||||||
|
|
||||||
|
import Data.Char (isDigit)
|
||||||
|
import Text.Blaze (toMarkup)
|
||||||
|
|
||||||
import Handler.Utils.Form.MassInput
|
import Handler.Utils.Form.MassInput
|
||||||
|
|
||||||
----------------------------
|
----------------------------
|
||||||
@ -241,35 +241,28 @@ htmlField' = htmlField
|
|||||||
}
|
}
|
||||||
|
|
||||||
natFieldI :: (Monad m, Integral i, RenderMessage (HandlerSite m) msg, RenderMessage (HandlerSite m) FormMessage) => msg -> Field m i
|
natFieldI :: (Monad m, Integral i, RenderMessage (HandlerSite m) msg, RenderMessage (HandlerSite m) FormMessage) => msg -> Field m i
|
||||||
natFieldI msg = checkBool (>= 0) msg intField
|
natFieldI msg = convertField fromInteger toInteger $ checkBool (>= 0) msg intField
|
||||||
|
|
||||||
natField :: (Monad m, Integral i, RenderMessage (HandlerSite m) FormMessage) => Text -> Field m i
|
natField :: (Monad m, Integral i, RenderMessage (HandlerSite m) FormMessage) => Text -> Field m i
|
||||||
natField d = checkBool (>= 0) (T.append d " muss eine natürliche Zahl sein.") intField
|
natField d = convertField fromInteger toInteger $ checkBool (>= 0) (T.append d " muss eine natürliche Zahl sein.") intField
|
||||||
|
|
||||||
natIntField ::(Monad m, RenderMessage (HandlerSite m) FormMessage) => Text -> Field m Integer
|
natIntField ::(Monad m, RenderMessage (HandlerSite m) FormMessage) => Text -> Field m Integer
|
||||||
natIntField = natField
|
natIntField = natField
|
||||||
|
|
||||||
posIntField :: (Monad m, Integral i, RenderMessage (HandlerSite m) FormMessage) => Text -> Field m i
|
posIntField :: (Monad m, Integral i, RenderMessage (HandlerSite m) FormMessage) => Text -> Field m i
|
||||||
posIntField d = checkBool (> 0) (T.append d " muss eine positive Zahl sein.") intField
|
posIntField d = convertField fromInteger toInteger $ checkBool (> 0) (T.append d " muss eine positive Zahl sein.") intField
|
||||||
|
|
||||||
|
posIntFieldI :: (Monad m, Integral i, RenderMessage (HandlerSite m) msg, RenderMessage (HandlerSite m) FormMessage) => msg -> Field m i
|
||||||
|
posIntFieldI msg = convertField fromInteger toInteger $ checkBool (> 0) msg intField
|
||||||
|
|
||||||
-- | Field to request integral number > 'm'
|
-- | Field to request integral number > 'm'
|
||||||
minIntField :: (Monad m, Integral i, Show i, RenderMessage (HandlerSite m) FormMessage) => i -> Text -> Field m i
|
minIntField :: (Monad m, Integral i, Show i, RenderMessage (HandlerSite m) FormMessage) => i -> Text -> Field m i
|
||||||
minIntField m d = checkBool (> m) (T.concat [d," muss größer als ", T.pack $ show m, " sein."]) intField
|
minIntField m d = checkBool (> m) (T.concat [d," muss größer als ", T.pack $ show m, " sein."]) intField
|
||||||
|
|
||||||
pointsField :: (Monad m, HandlerSite m ~ UniWorX) => Field m Points --TODO allow fractions
|
pointsField :: (Monad m, HandlerSite m ~ UniWorX) => Field m Points
|
||||||
pointsField = checkBool (>= 0) MsgPointsNotPositive Field{..}
|
pointsField = checkBool (>= 0) MsgPointsNotPositive fixedPrecField
|
||||||
where
|
|
||||||
fieldEnctype = UrlEncoded
|
|
||||||
fieldView theId name attrs val isReq
|
|
||||||
= [whamlet|
|
|
||||||
$newline never
|
|
||||||
<input id=#{theId} name=#{name} *{attrs} type=number step="0.01" :isReq:required value=#{either id tshow val}>
|
|
||||||
|]
|
|
||||||
fieldParse = parseHelper $ \t -> do
|
|
||||||
sci <- maybe (Left $ MsgInvalidNumber t) Right (readMaybe $ unpack t :: Maybe Scientific)
|
|
||||||
return . fromRational $ round (sci * 100) % 100
|
|
||||||
|
|
||||||
pointsFieldMax :: (Monad m, HandlerSite m ~ UniWorX) => Maybe Points -> Field m Points --TODO allow fractions
|
pointsFieldMax :: (Monad m, HandlerSite m ~ UniWorX) => Maybe Points -> Field m Points
|
||||||
pointsFieldMax Nothing = pointsField
|
pointsFieldMax Nothing = pointsField
|
||||||
pointsFieldMax (Just maxp) = checkBool (<= maxp) (MsgPointsTooHigh maxp) pointsField
|
pointsFieldMax (Just maxp) = checkBool (<= maxp) (MsgPointsTooHigh maxp) pointsField
|
||||||
|
|
||||||
@ -448,6 +441,138 @@ submissionModeForm prev = multiActionA actions (fslI MsgSheetSubmissionMode) $ c
|
|||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
data ExamBonusRule' = ExamNoBonus'
|
||||||
|
| ExamBonusPoints'
|
||||||
|
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable)
|
||||||
|
instance Universe ExamBonusRule'
|
||||||
|
instance Finite ExamBonusRule'
|
||||||
|
|
||||||
|
nullaryPathPiece ''ExamBonusRule' $ camelToPathPiece' 1 . dropSuffix "'"
|
||||||
|
embedRenderMessage ''UniWorX ''ExamBonusRule' id
|
||||||
|
|
||||||
|
classifyBonusRule :: ExamBonusRule -> ExamBonusRule'
|
||||||
|
classifyBonusRule = \case
|
||||||
|
ExamNoBonus -> ExamNoBonus'
|
||||||
|
ExamBonusPoints{} -> ExamBonusPoints'
|
||||||
|
|
||||||
|
bonusRuleForm :: Maybe ExamBonusRule -> AForm Handler ExamBonusRule
|
||||||
|
bonusRuleForm prev = multiActionA actions (fslI MsgExamBonusRule) $ classifyBonusRule <$> prev
|
||||||
|
where
|
||||||
|
actions :: Map ExamBonusRule' (AForm Handler ExamBonusRule)
|
||||||
|
actions = Map.fromList
|
||||||
|
[ ( ExamNoBonus'
|
||||||
|
, pure ExamNoBonus
|
||||||
|
)
|
||||||
|
, ( ExamBonusPoints'
|
||||||
|
, ExamBonusPoints
|
||||||
|
<$> apreq (checkBool (> 0) MsgExamBonusMaxPointsNonPositive pointsField) (fslI MsgExamBonusMaxPoints) (preview _bonusMaxPoints =<< prev)
|
||||||
|
<*> (fromMaybe False <$> aopt checkBoxField (fslI MsgExamBonusOnlyPassed) (Just <$> preview _bonusOnlyPassed =<< prev))
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
data ExamOccurrenceRule' = ExamRoomManual'
|
||||||
|
| ExamRoomSurname'
|
||||||
|
| ExamRoomMatriculation'
|
||||||
|
| ExamRoomRandom'
|
||||||
|
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable)
|
||||||
|
instance Universe ExamOccurrenceRule'
|
||||||
|
instance Finite ExamOccurrenceRule'
|
||||||
|
|
||||||
|
nullaryPathPiece ''ExamOccurrenceRule' $ camelToPathPiece' 1 . dropSuffix "'"
|
||||||
|
embedRenderMessage ''UniWorX ''ExamOccurrenceRule' id
|
||||||
|
|
||||||
|
classifyExamOccurrenceRule :: ExamOccurrenceRule -> ExamOccurrenceRule'
|
||||||
|
classifyExamOccurrenceRule = \case
|
||||||
|
ExamRoomManual -> ExamRoomManual'
|
||||||
|
ExamRoomSurname -> ExamRoomSurname'
|
||||||
|
ExamRoomMatriculation -> ExamRoomMatriculation'
|
||||||
|
ExamRoomRandom -> ExamRoomRandom'
|
||||||
|
|
||||||
|
examOccurrenceRuleForm :: Maybe ExamOccurrenceRule -> AForm Handler ExamOccurrenceRule
|
||||||
|
examOccurrenceRuleForm = fmap reverseClassify . areq (selectField optionsFinite) (fslI MsgExamOccurrenceRule) . fmap classifyExamOccurrenceRule
|
||||||
|
where
|
||||||
|
reverseClassify = \case
|
||||||
|
ExamRoomManual' -> ExamRoomManual
|
||||||
|
ExamRoomSurname' -> ExamRoomSurname
|
||||||
|
ExamRoomMatriculation' -> ExamRoomMatriculation
|
||||||
|
ExamRoomRandom' -> ExamRoomRandom
|
||||||
|
|
||||||
|
data ExamGradingRule' = ExamGradingManual'
|
||||||
|
| ExamGradingKey'
|
||||||
|
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable)
|
||||||
|
instance Universe ExamGradingRule'
|
||||||
|
instance Finite ExamGradingRule'
|
||||||
|
|
||||||
|
nullaryPathPiece ''ExamGradingRule' $ camelToPathPiece' 2 . dropSuffix "'"
|
||||||
|
embedRenderMessage ''UniWorX ''ExamGradingRule' id
|
||||||
|
|
||||||
|
classifyExamGradingRule :: ExamGradingRule -> ExamGradingRule'
|
||||||
|
classifyExamGradingRule = \case
|
||||||
|
ExamGradingManual -> ExamGradingManual'
|
||||||
|
ExamGradingKey{} -> ExamGradingKey'
|
||||||
|
|
||||||
|
examGradingRuleForm :: Maybe ExamGradingRule -> AForm Handler ExamGradingRule
|
||||||
|
examGradingRuleForm prev = multiActionA actions (fslI MsgExamGradingRule) $ classifyExamGradingRule <$> prev
|
||||||
|
where
|
||||||
|
actions :: Map ExamGradingRule' (AForm Handler ExamGradingRule)
|
||||||
|
actions = Map.fromList
|
||||||
|
[ ( ExamGradingManual'
|
||||||
|
, pure ExamGradingManual
|
||||||
|
)
|
||||||
|
, ( ExamGradingKey'
|
||||||
|
, ExamGradingKey <$> gradingKeyForm (fslI MsgExamGradingKey & setTooltip MsgExamGradingKeyTip) (preview _examGradingKey =<< prev)
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
gradingKeyForm :: FieldSettings UniWorX -> Maybe [Points] -> AForm Handler [Points]
|
||||||
|
gradingKeyForm FieldSettings{..} template = formToAForm . over (mapped . _2) pure $ do
|
||||||
|
MsgRenderer mr <- getMsgRenderer
|
||||||
|
|
||||||
|
fvId <- maybe newIdent return fsId
|
||||||
|
fvName <- maybe newFormIdent return fsName
|
||||||
|
|
||||||
|
let
|
||||||
|
grades :: [ExamGrade]
|
||||||
|
grades = universeF
|
||||||
|
|
||||||
|
let boundsFS (Text.filter isDigit . toPathPiece -> g) = ""
|
||||||
|
& addPlaceholder (mr MsgPoints)
|
||||||
|
& addName (fvName <> "__" <> g)
|
||||||
|
& addId (fvId <> "__" <> g)
|
||||||
|
bounds <- forM grades $ \case
|
||||||
|
g@Grade50 -> mforced pointsField (boundsFS g) 0
|
||||||
|
grade -> mpreq pointsField (boundsFS grade) $ preview (ix . pred $ fromEnum grade) =<< template
|
||||||
|
|
||||||
|
let errors
|
||||||
|
| anyOf (folded . _1 . _FormSuccess) (< 0) bounds = [mr MsgPointsMustBeNonNegative]
|
||||||
|
| FormSuccess bounds' <- sequence $ map (view _1) bounds
|
||||||
|
, not $ monotone bounds'
|
||||||
|
= [mr MsgPointsMustBeMonotonic]
|
||||||
|
| otherwise
|
||||||
|
= []
|
||||||
|
|
||||||
|
return ( if
|
||||||
|
| null errors -> sequence . unsafeTail $ map fst bounds
|
||||||
|
| otherwise -> FormFailure errors
|
||||||
|
, FieldView
|
||||||
|
{ fvLabel = toMarkup $ mr fsLabel
|
||||||
|
, fvTooltip = toMarkup . mr <$> fsTooltip
|
||||||
|
, fvId
|
||||||
|
, fvInput = let boundWidgets = map (fvInput . snd) bounds
|
||||||
|
in $(widgetFile "widgets/gradingKey")
|
||||||
|
, fvErrors = if
|
||||||
|
| (e : _) <- errors -> Just $ toMarkup e
|
||||||
|
| otherwise -> Nothing
|
||||||
|
, fvRequired = True
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
where
|
||||||
|
monotone (x1:x2:xs) = x1 <= x2 && monotone (x2:xs)
|
||||||
|
monotone _ = True
|
||||||
|
|
||||||
|
|
||||||
pseudonymWordField :: Field Handler PseudonymWord
|
pseudonymWordField :: Field Handler PseudonymWord
|
||||||
pseudonymWordField = checkMMap doCheck CI.original $ textField & addDatalist (return $ map CI.original pseudonymWordlist)
|
pseudonymWordField = checkMMap doCheck CI.original $ textField & addDatalist (return $ map CI.original pseudonymWordlist)
|
||||||
where
|
where
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
module Handler.Utils.Form.Occurences
|
module Handler.Utils.Form.Occurrences
|
||||||
( occurencesAForm
|
( occurrencesAForm
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import Import
|
import Import
|
||||||
@ -12,33 +12,33 @@ import qualified Data.Map as Map
|
|||||||
|
|
||||||
import Utils.Lens
|
import Utils.Lens
|
||||||
|
|
||||||
|
|
||||||
data OccurenceScheduleKind = ScheduleKindWeekly
|
data OccurrenceScheduleKind = ScheduleKindWeekly
|
||||||
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic, Typeable)
|
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic, Typeable)
|
||||||
|
|
||||||
instance Universe OccurenceScheduleKind
|
instance Universe OccurrenceScheduleKind
|
||||||
instance Finite OccurenceScheduleKind
|
instance Finite OccurrenceScheduleKind
|
||||||
|
|
||||||
nullaryPathPiece ''OccurenceScheduleKind $ camelToPathPiece' 2
|
nullaryPathPiece ''OccurrenceScheduleKind $ camelToPathPiece' 2
|
||||||
embedRenderMessage ''UniWorX ''OccurenceScheduleKind id
|
embedRenderMessage ''UniWorX ''OccurrenceScheduleKind id
|
||||||
|
|
||||||
data OccurenceExceptionKind = ExceptionKindOccur
|
data OccurrenceExceptionKind = ExceptionKindOccur
|
||||||
| ExceptionKindNoOccur
|
| ExceptionKindNoOccur
|
||||||
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic, Typeable)
|
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic, Typeable)
|
||||||
|
|
||||||
instance Universe OccurenceExceptionKind
|
instance Universe OccurrenceExceptionKind
|
||||||
instance Finite OccurenceExceptionKind
|
instance Finite OccurrenceExceptionKind
|
||||||
|
|
||||||
nullaryPathPiece ''OccurenceExceptionKind $ camelToPathPiece' 2
|
nullaryPathPiece ''OccurrenceExceptionKind $ camelToPathPiece' 2
|
||||||
embedRenderMessage ''UniWorX ''OccurenceExceptionKind id
|
embedRenderMessage ''UniWorX ''OccurrenceExceptionKind id
|
||||||
|
|
||||||
|
|
||||||
occurencesAForm :: PathPiece ident => ident -> Maybe Occurences -> AForm Handler Occurences
|
occurrencesAForm :: PathPiece ident => ident -> Maybe Occurrences -> AForm Handler Occurrences
|
||||||
occurencesAForm (toPathPiece -> miIdent') mPrev = wFormToAForm $ do
|
occurrencesAForm (toPathPiece -> miIdent') mPrev = wFormToAForm $ do
|
||||||
Just cRoute <- getCurrentRoute
|
Just cRoute <- getCurrentRoute
|
||||||
|
|
||||||
let
|
let
|
||||||
scheduled :: AForm Handler (Set OccurenceSchedule)
|
scheduled :: AForm Handler (Set OccurrenceSchedule)
|
||||||
scheduled = Set.fromList <$> massInputAccumA
|
scheduled = Set.fromList <$> massInputAccumA
|
||||||
miAdd'
|
miAdd'
|
||||||
miCell'
|
miCell'
|
||||||
@ -47,16 +47,16 @@ occurencesAForm (toPathPiece -> miIdent') mPrev = wFormToAForm $ do
|
|||||||
(miIdent' <> "__scheduled" :: Text)
|
(miIdent' <> "__scheduled" :: Text)
|
||||||
(fslI MsgScheduleRegular & setTooltip MsgMassInputTip)
|
(fslI MsgScheduleRegular & setTooltip MsgMassInputTip)
|
||||||
False
|
False
|
||||||
(Set.toList . occurencesScheduled <$> mPrev)
|
(Set.toList . occurrencesScheduled <$> mPrev)
|
||||||
where
|
where
|
||||||
miAdd' :: (Text -> Text) -> FieldView UniWorX -> Form ([OccurenceSchedule] -> FormResult [OccurenceSchedule])
|
miAdd' :: (Text -> Text) -> FieldView UniWorX -> Form ([OccurrenceSchedule] -> FormResult [OccurrenceSchedule])
|
||||||
miAdd' nudge submitView = over (mapped . mapped . _2) (\addWidget -> $(widgetFile "widgets/occurence/form/scheduled-add")) . renderAForm FormStandard . wFormToAForm $ do
|
miAdd' nudge submitView = over (mapped . mapped . _2) (\addWidget -> $(widgetFile "widgets/occurrence/form/scheduled-add")) . renderAForm FormStandard . wFormToAForm $ do
|
||||||
newSched <- multiActionW
|
newSched <- multiActionW
|
||||||
(Map.fromList [ ( ScheduleKindWeekly
|
(Map.fromList [ ( ScheduleKindWeekly
|
||||||
, ScheduleWeekly
|
, ScheduleWeekly
|
||||||
<$> apreq (selectField optionsFinite) (fslI MsgWeekDay & addName (nudge "occur-week-day")) Nothing
|
<$> apreq (selectField optionsFinite) (fslI MsgWeekDay & addName (nudge "occur-week-day")) Nothing
|
||||||
<*> apreq timeFieldTypeTime (fslI MsgOccurenceStart & addName (nudge "occur-start")) Nothing
|
<*> apreq timeFieldTypeTime (fslI MsgOccurrenceStart & addName (nudge "occur-start")) Nothing
|
||||||
<*> apreq timeFieldTypeTime (fslI MsgOccurenceEnd & addName (nudge "occur-end")) Nothing
|
<*> apreq timeFieldTypeTime (fslI MsgOccurrenceEnd & addName (nudge "occur-end")) Nothing
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
) (fslI MsgScheduleRegularKind & addName (nudge "kind")) Nothing
|
) (fslI MsgScheduleRegularKind & addName (nudge "kind")) Nothing
|
||||||
@ -65,16 +65,16 @@ occurencesAForm (toPathPiece -> miIdent') mPrev = wFormToAForm $ do
|
|||||||
| newSched' `elem` oldScheds -> FormFailure [mr MsgScheduleExists]
|
| newSched' `elem` oldScheds -> FormFailure [mr MsgScheduleExists]
|
||||||
| otherwise -> FormSuccess $ pure newSched'
|
| otherwise -> FormSuccess $ pure newSched'
|
||||||
|
|
||||||
miCell' :: OccurenceSchedule -> Widget
|
miCell' :: OccurrenceSchedule -> Widget
|
||||||
miCell' ScheduleWeekly{..} = do
|
miCell' ScheduleWeekly{..} = do
|
||||||
scheduleStart' <- formatTime SelFormatTime scheduleStart
|
scheduleStart' <- formatTime SelFormatTime scheduleStart
|
||||||
scheduleEnd' <- formatTime SelFormatTime scheduleEnd
|
scheduleEnd' <- formatTime SelFormatTime scheduleEnd
|
||||||
$(widgetFile "widgets/occurence/form/weekly")
|
$(widgetFile "widgets/occurrence/form/weekly")
|
||||||
|
|
||||||
miLayout' :: MassInputLayout ListLength OccurenceSchedule ()
|
miLayout' :: MassInputLayout ListLength OccurrenceSchedule ()
|
||||||
miLayout' lLength _ cellWdgts delButtons addWdgts = $(widgetFile "widgets/occurence/form/scheduled-layout")
|
miLayout' lLength _ cellWdgts delButtons addWdgts = $(widgetFile "widgets/occurrence/form/scheduled-layout")
|
||||||
|
|
||||||
exceptions :: AForm Handler (Set OccurenceException)
|
exceptions :: AForm Handler (Set OccurrenceException)
|
||||||
exceptions = Set.fromList <$> massInputAccumA
|
exceptions = Set.fromList <$> massInputAccumA
|
||||||
miAdd'
|
miAdd'
|
||||||
miCell'
|
miCell'
|
||||||
@ -83,16 +83,16 @@ occurencesAForm (toPathPiece -> miIdent') mPrev = wFormToAForm $ do
|
|||||||
(miIdent' <> "__exceptions" :: Text)
|
(miIdent' <> "__exceptions" :: Text)
|
||||||
(fslI MsgScheduleExceptions & setTooltip (UniWorXMessages [SomeMessage MsgScheduleExceptionsTip, SomeMessage MsgMassInputTip]))
|
(fslI MsgScheduleExceptions & setTooltip (UniWorXMessages [SomeMessage MsgScheduleExceptionsTip, SomeMessage MsgMassInputTip]))
|
||||||
False
|
False
|
||||||
(Set.toList . occurencesExceptions <$> mPrev)
|
(Set.toList . occurrencesExceptions <$> mPrev)
|
||||||
where
|
where
|
||||||
miAdd' :: (Text -> Text) -> FieldView UniWorX -> Form ([OccurenceException] -> FormResult [OccurenceException])
|
miAdd' :: (Text -> Text) -> FieldView UniWorX -> Form ([OccurrenceException] -> FormResult [OccurrenceException])
|
||||||
miAdd' nudge submitView = over (mapped . mapped . _2) (\addWidget -> $(widgetFile "widgets/occurence/form/except-add")) . renderAForm FormStandard . wFormToAForm $ do
|
miAdd' nudge submitView = over (mapped . mapped . _2) (\addWidget -> $(widgetFile "widgets/occurrence/form/except-add")) . renderAForm FormStandard . wFormToAForm $ do
|
||||||
newExc <- multiActionW
|
newExc <- multiActionW
|
||||||
(Map.fromList [ ( ExceptionKindOccur
|
(Map.fromList [ ( ExceptionKindOccur
|
||||||
, ExceptOccur
|
, ExceptOccur
|
||||||
<$> apreq dayField (fslI MsgDay & addName (nudge "occur-day")) Nothing
|
<$> apreq dayField (fslI MsgDay & addName (nudge "occur-day")) Nothing
|
||||||
<*> apreq timeFieldTypeTime (fslI MsgOccurenceStart & addName (nudge "occur-start")) Nothing
|
<*> apreq timeFieldTypeTime (fslI MsgOccurrenceStart & addName (nudge "occur-start")) Nothing
|
||||||
<*> apreq timeFieldTypeTime (fslI MsgOccurenceEnd & addName (nudge "occur-end")) Nothing
|
<*> apreq timeFieldTypeTime (fslI MsgOccurrenceEnd & addName (nudge "occur-end")) Nothing
|
||||||
)
|
)
|
||||||
, ( ExceptionKindNoOccur
|
, ( ExceptionKindNoOccur
|
||||||
, ExceptNoOccur
|
, ExceptNoOccur
|
||||||
@ -104,20 +104,20 @@ occurencesAForm (toPathPiece -> miIdent') mPrev = wFormToAForm $ do
|
|||||||
return $ newExc <&> \newExc' oldExcs -> if
|
return $ newExc <&> \newExc' oldExcs -> if
|
||||||
| newExc' `elem` oldExcs -> FormFailure [mr MsgExceptionExists]
|
| newExc' `elem` oldExcs -> FormFailure [mr MsgExceptionExists]
|
||||||
| otherwise -> FormSuccess $ pure newExc'
|
| otherwise -> FormSuccess $ pure newExc'
|
||||||
|
|
||||||
|
|
||||||
miCell' :: OccurenceException -> Widget
|
|
||||||
|
miCell' :: OccurrenceException -> Widget
|
||||||
miCell' ExceptOccur{..} = do
|
miCell' ExceptOccur{..} = do
|
||||||
exceptStart' <- formatTime SelFormatDateTime (LocalTime exceptDay exceptStart)
|
exceptStart' <- formatTime SelFormatDateTime (LocalTime exceptDay exceptStart)
|
||||||
exceptEnd' <- formatTime SelFormatTime exceptEnd
|
exceptEnd' <- formatTime SelFormatTime exceptEnd
|
||||||
$(widgetFile "widgets/occurence/form/except-occur")
|
$(widgetFile "widgets/occurrence/form/except-occur")
|
||||||
miCell' ExceptNoOccur{..} = do
|
miCell' ExceptNoOccur{..} = do
|
||||||
exceptTime' <- formatTime SelFormatDateTime exceptTime
|
exceptTime' <- formatTime SelFormatDateTime exceptTime
|
||||||
$(widgetFile "widgets/occurence/form/except-no-occur")
|
$(widgetFile "widgets/occurrence/form/except-no-occur")
|
||||||
|
|
||||||
miLayout' :: MassInputLayout ListLength OccurenceException ()
|
miLayout' :: MassInputLayout ListLength OccurrenceException ()
|
||||||
miLayout' lLength _ cellWdgts delButtons addWdgts = $(widgetFile "widgets/occurence/form/except-layout")
|
miLayout' lLength _ cellWdgts delButtons addWdgts = $(widgetFile "widgets/occurrence/form/except-layout")
|
||||||
|
|
||||||
aFormToWForm $ Occurences
|
aFormToWForm $ Occurrences
|
||||||
<$> scheduled
|
<$> scheduled
|
||||||
<*> exceptions
|
<*> exceptions
|
||||||
@ -56,7 +56,7 @@ instance Pretty SheetGrading where
|
|||||||
|
|
||||||
|
|
||||||
validateRating :: SheetType -> Rating' -> [RatingException]
|
validateRating :: SheetType -> Rating' -> [RatingException]
|
||||||
validateRating ratingSheetType Rating'{ratingPoints=Just rp, ..}
|
validateRating ratingSheetType Rating'{ ratingPoints=Just rp, .. }
|
||||||
| rp < 0
|
| rp < 0
|
||||||
= [RatingNegative]
|
= [RatingNegative]
|
||||||
| NotGraded <- ratingSheetType
|
| NotGraded <- ratingSheetType
|
||||||
@ -67,6 +67,11 @@ validateRating ratingSheetType Rating'{ratingPoints=Just rp, ..}
|
|||||||
| (Just PassBinary) <- ratingSheetType ^? _grading
|
| (Just PassBinary) <- ratingSheetType ^? _grading
|
||||||
, not (rp == 0 || rp == 1)
|
, not (rp == 0 || rp == 1)
|
||||||
= [RatingBinaryExpected]
|
= [RatingBinaryExpected]
|
||||||
|
validateRating ratingSheetType Rating'{ .. }
|
||||||
|
| has _grading ratingSheetType
|
||||||
|
, is _Nothing ratingPoints
|
||||||
|
, isn't _Nothing ratingTime
|
||||||
|
= [RatingPointsRequired]
|
||||||
validateRating _ _ = []
|
validateRating _ _ = []
|
||||||
|
|
||||||
getRating :: SubmissionId -> YesodDB UniWorX (Maybe Rating)
|
getRating :: SubmissionId -> YesodDB UniWorX (Maybe Rating)
|
||||||
|
|||||||
@ -66,7 +66,9 @@ assignSubmissions :: SheetId -- ^ Sheet to distribute to correctors
|
|||||||
-> YesodDB UniWorX ( Set SubmissionId
|
-> YesodDB UniWorX ( Set SubmissionId
|
||||||
, Set SubmissionId
|
, Set SubmissionId
|
||||||
) -- ^ Returns assigned and unassigned submissions; unassigned submissions occur only if no tutors have an assigned load
|
) -- ^ Returns assigned and unassigned submissions; unassigned submissions occur only if no tutors have an assigned load
|
||||||
assignSubmissions sid restriction = planSubmissions sid restriction >>= writeSubmissionPlan
|
assignSubmissions sid restriction = do
|
||||||
|
(plan,_) <- planSubmissions sid restriction
|
||||||
|
writeSubmissionPlan plan
|
||||||
|
|
||||||
-- | Assigns all submissions according to an already given assignment plan
|
-- | Assigns all submissions according to an already given assignment plan
|
||||||
writeSubmissionPlan :: Map SubmissionId (Maybe UserId)
|
writeSubmissionPlan :: Map SubmissionId (Maybe UserId)
|
||||||
@ -89,8 +91,8 @@ writeSubmissionPlan newSubmissionData = do
|
|||||||
-- May throw an exception if there are no suitable correctors
|
-- May throw an exception if there are no suitable correctors
|
||||||
planSubmissions :: SheetId -- ^ Sheet to distribute to correctors
|
planSubmissions :: SheetId -- ^ Sheet to distribute to correctors
|
||||||
-> Maybe (Set SubmissionId) -- ^ Optionally restrict submission to consider
|
-> Maybe (Set SubmissionId) -- ^ Optionally restrict submission to consider
|
||||||
-> YesodDB UniWorX (Map SubmissionId (Maybe UserId))
|
-> YesodDB UniWorX (Map SubmissionId (Maybe UserId), Map UserId Rational)
|
||||||
-- ^ Return map that assigns submissions to Corrector
|
-- ^ Return map that assigns submissions to Corrector and another map showing each current correctors _previous_ deficit
|
||||||
planSubmissions sid restriction = do
|
planSubmissions sid restriction = do
|
||||||
Sheet{..} <- getJust sid
|
Sheet{..} <- getJust sid
|
||||||
correctorsRaw <- E.select . E.from $ \(sheet `E.InnerJoin` sheetCorrector) -> do
|
correctorsRaw <- E.select . E.from $ \(sheet `E.InnerJoin` sheetCorrector) -> do
|
||||||
@ -171,6 +173,10 @@ planSubmissions sid restriction = do
|
|||||||
-> m b
|
-> m b
|
||||||
withSubmissionData f = f <$> (mappend <$> ask <*> State.get)
|
withSubmissionData f = f <$> (mappend <$> ask <*> State.get)
|
||||||
|
|
||||||
|
-- | Old Deficit for protocol purposes, not used here
|
||||||
|
oldDeficit :: Map UserId Rational
|
||||||
|
oldDeficit = Map.mapWithKey (\k _v -> calculateDeficit k submissionData) sheetCorrectors
|
||||||
|
|
||||||
-- | How many additional submission should the given corrector be assigned, if possible?
|
-- | How many additional submission should the given corrector be assigned, if possible?
|
||||||
calculateDeficit :: UserId -> Map SubmissionId (Maybe UserId, Map UserId _, SheetId) -> Rational
|
calculateDeficit :: UserId -> Map SubmissionId (Maybe UserId, Map UserId _, SheetId) -> Rational
|
||||||
calculateDeficit corrector submissionState = getSum $ foldMap Sum deficitBySheet
|
calculateDeficit corrector submissionState = getSum $ foldMap Sum deficitBySheet
|
||||||
@ -235,7 +241,7 @@ planSubmissions sid restriction = do
|
|||||||
|
|
||||||
ix subId . _1 <~ Just <$> liftIO (Rand.uniform bestCorrectors)
|
ix subId . _1 <~ Just <$> liftIO (Rand.uniform bestCorrectors)
|
||||||
|
|
||||||
return $ fmap (view _1) newSubmissionData
|
return (fmap (view _1) newSubmissionData, oldDeficit)
|
||||||
where
|
where
|
||||||
maximumsBy :: (Ord a, Ord b) => (a -> b) -> Set a -> Set a
|
maximumsBy :: (Ord a, Ord b) => (a -> b) -> Set a -> Set a
|
||||||
maximumsBy f xs = flip Set.filter xs $ \x -> maybe True (((==) `on` f) x . maximumBy (comparing f)) $ fromNullable xs
|
maximumsBy f xs = flip Set.filter xs $ \x -> maybe True (((==) `on` f) x . maximumBy (comparing f)) $ fromNullable xs
|
||||||
|
|||||||
@ -14,7 +14,7 @@ import Text.Blaze (ToMarkup(..))
|
|||||||
import Utils.Lens
|
import Utils.Lens
|
||||||
import Handler.Utils
|
import Handler.Utils
|
||||||
|
|
||||||
import Utils.Occurences
|
import Utils.Occurrences
|
||||||
|
|
||||||
import qualified Data.Set as Set
|
import qualified Data.Set as Set
|
||||||
|
|
||||||
@ -248,19 +248,19 @@ correctorLoadCell :: IsDBTable m a => SheetCorrector -> DBCell m a
|
|||||||
correctorLoadCell sc =
|
correctorLoadCell sc =
|
||||||
i18nCell $ sheetCorrectorLoad sc
|
i18nCell $ sheetCorrectorLoad sc
|
||||||
|
|
||||||
occurencesCell :: IsDBTable m a => Occurences -> DBCell m a
|
occurrencesCell :: IsDBTable m a => Occurrences -> DBCell m a
|
||||||
occurencesCell (normalizeOccurences -> Occurences{..}) = cell $ do
|
occurrencesCell (normalizeOccurrences -> Occurrences{..}) = cell $ do
|
||||||
let occurencesScheduled' = flip map (Set.toList occurencesScheduled) $ \case
|
let occurrencesScheduled' = flip map (Set.toList occurrencesScheduled) $ \case
|
||||||
ScheduleWeekly{..} -> do
|
ScheduleWeekly{..} -> do
|
||||||
scheduleStart' <- formatTime SelFormatTime scheduleStart
|
scheduleStart' <- formatTime SelFormatTime scheduleStart
|
||||||
scheduleEnd' <- formatTime SelFormatTime scheduleEnd
|
scheduleEnd' <- formatTime SelFormatTime scheduleEnd
|
||||||
$(widgetFile "widgets/occurence/cell/weekly")
|
$(widgetFile "widgets/occurrence/cell/weekly")
|
||||||
occurencesExceptions' = flip map (Set.toList occurencesExceptions) $ \case
|
occurrencesExceptions' = flip map (Set.toList occurrencesExceptions) $ \case
|
||||||
ExceptOccur{..} -> do
|
ExceptOccur{..} -> do
|
||||||
exceptStart' <- formatTime SelFormatDateTime (LocalTime exceptDay exceptStart)
|
exceptStart' <- formatTime SelFormatDateTime (LocalTime exceptDay exceptStart)
|
||||||
exceptEnd' <- formatTime SelFormatTime exceptStart
|
exceptEnd' <- formatTime SelFormatTime exceptStart
|
||||||
$(widgetFile "widgets/occurence/cell/except-occur")
|
$(widgetFile "widgets/occurrence/cell/except-occur")
|
||||||
ExceptNoOccur{..} -> do
|
ExceptNoOccur{..} -> do
|
||||||
exceptTime' <- formatTime SelFormatDateTime exceptTime
|
exceptTime' <- formatTime SelFormatDateTime exceptTime
|
||||||
$(widgetFile "widgets/occurence/cell/except-no-occur")
|
$(widgetFile "widgets/occurrence/cell/except-no-occur")
|
||||||
$(widgetFile "widgets/occurence/cell")
|
$(widgetFile "widgets/occurrence/cell")
|
||||||
|
|||||||
10
src/Model.hs
10
src/Model.hs
@ -35,6 +35,16 @@ deriving instance Eq (Unique Course) -- instance Eq TermSchoolCourseShort; in
|
|||||||
deriving instance Eq (Unique Sheet) -- instance Eq CourseSheet
|
deriving instance Eq (Unique Sheet) -- instance Eq CourseSheet
|
||||||
deriving instance Eq (Unique Material) -- instance Eq UniqueMaterial
|
deriving instance Eq (Unique Material) -- instance Eq UniqueMaterial
|
||||||
deriving instance Eq (Unique Tutorial) -- instance Eq Tutorial
|
deriving instance Eq (Unique Tutorial) -- instance Eq Tutorial
|
||||||
|
deriving instance Eq (Unique Exam)
|
||||||
|
|
||||||
|
instance Ord User where
|
||||||
|
compare User{userSurname=surnameA, userDisplayName=displayNameA, userEmail=emailA}
|
||||||
|
User{userSurname=surnameB, userDisplayName=displayNameB, userEmail=emailB}
|
||||||
|
= compare surnameA surnameB
|
||||||
|
<> compare displayNameA displayNameB
|
||||||
|
<> compare emailA emailB -- userEmail is unique, so this suffices
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
submissionRatingDone :: Submission -> Bool
|
submissionRatingDone :: Submission -> Bool
|
||||||
submissionRatingDone Submission{..} = isJust submissionRatingTime
|
submissionRatingDone Submission{..} = isJust submissionRatingTime
|
||||||
|
|||||||
@ -288,6 +288,10 @@ customMigrations = Map.fromListWith (>>)
|
|||||||
tableDropEmpty "tutorial"
|
tableDropEmpty "tutorial"
|
||||||
tableDropEmpty "tutorial_user"
|
tableDropEmpty "tutorial_user"
|
||||||
)
|
)
|
||||||
|
, ( AppliedMigrationKey [migrationVersion|12.0.0|] [version|13.0.0|]
|
||||||
|
, whenM (tableExists "exam") $ -- Exams were an unused stub before
|
||||||
|
tableDropEmpty "exam"
|
||||||
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -31,6 +31,7 @@ data RatingException = RatingNotUnicode UnicodeException -- ^ Rating failed to p
|
|||||||
| RatingExceedsMax -- ^ Rating point must not exceed maximum points
|
| RatingExceedsMax -- ^ Rating point must not exceed maximum points
|
||||||
| RatingNotExpected -- ^ Rating not expected
|
| RatingNotExpected -- ^ Rating not expected
|
||||||
| RatingBinaryExpected -- ^ Rating must be 0 or 1
|
| RatingBinaryExpected -- ^ Rating must be 0 or 1
|
||||||
|
| RatingPointsRequired -- ^ Rating without points for sheet that requires there to be points
|
||||||
deriving (Show, Eq, Generic, Typeable)
|
deriving (Show, Eq, Generic, Typeable)
|
||||||
|
|
||||||
instance Exception RatingException
|
instance Exception RatingException
|
||||||
|
|||||||
@ -27,6 +27,8 @@ type SheetName = CI Text
|
|||||||
type MaterialName = CI Text
|
type MaterialName = CI Text
|
||||||
type UserEmail = CI Email
|
type UserEmail = CI Email
|
||||||
type TutorialName = CI Text
|
type TutorialName = CI Text
|
||||||
|
type ExamName = CI Text
|
||||||
|
type ExamPartName = CI Text
|
||||||
|
|
||||||
type PWHashAlgorithm = ByteString -> PWStore.Salt -> Int -> ByteString
|
type PWHashAlgorithm = ByteString -> PWStore.Salt -> Int -> ByteString
|
||||||
type InstanceId = UUID
|
type InstanceId = UUID
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
Module: Model.Types.DateTime
|
Module: Model.Types.DateTime
|
||||||
Description: Time related types
|
Description: Time related types
|
||||||
|
|
||||||
Terms, Seasons, and Occurence schedules
|
Terms, Seasons, and Occurrence schedules
|
||||||
-}
|
-}
|
||||||
module Model.Types.DateTime
|
module Model.Types.DateTime
|
||||||
( module Model.Types.DateTime
|
( module Model.Types.DateTime
|
||||||
@ -152,11 +152,11 @@ time `withinTerm` term = timeYear `mod` 100 == termYear `mod` 100
|
|||||||
termYear = year term
|
termYear = year term
|
||||||
|
|
||||||
|
|
||||||
data OccurenceSchedule = ScheduleWeekly
|
data OccurrenceSchedule = ScheduleWeekly
|
||||||
{ scheduleDayOfWeek :: WeekDay
|
{ scheduleDayOfWeek :: WeekDay
|
||||||
, scheduleStart :: TimeOfDay
|
, scheduleStart :: TimeOfDay
|
||||||
, scheduleEnd :: TimeOfDay
|
, scheduleEnd :: TimeOfDay
|
||||||
}
|
}
|
||||||
deriving (Eq, Ord, Read, Show, Generic, Typeable)
|
deriving (Eq, Ord, Read, Show, Generic, Typeable)
|
||||||
|
|
||||||
deriveJSON defaultOptions
|
deriveJSON defaultOptions
|
||||||
@ -164,9 +164,9 @@ deriveJSON defaultOptions
|
|||||||
, constructorTagModifier = camelToPathPiece' 1
|
, constructorTagModifier = camelToPathPiece' 1
|
||||||
, tagSingleConstructors = True
|
, tagSingleConstructors = True
|
||||||
, sumEncoding = TaggedObject "repeat" "schedule"
|
, sumEncoding = TaggedObject "repeat" "schedule"
|
||||||
} ''OccurenceSchedule
|
} ''OccurrenceSchedule
|
||||||
|
|
||||||
data OccurenceException = ExceptOccur
|
data OccurrenceException = ExceptOccur
|
||||||
{ exceptDay :: Day
|
{ exceptDay :: Day
|
||||||
, exceptStart :: TimeOfDay
|
, exceptStart :: TimeOfDay
|
||||||
, exceptEnd :: TimeOfDay
|
, exceptEnd :: TimeOfDay
|
||||||
@ -180,15 +180,15 @@ deriveJSON defaultOptions
|
|||||||
{ fieldLabelModifier = camelToPathPiece' 1
|
{ fieldLabelModifier = camelToPathPiece' 1
|
||||||
, constructorTagModifier = camelToPathPiece' 1
|
, constructorTagModifier = camelToPathPiece' 1
|
||||||
, sumEncoding = TaggedObject "exception" "for"
|
, sumEncoding = TaggedObject "exception" "for"
|
||||||
} ''OccurenceException
|
} ''OccurrenceException
|
||||||
|
|
||||||
data Occurences = Occurences
|
data Occurrences = Occurrences
|
||||||
{ occurencesScheduled :: Set OccurenceSchedule
|
{ occurrencesScheduled :: Set OccurrenceSchedule
|
||||||
, occurencesExceptions :: Set OccurenceException
|
, occurrencesExceptions :: Set OccurrenceException
|
||||||
} deriving (Eq, Ord, Read, Show, Generic, Typeable)
|
} deriving (Eq, Ord, Read, Show, Generic, Typeable)
|
||||||
|
|
||||||
deriveJSON defaultOptions
|
deriveJSON defaultOptions
|
||||||
{ fieldLabelModifier = camelToPathPiece' 1
|
{ fieldLabelModifier = camelToPathPiece' 1
|
||||||
} ''Occurences
|
} ''Occurrences
|
||||||
derivePersistFieldJSON ''Occurences
|
derivePersistFieldJSON ''Occurrences
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
{-# LANGUAGE UndecidableInstances #-}
|
||||||
|
|
||||||
{-|
|
{-|
|
||||||
Module: Model.Types.Exam
|
Module: Model.Types.Exam
|
||||||
Description: Types for modeling Exams
|
Description: Types for modeling Exams
|
||||||
@ -7,10 +9,114 @@ module Model.Types.Exam
|
|||||||
) where
|
) where
|
||||||
|
|
||||||
import Import.NoModel
|
import Import.NoModel
|
||||||
|
import Model.Types.Common
|
||||||
|
|
||||||
import Database.Persist.TH (derivePersistField)
|
import Control.Lens
|
||||||
|
|
||||||
|
data ExamResult' res = ExamAttended { examResult :: res }
|
||||||
|
| ExamNoShow
|
||||||
|
| ExamVoided
|
||||||
|
deriving (Show, Read, Eq, Ord, Generic, Typeable)
|
||||||
|
deriveJSON defaultOptions
|
||||||
|
{ constructorTagModifier = camelToPathPiece' 1
|
||||||
|
, fieldLabelModifier = camelToPathPiece' 1
|
||||||
|
, omitNothingFields = True
|
||||||
|
, sumEncoding = TaggedObject "status" "result"
|
||||||
|
} ''ExamResult'
|
||||||
|
derivePersistFieldJSON ''ExamResult'
|
||||||
|
|
||||||
data ExamStatus = Attended | NoShow | Voided
|
data ExamBonusRule = ExamNoBonus
|
||||||
deriving (Show, Read, Eq, Ord, Enum, Bounded, Generic)
|
| ExamBonusPoints
|
||||||
derivePersistField "ExamStatus"
|
{ bonusMaxPoints :: Points
|
||||||
|
, bonusOnlyPassed :: Bool
|
||||||
|
}
|
||||||
|
deriving (Show, Read, Eq, Ord, Generic, Typeable)
|
||||||
|
deriveJSON defaultOptions
|
||||||
|
{ constructorTagModifier = camelToPathPiece' 1
|
||||||
|
, fieldLabelModifier = camelToPathPiece' 1
|
||||||
|
, sumEncoding = TaggedObject "rule" "settings"
|
||||||
|
} ''ExamBonusRule
|
||||||
|
derivePersistFieldJSON ''ExamBonusRule
|
||||||
|
|
||||||
|
data ExamOccurrenceRule = ExamRoomManual
|
||||||
|
| ExamRoomSurname
|
||||||
|
| ExamRoomMatriculation
|
||||||
|
| ExamRoomRandom
|
||||||
|
deriving (Show, Read, Eq, Ord, Generic, Typeable)
|
||||||
|
deriveJSON defaultOptions
|
||||||
|
{ constructorTagModifier = camelToPathPiece' 2
|
||||||
|
, fieldLabelModifier = camelToPathPiece' 1
|
||||||
|
, sumEncoding = TaggedObject "rule" "settings"
|
||||||
|
} ''ExamOccurrenceRule
|
||||||
|
derivePersistFieldJSON ''ExamOccurrenceRule
|
||||||
|
|
||||||
|
data ExamGrade
|
||||||
|
= Grade50
|
||||||
|
| Grade40
|
||||||
|
| Grade37
|
||||||
|
| Grade33
|
||||||
|
| Grade30
|
||||||
|
| Grade27
|
||||||
|
| Grade23
|
||||||
|
| Grade20
|
||||||
|
| Grade17
|
||||||
|
| Grade13
|
||||||
|
| Grade10
|
||||||
|
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable)
|
||||||
|
instance Universe ExamGrade
|
||||||
|
instance Finite ExamGrade
|
||||||
|
|
||||||
|
numberGrade :: Prism' Rational ExamGrade
|
||||||
|
numberGrade = prism toNumberGrade fromNumberGrade
|
||||||
|
where
|
||||||
|
toNumberGrade = \case
|
||||||
|
Grade50 -> 5.0
|
||||||
|
Grade40 -> 4.0
|
||||||
|
Grade37 -> 3.7
|
||||||
|
Grade33 -> 3.3
|
||||||
|
Grade30 -> 3.0
|
||||||
|
Grade27 -> 2.7
|
||||||
|
Grade23 -> 2.3
|
||||||
|
Grade20 -> 2.0
|
||||||
|
Grade17 -> 1.7
|
||||||
|
Grade13 -> 1.3
|
||||||
|
Grade10 -> 1.0
|
||||||
|
fromNumberGrade = \case
|
||||||
|
5.0 -> Right Grade50
|
||||||
|
4.0 -> Right Grade40
|
||||||
|
3.7 -> Right Grade37
|
||||||
|
3.3 -> Right Grade33
|
||||||
|
3.0 -> Right Grade30
|
||||||
|
2.7 -> Right Grade27
|
||||||
|
2.3 -> Right Grade23
|
||||||
|
2.0 -> Right Grade20
|
||||||
|
1.7 -> Right Grade17
|
||||||
|
1.3 -> Right Grade13
|
||||||
|
1.0 -> Right Grade10
|
||||||
|
n -> Left n
|
||||||
|
|
||||||
|
instance PathPiece ExamGrade where
|
||||||
|
toPathPiece = tshow . (fromRational :: Rational -> Deci) . review numberGrade
|
||||||
|
fromPathPiece = finiteFromPathPiece
|
||||||
|
|
||||||
|
pathPieceJSON ''ExamGrade
|
||||||
|
pathPieceJSONKey ''ExamGrade
|
||||||
|
|
||||||
|
passingGrade :: ExamGrade -> Bool
|
||||||
|
passingGrade = (>= Grade40)
|
||||||
|
|
||||||
|
data ExamGradingRule
|
||||||
|
= ExamGradingManual
|
||||||
|
| ExamGradingKey
|
||||||
|
{ examGradingKey :: [Points] -- ^ @[n1, n2, n3, ..., n11]@ means @0 <= p < n1 -> p ~= 5@, @n1 <= p < n2 -> p ~ 4.7@, @n2 <= p < n3 -> p ~ 4.3@, ..., @n11 <= p -> p ~ 1.0@
|
||||||
|
}
|
||||||
|
deriving (Eq, Ord, Read, Show, Generic, Typeable)
|
||||||
|
deriveJSON defaultOptions
|
||||||
|
{ constructorTagModifier = camelToPathPiece' 2
|
||||||
|
, fieldLabelModifier = camelToPathPiece' 2
|
||||||
|
, sumEncoding = TaggedObject "rule" "settings"
|
||||||
|
} ''ExamGradingRule
|
||||||
|
derivePersistFieldJSON ''ExamGradingRule
|
||||||
|
|
||||||
|
type ExamResultPoints = ExamResult' (Maybe Points)
|
||||||
|
type ExamResultGrade = ExamResult' ExamGrade
|
||||||
|
|||||||
@ -42,6 +42,7 @@ data AuthTag -- sortiert nach gewünschter Reihenfolge auf /authpreds, d.h. Prä
|
|||||||
| AuthTutor
|
| AuthTutor
|
||||||
| AuthCourseRegistered
|
| AuthCourseRegistered
|
||||||
| AuthTutorialRegistered
|
| AuthTutorialRegistered
|
||||||
|
| AuthExamRegistered
|
||||||
| AuthParticipant
|
| AuthParticipant
|
||||||
| AuthTime
|
| AuthTime
|
||||||
| AuthMaterials
|
| AuthMaterials
|
||||||
|
|||||||
62
src/Utils.hs
62
src/Utils.hs
@ -2,12 +2,13 @@ module Utils
|
|||||||
( module Utils
|
( module Utils
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import ClassyPrelude.Yesod hiding (foldlM)
|
import ClassyPrelude.Yesod hiding (foldlM, Proxy)
|
||||||
|
|
||||||
-- import Data.Double.Conversion.Text -- faster implementation for textPercent?
|
-- import Data.Double.Conversion.Text -- faster implementation for textPercent?
|
||||||
import qualified Data.Foldable as Fold
|
import qualified Data.Foldable as Fold
|
||||||
import Data.Foldable as Utils (foldlM, foldrM)
|
import Data.Foldable as Utils (foldlM, foldrM)
|
||||||
import Data.Monoid (Sum(..))
|
import Data.Monoid (Sum(..))
|
||||||
|
import Data.Proxy
|
||||||
|
|
||||||
import Data.CaseInsensitive (CI)
|
import Data.CaseInsensitive (CI)
|
||||||
import qualified Data.CaseInsensitive as CI
|
import qualified Data.CaseInsensitive as CI
|
||||||
@ -67,7 +68,7 @@ import qualified Crypto.Saltine.Class as Saltine
|
|||||||
import qualified Crypto.Data.PKCS7 as PKCS7
|
import qualified Crypto.Data.PKCS7 as PKCS7
|
||||||
|
|
||||||
import Data.Fixed
|
import Data.Fixed
|
||||||
import Data.Ratio ((%))
|
-- import Data.Ratio ((%))
|
||||||
|
|
||||||
import Data.Binary (Binary)
|
import Data.Binary (Binary)
|
||||||
import qualified Data.Binary as Binary
|
import qualified Data.Binary as Binary
|
||||||
@ -136,9 +137,25 @@ fontAwesomeIcon iconName =
|
|||||||
iconQuestion :: Markup
|
iconQuestion :: Markup
|
||||||
iconQuestion = fontAwesomeIcon "question-circle"
|
iconQuestion = fontAwesomeIcon "question-circle"
|
||||||
|
|
||||||
|
iconNew :: Markup
|
||||||
|
iconNew = fontAwesomeIcon "seedling"
|
||||||
|
|
||||||
|
iconOK :: Markup
|
||||||
|
iconOK = fontAwesomeIcon "check"
|
||||||
|
|
||||||
|
iconNotOK :: Markup
|
||||||
|
iconNotOK = fontAwesomeIcon "times"
|
||||||
|
|
||||||
|
iconWarning :: Markup
|
||||||
|
iconWarning = fontAwesomeIcon "exclamation"
|
||||||
|
|
||||||
|
iconProblem :: Markup
|
||||||
|
iconProblem = fontAwesomeIcon "bolt"
|
||||||
|
|
||||||
iconHint :: Markup
|
iconHint :: Markup
|
||||||
iconHint = fontAwesomeIcon "life-ring"
|
iconHint = fontAwesomeIcon "life-ring"
|
||||||
|
|
||||||
|
-- Icons for SheetFileType
|
||||||
iconSolution :: Markup
|
iconSolution :: Markup
|
||||||
iconSolution =fontAwesomeIcon "exclamation-circle"
|
iconSolution =fontAwesomeIcon "exclamation-circle"
|
||||||
|
|
||||||
@ -177,21 +194,21 @@ hasComment False = fontAwesomeIcon "comment-slash" -- comment-alt-slash is not a
|
|||||||
|
|
||||||
hasTickmark :: Bool -> Markup
|
hasTickmark :: Bool -> Markup
|
||||||
-- ^ Display an icon that denotes that something™ is okay
|
-- ^ Display an icon that denotes that something™ is okay
|
||||||
hasTickmark True = fontAwesomeIcon "check"
|
hasTickmark True = iconOK
|
||||||
hasTickmark False = mempty
|
hasTickmark False = mempty
|
||||||
|
|
||||||
isBad :: Bool -> Markup
|
isBad :: Bool -> Markup
|
||||||
-- ^ Display an icon that denotes that something™ is bad
|
-- ^ Display an icon that denotes that something™ is bad
|
||||||
isBad True = fontAwesomeIcon "bolt" -- or times?!
|
isBad True = iconProblem
|
||||||
isBad False = mempty
|
isBad False = mempty
|
||||||
|
|
||||||
isNew :: Bool -> Markup
|
isNew :: Bool -> Markup
|
||||||
isNew True = fontAwesomeIcon "seedling" -- was exclamation
|
isNew True = iconNew
|
||||||
isNew False = mempty
|
isNew False = mempty
|
||||||
|
|
||||||
boolSymbol :: Bool -> Markup
|
boolSymbol :: Bool -> Markup
|
||||||
boolSymbol True = fontAwesomeIcon "check"
|
boolSymbol True = iconOK
|
||||||
boolSymbol False = fontAwesomeIcon "times"
|
boolSymbol False = iconNotOK
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -293,15 +310,28 @@ instance {-# OVERLAPPABLE #-} Show a => DisplayAble a where -- The easy way out
|
|||||||
display = pack . show
|
display = pack . show
|
||||||
-}
|
-}
|
||||||
|
|
||||||
textPercent :: Real a => a -> Text -- slow, maybe use Data.Double.Conversion.Text.toFixed instead?
|
-- | Convert `part` and `whole` into percentage including symbol
|
||||||
textPercent x = lz <> pack (show rx) <> "%"
|
-- showing trailing zeroes and to decimal digits
|
||||||
where
|
textPercent :: Real a => a -> a -> Text
|
||||||
rx :: Centi
|
textPercent = textPercent' False 2
|
||||||
rx = realToFrac (x * 100)
|
|
||||||
lz = if rx < 10.0 then "0" else ""
|
-- | Convert `part` and `whole` into percentage including symbol
|
||||||
|
-- `trailZero` shows trailing Zeros, `precision` is number of decimal digits
|
||||||
|
textPercent' :: Real a => Bool -> Int -> a -> a -> Text
|
||||||
|
textPercent' trailZero precision part whole
|
||||||
|
| precision == 0 = showPercent (frac :: Uni)
|
||||||
|
| precision == 1 = showPercent (frac :: Deci)
|
||||||
|
| precision == 2 = showPercent (frac :: Centi)
|
||||||
|
| precision == 3 = showPercent (frac :: Milli)
|
||||||
|
| precision == 4 = showPercent (frac :: Micro)
|
||||||
|
| otherwise = showPercent (frac :: Pico)
|
||||||
|
where
|
||||||
|
frac :: forall a . HasResolution a => Fixed a
|
||||||
|
frac = MkFixed $ round $ (* (fromInteger $ resolution (Proxy :: Proxy a))) $ (100*) $ toRational part / toRational whole
|
||||||
|
|
||||||
|
showPercent :: HasResolution a => Fixed a -> Text
|
||||||
|
showPercent f = pack $ showFixed trailZero f <> "%"
|
||||||
|
|
||||||
textPercentInt :: Integral a => a -> a -> Text -- slow, maybe use Data.Double.Conversion.Text.toFixed instead?
|
|
||||||
textPercentInt part whole = textPercent $ fromIntegral part % fromIntegral whole
|
|
||||||
|
|
||||||
-- | Convert number of bytes to human readable format
|
-- | Convert number of bytes to human readable format
|
||||||
textBytes :: Integral a => a -> Text
|
textBytes :: Integral a => a -> Text
|
||||||
@ -386,7 +416,7 @@ cutOffPercent :: Double -> Double -> Double -> Double
|
|||||||
cutOffPercent offset full achieved
|
cutOffPercent offset full achieved
|
||||||
| full <= achieved = 0
|
| full <= achieved = 0
|
||||||
| full <= 0 = 0
|
| full <= 0 = 0
|
||||||
| otherwise = offset + (1-offset * (1 - percent))
|
| otherwise = offset + (1-offset) * (1 - percent)
|
||||||
where
|
where
|
||||||
percent = achieved / full
|
percent = achieved / full
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
||||||
{-# OPTIONS_GHC -fno-warn-deprecations #-}
|
{-# OPTIONS_GHC -fno-warn-deprecations #-}
|
||||||
|
|
||||||
module Utils.Form where
|
module Utils.Form where
|
||||||
@ -23,8 +24,11 @@ import qualified Data.Set as Set
|
|||||||
import Control.Monad.Trans.Maybe (MaybeT(..))
|
import Control.Monad.Trans.Maybe (MaybeT(..))
|
||||||
import Control.Monad.Reader.Class (MonadReader(..))
|
import Control.Monad.Reader.Class (MonadReader(..))
|
||||||
import Control.Monad.Writer.Class (MonadWriter(..))
|
import Control.Monad.Writer.Class (MonadWriter(..))
|
||||||
import Control.Monad.Trans.RWS (mapRWST)
|
import Control.Monad.State.Class (MonadState(..))
|
||||||
|
import Control.Monad.Trans.RWS (RWST, execRWST, mapRWST)
|
||||||
import Control.Monad.Trans.Except (ExceptT, runExceptT)
|
import Control.Monad.Trans.Except (ExceptT, runExceptT)
|
||||||
|
import Control.Monad.Fix (MonadFix)
|
||||||
|
import Control.Monad.Morph (MFunctor(..))
|
||||||
|
|
||||||
import Data.List ((!!))
|
import Data.List ((!!))
|
||||||
|
|
||||||
@ -34,6 +38,10 @@ import Web.PathPieces
|
|||||||
|
|
||||||
import Data.UUID
|
import Data.UUID
|
||||||
|
|
||||||
|
import Data.Ratio ((%))
|
||||||
|
import Data.Fixed
|
||||||
|
import Data.Scientific
|
||||||
|
|
||||||
import Utils
|
import Utils
|
||||||
-- import Utils.Message
|
-- import Utils.Message
|
||||||
-- import Utils.PathPiece
|
-- import Utils.PathPiece
|
||||||
@ -41,6 +49,10 @@ import Utils
|
|||||||
|
|
||||||
import Data.Proxy
|
import Data.Proxy
|
||||||
|
|
||||||
|
import Text.HTML.SanitizeXSS (sanitizeBalance)
|
||||||
|
import Text.Blaze (preEscapedText)
|
||||||
|
import Text.Blaze.Html.Renderer.Pretty (renderHtml)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -156,6 +168,11 @@ inputReadonly = addAttr "readonly" ""
|
|||||||
addAutosubmit :: FieldSettings site -> FieldSettings site
|
addAutosubmit :: FieldSettings site -> FieldSettings site
|
||||||
addAutosubmit = addAttr "uw-auto-submit-input" ""
|
addAutosubmit = addAttr "uw-auto-submit-input" ""
|
||||||
|
|
||||||
|
-- | Asynchronous Submit, e.g. use with forms in modals
|
||||||
|
asyncSubmitAttr :: (Text,Text)
|
||||||
|
asyncSubmitAttr = ("uw-async-form", "")
|
||||||
|
|
||||||
|
|
||||||
------------------------------------------------
|
------------------------------------------------
|
||||||
-- Unique Form Identifiers to avoid accidents --
|
-- Unique Form Identifiers to avoid accidents --
|
||||||
------------------------------------------------
|
------------------------------------------------
|
||||||
@ -474,8 +491,52 @@ optionsFinite = do
|
|||||||
}
|
}
|
||||||
return . mkOptionList $ mkOption <$> universeF
|
return . mkOptionList $ mkOption <$> universeF
|
||||||
|
|
||||||
|
fractionalField :: forall m a.
|
||||||
|
( RealFrac a
|
||||||
|
, Monad m
|
||||||
|
, RenderMessage (HandlerSite m) FormMessage
|
||||||
|
) => Field m a
|
||||||
|
-- | Form `Field` for any `Fractional` number
|
||||||
|
--
|
||||||
|
-- Use more specific `Field`s (i.e. `fixedPrecField`) whenever they exist
|
||||||
|
fractionalField = Field{..}
|
||||||
|
where
|
||||||
|
scientific' :: Iso' a Scientific
|
||||||
|
scientific' = iso (fromRational . toRational) (fromRational . toRational)
|
||||||
|
|
||||||
|
fieldEnctype = UrlEncoded
|
||||||
|
fieldView theId name attrs (fmap $ view scientific' -> val) isReq
|
||||||
|
= [whamlet|
|
||||||
|
$newline never
|
||||||
|
<input id=#{theId} name=#{name} *{attrs} type=number :isReq:required value=#{either id (pack . formatScientific Fixed Nothing) val}>
|
||||||
|
|]
|
||||||
|
fieldParse = parseHelper $ \t ->
|
||||||
|
maybe (Left $ MsgInvalidNumber t) (Right . review scientific') (readMay t :: Maybe Scientific)
|
||||||
|
|
||||||
|
fixedPrecField :: forall m p.
|
||||||
|
( Monad m
|
||||||
|
, RenderMessage (HandlerSite m) FormMessage
|
||||||
|
, HasResolution p
|
||||||
|
) => Field m (Fixed p)
|
||||||
|
fixedPrecField = Field{..}
|
||||||
|
where
|
||||||
|
resolution' :: Integer
|
||||||
|
resolution' = resolution $ Proxy @p
|
||||||
|
|
||||||
|
step = showFixed True (fromRational $ 1 % resolution' :: Fixed p)
|
||||||
|
|
||||||
|
fieldEnctype = UrlEncoded
|
||||||
|
fieldView theId name attrs val isReq
|
||||||
|
= [whamlet|
|
||||||
|
$newline never
|
||||||
|
<input id=#{theId} name=#{name} *{attrs} type=number step=#{step} :isReq:required value=#{either id (pack . showFixed True) val}>
|
||||||
|
|]
|
||||||
|
fieldParse = parseHelper $ \t -> do
|
||||||
|
sci <- maybe (Left $ MsgInvalidNumber t) Right (readMay t :: Maybe Scientific)
|
||||||
|
return . fromRational $ round (sci * fromIntegral resolution') % resolution'
|
||||||
|
|
||||||
rationalField :: (MonadHandler m, RenderMessage (HandlerSite m) FormMessage) => Field m Rational
|
rationalField :: (MonadHandler m, RenderMessage (HandlerSite m) FormMessage) => Field m Rational
|
||||||
rationalField = convertField toRational fromRational doubleField
|
rationalField = fractionalField
|
||||||
|
|
||||||
data SecretJSONFieldException = SecretJSONFieldDecryptFailure
|
data SecretJSONFieldException = SecretJSONFieldDecryptFailure
|
||||||
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable)
|
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable)
|
||||||
@ -501,6 +562,12 @@ secretJsonField = Field{..}
|
|||||||
|]
|
|]
|
||||||
fieldEnctype = UrlEncoded
|
fieldEnctype = UrlEncoded
|
||||||
|
|
||||||
|
htmlFieldSmall :: forall m. (Monad m, RenderMessage (HandlerSite m) FormMessage) => Field m Html
|
||||||
|
htmlFieldSmall = checkMMap sanitize (pack . renderHtml) textField
|
||||||
|
where
|
||||||
|
sanitize :: Text -> m (Either FormMessage Html)
|
||||||
|
sanitize = return . Right . preEscapedText . sanitizeBalance
|
||||||
|
|
||||||
-----------
|
-----------
|
||||||
-- Forms --
|
-- Forms --
|
||||||
-----------
|
-----------
|
||||||
@ -721,6 +788,57 @@ prismAForm p outer form = review p <$> form inner
|
|||||||
where
|
where
|
||||||
inner = outer >>= preview p
|
inner = outer >>= preview p
|
||||||
|
|
||||||
|
newtype FormValidator r m a = FormValidator { unFormValidator :: RWST () [SomeMessage (HandlerSite m)] r m a }
|
||||||
|
|
||||||
|
deriving newtype instance Functor m => Functor (FormValidator r m)
|
||||||
|
deriving newtype instance Monad m => Applicative (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 MonadFix m => MonadFix (FormValidator r m)
|
||||||
|
instance MonadTrans (FormValidator r) where
|
||||||
|
lift = FormValidator . lift
|
||||||
|
|
||||||
|
validateForm :: MonadHandler m
|
||||||
|
=> FormValidator a m ()
|
||||||
|
-> (Markup -> MForm m (FormResult a, xml))
|
||||||
|
-> (Markup -> MForm m (FormResult a, xml))
|
||||||
|
validateForm valF form csrf = do
|
||||||
|
(res, xml) <- form csrf
|
||||||
|
res' <- for res $ lift . execRWST (unFormValidator valF) ()
|
||||||
|
(, xml) <$> case res' of
|
||||||
|
FormSuccess (x, [] ) -> return $ FormSuccess x
|
||||||
|
FormSuccess (_, msgs) -> formFailure msgs
|
||||||
|
FormMissing -> return FormMissing
|
||||||
|
FormFailure errs -> return $ FormFailure errs
|
||||||
|
|
||||||
|
validateFormDB :: ( MonadHandler m
|
||||||
|
, YesodPersist (HandlerSite m)
|
||||||
|
)
|
||||||
|
=> FormValidator a (YesodDB (HandlerSite m)) ()
|
||||||
|
-> (Markup -> MForm m (FormResult a, xml))
|
||||||
|
-> (Markup -> MForm m (FormResult a, xml))
|
||||||
|
validateFormDB (FormValidator valF) = validateForm . FormValidator $ hoist (liftHandlerT . runDB) valF
|
||||||
|
|
||||||
|
tellValidationError :: ( MonadHandler m
|
||||||
|
, RenderMessage (HandlerSite m) msg
|
||||||
|
)
|
||||||
|
=> msg -> FormValidator r m ()
|
||||||
|
tellValidationError = FormValidator . tell . pure . SomeMessage
|
||||||
|
|
||||||
|
guardValidation :: ( MonadHandler m
|
||||||
|
, RenderMessage (HandlerSite m) msg
|
||||||
|
)
|
||||||
|
=> msg -- ^ Message describing violation
|
||||||
|
-> Bool -- ^ @False@ iff constraint is violated
|
||||||
|
-> FormValidator r m ()
|
||||||
|
guardValidation msg isValid = when (not isValid) $ tellValidationError msg
|
||||||
|
|
||||||
|
guardValidationM :: ( MonadHandler m
|
||||||
|
, RenderMessage (HandlerSite m) msg
|
||||||
|
)
|
||||||
|
=> msg -> m Bool -> FormValidator r m ()
|
||||||
|
guardValidationM = (. lift) . (=<<) . guardValidation
|
||||||
|
|
||||||
-----------------------
|
-----------------------
|
||||||
-- Form Manipulation --
|
-- Form Manipulation --
|
||||||
-----------------------
|
-----------------------
|
||||||
|
|||||||
@ -111,18 +111,23 @@ makeLenses_ ''SubmissionMode
|
|||||||
|
|
||||||
makePrisms ''E.Value
|
makePrisms ''E.Value
|
||||||
|
|
||||||
makeLenses_ ''OccurenceSchedule
|
makeLenses_ ''OccurrenceSchedule
|
||||||
|
|
||||||
makePrisms ''OccurenceSchedule
|
makePrisms ''OccurrenceSchedule
|
||||||
|
|
||||||
makeLenses_ ''OccurenceException
|
makeLenses_ ''OccurrenceException
|
||||||
|
|
||||||
makePrisms ''OccurenceException
|
makePrisms ''OccurrenceException
|
||||||
|
|
||||||
makeLenses_ ''Occurences
|
makeLenses_ ''Occurrences
|
||||||
|
|
||||||
makeLenses_ ''PredDNF
|
makeLenses_ ''PredDNF
|
||||||
|
|
||||||
|
makeLenses_ ''ExamBonusRule
|
||||||
|
makeLenses_ ''ExamGradingRule
|
||||||
|
|
||||||
|
makeLenses_ ''UTCTime
|
||||||
|
|
||||||
|
|
||||||
-- makeClassy_ ''Load
|
-- makeClassy_ ''Load
|
||||||
|
|
||||||
@ -132,6 +137,6 @@ makeLenses_ ''PredDNF
|
|||||||
|
|
||||||
class HasInstanceID s a | s -> a where
|
class HasInstanceID s a | s -> a where
|
||||||
instanceID :: Lens' s a
|
instanceID :: Lens' s a
|
||||||
|
|
||||||
class HasJSONWebKeySet s a | s -> a where
|
class HasJSONWebKeySet s a | s -> a where
|
||||||
jsonWebKeySet :: Lens' s a
|
jsonWebKeySet :: Lens' s a
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{-# OPTIONS_GHC -fno-warn-overlapping-patterns #-}
|
{-# OPTIONS_GHC -fno-warn-overlapping-patterns #-}
|
||||||
|
|
||||||
module Utils.Occurences
|
module Utils.Occurrences
|
||||||
( normalizeOccurences
|
( normalizeOccurrences
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import ClassyPrelude
|
import ClassyPrelude
|
||||||
@ -20,21 +20,21 @@ import Data.Time
|
|||||||
import Data.Time.Calendar.WeekDate
|
import Data.Time.Calendar.WeekDate
|
||||||
|
|
||||||
|
|
||||||
normalizeOccurences :: Occurences -> Occurences
|
normalizeOccurrences :: Occurrences -> Occurrences
|
||||||
-- ^
|
-- ^
|
||||||
--
|
--
|
||||||
-- - Removes unnecessary exceptions
|
-- - Removes unnecessary exceptions
|
||||||
-- - Merges overlapping schedules
|
-- - Merges overlapping schedules
|
||||||
normalizeOccurences initial
|
normalizeOccurrences initial
|
||||||
| Left new <- runReader (runExceptT go) initial
|
| Left new <- runReader (runExceptT go) initial
|
||||||
= normalizeOccurences new
|
= normalizeOccurrences new
|
||||||
| otherwise
|
| otherwise
|
||||||
= initial
|
= initial
|
||||||
where
|
where
|
||||||
go :: ExceptT Occurences (Reader Occurences) ()
|
go :: ExceptT Occurrences (Reader Occurrences) ()
|
||||||
-- Find some inconsistency and `throwE` a version without it
|
-- Find some inconsistency and `throwE` a version without it
|
||||||
go = do
|
go = do
|
||||||
scheduled <- view _occurencesScheduled
|
scheduled <- view _occurrencesScheduled
|
||||||
forM_ scheduled $ \case
|
forM_ scheduled $ \case
|
||||||
a@ScheduleWeekly{} -> do
|
a@ScheduleWeekly{} -> do
|
||||||
let
|
let
|
||||||
@ -50,35 +50,35 @@ normalizeOccurences initial
|
|||||||
| otherwise
|
| otherwise
|
||||||
= Nothing
|
= Nothing
|
||||||
merge _ = Nothing
|
merge _ = Nothing
|
||||||
merges <- views _occurencesScheduled $ mapMaybe (\b -> (,) <$> pure b <*> merge b) . Set.toList . Set.delete a
|
merges <- views _occurrencesScheduled $ mapMaybe (\b -> (,) <$> pure b <*> merge b) . Set.toList . Set.delete a
|
||||||
case merges of
|
case merges of
|
||||||
[] -> return ()
|
[] -> return ()
|
||||||
((b, merged) : _) -> throwE =<< asks (over _occurencesScheduled $ Set.insert merged . Set.delete b . Set.delete a)
|
((b, merged) : _) -> throwE =<< asks (over _occurrencesScheduled $ Set.insert merged . Set.delete b . Set.delete a)
|
||||||
|
|
||||||
exceptions <- view _occurencesExceptions
|
exceptions <- view _occurrencesExceptions
|
||||||
forM_ exceptions $ \case
|
forM_ exceptions $ \case
|
||||||
needle@ExceptNoOccur{..} -> do
|
needle@ExceptNoOccur{..} -> do
|
||||||
let LocalTime{..} = exceptTime
|
let LocalTime{..} = exceptTime
|
||||||
(_, _, toEnum . (`mod` 7) -> localWeekDay) = toWeekDate localDay
|
(_, _, toEnum . (`mod` 7) -> localWeekDay) = toWeekDate localDay
|
||||||
needed <- views _occurencesScheduled . any $ \case
|
needed <- views _occurrencesScheduled . any $ \case
|
||||||
ScheduleWeekly{..} -> and
|
ScheduleWeekly{..} -> and
|
||||||
[ scheduleDayOfWeek == localWeekDay
|
[ scheduleDayOfWeek == localWeekDay
|
||||||
, scheduleStart <= localTimeOfDay
|
, scheduleStart <= localTimeOfDay
|
||||||
, localTimeOfDay <= scheduleEnd
|
, localTimeOfDay <= scheduleEnd
|
||||||
]
|
]
|
||||||
unless needed $
|
unless needed $
|
||||||
throwE =<< asks (over _occurencesExceptions $ Set.delete needle)
|
throwE =<< asks (over _occurrencesExceptions $ Set.delete needle)
|
||||||
needle@ExceptOccur{..} -> do
|
needle@ExceptOccur{..} -> do
|
||||||
let (_, _, toEnum . (`mod` 7) -> localWeekDay) = toWeekDate exceptDay
|
let (_, _, toEnum . (`mod` 7) -> localWeekDay) = toWeekDate exceptDay
|
||||||
-- | Does this ExceptNoOccur target within needle?
|
-- | Does this ExceptNoOccur target within needle?
|
||||||
withinNeedle ExceptNoOccur{..} = LocalTime exceptDay exceptStart <= exceptTime
|
withinNeedle ExceptNoOccur{..} = LocalTime exceptDay exceptStart <= exceptTime
|
||||||
&& exceptTime <= LocalTime exceptDay exceptEnd
|
&& exceptTime <= LocalTime exceptDay exceptEnd
|
||||||
withinNeedle _ = False
|
withinNeedle _ = False
|
||||||
needed <- views _occurencesScheduled . none $ \case
|
needed <- views _occurrencesScheduled . none $ \case
|
||||||
ScheduleWeekly{..} -> and
|
ScheduleWeekly{..} -> and
|
||||||
[ scheduleDayOfWeek == localWeekDay
|
[ scheduleDayOfWeek == localWeekDay
|
||||||
, scheduleStart == exceptStart
|
, scheduleStart == exceptStart
|
||||||
, scheduleEnd == exceptEnd
|
, scheduleEnd == exceptEnd
|
||||||
]
|
]
|
||||||
unless needed $
|
unless needed $
|
||||||
throwE =<< asks (over _occurencesExceptions $ Set.filter (not . withinNeedle) . Set.delete needle)
|
throwE =<< asks (over _occurrencesExceptions $ Set.filter (not . withinNeedle) . Set.delete needle)
|
||||||
@ -5,7 +5,7 @@ module Utils.PathPiece
|
|||||||
, splitCamel
|
, splitCamel
|
||||||
, camelToPathPiece, camelToPathPiece'
|
, camelToPathPiece, camelToPathPiece'
|
||||||
, tuplePathPiece
|
, tuplePathPiece
|
||||||
, pathPieceJSONKey
|
, pathPieceJSON, pathPieceJSONKey
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import ClassyPrelude.Yesod
|
import ClassyPrelude.Yesod
|
||||||
@ -25,6 +25,7 @@ import Numeric.Natural
|
|||||||
import Data.List (foldl)
|
import Data.List (foldl)
|
||||||
|
|
||||||
import Data.Aeson.Types
|
import Data.Aeson.Types
|
||||||
|
import qualified Data.Aeson.Types as Aeson
|
||||||
|
|
||||||
|
|
||||||
finiteFromPathPiece :: (PathPiece a, Finite a) => Text -> Maybe a
|
finiteFromPathPiece :: (PathPiece a, Finite a) => Text -> Maybe a
|
||||||
@ -120,5 +121,14 @@ pathPieceJSONKey tName
|
|||||||
= [d| instance ToJSONKey $(conT tName) where
|
= [d| instance ToJSONKey $(conT tName) where
|
||||||
toJSONKey = toJSONKeyText toPathPiece
|
toJSONKey = toJSONKeyText toPathPiece
|
||||||
instance FromJSONKey $(conT tName) where
|
instance FromJSONKey $(conT tName) where
|
||||||
fromJSONKey = FromJSONKeyTextParser $ \t -> maybe (fail $ "Could not parse ‘" <> unpack t <> "’ as value for " <> $(TH.lift $ nameBase tName) <> "via PathPiece") return $ fromPathPiece t
|
fromJSONKey = FromJSONKeyTextParser $ \t -> maybe (fail $ "Could not parse ‘" <> unpack t <> "’ as value for " <> $(TH.lift $ nameBase tName) <> " via PathPiece") return $ fromPathPiece t
|
||||||
|
|]
|
||||||
|
|
||||||
|
pathPieceJSON :: Name -> DecsQ
|
||||||
|
-- ^ Derive `ToJSON`- and `FromJSON`-Instances from a `PathPiece`-Instance
|
||||||
|
pathPieceJSON tName
|
||||||
|
= [d| instance ToJSON $(conT tName) where
|
||||||
|
toJSON = Aeson.String . toPathPiece
|
||||||
|
instance FromJSON $(conT tName) where
|
||||||
|
parseJSON = Aeson.withText $(TH.lift $ nameBase tName) $ \t -> maybe (fail $ "Could not parse ‘" <> unpack t <> "’ as value for " <> $(TH.lift $ nameBase tName) <> " via PathPiece") return $ fromPathPiece t
|
||||||
|]
|
|]
|
||||||
|
|||||||
13
stack.nix
13
stack.nix
@ -1,16 +1,11 @@
|
|||||||
{ ghc, nixpkgs ? import <nixpkgs> }:
|
{ ghc, nixpkgs ? import <nixpkgs> }:
|
||||||
|
|
||||||
let
|
let
|
||||||
snapshot = "lts-10.5";
|
haskellPackages = import ./stackage.nix { inherit nixpkgs; };
|
||||||
stackage = import (fetchTarball {
|
inherit (nixpkgs {}) pkgs;
|
||||||
url = "https://stackage.serokell.io/drczwlyf6mi0ilh3kgv01wxwjfgvq14b-stackage/default.nix.tar.gz";
|
in pkgs.haskell.lib.buildStackProject {
|
||||||
sha256 = "1bwlbxx6np0jfl6z9gkmmcq22crm0pa07a8zrwhz5gkal64y6jpz";
|
|
||||||
});
|
|
||||||
inherit (nixpkgs { overlays = [ stackage."${snapshot}" ]; }) haskell pkgs;
|
|
||||||
|
|
||||||
haskellPackages = pkgs.haskell.packages."${snapshot}";
|
|
||||||
in haskell.lib.buildStackProject {
|
|
||||||
inherit ghc;
|
inherit ghc;
|
||||||
|
inherit (haskellPackages) stack;
|
||||||
name = "stackenv";
|
name = "stackenv";
|
||||||
buildInputs = (with pkgs;
|
buildInputs = (with pkgs;
|
||||||
[ postgresql zlib libsodium
|
[ postgresql zlib libsodium
|
||||||
|
|||||||
21
stack.yaml
21
stack.yaml
@ -9,20 +9,15 @@ extra-package-dbs: []
|
|||||||
|
|
||||||
packages:
|
packages:
|
||||||
- .
|
- .
|
||||||
- location:
|
|
||||||
git: https://github.com/pngwjpgh/zip-stream.git
|
|
||||||
commit: 9272bbed000928d500febad1cdc98d1da29d399e
|
|
||||||
extra-dep: true
|
|
||||||
- location:
|
|
||||||
git: https://github.com/pngwjpgh/encoding.git
|
|
||||||
commit: 67bb87ceff53f0178c988dd4e15eeb2daee92b84
|
|
||||||
extra-dep: true
|
|
||||||
- location:
|
|
||||||
git: https://github.com/pngwjpgh/memcached-binary.git
|
|
||||||
commit: b5461747e7be226d3b67daebc3c9aefe8a4490ad
|
|
||||||
extra-dep: true
|
|
||||||
|
|
||||||
extra-deps:
|
extra-deps:
|
||||||
|
- git: https://github.com/pngwjpgh/zip-stream.git
|
||||||
|
commit: 9272bbed000928d500febad1cdc98d1da29d399e
|
||||||
|
- git: https://github.com/pngwjpgh/encoding.git
|
||||||
|
commit: 67bb87ceff53f0178c988dd4e15eeb2daee92b84
|
||||||
|
- git: https://github.com/pngwjpgh/memcached-binary.git
|
||||||
|
commit: b5461747e7be226d3b67daebc3c9aefe8a4490ad
|
||||||
|
|
||||||
- colonnade-1.2.0
|
- colonnade-1.2.0
|
||||||
- yesod-colonnade-1.2.0
|
- yesod-colonnade-1.2.0
|
||||||
|
|
||||||
@ -53,4 +48,6 @@ extra-deps:
|
|||||||
|
|
||||||
- filepath-1.4.2
|
- filepath-1.4.2
|
||||||
|
|
||||||
|
- haskell-src-exts-util-0.2.1.2
|
||||||
|
|
||||||
resolver: lts-10.5
|
resolver: lts-10.5
|
||||||
|
|||||||
30
stackage.nix
Normal file
30
stackage.nix
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
{ nixpkgs ? import <nixpkgs>
|
||||||
|
, snapshot ? "lts-10.5"
|
||||||
|
}:
|
||||||
|
|
||||||
|
let
|
||||||
|
stackage = import (fetchTarball {
|
||||||
|
url = "https://stackage.serokell.io/drczwlyf6mi0ilh3kgv01wxwjfgvq14b-stackage/default.nix.tar.gz";
|
||||||
|
sha256 = "1bwlbxx6np0jfl6z9gkmmcq22crm0pa07a8zrwhz5gkal64y6jpz";
|
||||||
|
});
|
||||||
|
|
||||||
|
overlays =
|
||||||
|
[ stackage."${snapshot}"
|
||||||
|
(self: super: {
|
||||||
|
haskell = super.haskell // {
|
||||||
|
packages = super.haskell.packages // {
|
||||||
|
"${snapshot}" = super.haskell.packages."${snapshot}".override {
|
||||||
|
overrides = hself: hsuper: {
|
||||||
|
zip-archive = self.haskell.lib.overrideCabal hsuper.zip-archive (old: {
|
||||||
|
testToolDepends = old.testToolDepends ++ (with self; [ unzip ]);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
];
|
||||||
|
|
||||||
|
inherit (nixpkgs { inherit overlays; }) pkgs;
|
||||||
|
in pkgs.haskell.packages."${snapshot}"
|
||||||
@ -1,77 +1,130 @@
|
|||||||
<div>
|
<div>
|
||||||
<h2>_{MsgCorrectionSheets}
|
<h2>_{MsgCorrectionSheets}
|
||||||
|
|
||||||
_{MsgCourseParticipants nrParticipants}
|
_{MsgCourseParticipants nrParticipants}
|
||||||
<table .table .table--striped .table--hover>
|
<table .table .table--striped .table--hover>
|
||||||
<tr .table__row .table__row--head>
|
<tr .table__row .table__row--head>
|
||||||
<th .table__th>_{MsgSheet}
|
<th .table__th rowspan=2>_{MsgSheet}
|
||||||
$if groupsPossible
|
$if groupsPossible
|
||||||
<th .table__th>_{MsgNrSubmittorsTotal}
|
<th .table__th rowspan=2>_{MsgNrSubmittorsTotal}
|
||||||
<th .table__th >_{MsgNrSubmissionsTotal}
|
<th .table__th rowspan=2>_{MsgNrSubmissionsTotal}
|
||||||
<th .table__th colspan=2>_{MsgNrSubmissionsNotAssigned}
|
<th .table__th colspan=2>_{MsgNrSubmissionsNotAssigned}
|
||||||
<th .table__th>_{MsgNrSubmissionsNotCorrected}
|
<th .table__th rowspan=2>_{MsgNrSubmissionsNotCorrected}
|
||||||
<th .table__th colspan=3>_{MsgCorrectionTime}
|
<th .table__th colspan=3>_{MsgCorrectionTime}
|
||||||
$forall (sheetName, CorrectionInfo{ciSubmittors, ciSubmissions, ciAssigned, ciCorrected, ciMin, ciTot, ciMax}) <- Map.toList sheetMap
|
<tr .table__row .table__row--head>
|
||||||
|
<th .table__th>
|
||||||
|
<th .table__th>_{MsgGenericNumChange}
|
||||||
|
<th .table__th>_{MsgGenericMin}
|
||||||
|
<th .table__th>_{MsgGenericAvg}
|
||||||
|
<th .table__th>_{MsgGenericMax}
|
||||||
|
$# Always iterate over sheetList for consistent sorting! Newest first, except in this table
|
||||||
|
$forall (sheetName, CorrectionInfo{ciSubmittors, ciSubmissions, ciAssigned, ciCorrected, ciMin, ciTot, ciMax}) <- reverse sheetList
|
||||||
<tr .table__row>
|
<tr .table__row>
|
||||||
<td .table__td>^{simpleLink (toWidget sheetName) (CSheetR tid ssh csh sheetName SSubsR)}
|
<td .table__td>^{simpleLink (toWidget sheetName) (CSheetR tid ssh csh sheetName SSubsR)}
|
||||||
$if groupsPossible
|
$if groupsPossible
|
||||||
<td .table__td>#{ciSubmittors}
|
<td .table__td>#{ciSubmittors}
|
||||||
<td .table__td>#{ciSubmissions}
|
<td .table__td>#{ciSubmissions}
|
||||||
$maybe ((splus,sfailed),_) <- Map.lookup sheetName assignment
|
$maybe ((splus,sfailed),_,_) <- Map.lookup sheetName assignment
|
||||||
$if 0 < Set.size sfailed
|
$if 0 < Set.size sfailed
|
||||||
<td .table__td>#{ciSubmissions - ciAssigned}
|
<td .table__td>#{ciSubmissions - ciAssigned}
|
||||||
<td .table__td .alert-danger>(-#{show (Set.size splus)}, failed: #{show (Set.size sfailed)})
|
<td .table__td .alert-danger>(-#{show (Set.size splus)}, failed: #{show (Set.size sfailed)})
|
||||||
$elseif 0 < Set.size splus
|
$elseif 0 < Set.size splus
|
||||||
<td .table__td>#{ciSubmissions - ciAssigned}
|
<td .table__td>#{ciSubmissions - ciAssigned}
|
||||||
<td .table__td .alert-success>(-#{show (Set.size splus)})
|
<td .table__td .alert-info>(-#{show (Set.size splus)})
|
||||||
$else
|
$else
|
||||||
<td .table__td colspan=2>#{ciSubmissions - ciAssigned}
|
<td .table__td>#{ciSubmissions - ciAssigned}
|
||||||
|
<td .table__td>
|
||||||
$nothing
|
$nothing
|
||||||
<td .table__td colspan=2>#{ciSubmissions - ciAssigned}
|
<td .table__td>#{ciSubmissions - ciAssigned}
|
||||||
|
<td .table__td>
|
||||||
<td .table__td>#{ciSubmissions - ciCorrected}
|
<td .table__td>#{ciSubmissions - ciCorrected}
|
||||||
<td .table__td>#{showDiffDays ciMin}
|
<td .table__td>#{showDiffDays ciMin}
|
||||||
<td .table__td>#{showAvgsDays ciTot ciCorrected}
|
<td .table__td>#{showAvgsDays ciTot ciCorrected}
|
||||||
<td .table__td>#{showDiffDays ciMax}
|
<td .table__td>#{showDiffDays ciMax}
|
||||||
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h2>_{MsgCorrectionCorrectors}
|
<h2>_{MsgCorrectionCorrectors}
|
||||||
|
|
||||||
<table .table .table--striped .table--hover>
|
<table .table .table--striped .table--hover>
|
||||||
<tr .table__row .table__row--head>
|
<tr .table__row .table__row--head>
|
||||||
<th .table__th>_{MsgCorrector}
|
<th .table__th rowspan=2>_{MsgCorrector}
|
||||||
<th .table__th>_{MsgNrSubmissionsTotal}
|
<th .table__th colspan=2>_{MsgGenericAll}
|
||||||
<th .table__th>_{MsgNrSubmissionsNotCorrected}
|
<th .table__th rowspan=2>_{MsgCorDeficitProportion}
|
||||||
<th .table__th colspan=3>_{MsgCorrectionTime}
|
<th .table__th colspan=3>_{MsgCorrectionTime}
|
||||||
$forall shn <- sheetNames
|
$# Always iterate over sheetList for consistent sorting! Newest first, except in this table
|
||||||
|
$forall (shn,_) <- sheetList
|
||||||
<th .table__th colspan=5>#{shn}
|
<th .table__th colspan=5>#{shn}
|
||||||
$# ^{simpleLinkI (SomeMessage MsgMenuCorrectors) (CSheetR tid ssh csh shn SCorrR)}
|
$# ^{simpleLinkI (SomeMessage MsgMenuCorrectors) (CSheetR tid ssh csh shn SCorrR)}
|
||||||
$forall (CorrectionInfo{ciCorrector, ciSubmissions, ciCorrected, ciMin, ciTot, ciMax}) <- Map.elems corrMap
|
<tr .table__row .table__row--head>
|
||||||
|
<th .table__th>_{MsgNrSubmissionsTotal}
|
||||||
|
<th .table__th>_{MsgNrSubmissionsNotCorrected}
|
||||||
|
<th .table__th>_{MsgGenericMin}
|
||||||
|
<th .table__th>_{MsgGenericAvg}
|
||||||
|
<th .table__th>_{MsgGenericMax}
|
||||||
|
$# Always iterate over sheetList for consistent sorting! Newest first, except in this table
|
||||||
|
$forall _shn <- sheetList
|
||||||
|
<th .table__th>_{MsgCorProportion}
|
||||||
|
<th .table__th>_{MsgNrSubmissionsTotalShort}
|
||||||
|
<th .table__th>_{MsgGenericNumChange}
|
||||||
|
<th .table__th>_{MsgNrSubmissionsNotCorrectedShort}
|
||||||
|
<th .table__th>_{MsgGenericAvg}
|
||||||
|
$forall (CorrectionInfo{ciCorrector, ciSubmissions=ciSubmissionsNr, ciCorrected, ciMin, ciTot, ciMax}) <- corrInfos
|
||||||
$with (nameW,loadM) <- getCorrector ciCorrector
|
$with (nameW,loadM) <- getCorrector ciCorrector
|
||||||
<tr .table__row>
|
<tr .table__row>
|
||||||
<td .table__td>^{nameW}
|
<td .table__td>^{nameW}
|
||||||
<td .table__td>#{ciSubmissions}
|
<td .table__td>#{ciSubmissionsNr}
|
||||||
<td .table__td .heated style="--hotness: #{heat ciSubmissions ciCorrected}">#{ciSubmissions - ciCorrected}
|
$with total <- ciSubmissions corrMapSum
|
||||||
|
$if total > 0
|
||||||
|
\ (#{textPercent' True 0 ciSubmissionsNr total})
|
||||||
|
<td .table__td .heated style="--hotness: #{heat ciSubmissionsNr ciCorrected}">#{ciSubmissionsNr - ciCorrected}
|
||||||
|
<td .table__td>
|
||||||
|
$maybe deficit <- getCorrDeficit ciCorrector
|
||||||
|
#{display deficit}
|
||||||
<td .table__td>#{showDiffDays ciMin}
|
<td .table__td>#{showDiffDays ciMin}
|
||||||
<td .table__td>#{showAvgsDays ciTot ciCorrected}
|
<td .table__td>#{showAvgsDays ciTot ciCorrected}
|
||||||
<td .table__td>#{showDiffDays ciMax}
|
<td .table__td>#{showDiffDays ciMax}
|
||||||
$forall shn <- sheetNames
|
$# Always iterate over sheetList for consistent sorting! Newest first, except in this table
|
||||||
$maybe SheetCorrector{sheetCorrectorLoad, sheetCorrectorState} <- Map.lookup shn loadM
|
$forall (shn, CorrectionInfo{ciSubmissions=sheetSubmissionsNr}) <- sheetList
|
||||||
<td .table__td>#{showCompactCorrectorLoad sheetCorrectorLoad sheetCorrectorState}
|
<td .table__td>
|
||||||
$nothing
|
$maybe SheetCorrector{sheetCorrectorLoad, sheetCorrectorState} <- Map.lookup shn loadM
|
||||||
<td .table__td>
|
#{showCompactCorrectorLoad sheetCorrectorLoad sheetCorrectorState}
|
||||||
|
$if sheetCorrectorState == CorrectorNormal
|
||||||
|
$maybe Load{byProportion=total} <- Map.lookup shn sheetLoad
|
||||||
|
$if total > 0
|
||||||
|
\ (#{textPercent' True 0 (byProportion sheetCorrectorLoad) total})
|
||||||
$maybe CorrectionInfo{ciSubmissions,ciCorrected,ciTot} <- getCorrSheetStatus ciCorrector shn
|
$maybe CorrectionInfo{ciSubmissions,ciCorrected,ciTot} <- getCorrSheetStatus ciCorrector shn
|
||||||
|
<td .table__td>#{ciSubmissions}
|
||||||
|
$if sheetSubmissionsNr > 0
|
||||||
|
\ (#{textPercent' True 0 ciSubmissions sheetSubmissionsNr})
|
||||||
$maybe nrNew <- getCorrNewAssignment ciCorrector shn
|
$maybe nrNew <- getCorrNewAssignment ciCorrector shn
|
||||||
<td .table__td>#{ciSubmissions}
|
|
||||||
$# <td .table__td>#{ciAssigned} `ciSubmissions` is here always identical to `ciAssigned` and also works for `ciCorrector == Nothing`. ciAssigned only useful in aggregate maps like `sheetMap`
|
$# <td .table__td>#{ciAssigned} `ciSubmissions` is here always identical to `ciAssigned` and also works for `ciCorrector == Nothing`. ciAssigned only useful in aggregate maps like `sheetMap`
|
||||||
<td .table__td .alert-success>(+#{nrNew})
|
<td .table__td .alert-info>(+#{nrNew})
|
||||||
$nothing
|
$nothing
|
||||||
<td .table__td colspan=2>#{ciSubmissions}
|
<td .table__td>
|
||||||
<td .table__td .heated style="--hotness: #{heat ciSubmissions ciCorrected}">#{ciSubmissions - ciCorrected}
|
<td .table__td .heated style="--hotness: #{heat ciSubmissions ciCorrected}">#{ciSubmissions - ciCorrected}
|
||||||
<td .table__td>#{showAvgsDays ciTot ciCorrected}
|
<td .table__td>#{showAvgsDays ciTot ciCorrected}
|
||||||
$nothing
|
$nothing
|
||||||
<td .table__td colspan=4>
|
<td .table__td>
|
||||||
$if 0 < length sheetNames
|
<td .table__td>
|
||||||
|
<td .table__td>
|
||||||
|
<td .table__td>
|
||||||
|
$if not (null sheetList)
|
||||||
<tr .table__row>
|
<tr .table__row>
|
||||||
<td colspan=6>
|
<td .table__th>Σ
|
||||||
$forall shn <- sheetNames
|
$with ciSubmissionsNr <- ciSubmissions corrMapSum
|
||||||
<td .table__td colspan=5>^{simpleLinkI (SomeMessage MsgMenuCorrectors) (CSheetR tid ssh csh shn SCorrR)}
|
$with ciCorrectedNr <- ciCorrected corrMapSum
|
||||||
|
<td .table__th>#{ciSubmissionsNr}
|
||||||
|
<td .table__td .heated style="--hotness: #{heat ciSubmissionsNr ciCorrectedNr}">#{ciSubmissionsNr - ciCorrectedNr}
|
||||||
|
<td .table__th>#{ciCorrected corrMapSum}
|
||||||
|
<td .table__th>#{showDiffDays (ciMin corrMapSum)}
|
||||||
|
<td .table__th>#{showAvgsDays (ciTot corrMapSum) (ciCorrected corrMapSum)}
|
||||||
|
<td .table__th>#{showDiffDays (ciMax corrMapSum)}
|
||||||
|
$# Always iterate over sheetList for consistent sorting! Newest first, except in this table
|
||||||
|
$forall (shn, CorrectionInfo{ciSubmissions}) <- sheetList
|
||||||
|
<td .table__th>#{getLoadSum shn}
|
||||||
|
<td .table__th>#{ciSubmissions}
|
||||||
|
<td .table__td colspan=3>^{simpleLinkI (SomeMessage MsgMenuCorrectorsChange) (CSheetR tid ssh csh shn SCorrR)}
|
||||||
^{btnWdgt}
|
^{btnWdgt}
|
||||||
<div>
|
<div>
|
||||||
<p>_{MsgAssignSubmissionsRandomWarning}
|
<p>_{MsgAssignSubmissionsRandomWarning}
|
||||||
@ -93,6 +93,10 @@ $# $if NTop (Just 0) < NTop (courseCapacity course)
|
|||||||
$else
|
$else
|
||||||
Eine Anmeldung zum Kurs ist Voraussetzung zum Zugang zu Kursmaterial
|
Eine Anmeldung zum Kurs ist Voraussetzung zum Zugang zu Kursmaterial
|
||||||
(z.B. Übungsblätter).
|
(z.B. Übungsblätter).
|
||||||
|
$if hasExams
|
||||||
|
<dt .deflist__dt>_{MsgCourseExams}
|
||||||
|
<dd .deflist__dd>
|
||||||
|
^{examTable}
|
||||||
$if hasTutorials
|
$if hasTutorials
|
||||||
<dt .deflist__dt>_{MsgCourseTutorials}
|
<dt .deflist__dt>_{MsgCourseTutorials}
|
||||||
<dd .deflist__dd>
|
<dd .deflist__dd>
|
||||||
|
|||||||
@ -502,7 +502,7 @@ ul.list--inline {
|
|||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
|
|
||||||
.deflist {
|
.deflist {
|
||||||
grid-template-columns: max-content minmax(auto, max-content);
|
grid-template-columns: max-content minmax(0, max-content);
|
||||||
|
|
||||||
.deflist {
|
.deflist {
|
||||||
margin-top: -10px;
|
margin-top: -10px;
|
||||||
@ -580,7 +580,7 @@ section {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group__input > .notification {
|
.form-group__input > .notification {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
2
templates/exam-edit.hamlet
Normal file
2
templates/exam-edit.hamlet
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
$newline never
|
||||||
|
^{editExamForm}
|
||||||
2
templates/exam-list.hamlet
Normal file
2
templates/exam-list.hamlet
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
$newline never
|
||||||
|
^{examTable}
|
||||||
2
templates/exam-new.hamlet
Normal file
2
templates/exam-new.hamlet
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
$newline never
|
||||||
|
^{newExamForm}
|
||||||
6
templates/exam-show.cassius
Normal file
6
templates/exam-show.cassius
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
.occurrence--not-registered
|
||||||
|
text-decoration: strike-through;
|
||||||
|
|
||||||
|
.result
|
||||||
|
padding-left: 2em;
|
||||||
|
font-size: 20px;
|
||||||
153
templates/exam-show.hamlet
Normal file
153
templates/exam-show.hamlet
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
$newline never
|
||||||
|
$maybe Entity _ ExamResult{examResultResult} <- result
|
||||||
|
$if gradingShown
|
||||||
|
<section>
|
||||||
|
<h2>
|
||||||
|
_{MsgExamResult}
|
||||||
|
$if gradingShown && not gradingVisible
|
||||||
|
\ ^{isVisible False}
|
||||||
|
<p .result>
|
||||||
|
$case examResultResult
|
||||||
|
$of ExamAttended grade
|
||||||
|
$if examShowGrades
|
||||||
|
_{grade}
|
||||||
|
$else
|
||||||
|
$if passingGrade grade
|
||||||
|
_{MsgExamPassed}
|
||||||
|
$else
|
||||||
|
_{MsgExamNotPassed}
|
||||||
|
$of ExamNoShow
|
||||||
|
_{MsgExamNoShow}
|
||||||
|
$of ExamVoided
|
||||||
|
_{MsgExamVoided}
|
||||||
|
|
||||||
|
$maybe desc <- examDescription
|
||||||
|
<section>
|
||||||
|
#{desc}
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<dl .deflist>
|
||||||
|
$if not examVisible
|
||||||
|
<dt .deflist__dt>_{MsgExamVisibleFrom}
|
||||||
|
<dd .deflist__dd>
|
||||||
|
$maybe from <- examVisibleFrom
|
||||||
|
^{formatTimeW SelFormatDateTime from}
|
||||||
|
$nothing
|
||||||
|
_{MsgNever}
|
||||||
|
\ ^{isVisible False}
|
||||||
|
$maybe regFrom <- examRegisterFrom
|
||||||
|
<dt .deflist__dt>_{MsgExamRegisterFrom}
|
||||||
|
<dd .deflist__dd>^{formatTimeW SelFormatDateTime regFrom}
|
||||||
|
$maybe regTo <- examRegisterTo
|
||||||
|
<dt .deflist__dt>_{MsgExamRegisterTo}
|
||||||
|
<dd .deflist__dd>^{formatTimeW SelFormatDateTime regTo}
|
||||||
|
$maybe deregUntil <- examDeregisterUntil
|
||||||
|
<dt .deflist__dt>_{MsgExamDeregisterUntil}
|
||||||
|
<dd .deflist__dd>^{formatTimeW SelFormatDateTime deregUntil}
|
||||||
|
<dt .deflist__dt>_{MsgExamPublishOccurrenceAssignmentsParticipant}
|
||||||
|
<dd .deflist__dd>^{formatTimeW SelFormatDateTime examPublishOccurrenceAssignments}
|
||||||
|
$if examTimes
|
||||||
|
<dt .deflist__dt>_{MsgExamTime}
|
||||||
|
<dd .deflist__dd>
|
||||||
|
^{formatTimeW SelFormatDateTime examStart}
|
||||||
|
$maybe end <- examEnd
|
||||||
|
\ – ^{formatTimeW (bool SelFormatDateTime SelFormatTime ((on (==) utctDay) examStart end)) end}
|
||||||
|
$maybe finished <- examFinished
|
||||||
|
<dt .deflist__dt>_{MsgExamFinishedParticipant}
|
||||||
|
<dd .deflist__dd>^{formatTimeW SelFormatDateTime finished}
|
||||||
|
$if gradingShown
|
||||||
|
$if examGradingRule /= ExamGradingManual
|
||||||
|
<dt .deflist__dt>
|
||||||
|
_{MsgExamGradingRule}
|
||||||
|
$if not gradingVisible
|
||||||
|
\ ^{isVisible False}
|
||||||
|
<dd .deflist__dd>
|
||||||
|
$case examGradingRule
|
||||||
|
$of ExamGradingManual
|
||||||
|
_{MsgExamGradingManual'}
|
||||||
|
$of ExamGradingKey{..}
|
||||||
|
^{gradingKeyW examGradingKey}
|
||||||
|
$if examBonusRule /= ExamNoBonus
|
||||||
|
<dt .deflist__dt>
|
||||||
|
_{MsgExamBonusRule}
|
||||||
|
$if not gradingVisible
|
||||||
|
\ ^{isVisible False}
|
||||||
|
<dd .deflist__dd>
|
||||||
|
^{examBonusW examBonusRule}
|
||||||
|
$if occurrenceAssignmentsShown
|
||||||
|
<dt .deflist__dt>
|
||||||
|
_{MsgExamOccurrenceRuleParticipant}
|
||||||
|
$if not occurrenceAssignmentsVisible
|
||||||
|
\ ^{isVisible False}
|
||||||
|
<dd .deflist__dd>
|
||||||
|
$# TODO
|
||||||
|
$maybe registerWdgt <- registerWidget
|
||||||
|
<dt .deflist__dt>_{MsgExamRegistration}
|
||||||
|
<dd .deflist__dd>^{registerWdgt}
|
||||||
|
|
||||||
|
|
||||||
|
$if not (null occurrences)
|
||||||
|
<section>
|
||||||
|
<h2>
|
||||||
|
_{MsgExamOccurrences}
|
||||||
|
<table .table .table--striped .table--hover>
|
||||||
|
<thead>
|
||||||
|
<tr .table__row .table__row--head>
|
||||||
|
<th .table__th>_{MsgExamRoom}
|
||||||
|
$if not examTimes
|
||||||
|
<th .table__th>_{MsgExamRoomTime}
|
||||||
|
<th .table__th>_{MsgExamRoomDescription}
|
||||||
|
$if occurrenceAssignmentsShown
|
||||||
|
<th .table__th>
|
||||||
|
_{MsgExamRoomRegistered}
|
||||||
|
$if not occurrenceAssignmentsVisible
|
||||||
|
\ ^{isVisible False}
|
||||||
|
<tbody>
|
||||||
|
$forall (Entity _occId ExamOccurrence{examOccurrenceRoom, examOccurrenceStart, examOccurrenceEnd, examOccurrenceDescription}, registered) <- occurrences
|
||||||
|
<tr .table__row :occurrenceAssignmentsShown && not registered:.occurrence--not-registered>
|
||||||
|
<td .table__td>#{examOccurrenceRoom}
|
||||||
|
$if not examTimes
|
||||||
|
<td .table__td>
|
||||||
|
^{formatTimeW SelFormatDateTime examOccurrenceStart}
|
||||||
|
$maybe end <- examOccurrenceEnd
|
||||||
|
\ – ^{formatTimeW (bool SelFormatDateTime SelFormatTime ((on (==) utctDay) examStart end)) end}
|
||||||
|
<td .table__td>
|
||||||
|
$maybe desc <- examOccurrenceDescription
|
||||||
|
#{desc}
|
||||||
|
$if occurrenceAssignmentsShown
|
||||||
|
<td .table__td>
|
||||||
|
$if registered
|
||||||
|
#{fontAwesomeIcon "check"}
|
||||||
|
|
||||||
|
$if gradingShown && not (null parts)
|
||||||
|
<section>
|
||||||
|
<h2>
|
||||||
|
_{MsgExamParts}
|
||||||
|
$if gradingShown && not gradingVisible
|
||||||
|
\ ^{isVisible False}
|
||||||
|
<table .table .table--striped .table--hover >
|
||||||
|
<thead>
|
||||||
|
<tr .table__row .table__row--head>
|
||||||
|
<th .table__th>_{MsgExamPartName}
|
||||||
|
<th .table__th>_{MsgExamPartMaxPoints}
|
||||||
|
<th .table__th>_{MsgExamPartResultPoints}
|
||||||
|
<tbody>
|
||||||
|
$forall Entity partId ExamPart{examPartName, examPartWeight, examPartMaxPoints} <- parts
|
||||||
|
<tr .table__row>
|
||||||
|
<td .table__td>#{examPartName}
|
||||||
|
<td .table__td>
|
||||||
|
$maybe mPoints <- examPartMaxPoints
|
||||||
|
#{showFixed True (fromRational examPartWeight * mPoints)}
|
||||||
|
<td .table__td>
|
||||||
|
$case fmap (examPartResultResult . entityVal) (results !? partId)
|
||||||
|
$of Nothing
|
||||||
|
$of Just (ExamAttended (Just ps))
|
||||||
|
#{showFixed True ps}
|
||||||
|
$of Just (ExamAttended Nothing)
|
||||||
|
#{fontAwesomeIcon "check"}
|
||||||
|
$of Just ExamNoShow
|
||||||
|
_{MsgExamNoShow}
|
||||||
|
$of Just ExamVoided
|
||||||
|
_{MsgExamVoided}
|
||||||
|
|
||||||
|
$# TODO: Statistics
|
||||||
124
templates/i18n/changelog/de.hamlet
Normal file
124
templates/i18n/changelog/de.hamlet
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
$newline never
|
||||||
|
<dl .deflist>
|
||||||
|
<dt .deflist__dt>26.06.2019
|
||||||
|
<dd .deflist__dd>
|
||||||
|
<ul>
|
||||||
|
<li>Rudimentäre Unterstützung für Klausurbetrieb
|
||||||
|
|
||||||
|
<dt .deflist__dt>07.06.2019
|
||||||
|
<dd .deflist__dd>
|
||||||
|
<ul>
|
||||||
|
<li>Abgaben können bestimmte Dateinamen und Endungen erzwingen
|
||||||
|
<li>Übungsblätter bieten nun Zip-Archive für alle veröffentlichte Dateien, bzw. Dateigruppen an
|
||||||
|
|
||||||
|
<dt .deflist__dt>20.05.2019
|
||||||
|
<dd .deflist__dd>
|
||||||
|
<ul>
|
||||||
|
<li>Komplett überarbeitete Funktionalität zur automatischen Verteilung von Korrekturen
|
||||||
|
|
||||||
|
<dt .deflist__dt>13.05.2019
|
||||||
|
<dd .deflist__dd>
|
||||||
|
<ul>
|
||||||
|
<li>Kursverwalter können Teilnehmer hinzufügen
|
||||||
|
|
||||||
|
<dt .deflist__dt>10.05.2019
|
||||||
|
<dd .deflist__dd>
|
||||||
|
<ul>
|
||||||
|
<li>Besseres Interface zum Einstellen von Abgebenden
|
||||||
|
<li>Download von allen Dateien pro Kursmaterial/Übungsblatt
|
||||||
|
|
||||||
|
<dt .deflist__dt>04.05.2019
|
||||||
|
<dd .deflist__dd>
|
||||||
|
<ul>
|
||||||
|
<li>Kursmaterial
|
||||||
|
|
||||||
|
<dt .deflist__dt>29.04.2019
|
||||||
|
<dd .deflist__dd>
|
||||||
|
<ul>
|
||||||
|
<li>Tutorien
|
||||||
|
<li>Anzeige von Korrektoren auf den Kursseiten
|
||||||
|
|
||||||
|
<dt .deflist__dt>20.04.2019
|
||||||
|
<dd .deflist__dd>
|
||||||
|
<ul>
|
||||||
|
<li>Versand von Benachrichtigungen an Kursteilnehmer
|
||||||
|
<li>Eintragen von Korrektoren und Kursverwaltern auch ohne bestehenden Account
|
||||||
|
|
||||||
|
<dt .deflist__dt>27.03.2019
|
||||||
|
<dd .deflist__dd>
|
||||||
|
<ul>
|
||||||
|
<li>Kurse Veranstalter können nun mehrere Dozenten und Assistenten selbst eintragen
|
||||||
|
<li>Erfassung Studiengangsdaten
|
||||||
|
|
||||||
|
<dt .deflist__dt>20.03.2019
|
||||||
|
<dd .deflist__dd>
|
||||||
|
<ul>
|
||||||
|
<li>Kursanmeldung benötigen assoziertes Hauptfach (für Studierende mit mehreren Hauptfächern)
|
||||||
|
|
||||||
|
<dt .deflist__dt>30.01.2019
|
||||||
|
<dd .deflist__dd>
|
||||||
|
<ul>
|
||||||
|
<li>Designänderungen
|
||||||
|
|
||||||
|
<dt .deflist__dt>16.01.2019
|
||||||
|
<dd .deflist__dd>
|
||||||
|
<ul>
|
||||||
|
<li>Links für Bequemlichkeiten hinzugefügt (z.B. aktuelles Übungsblatt)
|
||||||
|
<li>Liste zugewiesener Abgaben lassen sich nun filtern
|
||||||
|
<li><i>Bugfix</i>: Wenn zwischen Anzeige und Empfang eines Tabellen-Formulars Zeilen verschwinden wird nun eine sinnvolle Fehlermeldung angezeigt
|
||||||
|
|
||||||
|
<dt .deflist__dt>30.11.2018
|
||||||
|
<dd .deflist__dd>
|
||||||
|
<ul>
|
||||||
|
<li><i>Bugfix</i>: Übungsblätter im "bestehen nach Punkten"-Modus werden wieder korrekt gespeichert
|
||||||
|
|
||||||
|
<dt .deflist__dt>29.11.2018
|
||||||
|
<dd .deflist__dd>
|
||||||
|
<ul>
|
||||||
|
<li><i>Bugfix</i>: Formulare innerhalb von Tabellen funktionieren nun auch nach Javascript-Seitenwechsel oder Ändern der Sortierung
|
||||||
|
|
||||||
|
<dt .deflist__dt>09.11.2018
|
||||||
|
<dd .deflist__dd>
|
||||||
|
<ul>
|
||||||
|
<li><i>Bugfix</i>: Zahlreiche Knöpfe/Formulare funktionieren wieder bei eingeschaltetem Javascript
|
||||||
|
<li>Verschiedene Verbesserungen für Korrektoren
|
||||||
|
|
||||||
|
<dt .deflist__dt>19.10.2018
|
||||||
|
<dd .deflist__dd>
|
||||||
|
<ul>
|
||||||
|
<li>Benutzer können sich in der Testphase komplett selbst löschen
|
||||||
|
<li>Hilfe Widget
|
||||||
|
<li>Benachrichtigungen per eMail für einige Ereignisse
|
||||||
|
|
||||||
|
<dt .deflist__dt>18.09.2018
|
||||||
|
<dd .deflist__dd>
|
||||||
|
<ul>
|
||||||
|
<li>Tooltips funktionieren auch ohne JavaScript
|
||||||
|
<li>Kurskürzel müssen nur innerhalb eines Instituts eindeutig sein
|
||||||
|
<li>User Data zeigt nun alle momentan gespeicherten Datensätze an
|
||||||
|
<li>Unterstützung von Tabellenzusammenfassungen, z.B. Punktsummen
|
||||||
|
<li>Intelligente Verteilung von Abgaben auf Korrektoren (z.B. bei Krankheit)
|
||||||
|
<li>Übungsblätter können Abgabe von Dateien verbieten und angeben ob ZIP-Archive entpackt werden sollen
|
||||||
|
|
||||||
|
<dt .deflist__dt>06.08.2018
|
||||||
|
<dd .deflist__dd>
|
||||||
|
<ul>
|
||||||
|
<li>Einführung einer Option, ob Dateien automatisch heruntergeladen werden sollen
|
||||||
|
|
||||||
|
<dt .deflist__dt>01.08.2018
|
||||||
|
<dd .deflist__dd>
|
||||||
|
<ul>
|
||||||
|
<li>Verbesserter Campus-Login <br />
|
||||||
|
(Ersatz einer C-Bibliothek mit undokumentierter Abhängigkeit durch selbst entwickelten Haskell-Code erlaubt nun auch Umlaute)
|
||||||
|
|
||||||
|
<dt .deflist__dt>31.07.2018
|
||||||
|
<dd .deflist__dd>
|
||||||
|
<ul>
|
||||||
|
<li>Viele Verbesserung zur Anzeige von Korrekturen
|
||||||
|
<li>Kursliste über alle Semester hinweg (Top-Level-Navigation "Kurse"), wird in Zukunft Filter/Suchfunktion erhalten
|
||||||
|
|
||||||
|
<dt .deflist__dt>10.07.2018
|
||||||
|
<dd .deflist__dd>
|
||||||
|
<ul>
|
||||||
|
<li>Bugfixes
|
||||||
|
<li>Wählbares Format für Datum
|
||||||
@ -6,7 +6,6 @@
|
|||||||
<h4>
|
<h4>
|
||||||
aus UniWorX bekannt:
|
aus UniWorX bekannt:
|
||||||
<ul>
|
<ul>
|
||||||
<li> Klausuren
|
|
||||||
<li> Zentralanmeldungen
|
<li> Zentralanmeldungen
|
||||||
<li>
|
<li>
|
||||||
<h4>
|
<h4>
|
||||||
@ -7,8 +7,16 @@ $newline text
|
|||||||
<h2>Bekannte Probleme in Bearbeitung
|
<h2>Bekannte Probleme in Bearbeitung
|
||||||
|
|
||||||
<dl .deflist>
|
<dl .deflist>
|
||||||
<dt .deflist__dt>Derzeit keine bekannt.
|
<dt .deflist__dt>Klausuren #{iconNew}
|
||||||
|
<dd .deflist__dd>
|
||||||
|
Klausuren werden ab sofort teilweise unterstüzt.
|
||||||
|
Der genaue Stand der Entwicklung ist weiter unter auf dieser
|
||||||
|
Seite in einem eigenem Abschnitt detailliert.
|
||||||
|
|
||||||
|
<dt .deflist__dt>Benachrichtigungen
|
||||||
|
<dd .deflist__dd>
|
||||||
|
Benachrichtigungen werden momentan oft mit großer Verzögerung versandt.
|
||||||
|
Die Ursache ist derzeit noch unbekannt, da das Problem noch nicht genauer untersucht werden konnte.
|
||||||
|
|
||||||
$#
|
$#
|
||||||
$# MOVE ITEM TO SECTION "VERANSTALTUNGEN", once it is implemented:
|
$# MOVE ITEM TO SECTION "VERANSTALTUNGEN", once it is implemented:
|
||||||
@ -241,10 +249,76 @@ $newline text
|
|||||||
|
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2>Klausuren
|
<h2> Klausuren
|
||||||
Das Verwalten von Klausuren und Notenmeldungen
|
<p> Das Verwalten von Klausuren und Notenmeldungen wurde nun teilweise implementiert und ist ab sofort einsetzbar.
|
||||||
ist leider noch nicht fertig implementiert.
|
<dl .deflist>
|
||||||
|
<dt .deflist__dt> Anlegen/Editieren
|
||||||
|
<dd .deflist__dd>
|
||||||
|
Klausuren können von Dozenten und Assistenten angelegt werden.
|
||||||
|
Eine Vielzahl von optionalen Eigenschaften können sofort oder später angegeben werden,
|
||||||
|
z.B. Sichtbarkeit und Anmeldezeitraum.
|
||||||
|
|
||||||
|
<dt .deflist__dt> Prüfungen
|
||||||
|
<dd .deflist__dd>
|
||||||
|
Eine Klausur kann in mehrere Prüfungen unterteilt sein, welche jeweils einen eigenen Ort und Zeitraum besitzen.
|
||||||
|
<p>
|
||||||
|
Im einfachsten Fall lassen sich damit Klausuren abbilden, welche gleichzeitig in verschiedenen Räumen stattfinden.
|
||||||
|
<p>
|
||||||
|
Es lassen sich aber auch zeitlich getrennte Prüfungen verwalten, wie z.B. mündliche Prüfungen bei Seminaren oder Praktika.
|
||||||
|
Teilnehmern wird eine übersichtliche Tabelle aller Prüfungen angezeigt.
|
||||||
|
|
||||||
|
<dt .deflist__dt> #{iconProblem} Prüfungszuteilung
|
||||||
|
<dd .deflist__dd>
|
||||||
|
Auf Wunsch kann Uni2work die Zuteilung der Teilnehmer auf die Prüfungen (Räume bzw. Prüfungstermine)
|
||||||
|
nach verschiedenen Kriterien wie Name oder Matrikelnummer vornehmen.
|
||||||
|
|
||||||
|
<dt .deflist__dt> #{iconWarning} Anmeldungen
|
||||||
|
<dd .deflist__dd>
|
||||||
|
Teilnehmer können sich bereits zu sichtbaren Klausur innerhalb des eingestellten
|
||||||
|
Anmeldezeitraums anmelden.
|
||||||
|
<p>
|
||||||
|
<em>
|
||||||
|
Achtung: #
|
||||||
|
Die Liste der angemeldeten Teilnehmer ist momentan noch nicht einsehbar oder exportierbar, wird aber sicher gespeichert.
|
||||||
|
Dieses Feature folgt in Kürze.
|
||||||
|
|
||||||
|
<dt .deflist__dt> #{iconProblem} Korrekturen
|
||||||
|
<dd .deflist__dd>
|
||||||
|
<p>
|
||||||
|
Korrekturen können derzeit noch nicht eingetragen werden.
|
||||||
|
Die Realisierung sollte in wenigen Wochen erfolgen.
|
||||||
|
<p>
|
||||||
|
Die Eintragung von Korrekturen erfolgt immer pro Teilaufgabe.
|
||||||
|
Optional kann aus der erreichten Punktesumme dann automatisch eine Gesamtnote berechnet werden.
|
||||||
|
<p>
|
||||||
|
Optional können Klausurkorrektoren angegeben werden, die ab Durchführung der Klausur berechtigt sind eigenständig Korrekturergebnisse einzutragen.
|
||||||
|
Es kann das Recht Ergebnisse einzutragen pro Korrektor auf bestimmte Teilaufgaben beschränkt werden.
|
||||||
|
|
||||||
|
<dt .deflist__dt> #{iconProblem} Klausurbonus
|
||||||
|
<dd .deflist__dd>
|
||||||
|
Es werden verschiedene Möglichkeiten angebotenen werden,
|
||||||
|
die erzielten Bewertungen der Hausübungen
|
||||||
|
unter einstellbaren Bedingungen
|
||||||
|
in einen Klausurbonus umzurechnen (z.B. anrechnung nur, falls bereits ohne Bonus bestanden).
|
||||||
|
|
||||||
|
<dt .deflist__dt> #{iconProblem} Notenmeldung
|
||||||
|
<dd .deflist__dd>
|
||||||
|
<p>
|
||||||
|
Endnoten können leider noch nicht ans Prüfungsamt gemeldet werden.
|
||||||
|
<p>
|
||||||
|
Im Unterschied zum alten UniWorX gibt es keinen Knopf mehr zu Notenmeldung.
|
||||||
|
Stattdessen kann ein Datum eingetragen werden, ab dem die Klausur an das Prüfungsamt übergeben wird.
|
||||||
|
Dadurch kann die Notenmeldung nicht mehr vergessen werden.
|
||||||
|
<p>
|
||||||
|
Damit nachträgliche Änderungen nicht mehr verloren gehen können,
|
||||||
|
dürfen Dozenten nach dem Übergabedatum an das Prüfungsamt
|
||||||
|
keine Änderungen mehr an der Klausur vornehmen, da diese dann
|
||||||
|
ein Teil der Unterlagen des Prüfungsamtes ist.
|
||||||
|
<p>
|
||||||
|
Dozenten können jedoch explizit kleinere nachträgliche Änderungen an das
|
||||||
|
Prüfungsamt übermitteln. Für größere Änderungen kann das Prüfungsamt
|
||||||
|
die Klausur auch wieder an den Dozenten zurück übergeben;
|
||||||
|
der Dozent trägt dann einfach ein späteres Datum für die Übergabe ein.
|
||||||
<section>
|
<section>
|
||||||
<h2>Sonstiges
|
<h2>Sonstiges
|
||||||
<dl .deflist>
|
<dl .deflist>
|
||||||
|
|||||||
6
templates/i18n/knownBugs/de.hamlet
Normal file
6
templates/i18n/knownBugs/de.hamlet
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
$newline never
|
||||||
|
<p>
|
||||||
|
Stand: Mai 2019
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
Format von Bewertungsdateien ist noch provisorisch
|
||||||
@ -2,6 +2,10 @@ $newline never
|
|||||||
$if null rows && (dbsEmptyStyle == DBESNoHeading)
|
$if null rows && (dbsEmptyStyle == DBESNoHeading)
|
||||||
_{dbsEmptyMessage}
|
_{dbsEmptyMessage}
|
||||||
$else
|
$else
|
||||||
|
<div .table-header>
|
||||||
|
<div .table__row-count>
|
||||||
|
_{MsgRowCount rowCount}
|
||||||
|
|
||||||
^{table}
|
^{table}
|
||||||
|
|
||||||
<div .table-footer>
|
<div .table-footer>
|
||||||
|
|||||||
@ -1,3 +1,10 @@
|
|||||||
|
/* TABLE HEADER */
|
||||||
|
.table-header {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row-reverse;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
/* TABLE FOOTER */
|
/* TABLE FOOTER */
|
||||||
.table-footer {
|
.table-footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@ -1,23 +1,18 @@
|
|||||||
<div .container>
|
$newline never
|
||||||
|
<section>
|
||||||
|
^{features}
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
^{features}
|
<h2>
|
||||||
|
_{MsgKnownBugs}
|
||||||
|
^{knownBugs}
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2>
|
<h2>
|
||||||
Bekannte Bugs
|
_{MsgVersionHistory}
|
||||||
<h3>
|
<p #changelog>
|
||||||
Stand: Mai 2019
|
^{changeLog}
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
Format von Bewertungsdateien ist noch provisorisch
|
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2>
|
<p #gitrev>
|
||||||
Versionsgeschichte
|
#{gitInfo}
|
||||||
<p #changelog>
|
|
||||||
#{changeLog}
|
|
||||||
|
|
||||||
<section>
|
|
||||||
<p #gitrev>
|
|
||||||
#{gitInfo}
|
|
||||||
|
|||||||
8
templates/widgets/bonusRule.hamlet
Normal file
8
templates/widgets/bonusRule.hamlet
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
$newline never
|
||||||
|
$case bonusRule
|
||||||
|
$of ExamNoBonus
|
||||||
|
_{MsgExamNoBonus'}
|
||||||
|
$of ExamBonusPoints ps False
|
||||||
|
_{MsgExamBonusPoints ps}
|
||||||
|
$of ExamBonusPoints ps True
|
||||||
|
_{MsgExamBonusPointsPassed ps}
|
||||||
@ -19,7 +19,7 @@ $#
|
|||||||
$with Sum pacv <- summary ^. _achievedPasses
|
$with Sum pacv <- summary ^. _achievedPasses
|
||||||
<td .table__td>
|
<td .table__td>
|
||||||
$if pmax > 0
|
$if pmax > 0
|
||||||
#{textPercentInt pacv pmax}
|
#{textPercent pacv pmax}
|
||||||
<td .table__td>
|
<td .table__td>
|
||||||
#{display pacv} / #{display pmax}
|
#{display pacv} / #{display pmax}
|
||||||
$else
|
$else
|
||||||
@ -35,7 +35,7 @@ $#
|
|||||||
$with Sum pacv <- summary ^. _achievedPoints
|
$with Sum pacv <- summary ^. _achievedPoints
|
||||||
<td .table__td>
|
<td .table__td>
|
||||||
$if pmax > 0
|
$if pmax > 0
|
||||||
#{textPercent $ realToFrac $ pacv / pmax}
|
#{textPercent pacv pmax}
|
||||||
<td .table__td>
|
<td .table__td>
|
||||||
#{display pacv} / #{display pmax}
|
#{display pacv} / #{display pmax}
|
||||||
$if ((summary ^. _numMarkedPoints) /= (summary ^. _numSheets))
|
$if ((summary ^. _numMarkedPoints) /= (summary ^. _numSheets))
|
||||||
|
|||||||
3
templates/widgets/gradingKey.cassius
Normal file
3
templates/widgets/gradingKey.cassius
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
.table--grading-key
|
||||||
|
th, td
|
||||||
|
padding: 3px;
|
||||||
15
templates/widgets/gradingKey.hamlet
Normal file
15
templates/widgets/gradingKey.hamlet
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
$newline never
|
||||||
|
<table .table--grading-key>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
$forall g <- grades
|
||||||
|
<th>
|
||||||
|
_{g}
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
_{MsgGradingFrom}
|
||||||
|
$forall w <- boundWidgets
|
||||||
|
<td>
|
||||||
|
^{w}
|
||||||
6
templates/widgets/massinput/examCorrectors/add.hamlet
Normal file
6
templates/widgets/massinput/examCorrectors/add.hamlet
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
$newline never
|
||||||
|
<td colspan=2>
|
||||||
|
#{csrf}
|
||||||
|
^{fvInput addView}
|
||||||
|
<td>
|
||||||
|
^{fvInput submitView}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
$newline never
|
||||||
|
<td>
|
||||||
|
<span style="font-family: monospace">
|
||||||
|
#{email}
|
||||||
|
<td>
|
||||||
|
<div .tooltip>
|
||||||
|
<div .tooltip__handle>
|
||||||
|
<div .tooltip__content>
|
||||||
|
_{MsgEmailInvitationWarning}
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
$newline never
|
||||||
|
<td colspan=2>
|
||||||
|
^{nameEmailWidget userEmail userDisplayName userSurname}
|
||||||
4
templates/widgets/massinput/examParts/add.hamlet
Normal file
4
templates/widgets/massinput/examParts/add.hamlet
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
$newline never
|
||||||
|
^{formWidget}
|
||||||
|
<td>
|
||||||
|
^{fvInput submitView}
|
||||||
4
templates/widgets/massinput/examParts/form.hamlet
Normal file
4
templates/widgets/massinput/examParts/form.hamlet
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
$newline never
|
||||||
|
<td>#{csrf}^{fvInput epfIdView}^{fvInput epfNameView}
|
||||||
|
<td>^{fvInput epfMaxPointsView}
|
||||||
|
<td>^{fvInput epfWeightView}
|
||||||
16
templates/widgets/massinput/examParts/layout.hamlet
Normal file
16
templates/widgets/massinput/examParts/layout.hamlet
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
$newline never
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<th>_{MsgExamPartName}
|
||||||
|
<th>_{MsgExamPartMaxPoints}
|
||||||
|
<th>_{MsgExamPartWeight}
|
||||||
|
<td>
|
||||||
|
<tbody>
|
||||||
|
$forall coord <- review liveCoords lLength
|
||||||
|
<tr .massinput__cell>
|
||||||
|
^{cellWdgts ! coord}
|
||||||
|
<td>
|
||||||
|
^{fvInput (delButtons ! coord)}
|
||||||
|
<tfoot>
|
||||||
|
<tr .massinput__cell.massinput__cell--add>
|
||||||
|
^{addWdgts ! (0, 0)}
|
||||||
4
templates/widgets/massinput/examRooms/add.hamlet
Normal file
4
templates/widgets/massinput/examRooms/add.hamlet
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
$newline never
|
||||||
|
^{formWidget}
|
||||||
|
<td>
|
||||||
|
^{fvInput submitView}
|
||||||
6
templates/widgets/massinput/examRooms/form.hamlet
Normal file
6
templates/widgets/massinput/examRooms/form.hamlet
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
$newline never
|
||||||
|
<td>#{csrf}^{fvInput eofIdView}^{fvInput eofRoomView}
|
||||||
|
<td>^{fvInput eofCapacityView}
|
||||||
|
<td>^{fvInput eofStartView}
|
||||||
|
<td>^{fvInput eofEndView}
|
||||||
|
<td>^{fvInput eofDescView}
|
||||||
18
templates/widgets/massinput/examRooms/layout.hamlet
Normal file
18
templates/widgets/massinput/examRooms/layout.hamlet
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
$newline never
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<th>_{MsgExamRoom}
|
||||||
|
<th>_{MsgExamRoomCapacity}
|
||||||
|
<th>_{MsgExamRoomStart}
|
||||||
|
<th>_{MsgExamRoomEnd}
|
||||||
|
<th>_{MsgExamRoomDescription}
|
||||||
|
<td>
|
||||||
|
<tbody>
|
||||||
|
$forall coord <- review liveCoords lLength
|
||||||
|
<tr .massinput__cell>
|
||||||
|
^{cellWdgts ! coord}
|
||||||
|
<td>
|
||||||
|
^{fvInput (delButtons ! coord)}
|
||||||
|
<tfoot>
|
||||||
|
<tr .massinput__cell.massinput__cell--add>
|
||||||
|
^{addWdgts ! (0, 0)}
|
||||||
@ -4,7 +4,7 @@ $newline never
|
|||||||
<th>_{MsgUploadSpecificFileLabel}
|
<th>_{MsgUploadSpecificFileLabel}
|
||||||
<th>_{MsgUploadSpecificFileName}
|
<th>_{MsgUploadSpecificFileName}
|
||||||
<th>_{MsgUploadSpecificFileRequired}
|
<th>_{MsgUploadSpecificFileRequired}
|
||||||
<th>
|
<td>
|
||||||
<tbody>
|
<tbody>
|
||||||
$forall coord <- review liveCoords lLength
|
$forall coord <- review liveCoords lLength
|
||||||
<tr .massinput__cell>
|
<tr .massinput__cell>
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
$newline never
|
$newline never
|
||||||
<ul .list--inline .list--iconless .list--comma-separated>
|
<ul .list--inline .list--iconless .list--comma-separated>
|
||||||
$forall sched <- occurencesScheduled'
|
$forall sched <- occurrencesScheduled'
|
||||||
<li>^{sched}
|
<li>^{sched}
|
||||||
|
|
||||||
$if not (null occurencesExceptions)
|
$if not (null occurrencesExceptions)
|
||||||
$# <div .tooltip>
|
$# <div .tooltip>
|
||||||
$# <div .tooltip__handle .tooltip__handle--danger>
|
$# <div .tooltip__handle .tooltip__handle--danger>
|
||||||
$# <div .tooltip__content>
|
$# <div .tooltip__content>
|
||||||
<ul>
|
<ul>
|
||||||
$forall exc <- occurencesExceptions'
|
$forall exc <- occurrencesExceptions'
|
||||||
<li>^{exc}
|
<li>^{exc}
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
$newline never
|
||||||
|
_{MsgExceptionKindNoOccur}: #{exceptTime'}
|
||||||
2
templates/widgets/occurrence/cell/except-occurr.hamlet
Normal file
2
templates/widgets/occurrence/cell/except-occurr.hamlet
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
$newline never
|
||||||
|
_{MsgExceptionKindOccur}: #{exceptStart'}–#{exceptEnd'}
|
||||||
11
templates/widgets/occurrence/form/scheduled-layout.hamlet
Normal file
11
templates/widgets/occurrence/form/scheduled-layout.hamlet
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
$newline never
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
$forall coord <- review liveCoords lLength
|
||||||
|
<tr .massinput__cell>
|
||||||
|
^{cellWdgts ! coord}
|
||||||
|
<td>
|
||||||
|
^{fvInput (delButtons ! coord)}
|
||||||
|
<tfoot>
|
||||||
|
<tr .massinput__cell.massinput__cell--add>
|
||||||
|
^{addWdgts ! (0, 0)}
|
||||||
@ -586,9 +586,9 @@ fillDb = do
|
|||||||
, tutorialType = "Tutorium"
|
, tutorialType = "Tutorium"
|
||||||
, tutorialCapacity = Just 30
|
, tutorialCapacity = Just 30
|
||||||
, tutorialRoom = "Hilbert-Raum"
|
, tutorialRoom = "Hilbert-Raum"
|
||||||
, tutorialTime = Occurences
|
, tutorialTime = Occurrences
|
||||||
{ occurencesScheduled = Set.singleton $ ScheduleWeekly Tuesday (TimeOfDay 08 15 00) (TimeOfDay 10 00 00)
|
{ occurrencesScheduled = Set.singleton $ ScheduleWeekly Tuesday (TimeOfDay 08 15 00) (TimeOfDay 10 00 00)
|
||||||
, occurencesExceptions = Set.empty
|
, occurrencesExceptions = Set.empty
|
||||||
}
|
}
|
||||||
, tutorialRegGroup = Just "tutorium"
|
, tutorialRegGroup = Just "tutorium"
|
||||||
, tutorialRegisterFrom = Just now
|
, tutorialRegisterFrom = Just now
|
||||||
@ -604,9 +604,9 @@ fillDb = do
|
|||||||
, tutorialType = "Tutorium"
|
, tutorialType = "Tutorium"
|
||||||
, tutorialCapacity = Just 30
|
, tutorialCapacity = Just 30
|
||||||
, tutorialRoom = "Hilbert-Raum"
|
, tutorialRoom = "Hilbert-Raum"
|
||||||
, tutorialTime = Occurences
|
, tutorialTime = Occurrences
|
||||||
{ occurencesScheduled = Set.singleton $ ScheduleWeekly Tuesday (TimeOfDay 10 15 00) (TimeOfDay 12 00 00)
|
{ occurrencesScheduled = Set.singleton $ ScheduleWeekly Tuesday (TimeOfDay 10 15 00) (TimeOfDay 12 00 00)
|
||||||
, occurencesExceptions = Set.empty
|
, occurrencesExceptions = Set.empty
|
||||||
}
|
}
|
||||||
, tutorialRegGroup = Just "tutorium"
|
, tutorialRegGroup = Just "tutorium"
|
||||||
, tutorialRegisterFrom = Just now
|
, tutorialRegisterFrom = Just now
|
||||||
|
|||||||
@ -38,6 +38,10 @@ instance Arbitrary TutorialR where
|
|||||||
arbitrary = genericArbitrary
|
arbitrary = genericArbitrary
|
||||||
shrink = genericShrink
|
shrink = genericShrink
|
||||||
|
|
||||||
|
instance Arbitrary ExamR where
|
||||||
|
arbitrary = genericArbitrary
|
||||||
|
shrink = genericShrink
|
||||||
|
|
||||||
instance Arbitrary (Route UniWorX) where
|
instance Arbitrary (Route UniWorX) where
|
||||||
arbitrary = genericArbitrary
|
arbitrary = genericArbitrary
|
||||||
shrink = genericShrink
|
shrink = genericShrink
|
||||||
|
|||||||
@ -26,7 +26,7 @@ import Time.Types (WeekDay(..))
|
|||||||
|
|
||||||
instance (Arbitrary a, MonoFoldable a) => Arbitrary (NonNull a) where
|
instance (Arbitrary a, MonoFoldable a) => Arbitrary (NonNull a) where
|
||||||
arbitrary = arbitrary `suchThatMap` fromNullable
|
arbitrary = arbitrary `suchThatMap` fromNullable
|
||||||
|
|
||||||
instance Arbitrary Season where
|
instance Arbitrary Season where
|
||||||
arbitrary = genericArbitrary
|
arbitrary = genericArbitrary
|
||||||
shrink = genericShrink
|
shrink = genericShrink
|
||||||
@ -71,7 +71,7 @@ instance Arbitrary SheetGradeSummary where
|
|||||||
instance Arbitrary SheetGroup where
|
instance Arbitrary SheetGroup where
|
||||||
arbitrary = genericArbitrary
|
arbitrary = genericArbitrary
|
||||||
shrink = genericShrink
|
shrink = genericShrink
|
||||||
|
|
||||||
instance Arbitrary SheetTypeSummary where
|
instance Arbitrary SheetTypeSummary where
|
||||||
arbitrary = genericArbitrary
|
arbitrary = genericArbitrary
|
||||||
shrink = genericShrink
|
shrink = genericShrink
|
||||||
@ -79,7 +79,7 @@ instance Arbitrary SheetTypeSummary where
|
|||||||
instance Arbitrary SheetFileType where
|
instance Arbitrary SheetFileType where
|
||||||
arbitrary = genericArbitrary
|
arbitrary = genericArbitrary
|
||||||
shrink = genericShrink
|
shrink = genericShrink
|
||||||
|
|
||||||
instance Arbitrary SubmissionFileType where
|
instance Arbitrary SubmissionFileType where
|
||||||
arbitrary = genericArbitrary
|
arbitrary = genericArbitrary
|
||||||
shrink = genericShrink
|
shrink = genericShrink
|
||||||
@ -113,10 +113,6 @@ instance Arbitrary SubmissionModeDescr where
|
|||||||
arbitrary = genericArbitrary
|
arbitrary = genericArbitrary
|
||||||
shrink = genericShrink
|
shrink = genericShrink
|
||||||
|
|
||||||
instance Arbitrary ExamStatus where
|
|
||||||
arbitrary = genericArbitrary
|
|
||||||
shrink = genericShrink
|
|
||||||
|
|
||||||
instance Arbitrary Load where
|
instance Arbitrary Load where
|
||||||
arbitrary = do
|
arbitrary = do
|
||||||
byTutorial <- arbitrary
|
byTutorial <- arbitrary
|
||||||
@ -151,7 +147,7 @@ instance Arbitrary AuthTag where
|
|||||||
shrink = genericShrink
|
shrink = genericShrink
|
||||||
instance CoArbitrary AuthTag where
|
instance CoArbitrary AuthTag where
|
||||||
coarbitrary = genericCoarbitrary
|
coarbitrary = genericCoarbitrary
|
||||||
|
|
||||||
instance Arbitrary AuthTagActive where
|
instance Arbitrary AuthTagActive where
|
||||||
arbitrary = AuthTagActive <$> arbitrary
|
arbitrary = AuthTagActive <$> arbitrary
|
||||||
shrink = genericShrink
|
shrink = genericShrink
|
||||||
@ -180,7 +176,7 @@ instance Arbitrary AuthenticationMode where
|
|||||||
authPWHash = unsafePerformIO . fmap decodeUtf8 $ makePasswordWith pwHashAlgorithm pw (pwHashStrength `div` 2)
|
authPWHash = unsafePerformIO . fmap decodeUtf8 $ makePasswordWith pwHashAlgorithm pw (pwHashStrength `div` 2)
|
||||||
return $ AuthPWHash{..}
|
return $ AuthPWHash{..}
|
||||||
]
|
]
|
||||||
|
|
||||||
shrink AuthLDAP = []
|
shrink AuthLDAP = []
|
||||||
shrink (AuthPWHash _) = [AuthLDAP]
|
shrink (AuthPWHash _) = [AuthLDAP]
|
||||||
|
|
||||||
@ -199,18 +195,18 @@ instance Arbitrary Html where
|
|||||||
instance Arbitrary WeekDay where
|
instance Arbitrary WeekDay where
|
||||||
arbitrary = oneof $ map pure [minBound..maxBound]
|
arbitrary = oneof $ map pure [minBound..maxBound]
|
||||||
|
|
||||||
instance Arbitrary OccurenceSchedule where
|
instance Arbitrary OccurrenceSchedule where
|
||||||
arbitrary = genericArbitrary
|
arbitrary = genericArbitrary
|
||||||
shrink = genericShrink
|
shrink = genericShrink
|
||||||
|
|
||||||
instance Arbitrary OccurenceException where
|
instance Arbitrary OccurrenceException where
|
||||||
arbitrary = genericArbitrary
|
arbitrary = genericArbitrary
|
||||||
shrink = genericShrink
|
shrink = genericShrink
|
||||||
|
|
||||||
instance Arbitrary Occurences where
|
instance Arbitrary Occurrences where
|
||||||
arbitrary = genericArbitrary
|
arbitrary = genericArbitrary
|
||||||
shrink = genericShrink
|
shrink = genericShrink
|
||||||
|
|
||||||
|
|
||||||
spec :: Spec
|
spec :: Spec
|
||||||
spec = do
|
spec = do
|
||||||
@ -245,8 +241,6 @@ spec = do
|
|||||||
[ eqLaws, showReadLaws, ordLaws, jsonLaws, persistFieldLaws ]
|
[ eqLaws, showReadLaws, ordLaws, jsonLaws, persistFieldLaws ]
|
||||||
lawsCheckHspec (Proxy @SubmissionModeDescr)
|
lawsCheckHspec (Proxy @SubmissionModeDescr)
|
||||||
[ eqLaws, showReadLaws, ordLaws, boundedEnumLaws, finiteLaws, pathPieceLaws ]
|
[ eqLaws, showReadLaws, ordLaws, boundedEnumLaws, finiteLaws, pathPieceLaws ]
|
||||||
lawsCheckHspec (Proxy @ExamStatus)
|
|
||||||
[ eqLaws, showReadLaws, ordLaws, boundedEnumLaws, persistFieldLaws ]
|
|
||||||
lawsCheckHspec (Proxy @Load)
|
lawsCheckHspec (Proxy @Load)
|
||||||
[ eqLaws, showReadLaws, ordLaws, jsonLaws, persistFieldLaws, commutativeSemigroupLaws, commutativeMonoidLaws ]
|
[ eqLaws, showReadLaws, ordLaws, jsonLaws, persistFieldLaws, commutativeSemigroupLaws, commutativeMonoidLaws ]
|
||||||
lawsCheckHspec (Proxy @Season)
|
lawsCheckHspec (Proxy @Season)
|
||||||
|
|||||||
Reference in New Issue
Block a user