Merge branch 'master' into 466-zeit-wird-beim-editieren-zuruckgesetzt

This commit is contained in:
Gregor Kleen 2019-09-26 11:02:56 +02:00
commit 2ac6fc55d2
162 changed files with 2587 additions and 1486 deletions

View File

@ -2,6 +2,49 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
## [7.0.0](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/compare/v6.11.1...v7.0.0) (2019-09-25)
### Bug Fixes
* fix startup on unix-socket ([39f1295](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/39f1295))
* improve async behaviour ([cc7a528](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/cc7a528))
* make migration idempotent again ([9778404](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/9778404))
* restore behaviour of waiting asynchronously for job-management ([5ebcd89](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/5ebcd89))
* **communication:** make communication form more intuitive ([7a2b972](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/7a2b972)), closes [#387](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/issues/387)
* fix migration ([d2478a3](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/d2478a3))
* fix migration & tests ([e05ea8e](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/e05ea8e))
* migration ([4383eb1](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/4383eb1))
* syntax ([7afd569](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/7afd569))
* **migration:** drop more tables in w.a. for inconsistent 21→22 ([d79dca6](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/d79dca6))
* typo ([fb1e42d](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/fb1e42d))
### chore
* bump versions ([67e3b38](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/67e3b38))
### Features
* **course:** additional crosslinking ([5eaba78](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/5eaba78))
* **exam-users:** document part-* family of columns ([fe07a22](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/fe07a22))
* **exams:** accept/reset computed results ([72342f1](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/72342f1))
* **exams:** automatically compute examResults ([ea5a398](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/ea5a398))
* **exams:** better display exam-result-information ([0ebda4d](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/0ebda4d))
* **exams:** csv-import of ExamPartResults ([29f4e28](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/29f4e28))
* **exams:** implement rounding of exambonus ([e97cd56](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/e97cd56))
* **exams:** refine exam form ([014a17a](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/014a17a))
### BREAKING CHANGES
* yesod >=1.6
* **exams:** examPartName no longer required
* **exams:** Introduces ExamPartNumbers
### [6.11.1](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/compare/v6.11.0...v6.11.1) (2019-09-17) ### [6.11.1](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/compare/v6.11.0...v6.11.1) (2019-09-17)

View File

@ -5,6 +5,14 @@ The following description applies to Ubuntu and similar debian based Linux distr
## Prerequisites ## Prerequisites
These are the things you need to do/install before you can get started working on Uni2work. These are the things you need to do/install before you can get started working on Uni2work.
### Install german locale
You will need to install the german locale at compile time.
Install:
- Edit `/etc/locale.gen` as root and uncomment/add the line `de_DE.UTF-8 UTF-8`
- Save the file and run `sudo locale-gen`
### Clone repository ### Clone repository
Clone this repository and navigate into it Clone this repository and navigate into it
```sh ```sh
@ -41,7 +49,7 @@ You'll get a prompt:
```sh ```sh
Enter name of role to add: uniworx Enter name of role to add: uniworx
Shall the new role be a superuser? (y/n) [not exactly sure. Guess not?] Shall the new role be a superuser? (y/n) y [user must be superuser to create extensions]
Password: uniworx Password: uniworx
... ...
``` ```
@ -89,18 +97,6 @@ $ sudo apt-get install pkg-config
$ sudo apt-get install libsodium-dev $ 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` & `npm`
Node and Npm are needed to compile the frontend. Node and Npm are needed to compile the frontend.
@ -110,6 +106,18 @@ $ curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash -
$ sudo apt-get install -y nodejs $ sudo apt-get install -y nodejs
``` ```
Build the app:
```sh
$ npm run build
```
This might take a few minutes... if not hours... be prepared.
install yesod:
```sh
$ stack install yesod-bin --install-ghc
```
### Add dummy data to the database ### Add dummy data to the database
After building the app you can prepare the database and add some dummy data: After building the app you can prepare the database and add some dummy data:
```sh ```sh
@ -118,7 +126,7 @@ $ ./db.sh -f
## Run Uni2work ## Run Uni2work
```sh ```sh
$ npm start $ npm run 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. 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.

View File

@ -67,7 +67,7 @@ update = do
restartAppInNewThread tidStore = modifyStoredIORef tidStore $ \tid -> do restartAppInNewThread tidStore = modifyStoredIORef tidStore $ \tid -> do
killThread tid killThread tid
withStore doneStore takeMVar withStore doneStore takeMVar
readStore doneStore >>= start withStore doneStore start
-- | Start the server in a separate thread. -- | Start the server in a separate thread.
@ -77,10 +77,7 @@ update = do
(port, site, app) <- getApplicationRepl (port, site, app) <- getApplicationRepl
resourceForkIO $ do resourceForkIO $ do
finally (liftIO $ runSettings (setPort port defaultSettings) app) finally (liftIO $ runSettings (setPort port defaultSettings) app)
-- Note that this implies concurrency (liftIO $ shutdownApp site `finally` putMVar done ())
-- between shutdownApp and the next app that is starting.
-- Normally this should be fine
(liftIO $ putMVar done () >> shutdownApp site)
-- | kill the server -- | kill the server
shutdown :: IO () shutdown :: IO ()

View File

@ -10,6 +10,8 @@ case $1 in
;; ;;
*) *)
target=".stack-work-${1}" target=".stack-work-${1}"
shift
if [[ ! -d "${target}" ]]; then if [[ ! -d "${target}" ]]; then
printf "%s does not exist or is no directory\n" "${target}" >&2 printf "%s does not exist or is no directory\n" "${target}" >&2
exit 1 exit 1
@ -20,7 +22,11 @@ case $1 in
fi fi
move-back() { move-back() {
mv -v .stack-work "${target}" if [[ -d .stack-work ]]; then
mv -v .stack-work "${target}"
else
mkdir -v "${target}"
fi
[[ -d .stack-work-clean ]] && mv -v .stack-work-clean .stack-work [[ -d .stack-work-clean ]] && mv -v .stack-work-clean .stack-work
} }
@ -28,6 +34,9 @@ case $1 in
mv -v "${target}" .stack-work mv -v "${target}" .stack-work
trap move-back EXIT trap move-back EXIT
stack clean (
set -ex
stack clean $@
)
;; ;;
esac esac

View File

@ -8,3 +8,5 @@ log-settings:
destination: "test.log" destination: "test.log"
auth-dummy-login: true auth-dummy-login: true
job-workers: 1

View File

@ -33,11 +33,14 @@
margin: 7px 0; margin: 7px 0;
} }
.form-section-title__hint { .form-group__hint, .form-section-title__hint {
margin-top: 7px;
color: var(--color-fontsec); color: var(--color-fontsec);
font-size: 0.9rem; font-size: 0.9rem;
font-weight: 600; font-weight: 600;
}
.form-section-title__hint {
margin-top: 7px;
+ .form-group { + .form-group {
margin-top: 11px; margin-top: 11px;
@ -58,6 +61,7 @@
.form-group--required .form-group-label__caption::after, .form-group__required-marker::before { .form-group--required .form-group-label__caption::after, .form-group__required-marker::before {
content: ' *'; content: ' *';
color: var(--color-error); color: var(--color-error);
font-weight: 600;
} }
.form-group--optional { .form-group--optional {

View File

@ -15,4 +15,4 @@ if [[ -d .stack-work-doc ]]; then
trap move-back EXIT trap move-back EXIT
fi fi
stack build --fast --flag uniworx:library-only --flag uniworx:dev --haddock --haddock-hyperlink-source --haddock-deps --haddock-internal stack build --fast --flag uniworx:library-only --flag uniworx:dev --haddock --haddock-hyperlink-source --haddock-deps --haddock-internal ${@}

View File

@ -1135,8 +1135,10 @@ NavigationFavourites: Favoriten
CommSubject: Betreff CommSubject: Betreff
CommBody: Nachricht CommBody: Nachricht
CommBodyTip: Das Eingabefeld akzeptiert derzeit ausschließlich Html. U.A. Zeilumbrüche werden dementsprechend ignoriert und müssen manuell mit <br> eingefügt werden.
CommRecipients: Empfänger CommRecipients: Empfänger
CommRecipientsTip: Sie selbst erhalten immer eine Kopie der Nachricht CommRecipientsTip: Sie selbst erhalten immer eine Kopie der Nachricht
CommRecipientsList: Die an Sie selbst verschickte Kopie der Nachricht wird, zu Archivierungszwecken, eine vollständige Liste aller Empfänger enthalten. Die Empfängerliste wird im CSV-Format and die E-Mail angehängt. Andere Empfänger erhalten die Liste nicht. Bitte entfernen Sie dementsprechend den Anhang bevor Sie die E-Mail weiterleiten oder anderweitig mit Dritten teilen.
CommDuplicateRecipients n@Int: #{n} #{pluralDE n "doppelter" "doppelte"} Empfänger ignoriert CommDuplicateRecipients n@Int: #{n} #{pluralDE n "doppelter" "doppelte"} Empfänger ignoriert
CommSuccess n@Int: Nachricht wurde an #{n} Empfänger versandt CommSuccess n@Int: Nachricht wurde an #{n} Empfänger versandt
@ -1329,28 +1331,40 @@ ExamClosedTip: Prüfungsämter, die im System Noten einsehen, werden zu diesem Z
ExamShowGrades: Klausur ist benotet ExamShowGrades: Klausur ist benotet
ExamShowGradesTip: Sollen genaue Noten angezeigt werden, oder sollen Teilnehmer und Prüfungsämter nur informiert werden, ob die Klausur bestanden wurde? ExamShowGradesTip: Sollen genaue Noten angezeigt werden, oder sollen Teilnehmer und Prüfungsämter nur informiert werden, ob die Klausur bestanden wurde?
ExamPublicStatistics: Statistik veröffentlichen ExamPublicStatistics: Statistik veröffentlichen
ExamPublicStatisticsTip: Soll die statistische Auswertung auch den Teilnehmern angezeigt werden, sobald diese ihre Noten einsehen können? ExamPublicStatisticsTip: Soll die automatisch berechnete statistische Auswertung auch den Teilnehmern angezeigt werden, sobald diese ihre Noten einsehen können?
ExamAutomaticGrading: Automatische Notenberechnung
ExamAutomaticGradingTip: Sollen die Gesamtleistungen der Teilnehmer automatisch aus den in den einzelnen Teilprüfungen erreichten Leistungen berechnet werden? Etwaige Bonuspunkte werden dabei berücksichtigt. Manuelles Überschreiben der Gesamtleistung ist dennoch möglich.
ExamGradingRule: Notenberechnung ExamGradingRule: Notenberechnung
ExamGradingManual': Keine automatische Berechnung ExamGradingManual': Keine automatische Berechnung
ExamGradingKey': Nach Schlüssel ExamGradingKey': Nach Schlüssel
ExamGradingKey: Notenschlüssel ExamGradingKey: Notenschlüssel
ExamGradingKeyTip: Die Grenzen beziehen sich auf die effektive Maximalpunktzahl, nachdem etwaige Bonuspunkte aus dem Übungsbetrieb angerechnet und die Ergebnise der Teilaufgaben mit ihrem Gewicht multipliziert wurden ExamGradingKeyTip: Die Grenzen beziehen sich auf die effektive Maximalpunktzahl, nachdem etwaige Bonuspunkte aus dem Übungsbetrieb angerechnet und die Ergebnise der Teilprüfungen mit ihrem Gewicht multipliziert wurden
Points: Punkte Points: Punkte
PointsMustBeNonNegative: Punktegrenzen dürfen nicht negativ sein PointsMustBeNonNegative: Punktegrenzen dürfen nicht negativ sein
PointsMustBeMonotonic: Punktegrenzen müssen aufsteigend sein PointsMustBeMonotonic: Punktegrenzen müssen aufsteigend sein
GradingFrom: Ab GradingFrom: Ab
ExamNew: Neue Prüfung ExamNew: Neue Prüfung
ExamBonus: Bonuspunkte-System
ExamBonusRule: Prüfungsbonus aus Übungsbetrieb ExamBonusRule: Prüfungsbonus aus Übungsbetrieb
ExamNoBonus': Kein automatischer Bonus ExamNoBonus': Kein automatischer Bonus
ExamBonusPoints': Umrechnung von Übungspunkten ExamBonusPoints': Umrechnung von Übungspunkten
ExamBonusManual': Manuelle Berechnung
ExamBonusAchieved: Bonuspunkte
ExamEditHeading examn@ExamName: #{examn} bearbeiten ExamEditHeading examn@ExamName: #{examn} bearbeiten
ExamBonusMaxPoints: Maximal erreichbare Prüfungs-Bonuspunkte ExamBonusMaxPoints: Maximal erreichbare Prüfungs-Bonuspunkte
ExamBonusMaxPointsTip: Bonuspunkte werden, anhand der erreichten Übungspunkte bzw. der Anzahl von bestandenen Übungsblättern, linear zwischen null und der angegebenen Schranke interpoliert.
ExamBonusMaxPointsNonPositive: Maximaler Prüfungsbonus muss positiv und größer null sein ExamBonusMaxPointsNonPositive: Maximaler Prüfungsbonus muss positiv und größer null sein
ExamBonusOnlyPassed: Bonus nur nach Bestehen anrechnen ExamBonusOnlyPassed: Bonus nur nach Bestehen anrechnen
ExamBonusRound: Bonus runden auf
ExamBonusRoundNonPositive: Vielfaches, auf das gerundet werden soll, muss positiv und größer null sein
ExamBonusRoundTip: Bonuspunkte werden kaufmännisch auf ein Vielfaches der angegeben Zahl gerundet.
ExamOccurrenceRule: Automatische Termin- bzw. Raumzuteilung ExamAutomaticOccurrenceAssignment: Automatische Termin- bzw. Raumzuteilung
ExamAutomaticOccurrenceAssignmentTip: Sollen Prüfungsteilnehmer zum Zeitpunkt der Bekanntgabe der Raum- bzw. Terminzuteilung automatisch auf die zur Verfügung stehenden Räume bzw. Termine verteilt werden? Manuelle Umverteilung bzw. vorheriges Festlegen von Zuteilungen einzelner Teilnehmer ist trotzdem möglich.
ExamOccurrenceRule: Verfahren
ExamOccurrenceRuleParticipant: Termin- bzw. Raumzuteilungsverfahren ExamOccurrenceRuleParticipant: Termin- bzw. Raumzuteilungsverfahren
ExamRoomManual': Keine automatische Zuteilung ExamRoomManual': Keine automatische Zuteilung
ExamRoomSurname': Nach Nachname ExamRoomSurname': Nach Nachname
@ -1384,12 +1398,17 @@ ExamFormParts: Teile
ExamCorrectors: Korrektoren ExamCorrectors: Korrektoren
ExamCorrectorAlreadyAdded: Ein Korrektor mit dieser E-Mail ist bereits für diese Prüfung eingetragen ExamCorrectorAlreadyAdded: Ein Korrektor mit dieser E-Mail ist bereits für diese Prüfung eingetragen
ExamParts: Teilaufgaben ExamParts: Teilprüfungen/Aufgaben
ExamPartWeightNegative: Gewicht aller Teilaufgaben muss größer oder gleich Null sein ExamPartWeightNegative: Gewicht aller Teilprüfungen muss größer oder gleich Null sein
ExamPartAlreadyExists: Teilaufgabe mit diesem Namen existiert bereits ExamPartAlreadyExists: Teilprüfung mit diesem Namen existiert bereits
ExamPartName: Name ExamPartNumber: Nummer
ExamPartNumbered examPartNumber@ExamPartNumber: Teil #{view _ExamPartNumber examPartNumber}
ExamPartNumberTip: Wird als interne Bezeichnung z.B. bei CSV-Export verwendet
ExamPartName: Titel
ExamPartNameTip: Wird den Studierenden angezeigt
ExamPartMaxPoints: Maximalpunktzahl ExamPartMaxPoints: Maximalpunktzahl
ExamPartWeight: Gewichtung ExamPartWeight: Gewichtung
ExamPartWeightTip: Wird vor Anzeige oder Notenberechnung mit der erreichten Punktzahl und der Maximalpunktzahl multipliziert; Änderungen hier passen auch bestehende Korrekturergebnisse an
ExamPartResultPoints: Erreichte Punkte ExamPartResultPoints: Erreichte Punkte
ExamNameTaken exam@ExamName: Es existiert bereits eine Prüfung mit Namen #{exam} ExamNameTaken exam@ExamName: Es existiert bereits eine Prüfung mit Namen #{exam}
@ -1399,6 +1418,7 @@ ExamEdited exam@ExamName: #{exam} erfolgreich bearbeitet
ExamNoShow: Nicht erschienen ExamNoShow: Nicht erschienen
ExamVoided: Entwertet ExamVoided: Entwertet
ExamBonusManualParticipants: Von den Kursverwaltern manuell berechnet
ExamBonusPoints possible@Points: Maximal #{showFixed True possible} Prüfungspunkte ExamBonusPoints possible@Points: Maximal #{showFixed True possible} Prüfungspunkte
ExamBonusPointsPassed possible@Points: Maximal #{showFixed True possible} Prüfungspunkte, falls die Prüfung auch ohne Bonus bereits bestanden ist ExamBonusPointsPassed possible@Points: Maximal #{showFixed True possible} Prüfungspunkte, falls die Prüfung auch ohne Bonus bereits bestanden ist
@ -1437,8 +1457,13 @@ ExamSynchronised: Synchronisiert
ExamUsersHeading: Prüfungsteilnehmer ExamUsersHeading: Prüfungsteilnehmer
ExamUserDeregister: Teilnehmer von Prüfung abmelden ExamUserDeregister: Teilnehmer von Prüfung abmelden
ExamUserAssignOccurrence: Termin/Raum zuweisen ExamUserAssignOccurrence: Termin/Raum zuweisen
ExamUserAcceptComputedResult: Berechnetes Prüfungsergebnis übernehmen
ExamUserResetToComputedResult: Prüfungsergebnis zurücksetzen
ExamUserResetBonus: Auch Bonuspunkte zurücksetzen
ExamUsersDeregistered count@Int64: #{show count} Teilnehmer von der Prüfung abgemeldet ExamUsersDeregistered count@Int64: #{show count} Teilnehmer von der Prüfung abgemeldet
ExamUsersOccurrenceUpdated count@Int64: Termin/Raum für #{show count} Teilnehmer gesetzt ExamUsersOccurrenceUpdated count@Int64: Termin/Raum für #{show count} Teilnehmer gesetzt
ExamUsersResultsAccepted count@Int64: Prüfungsergebnis für #{show count} Teilnehmer übernommen
ExamUsersResultsReset count@Int64: Prüfungsergebnis für #{show count} Teilnehmer zurückgesetzt
ExamUserSynchronised: Synchronisiert ExamUserSynchronised: Synchronisiert
ExamUserSyncOfficeName: Name ExamUserSyncOfficeName: Name
@ -1487,6 +1512,8 @@ CsvColumnExamUserExercisePoints: Anzahl von Punkten, die der Teilnehmer im Übun
CsvColumnExamUserExercisePointsMax: Maximale Anzahl von Punkten, die der Teilnehmer im Übungsbetrieb bis zu seinem Prüfungstermin erreichen hätte können CsvColumnExamUserExercisePointsMax: Maximale Anzahl von Punkten, die der Teilnehmer im Übungsbetrieb bis zu seinem Prüfungstermin erreichen hätte können
CsvColumnExamUserExercisePasses: Anzahl von Übungsblättern, die der Teilnehmer bestanden hat CsvColumnExamUserExercisePasses: Anzahl von Übungsblättern, die der Teilnehmer bestanden hat
CsvColumnExamUserExercisePassesMax: Maximale Anzahl von Übungsblättern, die der Teilnehmer bis zu seinem Prüfungstermin bestehen hätte können CsvColumnExamUserExercisePassesMax: Maximale Anzahl von Übungsblättern, die der Teilnehmer bis zu seinem Prüfungstermin bestehen hätte können
CsvColumnExamUserBonus: Anzurechnende Bonuspunkte
CsvColumnExamUserParts: Erreichte Punktezahlen in den Teilprüfungen, sofern vorhanden; eine Spalte pro Teilprüfung
CsvColumnExamUserResult: Erreichte Prüfungsleistung; "passed", "failed", "no-show", "voided", oder eine Note ("1.0", "1.3", "1.7", ..., "4.0", "5.0") CsvColumnExamUserResult: Erreichte Prüfungsleistung; "passed", "failed", "no-show", "voided", oder eine Note ("1.0", "1.3", "1.7", ..., "4.0", "5.0")
CsvColumnExamUserCourseNote: Notizen zum Teilnehmer CsvColumnExamUserCourseNote: Notizen zum Teilnehmer
@ -1518,8 +1545,13 @@ ExamUserCsvRegister: Kursteilnehmer zur Prüfung anmelden
ExamUserCsvAssignOccurrence: Teilnehmern einen anderen Termin/Raum zuweisen ExamUserCsvAssignOccurrence: Teilnehmern einen anderen Termin/Raum zuweisen
ExamUserCsvDeregister: Teilnehmer von der Prüfung abmelden ExamUserCsvDeregister: Teilnehmer von der Prüfung abmelden
ExamUserCsvSetCourseField: Kurs-assoziiertes Studienfach ändern ExamUserCsvSetCourseField: Kurs-assoziiertes Studienfach ändern
ExamUserCsvOverrideBonus: Bonuspunkte entgegen Bonusregelung überschreiben
ExamUserCsvOverrideResult: Ergebnis entgegen automatischer Notenberechnung überschreiben
ExamUserCsvSetBonus: Bonuspunkte eintragen
ExamUserCsvSetResult: Ergebnis eintragen ExamUserCsvSetResult: Ergebnis eintragen
ExamUserCsvSetPartResult: Ergebnis einer Teilprüfung eintragen
ExamUserCsvSetCourseNote: Teilnehmer-Notizen anpassen ExamUserCsvSetCourseNote: Teilnehmer-Notizen anpassen
ExamBonusNone: Keine Bonuspunkte
ExamUserCsvCourseNoteDeleted: Notiz wird gelöscht ExamUserCsvCourseNoteDeleted: Notiz wird gelöscht

View File

@ -17,11 +17,11 @@ Course -- Information about a single course; contained info is always visible
deregisterUntil UTCTime Maybe -- unenrolement may be prohibited from a given date onwards deregisterUntil UTCTime Maybe -- unenrolement may be prohibited from a given date onwards
registerSecret Text Maybe -- enrolement maybe protected by a simple common passphrase registerSecret Text Maybe -- enrolement maybe protected by a simple common passphrase
materialFree Bool -- False: only enrolled users may see course materials not stored in this table materialFree Bool -- False: only enrolled users may see course materials not stored in this table
applicationsRequired Bool applicationsRequired Bool default=false
applicationsInstructions Html Maybe applicationsInstructions Html Maybe
applicationsText Bool applicationsText Bool default=false
applicationsFiles UploadMode applicationsFiles UploadMode "default='{\"mode\": \"no-upload\"}'::jsonb"
applicationsRatingsVisible Bool applicationsRatingsVisible Bool default=false
TermSchoolCourseShort term school shorthand -- shorthand must be unique within school and semester TermSchoolCourseShort term school shorthand -- shorthand must be unique within school and semester
TermSchoolCourseName term school name -- name must be unique within school and semester TermSchoolCourseName term school name -- name must be unique within school and semester
deriving Generic deriving Generic

View File

@ -1,9 +1,9 @@
Exam Exam
course CourseId course CourseId
name ExamName name ExamName
gradingRule ExamGradingRule gradingRule ExamGradingRule Maybe
bonusRule ExamBonusRule bonusRule ExamBonusRule Maybe
occurrenceRule ExamOccurrenceRule occurrenceRule ExamOccurrenceRule Maybe
visibleFrom UTCTime Maybe visibleFrom UTCTime Maybe
registerFrom UTCTime Maybe registerFrom UTCTime Maybe
registerTo UTCTime Maybe registerTo UTCTime Maybe
@ -19,10 +19,12 @@ Exam
UniqueExam course name UniqueExam course name
ExamPart ExamPart
exam ExamId exam ExamId
name (CI Text) number ExamPartNumber
name ExamPartName Maybe
maxPoints Points Maybe maxPoints Points Maybe
weight Rational weight Rational
UniqueExamPart exam name UniqueExamPartNumber exam number
UniqueExamPartName exam name !force
ExamOccurrence ExamOccurrence
exam ExamId exam ExamId
name ExamOccurrenceName name ExamOccurrenceName
@ -42,7 +44,14 @@ ExamPartResult
examPart ExamPartId examPart ExamPartId
user UserId user UserId
result ExamResultPoints result ExamResultPoints
lastChanged UTCTime default=now()
UniqueExamPartResult examPart user UniqueExamPartResult examPart user
ExamBonus
exam ExamId
user UserId
bonus Points
lastChanged UTCTime default=now()
UniqueExamBonus exam user
ExamResult ExamResult
exam ExamId exam ExamId
user UserId user UserId

View File

@ -4,6 +4,6 @@
import ((nixpkgs {}).fetchFromGitHub { import ((nixpkgs {}).fetchFromGitHub {
owner = "NixOS"; owner = "NixOS";
repo = "nixpkgs"; repo = "nixpkgs";
rev = "19.03"; rev = "10e61bf5be57736035ec7a804cb0bf3d083bf2cf";
sha256 = "0q2m2qhyga9yq29yz90ywgjbn9hdahs7i8wwlq7b55rdbyiwa5dy"; sha256 = "0fplfm2zx4vk7gs8bdcxnvzkdmpx2w0llqwf8475z9dz9cl132rm";
}) })

14
package-lock.json generated
View File

@ -1,6 +1,6 @@
{ {
"name": "uni2work", "name": "uni2work",
"version": "6.11.1", "version": "7.0.0",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@ -7702,9 +7702,9 @@
"dev": true "dev": true
}, },
"handlebars": { "handlebars": {
"version": "4.1.2", "version": "4.3.1",
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.2.tgz", "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.3.1.tgz",
"integrity": "sha512-nvfrjqvt9xQ8Z/w0ijewdD/vvWDTOweBUm96NTr66Wfvo1mJenBLwcYmPs3TIBP5ruzYGD7Hx/DaM9RmhroGPw==", "integrity": "sha512-c0HoNHzDiHpBt4Kqe99N8tdLPKAnGCQ73gYMPWtAYM4PwGnf7xl8PBUHJqh9ijlzt2uQKaSRxbXRt+rZ7M2/kA==",
"dev": true, "dev": true,
"requires": { "requires": {
"neo-async": "^2.6.0", "neo-async": "^2.6.0",
@ -15623,9 +15623,9 @@
"dev": true "dev": true
}, },
"uglify-js": { "uglify-js": {
"version": "3.5.15", "version": "3.6.0",
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.5.15.tgz", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.0.tgz",
"integrity": "sha512-fe7aYFotptIddkwcm6YuA0HmknBZ52ZzOsUxZEdhhkSsz7RfjHDX2QDxwKTiv4JQ5t5NhfmpgAK+J7LiDhKSqg==", "integrity": "sha512-W+jrUHJr3DXKhrsS7NUVxn3zqMOFn0hL/Ei6v0anCIMoKC93TjcflTagwIHLW7SfMFfiQuktQyFVCFHGUE0+yg==",
"dev": true, "dev": true,
"optional": true, "optional": true,
"requires": { "requires": {

View File

@ -1,6 +1,6 @@
{ {
"name": "uni2work", "name": "uni2work",
"version": "6.11.1", "version": "7.0.0",
"description": "", "description": "",
"keywords": [], "keywords": [],
"author": "", "author": "",
@ -14,7 +14,9 @@
"yesod:start": "./start.sh", "yesod:start": "./start.sh",
"yesod:lint": "./hlint.sh", "yesod:lint": "./hlint.sh",
"yesod:test": "./test.sh", "yesod:test": "./test.sh",
"yesod:test:watch": "./test.sh --file-watch",
"yesod:build": "./build.sh", "yesod:build": "./build.sh",
"yesod:build:watch": "./build.sh --file-watch",
"frontend:lint": "eslint frontend/src", "frontend:lint": "eslint frontend/src",
"frontend:test": "karma start --conf karma.conf.js", "frontend:test": "karma start --conf karma.conf.js",
"frontend:test:watch": "karma start --conf karma.conf.js --single-run false", "frontend:test:watch": "karma start --conf karma.conf.js --single-run false",

View File

@ -1,41 +1,39 @@
name: uniworx name: uniworx
version: 6.11.1 version: 7.0.0
dependencies: dependencies:
# Due to a bug in GHC 8.0.1, we block its usage - base >=4.9.1.0 && <5
# See: https://ghc.haskell.org/trac/ghc/ticket/12130 - yesod >=1.6 && <1.7
- base >=4.8.2.0 && <4.9 || >=4.9.1.0 && <5 - yesod-core >=1.6 && <1.7
# version 1.0 had a bug in reexporting Handler, causing trouble - yesod-auth >=1.6 && <1.7
- classy-prelude-yesod >=0.10.2 && <1.0 || >=1.1 - yesod-static >=1.6 && <1.7
- foreign-store - yesod-form >=1.6 && <1.7
- yesod >=1.4.3 && <1.5 - classy-prelude >=1.5 && <1.6
- yesod-core >=1.4.30 && <1.5 - classy-prelude-conduit >=1.5 && <1.6
- yesod-auth >=1.4.0 && <1.5 - classy-prelude-yesod >=1.5 && <1.6
- yesod-static >=1.4.0.3 && <1.6 - bytestring >=0.10 && <0.11
- yesod-form >=1.4.0 && <1.5
- classy-prelude >=0.10.2
- classy-prelude-conduit >=0.10.2
- bytestring >=0.9 && <0.11
- text >=0.11 && <2.0 - text >=0.11 && <2.0
- persistent >=2.7.2 && <2.8 - persistent >=2.9 && <2.10
- persistent-postgresql >=2.1.1 && <2.8 - persistent-postgresql >=2.9 && <2.10
- persistent-template >=2.0 && <2.8 - persistent-template >=2.5 && <2.9
- persistent-qq >=2.9 && <2.10
- template-haskell - template-haskell
- shakespeare >=2.0 && <2.1 - shakespeare >=2.0 && <2.1
- hjsmin >=0.1 && <0.3 - hjsmin >=0.1 && <0.3
- monad-control >=0.3 && <1.1 - monad-control >=0.3 && <1.1
- wai-extra >=3.0 && <3.1 - wai-extra >=3.0 && <3.1
- yaml >=0.8 && <0.9 - yaml >=0.11 && <0.12
- http-conduit >=2.1 && <2.3 - http-conduit >=2.3 && <2.4
- directory >=1.1 && <1.4 - directory >=1.1 && <1.4
- warp >=3.0 && <3.3 - warp >=3.0 && <3.3
- data-default - data-default
- aeson >=0.6 && <1.3 - aeson >=1.4 && <1.5
- conduit >=1.0 && <2.0 - conduit >=1.0 && <2.0
- conduit-combinators - conduit-combinators
- monad-logger >=0.3 && <0.4 - monad-logger >=0.3 && <0.4
- fast-logger >=2.2 && <2.5 - fast-logger >=2.2 && <2.5
- wai-logger >=2.2 && <2.4 - wai-logger >=2.2 && <2.4
- foreign-store
- file-embed - file-embed
- safe - safe
- unordered-containers - unordered-containers
@ -52,11 +50,12 @@ dependencies:
- http-api-data - http-api-data
- profunctors - profunctors
- colonnade >=1.1.1 - colonnade >=1.1.1
- yesod-colonnade >=1.1.0
- blaze-markup - blaze-markup
- zip-stream - zip-stream
- encoding
- filepath - filepath
- transformers - transformers
- transformers-base
- wl-pprint-text - wl-pprint-text
- uuid-types - uuid-types
- path-pieces - path-pieces
@ -100,8 +99,10 @@ dependencies:
- th-abstraction - th-abstraction
- HaskellNet - HaskellNet
- HaskellNet-SSL - HaskellNet-SSL
- network - network >=3
- resource-pool - network-bsd
- unliftio
- unliftio-pool
- mime-mail - mime-mail
- hashable - hashable
- aeson-pretty - aeson-pretty
@ -116,7 +117,6 @@ dependencies:
- pkcs7 - pkcs7
- memcached-binary - memcached-binary
- directory-tree - directory-tree
- lifted-base
- lattices - lattices
- hsass - hsass
- semigroupoids - semigroupoids
@ -126,7 +126,6 @@ dependencies:
- mono-traversable - mono-traversable
- lens-aeson - lens-aeson
- systemd - systemd
- lifted-async
- streaming-commons - streaming-commons
- hourglass - hourglass
- unix - unix
@ -138,6 +137,7 @@ dependencies:
- pqueue - pqueue
- deepseq - deepseq
- multiset - multiset
- retry
other-extensions: other-extensions:
- GeneralizedNewtypeDeriving - GeneralizedNewtypeDeriving
@ -183,6 +183,7 @@ default-extensions:
- DeriveLift - DeriveLift
- DeriveFunctor - DeriveFunctor
- DerivingStrategies - DerivingStrategies
- DerivingVia
- DataKinds - DataKinds
- BinaryLiterals - BinaryLiterals
- PolyKinds - PolyKinds
@ -190,9 +191,12 @@ default-extensions:
- TypeApplications - TypeApplications
- RecursiveDo - RecursiveDo
- TypeFamilyDependencies - TypeFamilyDependencies
- QuantifiedConstraints
ghc-options: ghc-options:
- -Wall - -Wall
- -Wmissing-home-modules
- -Wredundant-constraints
- -fno-warn-type-defaults - -fno-warn-type-defaults
- -fno-warn-unrecognised-pragmas - -fno-warn-unrecognised-pragmas
- -fno-warn-partial-type-signatures - -fno-warn-partial-type-signatures

View File

@ -19,7 +19,7 @@ let
''; '';
override = oldAttrs: { override = oldAttrs: {
nativeBuildInputs = oldAttrs.nativeBuildInputs ++ (with pkgs; [ nodejs-8_x postgresql pgadmin openldap ]) ++ (with haskellPackages; [ stack yesod-bin hlint cabal-install ]); nativeBuildInputs = oldAttrs.nativeBuildInputs ++ (with pkgs; [ nodejs-12_x postgresql openldap ]) ++ (with haskellPackages; [ stack yesod-bin hlint cabal-install ]);
shellHook = '' shellHook = ''
export PROMPT_INFO="${oldAttrs.name}" export PROMPT_INFO="${oldAttrs.name}"
@ -47,6 +47,12 @@ let
set +xe set +xe
fi fi
if [ -n "$ZSH_VERSION" ]; then
autoload -U +X compinit && compinit
autoload -U +X bashcompinit && bashcompinit
fi
eval "$(stack --bash-completion-script stack)"
${oldAttrs.shellHook} ${oldAttrs.shellHook}
''; '';
}; };

View File

@ -24,7 +24,7 @@ import Language.Haskell.TH.Syntax (qLocation)
import Network.Wai (Middleware) import Network.Wai (Middleware)
import Network.Wai.Handler.Warp (Settings, defaultSettings, import Network.Wai.Handler.Warp (Settings, defaultSettings,
defaultShouldDisplayException, defaultShouldDisplayException,
runSettingsSocket, setHost, runSettings, runSettingsSocket, setHost,
setBeforeMainLoop, setBeforeMainLoop,
setOnException, setPort, getPort) setOnException, setPort, getPort)
import Data.Streaming.Network (bindPortTCP) import Data.Streaming.Network (bindPortTCP)
@ -54,7 +54,9 @@ import qualified Data.ByteString.Lazy as LBS
import Network.HaskellNet.SSL hiding (Settings) import Network.HaskellNet.SSL hiding (Settings)
import Network.HaskellNet.SMTP.SSL as SMTP hiding (Settings) import Network.HaskellNet.SMTP.SSL as SMTP hiding (Settings)
import Data.Pool
import UnliftIO.Concurrent
import UnliftIO.Pool
import Control.Monad.Trans.Resource import Control.Monad.Trans.Resource
@ -70,17 +72,17 @@ import System.Exit
import qualified Database.Memcached.Binary.IO as Memcached import qualified Database.Memcached.Binary.IO as Memcached
import qualified System.Systemd.Daemon as Systemd import qualified System.Systemd.Daemon as Systemd
import Control.Concurrent.Async.Lifted.Safe
import System.Environment (lookupEnv) import System.Environment (lookupEnv)
import System.Posix.Process (getProcessID) import System.Posix.Process (getProcessID)
import System.Posix.Signals (SignalInfo(..), installHandler, sigTERM) import System.Posix.Signals (SignalInfo(..), installHandler, sigTERM, sigINT)
import qualified System.Posix.Signals as Signals (Handler(..)) import qualified System.Posix.Signals as Signals (Handler(..))
import Network (socketPort) import Network.Socket (socketPort, Socket, PortNumber)
import qualified Network.Socket as Socket (close) import qualified Network.Socket as Socket (close)
import Control.Concurrent.STM.Delay import Control.Concurrent.STM.Delay
import Control.Monad.STM (retry) import Control.Monad.STM (retry)
import Control.Monad.Trans.Cont (runContT, callCC)
import qualified Data.Set as Set import qualified Data.Set as Set
@ -120,7 +122,7 @@ mkYesodDispatch "UniWorX" resourcesUniWorX
-- performs initialization and returns a foundation datatype value. This is also -- performs initialization and returns a foundation datatype value. This is also
-- the place to put your migrate statements to have automatic database -- the place to put your migrate statements to have automatic database
-- migrations handled by Yesod. -- migrations handled by Yesod.
makeFoundation :: (MonadResource m, MonadBaseControl IO m) => AppSettings -> m UniWorX makeFoundation :: (MonadResource m, MonadUnliftIO m) => AppSettings -> m UniWorX
makeFoundation appSettings'@AppSettings{..} = do makeFoundation appSettings'@AppSettings{..} = do
-- Some basic initializations: HTTP connection manager, logger, and static -- Some basic initializations: HTTP connection manager, logger, and static
-- subsite. -- subsite.
@ -146,7 +148,7 @@ makeFoundation appSettings'@AppSettings{..} = do
oldLogger <- atomically . swapTVar tVar =<< mkLogger newSettings oldLogger <- atomically . swapTVar tVar =<< mkLogger newSettings
rmLoggerSet $ loggerSet oldLogger rmLoggerSet $ loggerSet oldLogger
updateLogger newSettings updateLogger newSettings
(tVar, ) <$> fork (updateLogger initialSettings) (tVar, ) <$> forkIO (updateLogger initialSettings)
appLogger <- over _2 fst <$> allocate mkLogger' (\(tVar, tId) -> killThread tId >> (readTVarIO tVar >>= rmLoggerSet . loggerSet)) appLogger <- over _2 fst <$> allocate mkLogger' (\(tVar, tId) -> killThread tId >> (readTVarIO tVar >>= rmLoggerSet . loggerSet))
let appStatic = embeddedStatic let appStatic = embeddedStatic
@ -250,7 +252,7 @@ readInstanceIDFile idFile = liftIO . handle generateInstead $ LBS.readFile idFil
instanceId <- UUID.nextRandom instanceId <- UUID.nextRandom
LBS.writeFile idFile $ UUID.toByteString instanceId LBS.writeFile idFile $ UUID.toByteString instanceId
return instanceId return instanceId
| otherwise = throw e | otherwise = throwIO e
createSmtpPool :: MonadLoggerIO m => SmtpConf -> m SMTPPool createSmtpPool :: MonadLoggerIO m => SmtpConf -> m SMTPPool
createSmtpPool SmtpConf{ smtpPool = ResourcePoolConf{..}, .. } = do createSmtpPool SmtpConf{ smtpPool = ResourcePoolConf{..}, .. } = do
@ -327,7 +329,7 @@ warpSettings foundation = defaultSettings
void $ liftIO Systemd.notifyReady void $ liftIO Systemd.notifyReady
if if
| foundation ^. _appHealthCheckDelayNotify | foundation ^. _appHealthCheckDelayNotify
-> void . fork $ do -> void . forkIO $ do
let activeChecks = Set.fromList universeF let activeChecks = Set.fromList universeF
& Set.filter (is _Just . (foundation ^. _appHealthCheckInterval)) & Set.filter (is _Just . (foundation ^. _appHealthCheckInterval))
atomically $ do atomically $ do
@ -365,11 +367,20 @@ develMain = runResourceT $ do
wsettings <- liftIO . getDevSettings $ warpSettings foundation wsettings <- liftIO . getDevSettings $ warpSettings foundation
app <- makeApplication foundation app <- makeApplication foundation
let
awaitTermination :: IO ()
awaitTermination
= flip runContT return . forever $ do
lift $ threadDelay 100e3
whenM (lift $ doesFileExist "yesod-devel/devel-terminate") $
callCC ($ ())
void . liftIO $ installHandler sigINT (Signals.Catch $ return ()) Nothing
runAppLoggingT foundation $ handleJobs foundation runAppLoggingT foundation $ handleJobs foundation
liftIO . develMainHelper $ return (wsettings, app) void . liftIO $ awaitTermination `race` runSettings wsettings app
-- | The @main@ function for an executable running this site. -- | The @main@ function for an executable running this site.
appMain :: MonadResourceBase m => m () appMain :: forall m. MonadUnliftIO m => m ()
appMain = runResourceT $ do appMain = runResourceT $ do
settings <- getAppSettings settings <- getAppSettings
@ -397,7 +408,7 @@ appMain = runResourceT $ do
$logInfoS "bind" [st|Listening on #{tshow host} port #{tshow port} as per configuration|] $logInfoS "bind" [st|Listening on #{tshow host} port #{tshow port} as per configuration|]
liftIO $ pure <$> bindPortTCP port host liftIO $ pure <$> bindPortTCP port host
$logDebugS "bind" . tshow =<< mapM (liftIO . socketPort) sockets $logDebugS "bind" . tshow =<< mapM (liftIO . try . socketPort :: Socket -> _ (Either SomeException PortNumber)) sockets
mainThreadId <- myThreadId mainThreadId <- myThreadId
liftIO . void . flip (installHandler sigTERM) Nothing . Signals.CatchInfo $ \SignalInfo{..} -> runAppLoggingT foundation $ do liftIO . void . flip (installHandler sigTERM) Nothing . Signals.CatchInfo $ \SignalInfo{..} -> runAppLoggingT foundation $ do
@ -445,7 +456,7 @@ appMain = runResourceT $ do
_other -> return () _other -> return ()
go status go status
in void $ allocate (async notifyWatchdog >>= \a -> a <$ link a) cancel in void $ allocateLinkedAsync notifyWatchdog
_other -> return () _other -> return ()
let runWarp socket = runSettingsSocket (warpSettings foundation) socket app let runWarp socket = runSettingsSocket (warpSettings foundation) socket app
@ -461,7 +472,7 @@ appMain = runResourceT $ do
foundationStoreNum :: Word32 foundationStoreNum :: Word32
foundationStoreNum = 2 foundationStoreNum = 2
getApplicationRepl :: (MonadResource m, MonadBaseControl IO m) => m (Int, UniWorX, Application) getApplicationRepl :: (MonadResource m, MonadUnliftIO m) => m (Int, UniWorX, Application)
getApplicationRepl = do getApplicationRepl = do
settings <- getAppDevSettings settings <- getAppDevSettings
foundation <- makeFoundation settings foundation <- makeFoundation settings
@ -475,7 +486,7 @@ getApplicationRepl = do
return (getPort wsettings, foundation, app1) return (getPort wsettings, foundation, app1)
shutdownApp :: (MonadIO m, MonadBaseControl IO m) => UniWorX -> m () shutdownApp :: (MonadIO m, MonadUnliftIO m) => UniWorX -> m ()
shutdownApp app = do shutdownApp app = do
stopJobCtl app stopJobCtl app
liftIO $ do liftIO $ do
@ -494,7 +505,7 @@ handler :: Handler a -> IO a
handler h = runResourceT $ getAppDevSettings >>= makeFoundation >>= liftIO . flip unsafeHandler h handler h = runResourceT $ getAppDevSettings >>= makeFoundation >>= liftIO . flip unsafeHandler h
-- | Run DB queries -- | Run DB queries
db :: ReaderT SqlBackend (HandlerT UniWorX IO) a -> IO a db :: DB a -> IO a
db = handler . runDB db = handler . runDB
addPWEntry :: User addPWEntry :: User

View File

@ -22,7 +22,7 @@ import qualified Network.Socket as Wai
import qualified Net.IP as IP import qualified Net.IP as IP
import qualified Net.IPv6 as IPv6 import qualified Net.IPv6 as IPv6
import Control.Exception (ErrorCall(..), evaluate) import Control.Exception (ErrorCall(..))
{-# ANN module ("HLint: ignore Use newtype instead of data" :: String) #-} {-# ANN module ("HLint: ignore Use newtype instead of data" :: String) #-}
@ -79,7 +79,6 @@ instance Exception AuditException
audit :: ( AuthId (HandlerSite m) ~ Key User audit :: ( AuthId (HandlerSite m) ~ Key User
, AuthEntity (HandlerSite m) ~ User
, IsSqlBackend (YesodPersistBackend (HandlerSite m)) , IsSqlBackend (YesodPersistBackend (HandlerSite m))
, SqlBackendCanWrite (YesodPersistBackend (HandlerSite m)) , SqlBackendCanWrite (YesodPersistBackend (HandlerSite m))
, HasInstanceID (HandlerSite m) InstanceId , HasInstanceID (HandlerSite m) InstanceId
@ -99,7 +98,7 @@ audit (toJSON -> transactionLogInfo) = do
transactionLogTime <- liftIO getCurrentTime transactionLogTime <- liftIO getCurrentTime
transactionLogInstance <- getsYesod $ view instanceID transactionLogInstance <- getsYesod $ view instanceID
transactionLogInitiator <- liftHandlerT maybeAuthId transactionLogInitiator <- liftHandler maybeAuthId
transactionLogRemote <- handle (throwM . AuditRemoteException) $ Just <$> getRemote transactionLogRemote <- handle (throwM . AuditRemoteException) $ Just <$> getRemote
insert_ TransactionLog{..} insert_ TransactionLog{..}

View File

@ -23,6 +23,24 @@ data Transaction
{ transactionExam :: ExamId { transactionExam :: ExamId
, transactionUser :: UserId , transactionUser :: UserId
} }
| TransactionExamPartResultEdit
{ transactionExamPart :: ExamPartId
, transactionUser :: UserId
}
| TransactionExamPartResultDeleted
{ transactionExamPart :: ExamPartId
, transactionUser :: UserId
}
| TransactionExamBonusEdit
{ transactionExam :: ExamId
, transactionUser :: UserId
}
| TransactionExamBonusDeleted
{ transactionExam :: ExamId
, transactionUser :: UserId
}
| TransactionExamResultEdit | TransactionExamResultEdit
{ transactionExam :: ExamId { transactionExam :: ExamId

View File

@ -17,41 +17,47 @@ data DummyMessage = MsgDummyIdent
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic, Typeable) deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic, Typeable)
dummyForm :: ( RenderMessage site FormMessage dummyForm :: ( RenderMessage (HandlerSite m) FormMessage
, RenderMessage site DummyMessage , RenderMessage (HandlerSite m) DummyMessage
, YesodPersist site , YesodPersist (HandlerSite m)
, SqlBackendCanRead (YesodPersistBackend site) , SqlBackendCanRead (YesodPersistBackend (HandlerSite m))
, Button site ButtonSubmit , Button (HandlerSite m) ButtonSubmit
) => AForm (HandlerT site IO) (CI Text) , MonadHandler m
) => AForm m (CI Text)
dummyForm = areq (ciField & addDatalist userList) (fslI MsgDummyIdent & noAutocomplete) Nothing dummyForm = areq (ciField & addDatalist userList) (fslI MsgDummyIdent & noAutocomplete) Nothing
where where
userList = fmap mkOptionList . runDB $ map toOption <$> selectList [] [Asc UserIdent] userList = fmap mkOptionList . runDB $ withReaderT projectBackend (map toOption <$> selectList [] [Asc UserIdent] :: ReaderT SqlBackend _ [Option UserIdent])
toOption (Entity _ User{..}) = Option userDisplayName userIdent (CI.original userIdent) toOption (Entity _ User{..}) = Option userDisplayName userIdent (CI.original userIdent)
dummyLogin :: ( YesodAuth site dummyLogin :: forall site.
( YesodAuth site
, YesodPersist site , YesodPersist site
, SqlBackendCanRead (YesodPersistBackend site) , SqlBackendCanRead (YesodPersistBackend site)
, RenderMessage site FormMessage
, RenderMessage site AFormMessage , RenderMessage site AFormMessage
, RenderMessage site DummyMessage , RenderMessage site DummyMessage
, Button site ButtonSubmit , Button site ButtonSubmit
) => AuthPlugin site ) => AuthPlugin site
dummyLogin = AuthPlugin{..} dummyLogin = AuthPlugin{..}
where where
apName :: Text
apName = "dummy" apName = "dummy"
-- apDispatch :: Text -> [Text] -> HandlerT Auth (HandlerT site IO) TypedContent
apDispatch "POST" [] = do apDispatch :: forall m. MonadAuthHandler site m => Text -> [Text] -> m TypedContent
((loginRes, _), _) <- lift . runFormPost $ renderAForm FormStandard dummyForm apDispatch "POST" [] = liftSubHandler $ do
((loginRes, _), _) <- runFormPost $ renderAForm FormStandard dummyForm
tp <- getRouteToParent
case loginRes of case loginRes of
FormFailure errs -> do FormFailure errs -> do
lift . forM_ errs $ addMessage Error . toHtml forM_ errs $ addMessage Error . toHtml
redirect LoginR redirect $ tp LoginR
FormMissing -> do FormMissing -> do
lift $ addMessageI Warning MsgDummyNoFormData addMessageI Warning MsgDummyNoFormData
redirect LoginR redirect $ tp LoginR
FormSuccess ident -> FormSuccess ident ->
lift . setCredsRedirect $ Creds "dummy" (CI.original ident) [] setCredsRedirect $ Creds "dummy" (CI.original ident) []
apDispatch _ _ = notFound apDispatch _ _ = notFound
apLogin :: (Route Auth -> Route site) -> WidgetFor site ()
apLogin toMaster = do apLogin toMaster = do
(login, loginEnctype) <- handlerToWidget . generateFormPost $ renderAForm FormStandard dummyForm (login, loginEnctype) <- handlerToWidget . generateFormPost $ renderAForm FormStandard dummyForm
let loginForm = wrapForm login FormSettings let loginForm = wrapForm login FormSettings

View File

@ -84,7 +84,7 @@ instance Exception CampusUserException
makePrisms ''CampusUserException makePrisms ''CampusUserException
campusUser :: (MonadBaseControl IO m, MonadThrow m, MonadIO m) => LdapConf -> LdapPool -> Creds site -> m (Ldap.AttrList []) campusUser :: MonadUnliftIO m => LdapConf -> LdapPool -> Creds site -> m (Ldap.AttrList [])
campusUser conf@LdapConf{..} pool Creds{..} = liftIO . (`catches` errHandlers) $ either (throwM . CampusUserLdapError) return <=< withLdap pool $ \ldap -> do campusUser conf@LdapConf{..} pool Creds{..} = liftIO . (`catches` errHandlers) $ either (throwM . CampusUserLdapError) return <=< withLdap pool $ \ldap -> do
Ldap.bind ldap ldapDn ldapPassword Ldap.bind ldap ldapDn ldapPassword
results <- case lookup "DN" credsExtra of results <- case lookup "DN" credsExtra of
@ -109,15 +109,15 @@ campusUser conf@LdapConf{..} pool Creds{..} = liftIO . (`catches` errHandlers) $
, Exc.Handler $ \(HostCannotConnect host excs) -> throwM $ CampusUserHostCannotConnect host excs , Exc.Handler $ \(HostCannotConnect host excs) -> throwM $ CampusUserHostCannotConnect host excs
] ]
campusUser' :: (MonadBaseControl IO m, MonadCatch m, MonadIO m) => LdapConf -> LdapPool -> User -> m (Maybe (Ldap.AttrList [])) campusUser' :: (MonadCatch m, MonadUnliftIO m) => LdapConf -> LdapPool -> User -> m (Maybe (Ldap.AttrList []))
campusUser' conf pool User{userIdent} campusUser' conf pool User{userIdent}
= runMaybeT . catchIfMaybeT (is _CampusUserNoResult) $ campusUser conf pool (Creds apLdap (CI.original userIdent) []) = runMaybeT . catchIfMaybeT (is _CampusUserNoResult) $ campusUser conf pool (Creds apLdap (CI.original userIdent) [])
campusForm :: ( RenderMessage site FormMessage campusForm :: ( RenderMessage (HandlerSite m) FormMessage
, RenderMessage site CampusMessage , RenderMessage (HandlerSite m) CampusMessage
, Button site ButtonSubmit , MonadHandler m
) => WForm (HandlerT site IO) (FormResult CampusLogin) ) => WForm m (FormResult CampusLogin)
campusForm = do campusForm = do
MsgRenderer mr <- getMsgRenderer MsgRenderer mr <- getMsgRenderer
@ -133,24 +133,26 @@ apLdap = "LDAP"
campusLogin :: forall site. campusLogin :: forall site.
( YesodAuth site ( YesodAuth site
, RenderMessage site FormMessage
, RenderMessage site CampusMessage , RenderMessage site CampusMessage
, RenderMessage site AFormMessage , RenderMessage site AFormMessage
, Button site ButtonSubmit , Button site ButtonSubmit
) => LdapConf -> LdapPool -> AuthPlugin site ) => LdapConf -> LdapPool -> AuthPlugin site
campusLogin conf@LdapConf{..} pool = AuthPlugin{..} campusLogin conf@LdapConf{..} pool = AuthPlugin{..}
where where
apName :: Text
apName = apLdap apName = apLdap
apDispatch :: Text -> [Text] -> HandlerT Auth (HandlerT site IO) TypedContent
apDispatch "POST" [] = do apDispatch :: forall m. MonadAuthHandler site m => Text -> [Text] -> m TypedContent
((loginRes, _), _) <- lift . runFormPost $ renderWForm FormStandard campusForm apDispatch "POST" [] = liftSubHandler $ do
((loginRes, _), _) <- runFormPost $ renderWForm FormStandard campusForm
tp <- getRouteToParent
case loginRes of case loginRes of
FormFailure errs -> do FormFailure errs -> do
forM_ errs $ addMessage Error . toHtml forM_ errs $ addMessage Error . toHtml
redirect LoginR redirect $ tp LoginR
FormMissing -> redirect LoginR FormMissing -> redirect $ tp LoginR
FormSuccess CampusLogin{ campusIdent = CI.original -> campusIdent, ..} -> do FormSuccess CampusLogin{ campusIdent = CI.original -> campusIdent, ..} -> do
ldapResult <- withLdap pool $ \ldap -> do ldapResult <- withLdap pool $ \ldap -> liftIO $ do
Ldap.bind ldap ldapDn ldapPassword Ldap.bind ldap ldapDn ldapPassword
searchResults <- findUser conf ldap campusIdent [ldapUserPrincipalName] searchResults <- findUser conf ldap campusIdent [ldapUserPrincipalName]
case searchResults of case searchResults of
@ -169,11 +171,13 @@ campusLogin conf@LdapConf{..} pool = AuthPlugin{..}
$logErrorS "LDAP" $ "Error during login: " <> tshow err $logErrorS "LDAP" $ "Error during login: " <> tshow err
loginErrorMessageI LoginR Msg.AuthError loginErrorMessageI LoginR Msg.AuthError
Right (Right (userDN, credsIdent)) -> Right (Right (userDN, credsIdent)) ->
lift . setCredsRedirect $ Creds apName credsIdent [("DN", userDN)] setCredsRedirect $ Creds apName credsIdent [("DN", userDN)]
Right (Left searchResults) -> do Right (Left searchResults) -> do
$logWarnS "LDAP" $ "Could not extract principal name: " <> tshow searchResults $logWarnS "LDAP" $ "Could not extract principal name: " <> tshow searchResults
loginErrorMessageI LoginR Msg.AuthError loginErrorMessageI LoginR Msg.AuthError
apDispatch _ _ = notFound apDispatch _ _ = notFound
apLogin :: (Route Auth -> Route site) -> WidgetFor site ()
apLogin toMaster = do apLogin toMaster = do
(login, loginEnctype) <- handlerToWidget . generateFormPost $ renderWForm FormStandard campusForm (login, loginEnctype) <- handlerToWidget . generateFormPost $ renderWForm FormStandard campusForm
let loginForm = wrapForm login FormSettings let loginForm = wrapForm login FormSettings

View File

@ -26,68 +26,50 @@ data PWHashMessage = MsgPWHashIdent
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic, Typeable) deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic, Typeable)
hashForm :: ( RenderMessage site FormMessage hashForm :: ( RenderMessage (HandlerSite m) FormMessage
, RenderMessage site PWHashMessage , RenderMessage (HandlerSite m) PWHashMessage
, Button site ButtonSubmit , MonadHandler m
) => AForm (HandlerT site IO) HashLogin ) => AForm m HashLogin
hashForm = HashLogin hashForm = HashLogin
<$> areq ciField (fslpI MsgPWHashIdent "Identifikation") Nothing <$> areq ciField (fslpI MsgPWHashIdent "Identifikation") Nothing
<*> areq passwordField (fslpI MsgPWHashPassword "Passwort") Nothing <*> areq passwordField (fslpI MsgPWHashPassword "Passwort") Nothing
hashLogin :: ( YesodAuth site hashLogin :: forall site.
( YesodAuth site
, YesodPersist site , YesodPersist site
, SqlBackendCanRead (YesodPersistBackend site) , SqlBackendCanRead (YesodPersistBackend site)
, RenderMessage site FormMessage , PersistRecordBackend User (YesodPersistBackend site)
, RenderMessage site PWHashMessage , RenderMessage site PWHashMessage
, RenderMessage site AFormMessage , RenderMessage site AFormMessage
, Button site ButtonSubmit , Button site ButtonSubmit
) => PWHashAlgorithm -> AuthPlugin site ) => PWHashAlgorithm -> AuthPlugin site
hashLogin pwHashAlgo = AuthPlugin{..} hashLogin pwHashAlgo = AuthPlugin{..}
where where
apName :: Text
apName = "PWHash" apName = "PWHash"
apDispatch "POST" [] = do
((loginRes, _), _) <- lift . runFormPost $ renderAForm FormStandard hashForm apDispatch :: forall m. MonadAuthHandler site m => Text -> [Text] -> m TypedContent
apDispatch "POST" [] = liftSubHandler $ do
((loginRes, _), _) <- runFormPost $ renderAForm FormStandard hashForm
tp <- getRouteToParent
case loginRes of case loginRes of
FormFailure errs -> do FormFailure errs -> do
forM_ errs $ addMessage Error . toHtml forM_ errs $ addMessage Error . toHtml
redirect LoginR redirect $ tp LoginR
FormMissing -> redirect LoginR FormMissing -> redirect $ tp LoginR
FormSuccess HashLogin{..} -> do FormSuccess HashLogin{..} -> do
user <- lift . runDB . getBy $ UniqueAuthentication hashIdent user <- liftHandler . runDB . getBy $ UniqueAuthentication hashIdent
case user of case user of
Just (Entity _ User{ userAuthentication = AuthPWHash{..}, userIdent = CI.original -> userIdent }) Just (Entity _ User{ userAuthentication = AuthPWHash{..}, userIdent = CI.original -> userIdent })
| verifyPasswordWith pwHashAlgo (2^) (encodeUtf8 hashPassword) (encodeUtf8 authPWHash) -> -- (2^) is magic. | verifyPasswordWith pwHashAlgo (2^) (encodeUtf8 hashPassword) (encodeUtf8 authPWHash) -> -- (2^) is magic.
lift . setCredsRedirect $ Creds apName userIdent [] setCredsRedirect $ Creds apName userIdent []
other -> do other -> do
$logDebugS "PWHash" $ tshow other $logDebugS "PWHash" $ tshow other
loginErrorMessageI LoginR Msg.InvalidLogin loginErrorMessageI LoginR Msg.InvalidLogin
-- apDispatch "GET" [] = do
-- authData <- lookupBasicAuth
-- pwdata <- liftIO $ Yaml.decodeFileEither fp
-- addHeader "WWW-Authenticate" [st|Basic realm="uni2work maintenance auth" charset="UTF-8"|]
-- case pwdata of
-- Left err -> $logDebugS "Auth" $ tshow err
-- Right pws -> $logDebugS "Auth" $ tshow (length pws) <> " pw entries"
-- case (authData, pwdata) of
-- (Nothing, _) -> do
-- notAuthenticated
-- (Just (usr, (Text.encodeUtf8 -> pw)), Right pwdata')
-- | [ PWEntry{ pwUser = pwUser@(User{..}), pwHash = (Text.encodeUtf8 -> pwHash) } ]
-- <- [ pwe | pwe@PWEntry{..} <- pwdata'
-- , let User{..} = pwUser
-- , userIdent == usr
-- , userPlugin == apName
-- ]
-- , verifyPassword pw pwHash
-- -> lift $ do
-- runDB . void $ insertUnique pwUser
-- setCredsRedirect $ Creds apName userIdent []
-- _ -> permissionDenied "Invalid auth"
apDispatch _ _ = notFound apDispatch _ _ = notFound
apLogin :: (Route Auth -> Route site) -> WidgetFor site ()
apLogin toMaster = do apLogin toMaster = do
(login, loginEnctype) <- handlerToWidget . generateFormPost $ renderAForm FormStandard hashForm (login, loginEnctype) <- handlerToWidget . generateFormPost $ renderAForm FormStandard hashForm
let loginForm = wrapForm login FormSettings let loginForm = wrapForm login FormSettings

View File

@ -1,17 +0,0 @@
module Control.Concurrent.Async.Lifted.Safe.Utils
( allocateAsync, allocateLinkedAsync
) where
import ClassyPrelude hiding (cancel)
import Control.Lens
import Control.Concurrent.Async.Lifted.Safe
import Control.Monad.Trans.Resource
allocateLinkedAsync, allocateAsync :: forall m a.
MonadResource m
=> IO a -> m (Async a)
allocateAsync = fmap (view _2) . flip allocate cancel . async
allocateLinkedAsync = uncurry (<$) . (id &&& link) <=< allocateAsync

View File

@ -8,11 +8,12 @@ module CryptoID
, module System.FilePath.Cryptographic.ImplicitNamespace , module System.FilePath.Cryptographic.ImplicitNamespace
) where ) where
import CryptoID.TH
import ClassyPrelude import Import.NoModel
import Model import Model
import CryptoID.TH
import qualified Data.CryptoID as E import qualified Data.CryptoID as E
import Data.CryptoID.Poly.ImplicitNamespace import Data.CryptoID.Poly.ImplicitNamespace
import Data.UUID.Cryptographic.ImplicitNamespace import Data.UUID.Cryptographic.ImplicitNamespace
@ -20,9 +21,6 @@ import System.FilePath.Cryptographic.ImplicitNamespace
import qualified Data.Text as Text import qualified Data.Text as Text
-- import Data.UUID.Types
import Web.PathPieces
import Data.CaseInsensitive (CI) import Data.CaseInsensitive (CI)
import qualified Data.CaseInsensitive as CI import qualified Data.CaseInsensitive as CI

View File

@ -1,5 +1,6 @@
{-# LANGUAGE UndecidableInstances #-} {-# LANGUAGE UndecidableInstances #-}
{-# OPTIONS_GHC -fno-warn-orphans #-} {-# OPTIONS_GHC -fno-warn-orphans #-}
{-# OPTIONS_GHC -fno-warn-redundant-constraints #-}
module Data.CaseInsensitive.Instances module Data.CaseInsensitive.Instances
( (

View File

@ -43,5 +43,5 @@ instance Csv.FromField s => Csv.FromField (CID.CryptoID c s) where
instance Csv.ToField s => Csv.ToField (CID.CryptoID c s) where instance Csv.ToField s => Csv.ToField (CID.CryptoID c s) where
toField = Csv.toField . CID.ciphertext toField = Csv.toField . CID.ciphertext
instance {-# OVERLAPS #-} (Csv.ToField s, CI.FoldCase s) => Csv.ToField (CID.CryptoID c (CI s)) where instance {-# OVERLAPS #-} Csv.ToField s => Csv.ToField (CID.CryptoID c (CI s)) where
toField = Csv.toField . CI.foldedCase . CID.ciphertext toField = Csv.toField . CI.foldedCase . CID.ciphertext

View File

@ -1,12 +0,0 @@
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Data.List.NonEmpty.Instances
(
) where
import Data.List.NonEmpty
import Language.Haskell.TH.Syntax (Lift(..))
instance Lift a => Lift (NonEmpty a) where
lift (toList -> xs) = [e|fromList xs|]

View File

@ -1,4 +1,5 @@
{-# OPTIONS_GHC -fno-warn-orphans #-} {-# OPTIONS_GHC -fno-warn-orphans #-}
{-# OPTIONS_GHC -fno-warn-redundant-constraints #-}
module Database.Esqueleto.Utils module Database.Esqueleto.Utils
( true, false ( true, false
@ -61,24 +62,22 @@ false :: E.SqlExpr (E.Value Bool)
false = E.val False false = E.val False
-- | Negation of `isNothing` which is missing -- | Negation of `isNothing` which is missing
isJust :: (E.Esqueleto query expr backend, PersistField typ) => expr (E.Value (Maybe typ)) -> expr (E.Value Bool) isJust :: PersistField typ => E.SqlExpr (E.Value (Maybe typ)) -> E.SqlExpr (E.Value Bool)
isJust = E.not_ . E.isNothing isJust = E.not_ . E.isNothing
infix 4 `isInfixOf`, `hasInfix` infix 4 `isInfixOf`, `hasInfix`
-- | 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 isInfixOf :: ( E.SqlString s1
, E.SqlString s1
, E.SqlString s2 , E.SqlString s2
) )
=> expr (E.Value s1) -> expr (E.Value s2) -> expr (E.Value Bool) => E.SqlExpr (E.Value s1) -> E.SqlExpr (E.Value s2) -> E.SqlExpr (E.Value Bool)
isInfixOf needle strExpr = E.castString strExpr `E.ilike` (E.%) E.++. needle E.++. (E.%) isInfixOf needle strExpr = E.castString strExpr `E.ilike` (E.%) E.++. needle E.++. (E.%)
hasInfix :: ( E.Esqueleto query expr backend hasInfix :: ( E.SqlString s1
, E.SqlString s1
, E.SqlString s2 , E.SqlString s2
) )
=> expr (E.Value s2) -> expr (E.Value s1) -> expr (E.Value Bool) => E.SqlExpr (E.Value s2) -> E.SqlExpr (E.Value s1) -> E.SqlExpr (E.Value Bool)
hasInfix = flip isInfixOf hasInfix = flip isInfixOf
and, or :: Foldable f => f (E.SqlExpr (E.Value Bool)) -> E.SqlExpr (E.Value Bool) and, or :: Foldable f => f (E.SqlExpr (E.Value Bool)) -> E.SqlExpr (E.Value Bool)

View File

@ -8,11 +8,14 @@ module Database.Persist.Class.Instances
import ClassyPrelude import ClassyPrelude
import Database.Persist.Class import Database.Persist.Class
import Database.Persist.Types (HaskellName, DBName, PersistValue)
import Database.Persist.Types.Instances () import Database.Persist.Types.Instances ()
import Data.Binary (Binary) import Data.Binary (Binary)
import qualified Data.Binary as Binary import qualified Data.Binary as Binary
import qualified Data.Map as Map
instance PersistEntity record => Hashable (Key record) where instance PersistEntity record => Hashable (Key record) where
hashWithSalt s = hashWithSalt s . toPersistValue hashWithSalt s = hashWithSalt s . toPersistValue
@ -24,3 +27,13 @@ instance PersistEntity record => Binary (Key record) where
instance PersistEntity record => NFData (Key record) where instance PersistEntity record => NFData (Key record) where
rnf = rnf . keyToValues rnf = rnf . keyToValues
uniqueToMap :: PersistEntity record => Unique record -> Map (HaskellName, DBName) PersistValue
uniqueToMap = fmap Map.fromList $ zip <$> persistUniqueToFieldNames <*> persistUniqueToValues
instance PersistEntity record => Eq (Unique record) where
(==) = (==) `on` uniqueToMap
instance PersistEntity record => Show (Unique record) where
showsPrec p = showsPrec p . uniqueToMap

View File

@ -5,7 +5,6 @@
module Foundation where module Foundation where
import Import.NoFoundation hiding (embedFile) import Import.NoFoundation hiding (embedFile)
import qualified ClassyPrelude.Yesod as Yesod (getHttpManager)
import Database.Persist.Sql (ConnectionPool, runSqlPool) import Database.Persist.Sql (ConnectionPool, runSqlPool)
import Text.Hamlet (hamletFile) import Text.Hamlet (hamletFile)
@ -48,9 +47,6 @@ import Data.List (nubBy, (!!), findIndex)
import Data.Monoid (Any(..)) import Data.Monoid (Any(..))
import Data.Pool
import Data.Conduit (($$))
import Data.Conduit.List (sourceList) import Data.Conduit.List (sourceList)
import qualified Database.Esqueleto as E import qualified Database.Esqueleto as E
@ -94,6 +90,8 @@ import Data.FileEmbed (embedFile)
import qualified Ldap.Client as Ldap import qualified Ldap.Client as Ldap
import UnliftIO.Pool
type SMTPPool = Pool SMTPConnection type SMTPPool = Pool SMTPConnection
@ -162,9 +160,9 @@ deriving instance Generic (Route UniWorX)
-- | Convenient Type Synonyms: -- | Convenient Type Synonyms:
type DB = YesodDB UniWorX type DB = YesodDB UniWorX
type Form x = Html -> MForm (HandlerT UniWorX IO) (FormResult x, Widget) type Form x = Html -> MForm (HandlerFor UniWorX) (FormResult x, Widget)
type MsgRenderer = MsgRendererS UniWorX -- see Utils type MsgRenderer = MsgRendererS UniWorX -- see Utils
type MailM a = MailT (HandlerT UniWorX IO) a type MailM a = MailT (HandlerFor UniWorX) a
-- Pattern Synonyms for convenience -- Pattern Synonyms for convenience
pattern CSheetR :: TermId -> SchoolId -> CourseShorthand -> SheetName -> SheetR -> Route UniWorX pattern CSheetR :: TermId -> SchoolId -> CourseShorthand -> SheetName -> SheetR -> Route UniWorX
@ -531,13 +529,13 @@ class (MonadHandler m, HandlerSite m ~ UniWorX) => MonadAP m where
evalAccessPred :: AccessPredicate -> Maybe (AuthId UniWorX) -> Route UniWorX -> Bool -> m AuthResult evalAccessPred :: AccessPredicate -> Maybe (AuthId UniWorX) -> Route UniWorX -> Bool -> m AuthResult
instance {-# INCOHERENT #-} (MonadHandler m, HandlerSite m ~ UniWorX) => MonadAP m where instance {-# INCOHERENT #-} (MonadHandler m, HandlerSite m ~ UniWorX) => MonadAP m where
evalAccessPred aPred aid r w = liftHandlerT $ case aPred of evalAccessPred aPred aid r w = liftHandler $ case aPred of
(APPure p) -> runReader (p aid r w) <$> getMsgRenderer (APPure p) -> runReader (p aid r w) <$> getMsgRenderer
(APHandler p) -> p aid r w (APHandler p) -> p aid r w
(APDB p) -> runDB $ p aid r w (APDB p) -> runDB $ p aid r w
instance (MonadHandler m, HandlerSite m ~ UniWorX, backend ~ YesodPersistBackend UniWorX) => MonadAP (ReaderT backend m) where instance (MonadHandler m, HandlerSite m ~ UniWorX, backend ~ YesodPersistBackend UniWorX) => MonadAP (ReaderT backend m) where
evalAccessPred aPred aid r w = mapReaderT liftHandlerT $ case aPred of evalAccessPred aPred aid r w = mapReaderT liftHandler $ case aPred of
(APPure p) -> lift $ runReader (p aid r w) <$> getMsgRenderer (APPure p) -> lift $ runReader (p aid r w) <$> getMsgRenderer
(APHandler p) -> lift $ p aid r w (APHandler p) -> lift $ p aid r w
(APDB p) -> p aid r w (APDB p) -> p aid r w
@ -573,7 +571,6 @@ falseAP = APPure . const . const . const $ falseAR <$> ask -- included for compl
askTokenUnsafe :: forall m. askTokenUnsafe :: forall m.
( MonadHandler m ( MonadHandler m
, HandlerSite m ~ UniWorX , HandlerSite m ~ UniWorX
, MonadLogger m
, MonadCatch m , MonadCatch m
) )
=> ExceptT AuthResult m (BearerToken (UniWorX)) => ExceptT AuthResult m (BearerToken (UniWorX))
@ -690,7 +687,7 @@ tagAccessPredicate AuthNoEscalation = APDB $ \mAuthId route _ -> case route of
tagAccessPredicate AuthDeprecated = APHandler $ \_ r _ -> do tagAccessPredicate AuthDeprecated = APHandler $ \_ r _ -> do
$logWarnS "AccessControl" ("deprecated route: " <> tshow r) $logWarnS "AccessControl" ("deprecated route: " <> tshow r)
addMessageI Error MsgDeprecatedRoute addMessageI Error MsgDeprecatedRoute
allow <- view _appAllowDeprecated allow <- getsYesod $ view _appAllowDeprecated
return $ bool (Unauthorized "Deprecated Route") Authorized allow return $ bool (Unauthorized "Deprecated Route") Authorized allow
tagAccessPredicate AuthDevelopment = APHandler $ \_ r _ -> do tagAccessPredicate AuthDevelopment = APHandler $ \_ r _ -> do
$logWarnS "AccessControl" ("route in development: " <> tshow r) $logWarnS "AccessControl" ("route in development: " <> tshow r)
@ -1107,9 +1104,9 @@ tagAccessPredicate AuthAllocationRegistered = APDB $ \mAuthId route _ -> case ro
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
cTime <- liftIO getCurrentTime cTime <- liftIO getCurrentTime
let authorizedIfExists f = do let
[E.Value ok] <- lift . E.select . return . E.exists $ E.from f authorizedIfExists :: E.From a => (a -> E.SqlQuery b) -> ExceptT AuthResult DB ()
whenExceptT ok Authorized authorizedIfExists = flip whenExceptT Authorized <=< lift . E.selectExists . E.from
participant <- catchIfMExceptT (const $ unauthorizedI MsgUnauthorizedParticipant) (const True :: CryptoIDError -> Bool) $ decrypt cID participant <- catchIfMExceptT (const $ unauthorizedI MsgUnauthorizedParticipant) (const True :: CryptoIDError -> Bool) $ decrypt cID
-- participant is currently registered -- participant is currently registered
$cachedHereBinary (participant, tid, ssh, csh) . authorizedIfExists $ \(course `E.InnerJoin` courseParticipant) -> do $cachedHereBinary (participant, tid, ssh, csh) . authorizedIfExists $ \(course `E.InnerJoin` courseParticipant) -> do
@ -1395,42 +1392,42 @@ evalAuthTags AuthTagActive{..} (map (Set.toList . toNullable) . Set.toList . dnf
return result return result
evalAccessFor :: (MonadLogger m, MonadHandler m, HandlerSite m ~ UniWorX) => Maybe (AuthId UniWorX) -> Route UniWorX -> Bool -> m AuthResult evalAccessFor :: (MonadThrow m, MonadHandler m, HandlerSite m ~ UniWorX) => Maybe (AuthId UniWorX) -> Route UniWorX -> Bool -> m AuthResult
evalAccessFor mAuthId route isWrite = do evalAccessFor mAuthId route isWrite = do
dnf <- either throwM return $ routeAuthTags route dnf <- either throwM return $ routeAuthTags route
fmap fst . runWriterT $ evalAuthTags (AuthTagActive $ const True) dnf mAuthId route isWrite fmap fst . runWriterT $ evalAuthTags (AuthTagActive $ const True) dnf mAuthId route isWrite
evalAccessForDB :: (MonadLogger m, MonadHandler m, HandlerSite m ~ UniWorX) => Maybe (AuthId UniWorX) -> Route UniWorX -> Bool -> ReaderT (YesodPersistBackend UniWorX) m AuthResult evalAccessForDB :: (MonadThrow m, MonadHandler m, HandlerSite m ~ UniWorX) => Maybe (AuthId UniWorX) -> Route UniWorX -> Bool -> ReaderT (YesodPersistBackend UniWorX) m AuthResult
evalAccessForDB = evalAccessFor evalAccessForDB = evalAccessFor
evalAccess :: (MonadLogger m, MonadHandler m, HandlerSite m ~ UniWorX) => Route UniWorX -> Bool -> m AuthResult evalAccess :: (MonadThrow m, MonadHandler m, HandlerSite m ~ UniWorX) => Route UniWorX -> Bool -> m AuthResult
evalAccess route isWrite = do evalAccess route isWrite = do
mAuthId <- liftHandlerT maybeAuthId mAuthId <- liftHandler maybeAuthId
tagActive <- fromMaybe def <$> lookupSessionJson SessionActiveAuthTags tagActive <- fromMaybe def <$> lookupSessionJson SessionActiveAuthTags
dnf <- either throwM return $ routeAuthTags route dnf <- either throwM return $ routeAuthTags route
(result, deactivated) <- runWriterT $ evalAuthTags tagActive dnf mAuthId route isWrite (result, deactivated) <- runWriterT $ evalAuthTags tagActive dnf mAuthId route isWrite
result <$ tellSessionJson SessionInactiveAuthTags deactivated result <$ tellSessionJson SessionInactiveAuthTags deactivated
evalAccessDB :: (MonadLogger m, MonadHandler m, HandlerSite m ~ UniWorX) => Route UniWorX -> Bool -> ReaderT (YesodPersistBackend UniWorX) m AuthResult evalAccessDB :: (MonadThrow m, MonadHandler m, HandlerSite m ~ UniWorX) => Route UniWorX -> Bool -> ReaderT (YesodPersistBackend UniWorX) m AuthResult
evalAccessDB = evalAccess evalAccessDB = evalAccess
-- | Check whether the current user is authorized by `evalAccess` for the given route -- | Check whether the current user is authorized by `evalAccess` for the given route
-- Convenience function for a commonly used code fragment -- Convenience function for a commonly used code fragment
hasAccessTo :: (MonadLogger m, MonadHandler m, HandlerSite m ~ UniWorX) => Route UniWorX -> Bool -> m Bool hasAccessTo :: (MonadThrow m, MonadHandler m, HandlerSite m ~ UniWorX) => Route UniWorX -> Bool -> m Bool
hasAccessTo route isWrite = (== Authorized) <$> evalAccess route isWrite hasAccessTo route isWrite = (== Authorized) <$> evalAccess route isWrite
-- | Check whether the current user is authorized by `evalAccess` to read from the given route -- | Check whether the current user is authorized by `evalAccess` to read from the given route
-- Convenience function for a commonly used code fragment -- Convenience function for a commonly used code fragment
hasReadAccessTo :: (MonadLogger m, MonadHandler m, HandlerSite m ~ UniWorX) => Route UniWorX -> m Bool hasReadAccessTo :: (MonadThrow m, MonadHandler m, HandlerSite m ~ UniWorX) => Route UniWorX -> m Bool
hasReadAccessTo = flip hasAccessTo False hasReadAccessTo = flip hasAccessTo False
-- | Check whether the current user is authorized by `evalAccess` to rwrite to the given route -- | Check whether the current user is authorized by `evalAccess` to rwrite to the given route
-- Convenience function for a commonly used code fragment -- Convenience function for a commonly used code fragment
hasWriteAccessTo :: (MonadLogger m, MonadHandler m, HandlerSite m ~ UniWorX) => Route UniWorX -> m Bool hasWriteAccessTo :: (MonadThrow m, MonadHandler m, HandlerSite m ~ UniWorX) => Route UniWorX -> m Bool
hasWriteAccessTo = flip hasAccessTo True hasWriteAccessTo = flip hasAccessTo True
-- | Conditional redirect that hides the URL if the user is not authorized for the route -- | Conditional redirect that hides the URL if the user is not authorized for the route
redirectAccess :: (MonadLogger m, MonadHandler m, HandlerSite m ~ UniWorX) => Route UniWorX -> m a redirectAccess :: (MonadThrow m, MonadHandler m, HandlerSite m ~ UniWorX) => Route UniWorX -> m a
redirectAccess url = do redirectAccess url = do
-- must hide URL if not authorized -- must hide URL if not authorized
access <- evalAccess url False access <- evalAccess url False
@ -1439,7 +1436,7 @@ redirectAccess url = do
_ -> permissionDeniedI MsgUnauthorizedRedirect _ -> permissionDeniedI MsgUnauthorizedRedirect
-- | Verify that the currently logged in user is lecturer or corrector for at least one sheet for the given course -- | Verify that the currently logged in user is lecturer or corrector for at least one sheet for the given course
evalAccessCorrector :: (MonadLogger m, MonadHandler m, HandlerSite m ~ UniWorX) evalAccessCorrector :: (MonadThrow m, MonadHandler m, HandlerSite m ~ UniWorX)
=> TermId -> SchoolId -> CourseShorthand -> m AuthResult => TermId -> SchoolId -> CourseShorthand -> m AuthResult
evalAccessCorrector tid ssh csh = evalAccess (CourseR tid ssh csh CNotesR) False evalAccessCorrector tid ssh csh = evalAccess (CourseR tid ssh csh CNotesR) False
@ -1481,7 +1478,7 @@ instance Yesod UniWorX where
$logDebugS "updateFavourites" "Updating favourites" $logDebugS "updateFavourites" "Updating favourites"
now <- liftIO $ getCurrentTime now <- liftIO $ getCurrentTime
uid <- MaybeT $ liftHandlerT maybeAuthId uid <- MaybeT $ liftHandler maybeAuthId
cid <- MaybeT . getKeyBy $ TermSchoolCourseShort tid ssh csh cid <- MaybeT . getKeyBy $ TermSchoolCourseShort tid ssh csh
user <- MaybeT $ get uid user <- MaybeT $ get uid
let courseFavourite = CourseFavourite uid now cid let courseFavourite = CourseFavourite uid now cid
@ -1533,7 +1530,7 @@ instance Yesod UniWorX where
encrypted :: ToJSON a => a -> Widget -> Widget encrypted :: ToJSON a => a -> Widget -> Widget
encrypted plaintextJson plaintext = do encrypted plaintextJson plaintext = do
canDecrypt <- (== Authorized) <$> evalAccess AdminErrMsgR True canDecrypt <- (== Authorized) <$> evalAccess AdminErrMsgR True
shouldEncrypt <- view _appEncryptErrors shouldEncrypt <- getsYesod $ view _appEncryptErrors
if if
| shouldEncrypt | shouldEncrypt
, not canDecrypt -> do , not canDecrypt -> do
@ -1596,14 +1593,13 @@ instance Yesod UniWorX where
. decodeUtf8 . decodeUtf8
. Base64.encode . Base64.encode
. (convert :: Digest (SHAKE256 144) -> ByteString) . (convert :: Digest (SHAKE256 144) -> ByteString)
. runIdentity . runConduitPure
$ sourceList (Lazy.ByteString.toChunks content) $$ sinkHash $ sourceList (Lazy.ByteString.toChunks content) .| sinkHash
fileUpload _site _length = FileUploadMemory lbsBackEnd fileUpload _site _length = FileUploadMemory lbsBackEnd
-- What messages should be logged. The following includes all messages when -- What messages should be logged. The following includes all messages when
-- in development, and warnings and errors in production. -- in development, and warnings and errors in production.
shouldLog _ _ _ = error "Must use shouldLogIO"
shouldLogIO app _source level = do shouldLogIO app _source level = do
LogSettings{..} <- readTVarIO $ appLogSettings app LogSettings{..} <- readTVarIO $ appLogSettings app
return $ logAll || level >= logMinimumLevel return $ logAll || level >= logMinimumLevel
@ -1626,7 +1622,7 @@ siteLayout = siteLayout' . Just
siteLayout' :: Maybe Widget -- ^ Optionally override `pageHeading` siteLayout' :: Maybe Widget -- ^ Optionally override `pageHeading`
-> Widget -> Handler Html -> Widget -> Handler Html
siteLayout' headingOverride widget = do siteLayout' headingOverride widget = do
AppSettings { appUserDefaults = UserDefaultConf{..}, .. } <- view appSettings AppSettings { appUserDefaults = UserDefaultConf{..}, .. } <- getsYesod $ view appSettings
isModal <- hasCustomHeader HeaderIsModal isModal <- hasCustomHeader HeaderIsModal
@ -1747,7 +1743,7 @@ siteLayout' headingOverride widget = do
applySystemMessages :: (MonadHandler m, HandlerSite m ~ UniWorX) => m () applySystemMessages :: (MonadHandler m, HandlerSite m ~ UniWorX) => m ()
applySystemMessages = liftHandlerT . runDB . runConduit $ selectSource [] [] .| C.mapM_ applyMessage applySystemMessages = liftHandler . runDB . runConduit $ selectSource [] [] .| C.mapM_ applyMessage
where where
applyMessage (Entity smId SystemMessage{..}) = maybeT_ $ do applyMessage (Entity smId SystemMessage{..}) = maybeT_ $ do
cID <- encrypt smId cID <- encrypt smId
@ -2548,7 +2544,7 @@ pageActions (CourseR tid ssh csh SheetListR) =
case muid of case muid of
Nothing -> return False Nothing -> return False
(Just uid) -> do (Just uid) -> do
[E.Value ok] <- runDB . E.select . return . E.exists . E.from $ \(course `E.InnerJoin` sheet `E.InnerJoin` submission) -> do ok <- runDB . E.selectExists . E.from $ \(course `E.InnerJoin` sheet `E.InnerJoin` submission) -> do
E.on $ submission E.^. SubmissionSheet E.==. sheet E.^. SheetId E.on $ submission E.^. SubmissionSheet E.==. sheet E.^. SheetId
E.on $ sheet E.^. SheetCourse E.==. course E.^. CourseId E.on $ sheet E.^. SheetCourse E.==. course E.^. CourseId
E.where_ $ submission E.^. SubmissionRatingBy E.==. E.just (E.val uid) E.where_ $ submission E.^. SubmissionRatingBy E.==. E.just (E.val uid)
@ -2717,6 +2713,24 @@ pageActions (CExamR tid ssh csh examn EUsersR) =
, menuItemModal = True , menuItemModal = True
, menuItemAccessCallback' = return True , menuItemAccessCallback' = return True
} }
, MenuItem
{ menuItemType = PageActionPrime
, menuItemLabel = MsgMenuExamGrades
, menuItemIcon = Nothing
, menuItemRoute = SomeRoute $ CExamR tid ssh csh examn EGradesR
, menuItemModal = False
, menuItemAccessCallback' = return True
}
]
pageActions (CExamR tid ssh csh examn EGradesR) =
[ MenuItem
{ menuItemType = PageActionPrime
, menuItemLabel = MsgMenuExamUsers
, menuItemIcon = Nothing
, menuItemRoute = SomeRoute $ CExamR tid ssh csh examn EUsersR
, menuItemModal = False
, menuItemAccessCallback' = return True
}
] ]
pageActions (CSheetR tid ssh csh shn SShowR) = pageActions (CSheetR tid ssh csh shn SShowR) =
[ MenuItem [ MenuItem
@ -2726,7 +2740,7 @@ pageActions (CSheetR tid ssh csh shn SShowR) =
, menuItemRoute = SomeRoute $ CSheetR tid ssh csh shn SubmissionNewR , menuItemRoute = SomeRoute $ CSheetR tid ssh csh shn SubmissionNewR
, menuItemModal = True , menuItemModal = True
, menuItemAccessCallback' = runDB . maybeT (return False) $ do , menuItemAccessCallback' = runDB . maybeT (return False) $ do
uid <- MaybeT $ liftHandlerT maybeAuthId uid <- MaybeT $ liftHandler maybeAuthId
submissions <- lift $ submissionList tid csh shn uid submissions <- lift $ submissionList tid csh shn uid
guard $ null submissions guard $ null submissions
return True return True
@ -2738,7 +2752,7 @@ pageActions (CSheetR tid ssh csh shn SShowR) =
, menuItemRoute = SomeRoute $ CSheetR tid ssh csh shn SubmissionOwnR , menuItemRoute = SomeRoute $ CSheetR tid ssh csh shn SubmissionOwnR
, menuItemModal = False , menuItemModal = False
, menuItemAccessCallback' = runDB . maybeT (return False) $ do , menuItemAccessCallback' = runDB . maybeT (return False) $ do
uid <- MaybeT $ liftHandlerT maybeAuthId uid <- MaybeT $ liftHandler maybeAuthId
submissions <- lift $ submissionList tid csh shn uid submissions <- lift $ submissionList tid csh shn uid
guard . not $ null submissions guard . not $ null submissions
return True return True
@ -2948,7 +2962,7 @@ pageActions (CorrectionsR) =
, menuItemRoute = SomeRoute CorrectionsCreateR , menuItemRoute = SomeRoute CorrectionsCreateR
, menuItemModal = False , menuItemModal = False
, menuItemAccessCallback' = runDB . maybeT (return False) $ do , menuItemAccessCallback' = runDB . maybeT (return False) $ do
uid <- MaybeT $ liftHandlerT maybeAuthId uid <- MaybeT $ liftHandler maybeAuthId
sheets <- lift . E.select . E.from $ \(course `E.InnerJoin` sheet) -> do sheets <- lift . E.select . E.from $ \(course `E.InnerJoin` sheet) -> do
E.on $ course E.^. CourseId E.==. sheet E.^. SheetCourse E.on $ course E.^. CourseId E.==. sheet E.^. SheetCourse
let let
@ -2987,7 +3001,7 @@ pageActions (CorrectionsGradeR) =
, menuItemRoute = SomeRoute CorrectionsCreateR , menuItemRoute = SomeRoute CorrectionsCreateR
, menuItemModal = False , menuItemModal = False
, menuItemAccessCallback' = runDB . maybeT (return False) $ do , menuItemAccessCallback' = runDB . maybeT (return False) $ do
uid <- MaybeT $ liftHandlerT maybeAuthId uid <- MaybeT $ liftHandler maybeAuthId
sheets <- lift . E.select . E.from $ \(course `E.InnerJoin` sheet) -> do sheets <- lift . E.select . E.from $ \(course `E.InnerJoin` sheet) -> do
E.on $ course E.^. CourseId E.==. sheet E.^. SheetCourse E.on $ course E.^. CourseId E.==. sheet E.^. SheetCourse
let let
@ -3006,7 +3020,7 @@ pageActions _ = []
i18nHeading :: (MonadWidget m, RenderMessage site msg, HandlerSite m ~ site) => msg -> m () i18nHeading :: (MonadWidget m, RenderMessage site msg, HandlerSite m ~ site) => msg -> m ()
i18nHeading msg = liftWidgetT $ toWidget =<< getMessageRender <*> pure msg i18nHeading msg = liftWidget $ toWidget =<< getMessageRender <*> pure msg
-- | only used in defaultLayout; better use siteLayout instead! -- | only used in defaultLayout; better use siteLayout instead!
pageHeading :: Route UniWorX -> Maybe Widget pageHeading :: Route UniWorX -> Maybe Widget
@ -3113,7 +3127,7 @@ pageHeading _
= Nothing = Nothing
routeNormalizers :: [Route UniWorX -> WriterT Any (ReaderT (YesodPersistBackend UniWorX) (HandlerT UniWorX IO)) (Route UniWorX)] routeNormalizers :: [Route UniWorX -> WriterT Any DB (Route UniWorX)]
routeNormalizers = routeNormalizers =
[ normalizeRender [ normalizeRender
, ncSchool , ncSchool
@ -3124,7 +3138,7 @@ routeNormalizers =
] ]
where where
normalizeRender route = route <$ do normalizeRender route = route <$ do
YesodRequest{..} <- liftHandlerT getRequest YesodRequest{..} <- liftHandler getRequest
let original = (W.pathInfo reqWaiRequest, reqGetParams) let original = (W.pathInfo reqWaiRequest, reqGetParams)
rendered = renderRoute route rendered = renderRoute route
if if
@ -3320,10 +3334,10 @@ upsertCampusUser ldapData Creds{..} = do
. UUID.fromByteString . UUID.fromByteString
. fromStrict . fromStrict
. (convert :: Digest (SHAKE128 128) -> ByteString) . (convert :: Digest (SHAKE128 128) -> ByteString)
. runIdentity . runConduitPure
$ sourceList (toStrict . Binary.encode <$> Set.toList studyTermCandidates) $$ sinkHash $ sourceList (toStrict . Binary.encode <$> Set.toList studyTermCandidates) .| sinkHash
[E.Value candidatesRecorded] <- E.select . return . E.exists . E.from $ \candidate -> candidatesRecorded <- E.selectExists . E.from $ \candidate ->
E.where_ $ candidate E.^. StudyTermCandidateIncidence E.==. E.val studyTermCandidateIncidence E.where_ $ candidate E.^. StudyTermCandidateIncidence E.==. E.val studyTermCandidateIncidence
unless candidatesRecorded $ do unless candidatesRecorded $ do
@ -3401,14 +3415,14 @@ instance YesodAuth UniWorX where
loginHandler = do loginHandler = do
toParent <- getRouteToParent toParent <- getRouteToParent
lift . defaultLayout $ do liftHandler . defaultLayout $ do
plugins <- getsYesod authPlugins plugins <- getsYesod authPlugins
$logDebugS "Auth" $ "Enabled plugins: " <> Text.intercalate ", " (map apName plugins) $logDebugS "Auth" $ "Enabled plugins: " <> Text.intercalate ", " (map apName plugins)
setTitleI MsgLoginTitle setTitleI MsgLoginTitle
$(widgetFile "login") $(widgetFile "login")
authenticate Creds{..} = runDB $ do authenticate Creds{..} = liftHandler . runDB $ do
now <- liftIO getCurrentTime now <- liftIO getCurrentTime
let let
@ -3477,7 +3491,7 @@ instance YesodAuth UniWorX where
, dummyLogin <$ guard appAuthDummyLogin , dummyLogin <$ guard appAuthDummyLogin
] ]
authHttpManager = Yesod.getHttpManager authHttpManager = getsYesod appHttpManager
onLogin = addMessageI Success Auth.NowLoggedIn onLogin = addMessageI Success Auth.NowLoggedIn

View File

@ -54,7 +54,7 @@ instance Button UniWorX ButtonCreate where
btnClasses CreateInf = [BCIsButton, BCPrimary] btnClasses CreateInf = [BCIsButton, BCPrimary]
-- END Button needed only here -- END Button needed only here
emailTestForm :: AForm (HandlerT UniWorX IO) (Email, MailContext) emailTestForm :: AForm (HandlerFor UniWorX) (Email, MailContext)
emailTestForm = (,) emailTestForm = (,)
<$> areq emailField (fslI MsgMailTestFormEmail) Nothing <$> areq emailField (fslI MsgMailTestFormEmail) Nothing
<*> ( MailContext <*> ( MailContext
@ -112,7 +112,7 @@ postAdminTestR = do
jId <- queueJob $ JobSendTestEmail email ls jId <- queueJob $ JobSendTestEmail email ls
tell . pure $ Message Success [shamlet|Email-test gestartet (Job ##{tshow (fromSqlKey jId)})|] (Just IconEmail) tell . pure $ Message Success [shamlet|Email-test gestartet (Job ##{tshow (fromSqlKey jId)})|] (Just IconEmail)
return jId return jId
writeJobCtl $ JobCtlPerform jId runReaderT (writeJobCtl $ JobCtlPerform jId) =<< getYesod
addMessage Warning [shamlet|Inkorrekt ausgegebener Alert|] -- For testing alert handling when short circuiting; for proper (not fallback-solution) handling always use `tell` within `formResultModal` addMessage Warning [shamlet|Inkorrekt ausgegebener Alert|] -- For testing alert handling when short circuiting; for proper (not fallback-solution) handling always use `tell` within `formResultModal`
let emailWidget' = wrapForm emailWidget def let emailWidget' = wrapForm emailWidget def
@ -189,7 +189,7 @@ postAdminTestR = do
-- | How does the shape (`ListLength`) change if a certain cell is deleted? -- | How does the shape (`ListLength`) change if a certain cell is deleted?
deleteCell :: Map ListPosition Int -- ^ Current shape, including initialisation data deleteCell :: Map ListPosition Int -- ^ Current shape, including initialisation data
-> ListPosition -- ^ Coordinate to delete -> ListPosition -- ^ Coordinate to delete
-> MaybeT (MForm (HandlerT UniWorX IO)) (Map ListPosition ListPosition) -- ^ Monadically compute a set of new positions and their associated old positions -> MaybeT (MForm (HandlerFor UniWorX)) (Map ListPosition ListPosition) -- ^ Monadically compute a set of new positions and their associated old positions
deleteCell = miDeleteList deleteCell = miDeleteList
-- | Make a decision on whether an add widget should be allowed to further cells, given the current @liveliness@ (i.e. before performing the addition) -- | Make a decision on whether an add widget should be allowed to further cells, given the current @liveliness@ (i.e. before performing the addition)
allowAdd :: ListPosition -> Natural -> ListLength -> Bool allowAdd :: ListPosition -> Natural -> ListLength -> Bool
@ -374,7 +374,7 @@ postAdminFeaturesR = do
-> Getter (DBRow r) (Maybe Text) -> Getter (DBRow r) (Maybe Text)
-> Getter (DBRow r) i -> Getter (DBRow r) i
-> DBRow r -> DBRow r
-> DBCell (MForm (HandlerT UniWorX IO)) (FormResult (DBFormResult i a (DBRow r))) -> DBCell (MForm (HandlerFor UniWorX)) (FormResult (DBFormResult i a (DBRow r)))
textInputCell lensRes lensDefault lensIndex = formCell id (return . view lensIndex) textInputCell lensRes lensDefault lensIndex = formCell id (return . view lensIndex)
(\row _mkUnique -> (\(res,fieldView) -> (set lensRes . assertM (not . Text.null) <$> res, fvInput fieldView)) (\row _mkUnique -> (\(res,fieldView) -> (set lensRes . assertM (not . Text.null) <$> res, fvInput fieldView))
<$> mopt textField "" (Just $ row ^. lensDefault) <$> mopt textField "" (Just $ row ^. lensDefault)
@ -385,7 +385,7 @@ postAdminFeaturesR = do
-> Getter (DBRow r) Bool -> Getter (DBRow r) Bool
-> Getter (DBRow r) i -> Getter (DBRow r) i
-> DBRow r -> DBRow r
-> DBCell (MForm (HandlerT UniWorX IO)) (FormResult (DBFormResult i a (DBRow r))) -> DBCell (MForm (HandlerFor UniWorX)) (FormResult (DBFormResult i a (DBRow r)))
checkboxCell lensRes lensDefault lensIndex = formCell id (return . view lensIndex) checkboxCell lensRes lensDefault lensIndex = formCell id (return . view lensIndex)
( \row _mkUnique -> (\(res, fieldView) -> (set lensRes <$> res, fvInput fieldView)) ( \row _mkUnique -> (\(res, fieldView) -> (set lensRes <$> res, fvInput fieldView))
<$> mpopt checkBoxField "" (Just $ row ^. lensDefault) <$> mpopt checkBoxField "" (Just $ row ^. lensDefault)

View File

@ -52,7 +52,7 @@ data ApplicationForm = ApplicationForm
{ afPriority :: Maybe Natural { afPriority :: Maybe Natural
, afField :: Maybe StudyFeaturesId , afField :: Maybe StudyFeaturesId
, afText :: Maybe Text , afText :: Maybe Text
, afFiles :: Maybe (Source Handler File) , afFiles :: Maybe (ConduitT () File Handler ())
, afRatingVeto :: Bool , afRatingVeto :: Bool
, afRatingPoints :: Maybe ExamGrade , afRatingPoints :: Maybe ExamGrade
, afRatingComment :: Maybe Text , afRatingComment :: Maybe Text
@ -77,11 +77,11 @@ applicationForm :: (Maybe AllocationId)
-> Html -> MForm Handler (FormResult ApplicationForm, ApplicationFormView) -> Html -> MForm Handler (FormResult ApplicationForm, ApplicationFormView)
applicationForm maId@(is _Just -> isAlloc) cid uid ApplicationFormMode{..} csrf = do applicationForm maId@(is _Just -> isAlloc) cid uid ApplicationFormMode{..} csrf = do
(mApp, coursesNum, Course{..}, maxPrio) <- liftHandlerT . runDB $ do (mApp, coursesNum, Course{..}, maxPrio) <- liftHandler . runDB $ do
mApplication <- listToMaybe <$> selectList [CourseApplicationAllocation ==. maId, CourseApplicationUser ==. uid, CourseApplicationCourse ==. cid] [LimitTo 1] mApplication <- listToMaybe <$> selectList [CourseApplicationAllocation ==. maId, CourseApplicationUser ==. uid, CourseApplicationCourse ==. cid] [LimitTo 1]
coursesNum <- fromIntegral . fromMaybe 1 <$> for maId (\aId -> count [AllocationCourseAllocation ==. aId]) coursesNum <- fromIntegral . fromMaybe 1 <$> for maId (\aId -> count [AllocationCourseAllocation ==. aId])
course <- getJust cid course <- getJust cid
[E.Value (fromMaybe 0 -> maxPrio)] <- E.select . E.from $ \courseApplication -> do (fromMaybe 0 -> maxPrio) <- fmap ((>>= E.unValue) . listToMaybe) . E.select . E.from $ \courseApplication -> do
E.where_ $ courseApplication E.^. CourseApplicationCourse E.==. E.val cid E.where_ $ courseApplication E.^. CourseApplicationCourse E.==. E.val cid
E.&&. courseApplication E.^. CourseApplicationUser E.==. E.val uid E.&&. courseApplication E.^. CourseApplicationUser E.==. E.val uid
E.&&. courseApplication E.^. CourseApplicationAllocation E.==. E.val maId E.&&. courseApplication E.^. CourseApplicationAllocation E.==. E.val maId
@ -146,7 +146,7 @@ applicationForm maId@(is _Just -> isAlloc) cid uid ApplicationFormMode{..} csrf
-> over _2 Just . over (_1 . _FormSuccess) (assertM $ not . Text.null) <$> mopt textField' textFs (Just $ mApp >>= courseApplicationText . entityVal) -> over _2 Just . over (_1 . _FormSuccess) (assertM $ not . Text.null) <$> mopt textField' textFs (Just $ mApp >>= courseApplicationText . entityVal)
hasFiles <- for mApp $ \(Entity appId _) hasFiles <- for mApp $ \(Entity appId _)
-> fmap (not . null) . liftHandlerT . runDB $ selectKeysList [ CourseApplicationFileApplication ==. appId ] [ LimitTo 1 ] -> fmap (not . null) . liftHandler . runDB $ selectKeysList [ CourseApplicationFileApplication ==. appId ] [ LimitTo 1 ]
appCID <- for mApp $ encrypt . entityKey appCID <- for mApp $ encrypt . entityKey
let appFilesInfo = (,) <$> hasFiles <*> appCID let appFilesInfo = (,) <$> hasFiles <*> appCID
@ -296,7 +296,7 @@ editApplicationR maId uid cid mAppId afMode allowAction postAction = do
fId <- insert file fId <- insert file
insert_ $ CourseApplicationFile appId fId insert_ $ CourseApplicationFile appId fId
forM_ afFiles $ \afFiles' -> forM_ afFiles $ \afFiles' ->
runConduit $ transPipe liftHandlerT afFiles' .| C.mapM_ sinkFile' runConduit $ transPipe liftHandler afFiles' .| C.mapM_ sinkFile'
audit $ TransactionCourseApplicationEdit cid uid appId audit $ TransactionCourseApplicationEdit cid uid appId
addMessageI Success $ MsgCourseApplicationCreated courseShorthand addMessageI Success $ MsgCourseApplicationCreated courseShorthand
| is _BtnAllocationApplicationEdit afAction || is _BtnAllocationApplicationRate afAction | is _BtnAllocationApplicationEdit afAction || is _BtnAllocationApplicationRate afAction
@ -327,7 +327,7 @@ editApplicationR maId uid cid mAppId afMode allowAction postAction = do
fId <- lift $ insert file fId <- lift $ insert file
lift . insert_ $ CourseApplicationFile appId fId lift . insert_ $ CourseApplicationFile appId fId
modify $ Set.insert fId modify $ Set.insert fId
in runConduit $ transPipe liftHandlerT afFiles' .| C.mapM_ sinkFile' in runConduit $ transPipe liftHandler afFiles' .| C.mapM_ sinkFile'
deleteCascadeWhere [ FileId <-. Set.toList (oldFiles `Set.intersection` changes) ] deleteCascadeWhere [ FileId <-. Set.toList (oldFiles `Set.intersection` changes) ]
return changes return changes
| otherwise | otherwise

View File

@ -1,3 +1,5 @@
{-# OPTIONS_GHC -fno-warn-redundant-constraints #-}
module Handler.Allocation.List module Handler.Allocation.List
( getAllocationListR ( getAllocationListR
) where ) where

View File

@ -68,10 +68,10 @@ getAShowR tid ssh ash = do
let Entity cid Course{..} = cEntry ^. resultCourse let Entity cid Course{..} = cEntry ^. resultCourse
hasApplicationTemplate = cEntry ^. resultHasTemplate hasApplicationTemplate = cEntry ^. resultHasTemplate
mApp = cEntry ^? resultCourseApplication mApp = cEntry ^? resultCourseApplication
cID <- encrypt cid :: WidgetT UniWorX IO CryptoUUIDCourse cID <- encrypt cid :: WidgetFor UniWorX CryptoUUIDCourse
mayApply <- hasWriteAccessTo . AllocationR tid ssh ash $ AApplyR cID mayApply <- hasWriteAccessTo . AllocationR tid ssh ash $ AApplyR cID
isLecturer <- hasWriteAccessTo $ CourseR courseTerm courseSchool courseShorthand CEditR isLecturer <- hasWriteAccessTo $ CourseR courseTerm courseSchool courseShorthand CEditR
mApplyFormView <- liftHandlerT . for muid $ \uid -> generateFormPost . applicationForm (Just aId) cid uid $ ApplicationFormMode True mayApply isLecturer mApplyFormView <- liftHandler . for muid $ \uid -> generateFormPost . applicationForm (Just aId) cid uid $ ApplicationFormMode True mayApply isLecturer
tRoute <- case mApp of tRoute <- case mApp of
Nothing -> return . AllocationR tid ssh ash $ AApplyR cID Nothing -> return . AllocationR tid ssh ash $ AApplyR cID
Just (Entity appId _) -> CApplicationR courseTerm courseSchool courseShorthand <$> encrypt appId <*> pure CAEditR Just (Entity appId _) -> CApplicationR courseTerm courseSchool courseShorthand <$> encrypt appId <*> pure CAEditR

View File

@ -1,6 +1,6 @@
module Handler.Corrections where module Handler.Corrections where
import Import import Import hiding (link)
-- import System.FilePath (takeFileName) -- import System.FilePath (takeFileName)
import Jobs import Jobs
@ -71,8 +71,8 @@ correctionsTableQuery whereClause returnStatement t@((course `E.InnerJoin` sheet
E.where_ $ whereClause t E.where_ $ whereClause t
return $ returnStatement t return $ returnStatement t
lastEditQuery :: Database.Esqueleto.Internal.Language.From query expr backend (expr (Entity SubmissionEdit)) lastEditQuery :: Database.Esqueleto.Internal.Language.From (E.SqlExpr (Entity SubmissionEdit))
=> expr (Entity Submission) -> expr (E.Value (Maybe UTCTime)) => E.SqlExpr (Entity Submission) -> E.SqlExpr (E.Value (Maybe UTCTime))
lastEditQuery submission = E.sub_select $ E.from $ \edit -> do lastEditQuery submission = E.sub_select $ E.from $ \edit -> do
E.where_ $ edit E.^. SubmissionEditSubmission E.==. submission E.^. SubmissionId E.where_ $ edit E.^. SubmissionEditSubmission E.==. submission E.^. SubmissionId
return $ E.max_ $ edit E.^. SubmissionEditTime return $ E.max_ $ edit E.^. SubmissionEditTime
@ -216,7 +216,7 @@ colPointsField = sortable (Just "rating") (i18nCell MsgColumnRatingPoints) $ for
) )
colCommentField :: Colonnade Sortable CorrectionTableData (DBCell _ (FormResult (DBFormResult SubmissionId (a, b, Maybe Text) CorrectionTableData))) colCommentField :: Colonnade Sortable CorrectionTableData (DBCell _ (FormResult (DBFormResult SubmissionId (a, b, Maybe Text) CorrectionTableData)))
colCommentField = sortable (Just "comment") (i18nCell MsgRatingComment) $ fmap (addCellAttrs [("style","width:60%")]) $ formCell id colCommentField = sortable (Just "comment") (i18nCell MsgRatingComment) $ fmap (cellAttrs <>~ [("style","width:60%")]) $ formCell id
(\DBRow{ dbrOutput=(Entity subId _, _, _, _, _, _) } -> return subId) (\DBRow{ dbrOutput=(Entity subId _, _, _, _, _, _) } -> return subId)
(\DBRow{ dbrOutput=(Entity _ Submission{..}, _, _, _, _, _) } mkUnique -> over (_1.mapped) ((_3 .~) . assertM (not . null) . fmap (Text.strip . unTextarea)) . over _2 fvInput <$> mopt textareaField (fsUniq mkUnique "comment") (Just $ Textarea <$> submissionRatingComment)) (\DBRow{ dbrOutput=(Entity _ Submission{..}, _, _, _, _, _) } mkUnique -> over (_1.mapped) ((_3 .~) . assertM (not . null) . fmap (Text.strip . unTextarea)) . over _2 fvInput <$> mopt textareaField (fsUniq mkUnique "comment") (Just $ Textarea <$> submissionRatingComment))
@ -238,7 +238,7 @@ makeCorrectionsTable whereClause dbtColonnade dbtFilterUI psValidator dbtProj' d
) )
in (submission, sheet, crse, corrector, lastEditQuery submission) in (submission, sheet, crse, corrector, lastEditQuery submission)
) )
dbtProj :: DBRow _ -> MaybeT (ReaderT SqlBackend (HandlerT UniWorX IO)) CorrectionTableData dbtProj :: DBRow _ -> MaybeT (ReaderT SqlBackend (HandlerFor UniWorX)) CorrectionTableData
dbtProj = traverse $ \(submission@(Entity sId _), sheet@(Entity shId _), (E.Value courseName, E.Value courseShorthand, E.Value courseTerm, E.Value courseSchool), mCorrector, E.Value mbLastEdit) -> do dbtProj = traverse $ \(submission@(Entity sId _), sheet@(Entity shId _), (E.Value courseName, E.Value courseShorthand, E.Value courseTerm, E.Value courseSchool), mCorrector, E.Value mbLastEdit) -> do
submittors <- lift . E.select . E.from $ \((submissionUser `E.InnerJoin` user) `E.LeftOuterJoin` pseudonym) -> do submittors <- lift . E.select . E.from $ \((submissionUser `E.InnerJoin` user) `E.LeftOuterJoin` pseudonym) -> do
E.on $ pseudonym E.?. SheetPseudonymUser E.==. E.just (user E.^. UserId) E.on $ pseudonym E.?. SheetPseudonymUser E.==. E.just (user E.^. UserId)
@ -398,9 +398,9 @@ data ActionCorrectionsData = CorrDownloadData
| CorrAutoSetCorrectorData SheetId | CorrAutoSetCorrectorData SheetId
| CorrDeleteData | CorrDeleteData
correctionsR :: _ -> _ -> _ -> _ -> Map ActionCorrections (AForm (HandlerT UniWorX IO) ActionCorrectionsData) -> Handler TypedContent correctionsR :: _ -> _ -> _ -> _ -> Map ActionCorrections (AForm (HandlerFor UniWorX) ActionCorrectionsData) -> Handler TypedContent
correctionsR whereClause displayColumns dbtFilterUI psValidator actions = do correctionsR whereClause displayColumns dbtFilterUI psValidator actions = do
Just currentRoute <- getCurrentRoute -- This should never be called from a 404 handler currentRoute <- fromMaybe (error "correctionsR called from 404-handler") <$> getCurrentRoute -- This should never be called from a 404 handler
postDeleteR $ \drRecords -> (submissionDeleteRoute drRecords) postDeleteR $ \drRecords -> (submissionDeleteRoute drRecords)
{ drAbort = SomeRoute currentRoute { drAbort = SomeRoute currentRoute
@ -416,7 +416,7 @@ correctionsR whereClause displayColumns dbtFilterUI psValidator actions = do
, dbParamsFormAdditional = \frag -> do , dbParamsFormAdditional = \frag -> do
(actionRes, action) <- multiActionM actions "" Nothing mempty (actionRes, action) <- multiActionM actions "" Nothing mempty
return ((, mempty) . Last . Just <$> actionRes, toWidget frag <> action) return ((, mempty) . Last . Just <$> actionRes, toWidget frag <> action)
, dbParamsFormEvaluate = liftHandlerT . runFormPost , dbParamsFormEvaluate = liftHandler . runFormPost
, dbParamsFormResult = _1 , dbParamsFormResult = _1
, dbParamsFormIdent = def , dbParamsFormIdent = def
} }
@ -466,7 +466,7 @@ correctionsR whereClause displayColumns dbtFilterUI psValidator actions = do
] ]
addMessageI Success $ MsgUpdatedAssignedCorrectorSingle num addMessageI Success $ MsgUpdatedAssignedCorrectorSingle num
auditAllSubEdit sIds auditAllSubEdit sIds
(E.Value selfCorrectors:_) <- E.select . E.from $ \(submission `E.InnerJoin` subuser) -> do selfCorrectors <- fmap (maybe 0 (max 0 . E.unValue) . listToMaybe) . E.select . E.from $ \(submission `E.InnerJoin` subuser) -> do
E.on $ submission E.^. SubmissionId E.==. subuser E.^. SubmissionUserSubmission E.on $ submission E.^. SubmissionId E.==. subuser E.^. SubmissionUserSubmission
E.where_ $ submission E.^. SubmissionId `E.in_` E.valList subs E.where_ $ submission E.^. SubmissionId `E.in_` E.valList subs
E.&&. submission E.^. SubmissionRatingBy E.==. E.just (subuser E.^. SubmissionUserUser) E.&&. submission E.^. SubmissionRatingBy E.==. E.just (subuser E.^. SubmissionUserUser)
@ -537,7 +537,7 @@ correctionsR whereClause displayColumns dbtFilterUI psValidator actions = do
where where
authorizedToAssign :: SubmissionId -> DB Bool authorizedToAssign :: SubmissionId -> DB Bool
authorizedToAssign sId = do authorizedToAssign sId = do
[(E.Value tid, E.Value ssh, E.Value csh, E.Value shn)] <- (E.Value tid, E.Value ssh, E.Value csh, E.Value shn) <- maybe notFound return . listToMaybe <=<
E.select . E.from $ \(course `E.InnerJoin` sheet `E.InnerJoin` submission ) -> do E.select . E.from $ \(course `E.InnerJoin` sheet `E.InnerJoin` submission ) -> do
E.on $ sheet E.^. SheetId E.==. submission E.^. SubmissionSheet E.on $ sheet E.^. SheetId E.==. submission E.^. SubmissionSheet
E.on $ course E.^. CourseId E.==. sheet E.^. SheetCourse E.on $ course E.^. CourseId E.==. sheet E.^. SheetCourse
@ -547,7 +547,7 @@ correctionsR whereClause displayColumns dbtFilterUI psValidator actions = do
let route = CSubmissionR tid ssh csh shn cID SubAssignR let route = CSubmissionR tid ssh csh shn cID SubAssignR
(== Authorized) <$> evalAccessDB route True (== Authorized) <$> evalAccessDB route True
type ActionCorrections' = (ActionCorrections, AForm (HandlerT UniWorX IO) ActionCorrectionsData) type ActionCorrections' = (ActionCorrections, AForm (HandlerFor UniWorX) ActionCorrectionsData)
downloadAction, deleteAction :: ActionCorrections' downloadAction, deleteAction :: ActionCorrections'
downloadAction = ( CorrDownload downloadAction = ( CorrDownload
@ -560,7 +560,7 @@ deleteAction = ( CorrDelete
assignAction :: Either CourseId SheetId -> ActionCorrections' assignAction :: Either CourseId SheetId -> ActionCorrections'
assignAction selId = ( CorrSetCorrector assignAction selId = ( CorrSetCorrector
, wFormToAForm $ do , wFormToAForm $ do
correctors <- liftHandlerT . runDB . E.select . E.from $ \(course `E.InnerJoin` sheet `E.InnerJoin` sheetCorrector `E.InnerJoin` user) -> do correctors <- liftHandler . runDB . E.select . E.from $ \(course `E.InnerJoin` sheet `E.InnerJoin` sheetCorrector `E.InnerJoin` user) -> do
E.on $ user E.^. UserId E.==. sheetCorrector E.^. SheetCorrectorUser E.on $ user E.^. UserId E.==. sheetCorrector E.^. SheetCorrectorUser
E.on $ sheet E.^. SheetId E.==. sheetCorrector E.^. SheetCorrectorSheet E.on $ sheet E.^. SheetId E.==. sheetCorrector E.^. SheetCorrectorSheet
E.on $ course E.^. CourseId E.==. sheet E.^. SheetCourse E.on $ course E.^. CourseId E.==. sheet E.^. SheetCourse
@ -573,7 +573,7 @@ assignAction selId = ( CorrSetCorrector
correctors' <- forM correctors $ \Entity{ entityKey, entityVal = User{..} } -> (SomeMessage userDisplayName, ) <$> encrypt entityKey correctors' <- forM correctors $ \Entity{ entityKey, entityVal = User{..} } -> (SomeMessage userDisplayName, ) <$> encrypt entityKey
cId <- wopt (selectFieldList correctors' :: Field (HandlerT UniWorX IO) CryptoUUIDUser) (fslI MsgCorrector & setTooltip MsgCorrSetCorrectorTooltip) Nothing cId <- wopt (selectFieldList correctors' :: Field (HandlerFor UniWorX) CryptoUUIDUser) (fslI MsgCorrector & setTooltip MsgCorrSetCorrectorTooltip) Nothing
fmap CorrSetCorrectorData <$> (traverse.traverse) decrypt cId fmap CorrSetCorrectorData <$> (traverse.traverse) decrypt cId
) )
@ -740,7 +740,7 @@ postCorrectionR tid ssh csh shn cid = do
} }
formResult corrResult $ \(rated, ratingPoints', ratingComment') -> do formResult corrResult $ \(rated, ratingPoints', ratingComment') -> do
uid <- liftHandlerT requireAuthId uid <- liftHandler requireAuthId
now <- liftIO getCurrentTime now <- liftIO getCurrentTime
if if
@ -1013,7 +1013,7 @@ postCorrectionsGradeR = do
, colCommentField , colCommentField
] -- Continue here ] -- Continue here
psValidator = def psValidator = def
& defaultSorting [SortDescBy "ratingtime"] :: PSValidator (MForm (HandlerT UniWorX IO)) (FormResult (DBFormResult SubmissionId (Bool, Maybe Points, Maybe Text) CorrectionTableData)) & defaultSorting [SortDescBy "ratingtime"] :: PSValidator (MForm (HandlerFor UniWorX)) (FormResult (DBFormResult SubmissionId (Bool, Maybe Points, Maybe Text) CorrectionTableData))
unFormResult = getDBFormResult $ \DBRow{ dbrOutput = (Entity _ sub@Submission{..}, _, _, _, _, _) } -> (submissionRatingDone sub, submissionRatingPoints, submissionRatingComment) unFormResult = getDBFormResult $ \DBRow{ dbrOutput = (Entity _ sub@Submission{..}, _, _, _, _, _) } -> (submissionRatingDone sub, submissionRatingPoints, submissionRatingComment)
dbtProj' i@(Entity subId _, Entity _ Sheet{ sheetName = shn }, (_, csh, tid, ssh), _, _, _) = do dbtProj' i@(Entity subId _, Entity _ Sheet{ sheetName = shn }, (_, csh, tid, ssh), _, _, _) = do
cID <- encrypt subId cID <- encrypt subId

View File

@ -1,3 +1,5 @@
{-# OPTIONS_GHC -fno-warn-redundant-constraints #-}
module Handler.Course.Application.Files module Handler.Course.Application.Files
( getCAFilesR ( getCAFilesR
, getCAppsFilesR , getCAppsFilesR
@ -47,7 +49,7 @@ getCAppsFilesR tid ssh csh = do
archiveName <- fmap (flip addExtension (unpack extensionZip) . unpack) . ap getMessageRender . pure $ MsgCourseAllApplicationsArchiveName tid ssh csh archiveName <- fmap (flip addExtension (unpack extensionZip) . unpack) . ap getMessageRender . pure $ MsgCourseAllApplicationsArchiveName tid ssh csh
let let
fsSource :: Source DB File fsSource :: ConduitT () File DB ()
fsSource = do fsSource = do
apps <- lift . E.select . E.from $ \((course `E.InnerJoin` courseApplication `E.InnerJoin` user) `E.LeftOuterJoin` allocation) -> do apps <- lift . E.select . E.from $ \((course `E.InnerJoin` courseApplication `E.InnerJoin` user) `E.LeftOuterJoin` allocation) -> do
E.on $ allocation E.?. AllocationId E.==. courseApplication E.^. CourseApplicationAllocation E.on $ allocation E.?. AllocationId E.==. courseApplication E.^. CourseApplicationAllocation

View File

@ -1,4 +1,5 @@
{-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# OPTIONS_GHC -fno-warn-redundant-constraints #-}
module Handler.Course.Application.List module Handler.Course.Application.List
( getCApplicationsR, postCApplicationsR ( getCApplicationsR, postCApplicationsR
@ -103,7 +104,7 @@ instance Csv.ToField CourseApplicationsTableVeto where
instance Csv.FromField CourseApplicationsTableVeto where instance Csv.FromField CourseApplicationsTableVeto where
parseField f = do parseField f = do
(CI.map Text.strip -> t :: CI Text) <- Csv.parseField f (CI.map Text.strip -> t :: CI Text) <- Csv.parseField f
return . CourseApplicationsTableVeto $ any (== t) return . CourseApplicationsTableVeto $ elem t
[ "veto", "v", "yes", "y", "ja", "j", "wahr", "w", "true", "t", "1" ] [ "veto", "v", "yes", "y", "ja", "j", "wahr", "w", "true", "t", "1" ]
data CourseApplicationsTableCsv = CourseApplicationsTableCsv data CourseApplicationsTableCsv = CourseApplicationsTableCsv

View File

@ -44,7 +44,7 @@ data CourseForm = CourseForm
, cfAllocation :: Maybe AllocationCourseForm , cfAllocation :: Maybe AllocationCourseForm
, cfAppRequired :: Bool , cfAppRequired :: Bool
, cfAppInstructions :: Maybe Html , cfAppInstructions :: Maybe Html
, cfAppInstructionFiles :: Maybe (Source Handler (Either FileId File)) , cfAppInstructionFiles :: Maybe (ConduitT () (Either FileId File) Handler ())
, cfAppText :: Bool , cfAppText :: Bool
, cfAppFiles :: UploadMode , cfAppFiles :: UploadMode
, cfAppRatingsVisible :: Bool , cfAppRatingsVisible :: Bool
@ -101,13 +101,13 @@ allocationCourseToForm (Entity _ AllocationCourse{..}) = AllocationCourseForm
makeCourseForm :: (forall p. PathPiece p => p -> Maybe (SomeRoute UniWorX)) -> Maybe CourseForm -> Form CourseForm makeCourseForm :: (forall p. PathPiece p => p -> Maybe (SomeRoute UniWorX)) -> Maybe CourseForm -> Form CourseForm
makeCourseForm miButtonAction template = identifyForm FIDcourse . validateFormDB validateCourse $ \html -> do makeCourseForm miButtonAction template = identifyForm FIDcourse . validateFormDB validateCourse $ \html -> do
-- TODO: Refactor to avoid the four repeated calls to liftHandlerT and three runDBs -- TODO: Refactor to avoid the four repeated calls to liftHandler and three runDBs
-- let editCid = cfCourseId =<< template -- possible start for refactoring -- let editCid = cfCourseId =<< template -- possible start for refactoring
MsgRenderer mr <- getMsgRenderer MsgRenderer mr <- getMsgRenderer
uid <- liftHandlerT requireAuthId uid <- liftHandler requireAuthId
(lecturerSchools, adminSchools) <- liftHandlerT . runDB $ do (lecturerSchools, adminSchools) <- liftHandler . runDB $ do
lecturerSchools <- map (userFunctionSchool . entityVal) <$> selectList [UserFunctionUser ==. uid, UserFunctionFunction <-. [SchoolLecturer]] [] lecturerSchools <- map (userFunctionSchool . entityVal) <$> selectList [UserFunctionUser ==. uid, UserFunctionFunction <-. [SchoolLecturer]] []
protoAdminSchools <- map (userFunctionSchool . entityVal) <$> selectList [UserFunctionUser ==. uid, UserFunctionFunction <-. [SchoolAdmin]] [] protoAdminSchools <- map (userFunctionSchool . entityVal) <$> selectList [UserFunctionUser ==. uid, UserFunctionFunction <-. [SchoolAdmin]] []
adminSchools <- filterM (hasWriteAccessTo . flip SchoolR SchoolEditR) protoAdminSchools adminSchools <- filterM (hasWriteAccessTo . flip SchoolR SchoolEditR) protoAdminSchools
@ -116,7 +116,7 @@ makeCourseForm miButtonAction template = identifyForm FIDcourse . validateFormDB
termsField <- case template of termsField <- case template of
-- Change of term is only allowed if user may delete the course (i.e. no participants) or admin -- Change of term is only allowed if user may delete the course (i.e. no participants) or admin
(Just cform) | (Just cid) <- cfCourseId cform -> liftHandlerT $ do -- edit existing course (Just cform) | (Just cid) <- cfCourseId cform -> liftHandler $ do -- edit existing course
_courseOld@Course{..} <- runDB $ get404 cid _courseOld@Course{..} <- runDB $ get404 cid
mayEditTerm <- isAuthorized TermEditR True mayEditTerm <- isAuthorized TermEditR True
mayDelete <- isAuthorized (CourseR courseTerm courseSchool courseShorthand CDeleteR) True mayDelete <- isAuthorized (CourseR courseTerm courseSchool courseShorthand CDeleteR) True
@ -128,7 +128,7 @@ makeCourseForm miButtonAction template = identifyForm FIDcourse . validateFormDB
let miAdd :: ListPosition -> Natural -> (Text -> Text) -> FieldView UniWorX -> Maybe (Form (Map ListPosition (Either UserEmail UserId) -> FormResult (Map ListPosition (Either UserEmail UserId)))) let miAdd :: ListPosition -> Natural -> (Text -> Text) -> FieldView UniWorX -> Maybe (Form (Map ListPosition (Either UserEmail UserId) -> FormResult (Map ListPosition (Either UserEmail UserId))))
miAdd _ _ nudge btn = Just $ \csrf -> do miAdd _ _ nudge btn = Just $ \csrf -> do
(addRes, addView) <- mpreq emailField ("" & addName (nudge "user")) Nothing (addRes, addView) <- mpreq emailField ("" & addName (nudge "user")) Nothing
addRes' <- for addRes $ liftHandlerT . runDB . getKeyBy . UniqueEmail . CI.mk addRes' <- for addRes $ liftHandler . runDB . getKeyBy . UniqueEmail . CI.mk
let addRes'' = case (,) <$> addRes <*> addRes' of let addRes'' = case (,) <$> addRes <*> addRes' of
FormSuccess (CI.mk -> email, mLid) -> FormSuccess (CI.mk -> email, mLid) ->
let new = maybe (Left email) Right mLid let new = maybe (Left email) Right mLid
@ -143,7 +143,7 @@ makeCourseForm miButtonAction template = identifyForm FIDcourse . validateFormDB
miCell :: ListPosition -> Either UserEmail UserId -> Maybe (Maybe LecturerType) -> (Text -> Text) -> Form (Maybe LecturerType) miCell :: ListPosition -> Either UserEmail UserId -> Maybe (Maybe LecturerType) -> (Text -> Text) -> Form (Maybe LecturerType)
miCell _ (Right lid) defType nudge = \csrf -> do miCell _ (Right lid) defType nudge = \csrf -> do
(lrwRes,lrwView) <- mreq (selectField optionsFinite) ("" & addName (nudge "lecturer-type")) (join defType) (lrwRes,lrwView) <- mreq (selectField optionsFinite) ("" & addName (nudge "lecturer-type")) (join defType)
User{userEmail, userDisplayName, userSurname} <- liftHandlerT . runDB $ get404 lid User{userEmail, userDisplayName, userSurname} <- liftHandler . runDB $ get404 lid
let lrwView' = $(widgetFile "course/lecturerMassInput/cellKnown") let lrwView' = $(widgetFile "course/lecturerMassInput/cellKnown")
return (Just <$> lrwRes,lrwView') return (Just <$> lrwRes,lrwView')
miCell _ (Left lEmail) defType nudge = \csrf -> do miCell _ (Left lEmail) defType nudge = \csrf -> do
@ -153,7 +153,7 @@ makeCourseForm miButtonAction template = identifyForm FIDcourse . validateFormDB
miDelete :: Map ListPosition (Either UserEmail UserId) -- ^ Current shape miDelete :: Map ListPosition (Either UserEmail UserId) -- ^ Current shape
-> ListPosition -- ^ Coordinate to delete -> ListPosition -- ^ Coordinate to delete
-> MaybeT (MForm (HandlerT UniWorX IO)) (Map ListPosition ListPosition) -> MaybeT (MForm (HandlerFor UniWorX)) (Map ListPosition ListPosition)
miDelete = miDeleteList miDelete = miDeleteList
miAllowAdd :: ListPosition -> Natural -> ListLength -> Bool miAllowAdd :: ListPosition -> Natural -> ListLength -> Bool
@ -194,7 +194,7 @@ makeCourseForm miButtonAction template = identifyForm FIDcourse . validateFormDB
(newRegFrom,newRegTo,newDeRegUntil) <- case template of (newRegFrom,newRegTo,newDeRegUntil) <- case template of
(Just cform) | (Just _cid) <- cfCourseId cform -> return (Nothing,Nothing,Nothing) (Just cform) | (Just _cid) <- cfCourseId cform -> return (Nothing,Nothing,Nothing)
_allIOtherCases -> do _allIOtherCases -> do
mbLastTerm <- liftHandlerT $ runDB $ selectFirst [TermActive ==. True] [Desc TermName] mbLastTerm <- liftHandler $ runDB $ selectFirst [TermActive ==. True] [Desc TermName]
return ( (Just . toMidnight . termStart . entityVal) <$> mbLastTerm return ( (Just . toMidnight . termStart . entityVal) <$> mbLastTerm
, (Just . beforeMidnight . termEnd . entityVal) <$> mbLastTerm , (Just . beforeMidnight . termEnd . entityVal) <$> mbLastTerm
, (Just . beforeMidnight . termEnd . entityVal) <$> mbLastTerm ) , (Just . beforeMidnight . termEnd . entityVal) <$> mbLastTerm )
@ -202,7 +202,7 @@ makeCourseForm miButtonAction template = identifyForm FIDcourse . validateFormDB
let let
allocationForm :: AForm Handler (Maybe AllocationCourseForm) allocationForm :: AForm Handler (Maybe AllocationCourseForm)
allocationForm = wFormToAForm $ do allocationForm = wFormToAForm $ do
availableAllocations' <- liftHandlerT . runDB . E.select . E.from $ \(allocation `E.InnerJoin` term) -> do availableAllocations' <- liftHandler . runDB . E.select . E.from $ \(allocation `E.InnerJoin` term) -> do
E.on $ allocation E.^. AllocationTerm E.==. term E.^. TermId E.on $ allocation E.^. AllocationTerm E.==. term E.^. TermId
let alreadyParticipates = flip (maybe E.false) (template >>= cfCourseId) $ \cid -> let alreadyParticipates = flip (maybe E.false) (template >>= cfCourseId) $ \cid ->
@ -226,7 +226,7 @@ makeCourseForm miButtonAction template = identifyForm FIDcourse . validateFormDB
activeAllocations = availableAllocations' ^.. folded . filtered ((&&) <$> (not <$> allocationEnabled . view _1) <*> view (_2 . _Value)) . _1 activeAllocations = availableAllocations' ^.. folded . filtered ((&&) <$> (not <$> allocationEnabled . view _1) <*> view (_2 . _Value)) . _1
mkAllocationOption :: forall m. (MonadHandler m, HandlerSite m ~ UniWorX) => Entity Allocation -> m (Option AllocationId) mkAllocationOption :: forall m. (MonadHandler m, HandlerSite m ~ UniWorX) => Entity Allocation -> m (Option AllocationId)
mkAllocationOption (Entity aId Allocation{..}) = liftHandlerT $ do mkAllocationOption (Entity aId Allocation{..}) = liftHandler $ do
cID <- encrypt aId :: Handler CryptoUUIDAllocation cID <- encrypt aId :: Handler CryptoUUIDAllocation
return . Option (mr . MsgCourseAllocationOption (mr . ShortTermIdentifier $ unTermKey allocationTerm) $ CI.original allocationName) aId $ toPathPiece cID return . Option (mr . MsgCourseAllocationOption (mr . ShortTermIdentifier $ unTermKey allocationTerm) $ CI.original allocationName) aId $ toPathPiece cID
@ -295,7 +295,7 @@ validateCourse = do
CourseForm{..} <- State.get CourseForm{..} <- State.get
now <- liftIO getCurrentTime now <- liftIO getCurrentTime
uid <- liftHandlerT requireAuthId uid <- liftHandler requireAuthId
userAdmin <- hasWriteAccessTo $ SchoolR cfSchool SchoolEditR userAdmin <- hasWriteAccessTo $ SchoolR cfSchool SchoolEditR
allocationTerm <- for (acfAllocation <$> cfAllocation) $ lift . fmap allocationTerm . getJust allocationTerm <- for (acfAllocation <$> cfAllocation) $ lift . fmap allocationTerm . getJust
@ -517,7 +517,7 @@ courseEditHandler miButtonAction mbCourseForm = do
tell $ Set.singleton fId tell $ Set.singleton fId
lift $ lift $
void . insertUnique $ CourseAppInstructionFile cid fId void . insertUnique $ CourseAppInstructionFile cid fId
keep <- execWriterT . runConduit $ transPipe liftHandlerT (traverse_ id $ cfAppInstructionFiles res) .| C.mapM_ finsert keep <- execWriterT . runConduit $ transPipe liftHandler (traverse_ id $ cfAppInstructionFiles res) .| C.mapM_ finsert
acfs <- selectList [ CourseAppInstructionFileCourse ==. cid, CourseAppInstructionFileFile /<-. Set.toList keep ] [] acfs <- selectList [ CourseAppInstructionFileCourse ==. cid, CourseAppInstructionFileFile /<-. Set.toList keep ] []
mapM_ deleteCascade $ map (courseAppInstructionFileFile . entityVal) acfs mapM_ deleteCascade $ map (courseAppInstructionFileFile . entityVal) acfs
@ -534,7 +534,7 @@ courseEditHandler miButtonAction mbCourseForm = do
, formEncoding = formEnctype , formEncoding = formEnctype
} }
upsertAllocationCourse :: (MonadLogger m, MonadHandler m, HandlerSite m ~ UniWorX) => CourseId -> Maybe AllocationCourseForm -> ReaderT SqlBackend m () upsertAllocationCourse :: (MonadThrow m, MonadHandler m, HandlerSite m ~ UniWorX) => CourseId -> Maybe AllocationCourseForm -> ReaderT SqlBackend m ()
upsertAllocationCourse cid cfAllocation = do upsertAllocationCourse cid cfAllocation = do
now <- liftIO getCurrentTime now <- liftIO getCurrentTime
Course{..} <- getJust cid Course{..} <- getJust cid

View File

@ -57,16 +57,19 @@ lecturerInvitationConfig = InvitationConfig{..}
where where
invitationRoute (Entity _ Course{..}) _ = return . CourseR courseTerm courseSchool courseShorthand $ CLecInviteR invitationRoute (Entity _ Course{..}) _ = return . CourseR courseTerm courseSchool courseShorthand $ CLecInviteR
invitationResolveFor _ = do invitationResolveFor _ = do
Just (CourseR tid csh ssh CLecInviteR) <- getCurrentRoute cRoute <- getCurrentRoute
getKeyBy404 $ TermSchoolCourseShort tid csh ssh case cRoute of
Just (CourseR tid csh ssh CLecInviteR) ->
getKeyBy404 $ TermSchoolCourseShort tid csh ssh
_other -> error "lecturerInvitationConfig called from unsupported route"
invitationSubject (Entity _ Course{..}) _ = return . SomeMessage $ MsgMailSubjectLecturerInvitation courseTerm courseSchool courseShorthand invitationSubject (Entity _ Course{..}) _ = return . SomeMessage $ MsgMailSubjectLecturerInvitation courseTerm courseSchool courseShorthand
invitationHeading (Entity _ Course{..}) _ = return . SomeMessage $ MsgCourseLecInviteHeading $ CI.original courseName invitationHeading (Entity _ Course{..}) _ = return . SomeMessage $ MsgCourseLecInviteHeading $ CI.original courseName
invitationExplanation _ _ = return [ihamlet|_{SomeMessage MsgCourseLecInviteExplanation}|] invitationExplanation _ _ = return [ihamlet|_{SomeMessage MsgCourseLecInviteExplanation}|]
invitationTokenConfig _ _ = do invitationTokenConfig _ _ = do
itAuthority <- liftHandlerT requireAuthId itAuthority <- liftHandler requireAuthId
return $ InvitationTokenConfig itAuthority Nothing Nothing Nothing return $ InvitationTokenConfig itAuthority Nothing Nothing Nothing
invitationRestriction _ _ = return Authorized invitationRestriction _ _ = return Authorized
invitationForm _ (InvDBDataLecturer mlType, _) _ = hoistAForm liftHandlerT $ toJunction <$> case mlType of invitationForm _ (InvDBDataLecturer mlType, _) _ = hoistAForm liftHandler $ toJunction <$> case mlType of
Nothing -> areq (selectField optionsFinite) lFs Nothing Nothing -> areq (selectField optionsFinite) lFs Nothing
Just lType -> aforced (selectField optionsFinite) lFs lType Just lType -> aforced (selectField optionsFinite) lFs lType
where where

View File

@ -86,7 +86,7 @@ makeCourseTable whereClause colChoices psValidator = do
E.on $ user E.^. UserId E.==. lecturer E.^. LecturerUser E.on $ user E.^. UserId E.==. lecturer E.^. LecturerUser
E.where_ $ cid E.==. lecturer E.^. LecturerCourse E.&&. lecturer E.^. LecturerType E.==. E.val CourseLecturer E.where_ $ cid E.==. lecturer E.^. LecturerCourse E.&&. lecturer E.^. LecturerType E.==. E.val CourseLecturer
return user return user
dbtProj :: DBRow _ -> MaybeT (ReaderT SqlBackend (HandlerT UniWorX IO)) CourseTableData dbtProj :: DBRow _ -> MaybeT DB CourseTableData
dbtProj = traverse $ \(course, E.Value participants, E.Value registered, school) -> do dbtProj = traverse $ \(course, E.Value participants, E.Value registered, school) -> do
lecturerList <- lift $ E.select $ E.from $ lecturerQuery $ E.val $ entityKey course lecturerList <- lift $ E.select $ E.from $ lecturerQuery $ E.val $ entityKey course
courseAlloc <- lift $ getBy (UniqueAllocationCourse $ entityKey course) courseAlloc <- lift $ getBy (UniqueAllocationCourse $ entityKey course)

View File

@ -70,13 +70,17 @@ participantInvitationConfig = InvitationConfig{..}
where where
invitationRoute (Entity _ Course{..}) _ = return $ CourseR courseTerm courseSchool courseShorthand CInviteR invitationRoute (Entity _ Course{..}) _ = return $ CourseR courseTerm courseSchool courseShorthand CInviteR
invitationResolveFor _ = do invitationResolveFor _ = do
Just (CourseR tid csh ssh CInviteR) <- getCurrentRoute cRoute <- getCurrentRoute
getKeyBy404 $ TermSchoolCourseShort tid csh ssh case cRoute of
Just (CourseR tid csh ssh CInviteR) ->
getKeyBy404 $ TermSchoolCourseShort tid csh ssh
_other ->
error "participantInvitationConfig called from unsupported route"
invitationSubject (Entity _ Course{..}) _ = return . SomeMessage $ MsgMailSubjectParticipantInvitation courseTerm courseSchool courseShorthand invitationSubject (Entity _ Course{..}) _ = return . SomeMessage $ MsgMailSubjectParticipantInvitation courseTerm courseSchool courseShorthand
invitationHeading (Entity _ Course{..}) _ = return . SomeMessage $ MsgCourseParticipantInviteHeading $ CI.original courseName invitationHeading (Entity _ Course{..}) _ = return . SomeMessage $ MsgCourseParticipantInviteHeading $ CI.original courseName
invitationExplanation _ _ = return [ihamlet|_{SomeMessage MsgCourseParticipantInviteExplanation}|] invitationExplanation _ _ = return [ihamlet|_{SomeMessage MsgCourseParticipantInviteExplanation}|]
invitationTokenConfig _ _ = do invitationTokenConfig _ _ = do
itAuthority <- liftHandlerT requireAuthId itAuthority <- liftHandler requireAuthId
return $ InvitationTokenConfig itAuthority Nothing Nothing Nothing return $ InvitationTokenConfig itAuthority Nothing Nothing Nothing
invitationRestriction _ _ = return Authorized invitationRestriction _ _ = return Authorized
invitationForm (Entity _ Course{..}) _ uid = hoistAForm lift . wFormToAForm $ do invitationForm (Entity _ Course{..}) _ uid = hoistAForm lift . wFormToAForm $ do
@ -98,9 +102,12 @@ data AddRecipientsResult = AddRecipientsResult
, aurSuccess :: [UserEmail] , aurSuccess :: [UserEmail]
} deriving (Read, Show, Generic, Typeable) } deriving (Read, Show, Generic, Typeable)
instance Semigroup AddRecipientsResult where
(<>) = mappenddefault
instance Monoid AddRecipientsResult where instance Monoid AddRecipientsResult where
mempty = memptydefault mempty = memptydefault
mappend = mappenddefault mappend = (<>)
getCAddUserR, postCAddUserR :: TermId -> SchoolId -> CourseShorthand -> Handler Html getCAddUserR, postCAddUserR :: TermId -> SchoolId -> CourseShorthand -> Handler Html
getCAddUserR = postCAddUserR getCAddUserR = postCAddUserR

View File

@ -41,12 +41,12 @@ instance Button UniWorX ButtonCourseRegister where
data CourseRegisterForm = CourseRegisterForm data CourseRegisterForm = CourseRegisterForm
{ crfStudyFeatures :: Maybe StudyFeaturesId { crfStudyFeatures :: Maybe StudyFeaturesId
, crfApplicationText :: Maybe Text , crfApplicationText :: Maybe Text
, crfApplicationFiles :: Maybe (Source Handler File) , crfApplicationFiles :: Maybe (ConduitT () File Handler ())
} }
courseRegisterForm :: (MonadHandler m, HandlerSite m ~ UniWorX) => Entity Course -> m (AForm Handler CourseRegisterForm, ButtonCourseRegister) courseRegisterForm :: (MonadHandler m, HandlerSite m ~ UniWorX) => Entity Course -> m (AForm Handler CourseRegisterForm, ButtonCourseRegister)
-- ^ `CourseRegisterForm` for current user -- ^ `CourseRegisterForm` for current user
courseRegisterForm (Entity cid Course{..}) = liftHandlerT $ do courseRegisterForm (Entity cid Course{..}) = liftHandler $ do
muid <- maybeAuthId muid <- maybeAuthId
(registration, application) <- runDB $ do (registration, application) <- runDB $ do
registration <- fmap join . for muid $ getBy . flip UniqueParticipant cid registration <- fmap join . for muid $ getBy . flip UniqueParticipant cid
@ -108,7 +108,7 @@ courseRegisterForm (Entity cid Course{..}) = liftHandlerT $ do
-> fmap (assertM (not . Text.null) . fmap Text.strip) <$> wopt textField' fs (Just $ application >>= courseApplicationText . entityVal) -> fmap (assertM (not . Text.null) . fmap Text.strip) <$> wopt textField' fs (Just $ application >>= courseApplicationText . entityVal)
hasFiles <- for application $ \(Entity appId _) hasFiles <- for application $ \(Entity appId _)
-> fmap (not . null) . liftHandlerT . runDB $ selectKeysList [ CourseApplicationFileApplication ==. appId ] [ LimitTo 1 ] -> fmap (not . null) . liftHandler . runDB $ selectKeysList [ CourseApplicationFileApplication ==. appId ] [ LimitTo 1 ]
appCID <- for application $ encrypt . entityKey appCID <- for application $ encrypt . entityKey
let appFilesInfo = (,) <$> hasFiles <*> appCID let appFilesInfo = (,) <$> hasFiles <*> appCID
filesMsg = bool MsgCourseRegistrationFiles MsgCourseApplicationFiles courseApplicationsRequired filesMsg = bool MsgCourseRegistrationFiles MsgCourseApplicationFiles courseApplicationsRequired
@ -191,7 +191,7 @@ postCRegisterR tid ssh csh = do
whenIsJust appRes $ whenIsJust appRes $
audit . TransactionCourseApplicationEdit cid uid audit . TransactionCourseApplicationEdit cid uid
whenIsJust ((,) <$> appRes <*> crfApplicationFiles) $ \(appId, fSource) -> do whenIsJust ((,) <$> appRes <*> crfApplicationFiles) $ \(appId, fSource) -> do
runConduit $ transPipe liftHandlerT fSource .| C.mapM_ (\f -> insert f >>= insert_ . CourseApplicationFile appId) runConduit $ transPipe liftHandler fSource .| C.mapM_ (\f -> insert f >>= insert_ . CourseApplicationFile appId)
return appRes return appRes
| otherwise | otherwise
= return $ Just () = return $ Just ()

View File

@ -125,11 +125,12 @@ getCShowR tid ssh csh = do
, sortable Nothing (i18nCell MsgTutorialFreeCapacity) $ \DBRow{ dbrOutput = Entity tutid Tutorial{..} } -> case tutorialCapacity of , sortable Nothing (i18nCell MsgTutorialFreeCapacity) $ \DBRow{ dbrOutput = Entity tutid Tutorial{..} } -> case tutorialCapacity of
Nothing -> mempty Nothing -> mempty
Just tutorialCapacity' -> sqlCell $ do Just tutorialCapacity' -> sqlCell $ do
[E.Value freeCapacity] <- E.select $ let numParticipants = E.sub_select . E.from $ \participant -> do freeCapacity <- fmap (maybe 0 (max 0 . E.unValue) . listToMaybe)
E.where_ $ participant E.^. TutorialParticipantTutorial E.==. E.val tutid . E.select $ let numParticipants = E.sub_select . E.from $ \participant -> do
return E.countRows :: E.SqlQuery (E.SqlExpr (E.Value Int)) E.where_ $ participant E.^. TutorialParticipantTutorial E.==. E.val tutid
in return $ E.val tutorialCapacity' E.-. numParticipants return E.countRows :: E.SqlQuery (E.SqlExpr (E.Value Int))
return . toWidget . tshow $ max 0 freeCapacity in return $ E.val tutorialCapacity' E.-. numParticipants
return . toWidget $ tshow freeCapacity
, sortable Nothing mempty $ \DBRow{ dbrOutput = Entity tutId Tutorial{..} } -> sqlCell $ do , sortable Nothing mempty $ \DBRow{ dbrOutput = Entity tutId Tutorial{..} } -> sqlCell $ do
mayRegister <- (== Authorized) <$> evalAccessDB (CTutorialR tid ssh csh tutorialName TRegisterR) True mayRegister <- (== Authorized) <$> evalAccessDB (CTutorialR tid ssh csh tutorialName TRegisterR) True
isRegistered <- case mbAid of isRegistered <- case mbAid of
@ -137,7 +138,7 @@ getCShowR tid ssh csh = do
Just uid -> existsBy $ UniqueTutorialParticipant tutId uid Just uid -> existsBy $ UniqueTutorialParticipant tutId uid
if if
| mayRegister -> do | mayRegister -> do
(tutRegisterForm, tutRegisterEnctype) <- liftHandlerT . generateFormPost . buttonForm' $ bool [BtnRegister] [BtnDeregister] isRegistered (tutRegisterForm, tutRegisterEnctype) <- liftHandler . generateFormPost . buttonForm' $ bool [BtnRegister] [BtnDeregister] isRegistered
return $ wrapForm tutRegisterForm def return $ wrapForm tutRegisterForm def
{ formAction = Just . SomeRoute $ CTutorialR tid ssh csh tutorialName TRegisterR { formAction = Just . SomeRoute $ CTutorialR tid ssh csh tutorialName TRegisterR
, formEncoding = tutRegisterEnctype , formEncoding = tutRegisterEnctype
@ -198,7 +199,7 @@ getCShowR tid ssh csh = do
-- Just uid -> existsBy $ UniqueExamRegistration eId uid -- Just uid -> existsBy $ UniqueExamRegistration eId uid
-- if -- if
-- | mayRegister -> do -- | mayRegister -> do
-- (examRegisterForm, examRegisterEnctype) <- liftHandlerT . generateFormPost . buttonForm' $ bool [BtnExamRegister] [BtnExamDeregister] isRegistered -- (examRegisterForm, examRegisterEnctype) <- liftHandler . generateFormPost . buttonForm' $ bool [BtnExamRegister] [BtnExamDeregister] isRegistered
-- return $ wrapForm examRegisterForm def -- return $ wrapForm examRegisterForm def
-- { formAction = Just . SomeRoute $ CExamR tid ssh csh examName ERegisterR -- { formAction = Just . SomeRoute $ CExamR tid ssh csh examName ERegisterR
-- , formEncoding = examRegisterEnctype -- , formEncoding = examRegisterEnctype

View File

@ -159,7 +159,7 @@ postCUserR tid ssh csh uCId = do
addMessageIconI Success IconEnrolTrue MsgCourseRegisterOk addMessageIconI Success IconEnrolTrue MsgCourseRegisterOk
redirect currentRoute redirect currentRoute
Nothing -> invalidArgs ["User already registered"] Nothing -> invalidArgs ["User already registered"]
_other -> fail "Invalid @regButton@" _other -> error "Invalid @regButton@"
mRegAt <- for (courseParticipantRegistration . entityVal <$> mRegistration) $ formatTime SelFormatDateTime mRegAt <- for (courseParticipantRegistration . entityVal <$> mRegistration) $ formatTime SelFormatDateTime

View File

@ -139,7 +139,7 @@ makeCourseUserTable :: forall h acts.
-> PSValidator (MForm Handler) (FormResult (First (Element acts), DBFormResult UserId Bool UserTableData)) -> PSValidator (MForm Handler) (FormResult (First (Element acts), DBFormResult UserId Bool UserTableData))
-> DB (FormResult (Element acts, Set UserId), Widget) -> DB (FormResult (Element acts, Set UserId), Widget)
makeCourseUserTable cid acts restrict colChoices psValidator = do makeCourseUserTable cid acts restrict colChoices psValidator = do
Just currentRoute <- liftHandlerT getCurrentRoute currentRoute <- fromMaybe (error "makeCourseUserTable called from 404-handler") <$> liftHandler getCurrentRoute
-- -- psValidator has default sorting and filtering -- -- psValidator has default sorting and filtering
let dbtIdent = "courseUsers" :: Text let dbtIdent = "courseUsers" :: Text
dbtStyle = def { dbsFilterLayout = defaultDBSFilterLayout } dbtStyle = def { dbsFilterLayout = defaultDBSFilterLayout }
@ -210,7 +210,7 @@ makeCourseUserTable cid acts restrict colChoices psValidator = do
= renderAForm FormStandard = renderAForm FormStandard
$ (, mempty) . First . Just $ (, mempty) . First . Just
<$> areq (selectField $ optionsF acts) (fslI MsgAction) Nothing <$> areq (selectField $ optionsF acts) (fslI MsgAction) Nothing
, dbParamsFormEvaluate = liftHandlerT . runFormPost , dbParamsFormEvaluate = liftHandler . runFormPost
, dbParamsFormResult = id , dbParamsFormResult = id
, dbParamsFormIdent = def , dbParamsFormIdent = def
} }

View File

@ -29,9 +29,12 @@ data AddRecipientsResult = AddRecipientsResult
, aurSuccessCourse :: [UserEmail] , aurSuccessCourse :: [UserEmail]
} deriving (Read, Show, Generic, Typeable) } deriving (Read, Show, Generic, Typeable)
instance Semigroup AddRecipientsResult where
(<>) = mappenddefault
instance Monoid AddRecipientsResult where instance Monoid AddRecipientsResult where
mempty = memptydefault mempty = memptydefault
mappend = mappenddefault mappend = (<>)
getEAddUserR, postEAddUserR :: TermId -> SchoolId -> CourseShorthand -> ExamName -> Handler Html getEAddUserR, postEAddUserR :: TermId -> SchoolId -> CourseShorthand -> ExamName -> Handler Html
@ -40,7 +43,7 @@ postEAddUserR tid ssh csh examn = do
eEnt@(Entity eid Exam{..}) <- runDB $ fetchExam tid ssh csh examn eEnt@(Entity eid Exam{..}) <- runDB $ fetchExam tid ssh csh examn
((usersToEnlist,formWgt),formEncoding) <- runFormPost . renderWForm FormStandard $ do ((usersToEnlist,formWgt),formEncoding) <- runFormPost . renderWForm FormStandard $ do
now <- liftIO getCurrentTime now <- liftIO getCurrentTime
occurrences <- liftHandlerT . runDB $ selectList [ExamOccurrenceExam ==. eid] [] occurrences <- liftHandler . runDB $ selectList [ExamOccurrenceExam ==. eid] []
let let
localNow = utcToLocalTime now localNow = utcToLocalTime now

View File

@ -55,15 +55,19 @@ examCorrectorInvitationConfig = InvitationConfig{..}
Course{..} <- get404 examCourse Course{..} <- get404 examCourse
return $ CExamR courseTerm courseSchool courseShorthand examName ECInviteR return $ CExamR courseTerm courseSchool courseShorthand examName ECInviteR
invitationResolveFor _ = do invitationResolveFor _ = do
Just (CExamR tid csh ssh examn ECInviteR) <- getCurrentRoute cRoute <- getCurrentRoute
fetchExamId tid csh ssh examn case cRoute of
Just (CExamR tid csh ssh examn ECInviteR) ->
fetchExamId tid csh ssh examn
_other ->
error "examCorrectorInvitationConfig called from unsupported route"
invitationSubject (Entity _ Exam{..}) _ = do invitationSubject (Entity _ Exam{..}) _ = do
Course{..} <- get404 examCourse Course{..} <- get404 examCourse
return . SomeMessage $ MsgMailSubjectExamCorrectorInvitation courseTerm courseSchool courseShorthand examName return . SomeMessage $ MsgMailSubjectExamCorrectorInvitation courseTerm courseSchool courseShorthand examName
invitationHeading (Entity _ Exam{..}) _ = return . SomeMessage $ MsgExamCorrectorInviteHeading examName invitationHeading (Entity _ Exam{..}) _ = return . SomeMessage $ MsgExamCorrectorInviteHeading examName
invitationExplanation _ _ = return [ihamlet|_{SomeMessage MsgExamCorrectorInviteExplanation}|] invitationExplanation _ _ = return [ihamlet|_{SomeMessage MsgExamCorrectorInviteExplanation}|]
invitationTokenConfig _ _ = do invitationTokenConfig _ _ = do
itAuthority <- liftHandlerT requireAuthId itAuthority <- liftHandler requireAuthId
return $ InvitationTokenConfig itAuthority Nothing Nothing Nothing return $ InvitationTokenConfig itAuthority Nothing Nothing Nothing
invitationRestriction _ _ = return Authorized invitationRestriction _ _ = return Authorized
invitationForm _ _ _ = pure (JunctionExamCorrector, ()) invitationForm _ _ _ = pure (JunctionExamCorrector, ())

View File

@ -85,6 +85,7 @@ postEEditR tid ssh csh examn = do
ExamPartForm{ epfId = Nothing, .. } -> insert_ ExamPartForm{ epfId = Nothing, .. } -> insert_
ExamPart ExamPart
{ examPartExam = eId { examPartExam = eId
, examPartNumber = epfNumber
, examPartName = epfName , examPartName = epfName
, examPartMaxPoints = epfMaxPoints , examPartMaxPoints = epfMaxPoints
, examPartWeight = epfWeight , examPartWeight = epfWeight
@ -96,6 +97,7 @@ postEEditR tid ssh csh examn = do
guard $ examPartExam oldPart == eId guard $ examPartExam oldPart == eId
lift $ replace epfId' ExamPart lift $ replace epfId' ExamPart
{ examPartExam = eId { examPartExam = eId
, examPartNumber = epfNumber
, examPartName = epfName , examPartName = epfName
, examPartMaxPoints = epfMaxPoints , examPartMaxPoints = epfMaxPoints
, examPartWeight = epfWeight , examPartWeight = epfWeight

View File

@ -26,6 +26,7 @@ import Text.Blaze.Html.Renderer.String (renderHtml)
data ExamForm = ExamForm data ExamForm = ExamForm
{ efName :: ExamName { efName :: ExamName
, efDescription :: Maybe Html , efDescription :: Maybe Html
, efShowGrades :: Bool
, efStart :: Maybe UTCTime , efStart :: Maybe UTCTime
, efEnd :: Maybe UTCTime , efEnd :: Maybe UTCTime
, efVisibleFrom :: Maybe UTCTime , efVisibleFrom :: Maybe UTCTime
@ -35,11 +36,10 @@ data ExamForm = ExamForm
, efPublishOccurrenceAssignments :: Maybe UTCTime , efPublishOccurrenceAssignments :: Maybe UTCTime
, efFinished :: Maybe UTCTime , efFinished :: Maybe UTCTime
, efOccurrences :: Set ExamOccurrenceForm , efOccurrences :: Set ExamOccurrenceForm
, efShowGrades :: Bool
, efPublicStatistics :: Bool , efPublicStatistics :: Bool
, efGradingRule :: ExamGradingRule , efGradingRule :: Maybe ExamGradingRule
, efBonusRule :: ExamBonusRule , efBonusRule :: Maybe ExamBonusRule
, efOccurrenceRule :: ExamOccurrenceRule , efOccurrenceRule :: Maybe ExamOccurrenceRule
, efCorrectors :: Set (Either UserEmail UserId) , efCorrectors :: Set (Either UserEmail UserId)
, efExamParts :: Set ExamPartForm , efExamParts :: Set ExamPartForm
} }
@ -56,7 +56,8 @@ data ExamOccurrenceForm = ExamOccurrenceForm
data ExamPartForm = ExamPartForm data ExamPartForm = ExamPartForm
{ epfId :: Maybe CryptoUUIDExamPart { epfId :: Maybe CryptoUUIDExamPart
, epfName :: ExamPartName , epfNumber :: ExamPartNumber
, epfName :: Maybe ExamPartName
, epfMaxPoints :: Maybe Points , epfMaxPoints :: Maybe Points
, epfWeight :: Rational , epfWeight :: Rational
} deriving (Read, Show, Eq, Ord, Generic, Typeable) } deriving (Read, Show, Eq, Ord, Generic, Typeable)
@ -79,6 +80,7 @@ examForm template html = do
flip (renderAForm FormStandard) html $ ExamForm flip (renderAForm FormStandard) html $ ExamForm
<$> areq ciField (fslpI MsgExamName (mr MsgExamName) & setTooltip MsgExamNameTip) (efName <$> template) <$> areq ciField (fslpI MsgExamName (mr MsgExamName) & setTooltip MsgExamNameTip) (efName <$> template)
<*> (assertM (not . null . renderHtml) <$> aopt htmlField (fslpI MsgExamDescription "Html") (efDescription <$> template)) <*> (assertM (not . null . renderHtml) <$> aopt htmlField (fslpI MsgExamDescription "Html") (efDescription <$> template))
<*> apopt checkBoxField (fslI MsgExamShowGrades & setTooltip MsgExamShowGradesTip) (efShowGrades <$> template <|> Just True)
<* aformSection MsgExamFormTimes <* aformSection MsgExamFormTimes
<*> aopt utcTimeField (fslpI MsgExamStart (mr MsgDate) & setTooltip MsgExamTimeTip) (efStart <$> template) <*> aopt utcTimeField (fslpI MsgExamStart (mr MsgDate) & setTooltip MsgExamTimeTip) (efStart <$> template)
<*> aopt utcTimeField (fslpI MsgExamEnd (mr MsgDate) & setTooltip MsgExamTimeTip) (efEnd <$> template) <*> aopt utcTimeField (fslpI MsgExamEnd (mr MsgDate) & setTooltip MsgExamTimeTip) (efEnd <$> template)
@ -91,11 +93,10 @@ examForm template html = do
<* aformSection MsgExamFormOccurrences <* aformSection MsgExamFormOccurrences
<*> examOccurrenceForm (efOccurrences <$> template) <*> examOccurrenceForm (efOccurrences <$> template)
<* aformSection MsgExamFormAutomaticFunctions <* aformSection MsgExamFormAutomaticFunctions
<*> apopt checkBoxField (fslI MsgExamShowGrades & setTooltip MsgExamShowGradesTip) (efShowGrades <$> template <|> Just True)
<*> apopt checkBoxField (fslI MsgExamPublicStatistics & setTooltip MsgExamPublicStatisticsTip) (efPublicStatistics <$> template <|> Just True) <*> apopt checkBoxField (fslI MsgExamPublicStatistics & setTooltip MsgExamPublicStatisticsTip) (efPublicStatistics <$> template <|> Just True)
<*> examGradingRuleForm (efGradingRule <$> template) <*> optionalActionA (examGradingRuleForm $ efGradingRule =<< template) (fslI MsgExamAutomaticGrading & setTooltip MsgExamAutomaticGradingTip) (is _Just . efGradingRule <$> template)
<*> examBonusRuleForm (efBonusRule <$> template) <*> optionalActionA (examBonusRuleForm $ efBonusRule =<< template) (fslI MsgExamBonus) (is _Just . efBonusRule <$> template)
<*> examOccurrenceRuleForm (efOccurrenceRule <$> template) <*> optionalActionA (examOccurrenceRuleForm $ efOccurrenceRule =<< template) (fslI MsgExamAutomaticOccurrenceAssignment & setTooltip MsgExamAutomaticOccurrenceAssignmentTip) (is _Just . efOccurrenceRule <$> template)
<* aformSection MsgExamFormCorrection <* aformSection MsgExamFormCorrection
<*> examCorrectorsForm (efCorrectors <$> template) <*> examCorrectorsForm (efCorrectors <$> template)
<* aformSection MsgExamFormParts <* aformSection MsgExamFormParts
@ -104,8 +105,8 @@ examForm template html = do
examCorrectorsForm :: Maybe (Set (Either UserEmail UserId)) -> AForm Handler (Set (Either UserEmail UserId)) examCorrectorsForm :: Maybe (Set (Either UserEmail UserId)) -> AForm Handler (Set (Either UserEmail UserId))
examCorrectorsForm mPrev = wFormToAForm $ do examCorrectorsForm mPrev = wFormToAForm $ do
MsgRenderer mr <- getMsgRenderer MsgRenderer mr <- getMsgRenderer
Just currentRoute <- getCurrentRoute currentRoute <- fromMaybe (error "examCorrectorForm called from 404-handler") <$> getCurrentRoute
uid <- liftHandlerT requireAuthId uid <- liftHandler requireAuthId
let let
miButtonAction' :: forall p. PathPiece p => p -> Maybe (SomeRoute UniWorX) miButtonAction' :: forall p. PathPiece p => p -> Maybe (SomeRoute UniWorX)
@ -139,7 +140,7 @@ examCorrectorsForm mPrev = wFormToAForm $ do
miCell' (Left email) = miCell' (Left email) =
$(widgetFile "widgets/massinput/examCorrectors/cellInvitation") $(widgetFile "widgets/massinput/examCorrectors/cellInvitation")
miCell' (Right userId) = do miCell' (Right userId) = do
User{..} <- liftHandlerT . runDB $ get404 userId User{..} <- liftHandler . runDB $ get404 userId
$(widgetFile "widgets/massinput/examCorrectors/cellKnown") $(widgetFile "widgets/massinput/examCorrectors/cellKnown")
miLayout' :: MassInputLayout ListLength (Either UserEmail UserId) () miLayout' :: MassInputLayout ListLength (Either UserEmail UserId) ()
@ -149,7 +150,7 @@ examCorrectorsForm mPrev = wFormToAForm $ do
examOccurrenceForm :: Maybe (Set ExamOccurrenceForm) -> AForm Handler (Set ExamOccurrenceForm) examOccurrenceForm :: Maybe (Set ExamOccurrenceForm) -> AForm Handler (Set ExamOccurrenceForm)
examOccurrenceForm prev = wFormToAForm $ do examOccurrenceForm prev = wFormToAForm $ do
Just currentRoute <- getCurrentRoute currentRoute <- fromMaybe (error "examOccurrenceForm called from 404-handler") <$> getCurrentRoute
let let
miButtonAction' :: forall p. PathPiece p => p -> Maybe (SomeRoute UniWorX) miButtonAction' :: forall p. PathPiece p => p -> Maybe (SomeRoute UniWorX)
miButtonAction' frag = Just . SomeRoute $ currentRoute :#: frag miButtonAction' frag = Just . SomeRoute $ currentRoute :#: frag
@ -191,7 +192,7 @@ examOccurrenceForm prev = wFormToAForm $ do
examPartsForm :: Maybe (Set ExamPartForm) -> AForm Handler (Set ExamPartForm) examPartsForm :: Maybe (Set ExamPartForm) -> AForm Handler (Set ExamPartForm)
examPartsForm prev = wFormToAForm $ do examPartsForm prev = wFormToAForm $ do
Just currentRoute <- getCurrentRoute currentRoute <- fromMaybe (error "examPartsForm called from 404-handler") <$> getCurrentRoute
let let
miButtonAction' :: forall p. PathPiece p => p -> Maybe (SomeRoute UniWorX) miButtonAction' :: forall p. PathPiece p => p -> Maybe (SomeRoute UniWorX)
miButtonAction' frag = Just . SomeRoute $ currentRoute :#: frag miButtonAction' frag = Just . SomeRoute $ currentRoute :#: frag
@ -200,12 +201,14 @@ examPartsForm prev = wFormToAForm $ do
where where
examPartForm' nudge mPrev csrf = do examPartForm' nudge mPrev csrf = do
(epfIdRes, epfIdView) <- mopt hiddenField ("" & addName (nudge "id")) (Just $ epfId =<< mPrev) (epfIdRes, epfIdView) <- mopt hiddenField ("" & addName (nudge "id")) (Just $ epfId =<< mPrev)
(epfNameRes, epfNameView) <- mpreq (textField & cfStrip & cfCI) ("" & addName (nudge "name")) (epfName <$> mPrev) (epfNumberRes, epfNumberView) <- mpreq (isoField (from _ExamPartNumber) $ textField & cfStrip & cfCI) ("" & addName (nudge "number") & addPlaceholder "1, 6a, 3.1.4, ...") (epfNumber <$> mPrev)
(epfNameRes, epfNameView) <- mopt (textField & cfStrip & cfCI) ("" & addName (nudge "name")) (epfName <$> mPrev)
(epfMaxPointsRes, epfMaxPointsView) <- mopt pointsField ("" & addName (nudge "max-points")) (epfMaxPoints <$> 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) (epfWeightRes, epfWeightView) <- mpreq (checkBool (>= 0) MsgExamPartWeightNegative rationalField) ("" & addName (nudge "weight")) (epfWeight <$> mPrev <|> Just 1)
return ( ExamPartForm return ( ExamPartForm
<$> epfIdRes <$> epfIdRes
<*> epfNumberRes
<*> epfNameRes <*> epfNameRes
<*> epfMaxPointsRes <*> epfMaxPointsRes
<*> epfWeightRes <*> epfWeightRes
@ -217,7 +220,8 @@ examPartsForm prev = wFormToAForm $ do
(res, formWidget) <- examPartForm' nudge Nothing csrf (res, formWidget) <- examPartForm' nudge Nothing csrf
let let
addRes = res <&> \newDat (Set.fromList -> oldDat) -> if addRes = res <&> \newDat (Set.fromList -> oldDat) -> if
| any (((==) `on` epfName) newDat) oldDat -> FormFailure [mr MsgExamPartAlreadyExists] | any (\old -> fromMaybe False $ (==) <$> epfName newDat <*> epfName old) oldDat
-> FormFailure [mr MsgExamPartAlreadyExists]
| otherwise -> FormSuccess $ pure newDat | otherwise -> FormSuccess $ pure newDat
return (addRes, $(widgetFile "widgets/massinput/examParts/add")) return (addRes, $(widgetFile "widgets/massinput/examParts/add"))
miCell' nudge dat = examPartForm' nudge (Just dat) miCell' nudge dat = examPartForm' nudge (Just dat)
@ -266,6 +270,7 @@ examFormTemplate (Entity eId Exam{..}) = do
(Just -> epfId, ExamPart{..}) <- examParts' (Just -> epfId, ExamPart{..}) <- examParts'
return ExamPartForm return ExamPartForm
{ epfId { epfId
, epfNumber = examPartNumber
, epfName = examPartName , epfName = examPartName
, epfMaxPoints = examPartMaxPoints , epfMaxPoints = examPartMaxPoints
, epfWeight = examPartWeight , epfWeight = examPartWeight

View File

@ -50,6 +50,7 @@ postCExamNewR tid ssh csh = do
[ ExamPart{..} [ ExamPart{..}
| ExamPartForm{..} <- Set.toList efExamParts | ExamPartForm{..} <- Set.toList efExamParts
, let examPartExam = examid , let examPartExam = examid
examPartNumber = epfNumber
examPartName = epfName examPartName = epfName
examPartMaxPoints = epfMaxPoints examPartMaxPoints = epfMaxPoints
examPartWeight = epfWeight examPartWeight = epfWeight

View File

@ -63,15 +63,19 @@ examRegistrationInvitationConfig = InvitationConfig{..}
Course{..} <- get404 examCourse Course{..} <- get404 examCourse
return $ CExamR courseTerm courseSchool courseShorthand examName EInviteR return $ CExamR courseTerm courseSchool courseShorthand examName EInviteR
invitationResolveFor _ = do invitationResolveFor _ = do
Just (CExamR tid csh ssh examn EInviteR) <- getCurrentRoute cRoute <- getCurrentRoute
fetchExamId tid csh ssh examn case cRoute of
Just (CExamR tid csh ssh examn EInviteR) ->
fetchExamId tid csh ssh examn
_other ->
error "examRegistrationInvitationConfig called from unsupported route"
invitationSubject (Entity _ Exam{..}) _ = do invitationSubject (Entity _ Exam{..}) _ = do
Course{..} <- get404 examCourse Course{..} <- get404 examCourse
return . SomeMessage $ MsgMailSubjectExamRegistrationInvitation courseTerm courseSchool courseShorthand examName return . SomeMessage $ MsgMailSubjectExamRegistrationInvitation courseTerm courseSchool courseShorthand examName
invitationHeading (Entity _ Exam{..}) _ = return . SomeMessage $ MsgExamRegistrationInviteHeading examName invitationHeading (Entity _ Exam{..}) _ = return . SomeMessage $ MsgExamRegistrationInviteHeading examName
invitationExplanation _ _ = return [ihamlet|_{SomeMessage MsgExamRegistrationInviteExplanation}|] invitationExplanation _ _ = return [ihamlet|_{SomeMessage MsgExamRegistrationInviteExplanation}|]
invitationTokenConfig _ (InvDBDataExamRegistration{..}, _) = do invitationTokenConfig _ (InvDBDataExamRegistration{..}, _) = do
itAuthority <- liftHandlerT requireAuthId itAuthority <- liftHandler requireAuthId
let itExpiresAt = Just $ Just invDBExamRegistrationDeadline let itExpiresAt = Just $ Just invDBExamRegistrationDeadline
itAddAuth itAddAuth
| not invDBExamRegistrationCourseRegister | not invDBExamRegistrationCourseRegister
@ -81,8 +85,8 @@ examRegistrationInvitationConfig = InvitationConfig{..}
itStartsAt = Nothing itStartsAt = Nothing
return InvitationTokenConfig{..} return InvitationTokenConfig{..}
invitationRestriction _ _ = return Authorized invitationRestriction _ _ = return Authorized
invitationForm (Entity _ Exam{..}) (InvDBDataExamRegistration{..}, _) uid = hoistAForm liftHandlerT . wFormToAForm $ do invitationForm (Entity _ Exam{..}) (InvDBDataExamRegistration{..}, _) uid = hoistAForm liftHandler . wFormToAForm $ do
isRegistered <- fmap (is _Just) . liftHandlerT . runDB . getBy $ UniqueParticipant uid examCourse isRegistered <- fmap (is _Just) . liftHandler . runDB . getBy $ UniqueParticipant uid examCourse
now <- liftIO getCurrentTime now <- liftIO getCurrentTime
case (isRegistered, invDBExamRegistrationCourseRegister) of case (isRegistered, invDBExamRegistrationCourseRegister) of

View File

@ -22,7 +22,7 @@ getEShowR tid ssh csh examn = do
cTime <- liftIO getCurrentTime cTime <- liftIO getCurrentTime
mUid <- maybeAuthId mUid <- maybeAuthId
(Entity _ Exam{..}, examParts, examVisible, (gradingVisible, gradingShown), (occurrenceAssignmentsVisible, occurrenceAssignmentsShown), results, result, occurrences, (registered, mayRegister), occurrenceNamesShown) <- runDB $ do (Entity _ Exam{..}, examParts, examVisible, (gradingVisible, gradingShown), (occurrenceAssignmentsVisible, occurrenceAssignmentsShown), results, result, bonus, occurrences, (registered, mayRegister), lecturerInfoShown) <- runDB $ do
exam@(Entity eId Exam{..}) <- fetchExam tid ssh csh examn exam@(Entity eId Exam{..}) <- fetchExam tid ssh csh examn
let examVisible = NTop (Just cTime) >= NTop examVisibleFrom let examVisible = NTop (Just cTime) >= NTop examVisibleFrom
@ -33,7 +33,7 @@ getEShowR tid ssh csh examn = do
let occurrenceAssignmentsVisible = NTop (Just cTime) >= NTop examPublishOccurrenceAssignments let occurrenceAssignmentsVisible = NTop (Just cTime) >= NTop examPublishOccurrenceAssignments
occurrenceAssignmentsShown <- or2M (return occurrenceAssignmentsVisible) . hasReadAccessTo $ CExamR tid ssh csh examn EEditR occurrenceAssignmentsShown <- or2M (return occurrenceAssignmentsVisible) . hasReadAccessTo $ CExamR tid ssh csh examn EEditR
examParts <- selectList [ ExamPartExam ==. eId ] [ Asc ExamPartName ] examParts <- sortOn (view $ _entityVal . _examPartNumber) <$> selectList [ ExamPartExam ==. eId ] [ Asc ExamPartName ]
resultsRaw <- for mUid $ \uid -> resultsRaw <- for mUid $ \uid ->
E.select . E.from $ \examPartResult -> do E.select . E.from $ \examPartResult -> do
@ -43,6 +43,7 @@ getEShowR tid ssh csh examn = do
let results = maybe Map.empty (\rs -> Map.fromList [ (examPartResultExamPart, res) | res@(Entity _ ExamPartResult{..}) <- rs ]) resultsRaw let results = maybe Map.empty (\rs -> Map.fromList [ (examPartResultExamPart, res) | res@(Entity _ ExamPartResult{..}) <- rs ]) resultsRaw
result <- fmap join . for mUid $ getBy . UniqueExamResult eId result <- fmap join . for mUid $ getBy . UniqueExamResult eId
bonus <- fmap join . for mUid $ getBy . UniqueExamBonus eId
occurrencesRaw <- E.select . E.from $ \examOccurrence -> do occurrencesRaw <- E.select . E.from $ \examOccurrence -> do
E.where_ $ examOccurrence E.^. ExamOccurrenceExam E.==. E.val eId E.where_ $ examOccurrence E.^. ExamOccurrenceExam E.==. E.val eId
@ -62,15 +63,27 @@ getEShowR tid ssh csh examn = do
registered <- for mUid $ existsBy . UniqueExamRegistration eId registered <- for mUid $ existsBy . UniqueExamRegistration eId
mayRegister <- (== Authorized) <$> evalAccessDB (CExamR tid ssh csh examName ERegisterR) True mayRegister <- (== Authorized) <$> evalAccessDB (CExamR tid ssh csh examName ERegisterR) True
occurrenceNamesShown <- hasReadAccessTo $ CExamR tid ssh csh examn EEditR lecturerInfoShown <- hasReadAccessTo $ CExamR tid ssh csh examn EEditR
return (exam, examParts, examVisible, (gradingVisible, gradingShown), (occurrenceAssignmentsVisible, occurrenceAssignmentsShown), results, result, occurrences, (registered, mayRegister), occurrenceNamesShown) return (exam, examParts, examVisible, (gradingVisible, gradingShown), (occurrenceAssignmentsVisible, occurrenceAssignmentsShown), results, result, bonus, occurrences, (registered, mayRegister), lecturerInfoShown)
let occurrenceNamesShown = lecturerInfoShown
partNumbersShown = lecturerInfoShown
examClosedShown = lecturerInfoShown
sumMaxPoints = sum [ fromRational examPartWeight * mPoints | Entity _ ExamPart{..} <- examParts, let Just mPoints = examPartMaxPoints ]
sumPoints = getSum <$> foldMap (fmap Sum . examPartResultResult . entityVal) results
noBonus = fromMaybe False $ do
guardM $ bonusOnlyPassed <$> examBonusRule
return . fromMaybe True $ result ^? _Just . _entityVal . _examResultResult . _examResult . passingGrade . _Wrapped . to not
let examTimes = all (\(Entity _ ExamOccurrence{..}, _) -> Just examOccurrenceStart == examStart && examOccurrenceEnd == examEnd) occurrences let examTimes = all (\(Entity _ ExamOccurrence{..}, _) -> Just examOccurrenceStart == examStart && examOccurrenceEnd == examEnd) occurrences
registerWidget registerWidget
| Just isRegistered <- registered | Just isRegistered <- registered
, mayRegister = Just $ do , mayRegister = Just $ do
(examRegisterForm, examRegisterEnctype) <- liftHandlerT . generateFormPost . buttonForm' $ bool [BtnExamRegister] [BtnExamDeregister] isRegistered (examRegisterForm, examRegisterEnctype) <- liftHandler . generateFormPost . buttonForm' $ bool [BtnExamRegister] [BtnExamDeregister] isRegistered
[whamlet| [whamlet|
<p> <p>
$if isRegistered $if isRegistered
@ -86,6 +99,9 @@ getEShowR tid ssh csh examn = do
| fromMaybe False registered = Just [whamlet|_{MsgExamRegistered}|] | fromMaybe False registered = Just [whamlet|_{MsgExamRegistered}|]
| otherwise = Nothing | otherwise = Nothing
showMaxPoints = any (has $ _entityVal . _examPartMaxPoints . _Just) examParts
showAchievedPoints = not $ null results
let heading = prependCourseTitle tid ssh csh $ CI.original examName let heading = prependCourseTitle tid ssh csh $ CI.original examName
siteLayoutMsg heading $ do siteLayoutMsg heading $ do

View File

@ -4,7 +4,7 @@ module Handler.Exam.Users
( getEUsersR, postEUsersR ( getEUsersR, postEUsersR
) where ) where
import Import import Import hiding ((<.), (.>))
import Handler.Utils import Handler.Utils
import Handler.Utils.Exam import Handler.Utils.Exam
@ -18,11 +18,13 @@ import Database.Esqueleto.Utils.TH
import qualified Data.Csv as Csv import qualified Data.Csv as Csv
import Data.Map ((!)) import Data.Map ((!), (!?))
import qualified Data.Map as Map import qualified Data.Map as Map
import qualified Data.Set as Set import qualified Data.Set as Set
import qualified Data.HashMap.Lazy as HashMap
import qualified Data.Text as Text import qualified Data.Text as Text
import qualified Data.Text.Encoding as Text
import qualified Data.Text.Lens as Text import qualified Data.Text.Lens as Text
import qualified Data.Conduit.List as C import qualified Data.Conduit.List as C
@ -33,9 +35,33 @@ import Numeric.Lens (integral)
import Database.Persist.Sql (deleteWhereCount, updateWhereCount) import Database.Persist.Sql (deleteWhereCount, updateWhereCount)
import Control.Lens.Indexed ((<.), (.>))
type ExamUserTableExpr = (E.SqlExpr (Entity ExamRegistration) `E.InnerJoin` E.SqlExpr (Entity User)) `E.LeftOuterJoin` E.SqlExpr (Maybe (Entity ExamOccurrence)) `E.LeftOuterJoin` (E.SqlExpr (Maybe (Entity CourseParticipant)) `E.LeftOuterJoin` (E.SqlExpr (Maybe (Entity StudyFeatures)) `E.InnerJoin` E.SqlExpr (Maybe (Entity StudyDegree)) `E.InnerJoin` E.SqlExpr (Maybe (Entity StudyTerms)))) `E.LeftOuterJoin` E.SqlExpr (Maybe (Entity ExamResult)) `E.LeftOuterJoin` E.SqlExpr (Maybe (Entity CourseUserNote))
type ExamUserTableData = DBRow (Entity ExamRegistration, Entity User, Maybe (Entity ExamOccurrence), Maybe (Entity StudyFeatures), Maybe (Entity StudyDegree), Maybe (Entity StudyTerms), Maybe (Entity ExamResult), Maybe (Entity CourseUserNote)) type ExamUserTableExpr = ( E.SqlExpr (Entity ExamRegistration)
`E.InnerJoin` E.SqlExpr (Entity User)
)
`E.LeftOuterJoin` E.SqlExpr (Maybe (Entity ExamOccurrence))
`E.LeftOuterJoin` ( E.SqlExpr (Maybe (Entity CourseParticipant))
`E.LeftOuterJoin` ( E.SqlExpr (Maybe (Entity StudyFeatures))
`E.InnerJoin` E.SqlExpr (Maybe (Entity StudyDegree))
`E.InnerJoin` E.SqlExpr (Maybe (Entity StudyTerms))
)
)
`E.LeftOuterJoin` E.SqlExpr (Maybe (Entity ExamBonus))
`E.LeftOuterJoin` E.SqlExpr (Maybe (Entity ExamResult))
`E.LeftOuterJoin` E.SqlExpr (Maybe (Entity CourseUserNote))
type ExamUserTableData = DBRow ( Entity ExamRegistration
, Entity User
, Maybe (Entity ExamOccurrence)
, Maybe (Entity StudyFeatures)
, Maybe (Entity StudyDegree)
, Maybe (Entity StudyTerms)
, Maybe (Entity ExamBonus)
, Maybe (Entity ExamResult)
, Map ExamPartId (ExamPart, Maybe (Entity ExamPartResult))
, Maybe (Entity CourseUserNote)
)
instance HasEntity ExamUserTableData User where instance HasEntity ExamUserTableData User where
hasEntity = _dbrOutput . _2 hasEntity = _dbrOutput . _2
@ -47,28 +73,51 @@ _userTableOccurrence :: Lens' ExamUserTableData (Maybe (Entity ExamOccurrence))
_userTableOccurrence = _dbrOutput . _3 _userTableOccurrence = _dbrOutput . _3
queryUser :: ExamUserTableExpr -> E.SqlExpr (Entity User) queryUser :: ExamUserTableExpr -> E.SqlExpr (Entity User)
queryUser = $(sqlIJproj 2 2) . $(sqlLOJproj 5 1) queryUser = $(sqlIJproj 2 2) . $(sqlLOJproj 6 1)
queryStudyFeatures :: ExamUserTableExpr -> E.SqlExpr (Maybe (Entity StudyFeatures))
queryStudyFeatures = $(sqlIJproj 3 1) . $(sqlLOJproj 2 2) . $(sqlLOJproj 5 3)
queryExamRegistration :: ExamUserTableExpr -> E.SqlExpr (Entity ExamRegistration) queryExamRegistration :: ExamUserTableExpr -> E.SqlExpr (Entity ExamRegistration)
queryExamRegistration = $(sqlIJproj 2 1) . $(sqlLOJproj 5 1) queryExamRegistration = $(sqlIJproj 2 1) . $(sqlLOJproj 6 1)
queryExamOccurrence :: ExamUserTableExpr -> E.SqlExpr (Maybe (Entity ExamOccurrence)) queryExamOccurrence :: ExamUserTableExpr -> E.SqlExpr (Maybe (Entity ExamOccurrence))
queryExamOccurrence = $(sqlLOJproj 5 2) queryExamOccurrence = $(sqlLOJproj 6 2)
queryCourseParticipant :: ExamUserTableExpr -> E.SqlExpr (Maybe (Entity CourseParticipant))
queryCourseParticipant = $(sqlLOJproj 2 1) . $(sqlLOJproj 6 3)
queryStudyFeatures :: ExamUserTableExpr -> E.SqlExpr (Maybe (Entity StudyFeatures))
queryStudyFeatures = $(sqlIJproj 3 1) . $(sqlLOJproj 2 2) . $(sqlLOJproj 6 3)
queryStudyDegree :: ExamUserTableExpr -> E.SqlExpr (Maybe (Entity StudyDegree)) queryStudyDegree :: ExamUserTableExpr -> E.SqlExpr (Maybe (Entity StudyDegree))
queryStudyDegree = $(sqlIJproj 3 2) . $(sqlLOJproj 2 2) . $(sqlLOJproj 5 3) queryStudyDegree = $(sqlIJproj 3 2) . $(sqlLOJproj 2 2) . $(sqlLOJproj 6 3)
queryStudyField :: ExamUserTableExpr -> E.SqlExpr (Maybe (Entity StudyTerms)) queryStudyField :: ExamUserTableExpr -> E.SqlExpr (Maybe (Entity StudyTerms))
queryStudyField = $(sqlIJproj 3 3) . $(sqlLOJproj 2 2) . $(sqlLOJproj 5 3) queryStudyField = $(sqlIJproj 3 3) . $(sqlLOJproj 2 2) . $(sqlLOJproj 6 3)
queryExamBonus :: ExamUserTableExpr -> E.SqlExpr (Maybe (Entity ExamBonus))
queryExamBonus = $(sqlLOJproj 6 4)
queryExamResult :: ExamUserTableExpr -> E.SqlExpr (Maybe (Entity ExamResult)) queryExamResult :: ExamUserTableExpr -> E.SqlExpr (Maybe (Entity ExamResult))
queryExamResult = $(sqlLOJproj 5 4) queryExamResult = $(sqlLOJproj 6 5)
queryCourseNote :: ExamUserTableExpr -> E.SqlExpr (Maybe (Entity CourseUserNote)) queryCourseNote :: ExamUserTableExpr -> E.SqlExpr (Maybe (Entity CourseUserNote))
queryCourseNote = $(sqlLOJproj 5 5) queryCourseNote = $(sqlLOJproj 6 6)
queryExamPart :: forall a.
PersistField a
=> ExamPartId
-> (E.SqlExpr (Entity ExamPart) -> E.SqlExpr (Maybe (Entity ExamPartResult)) -> E.SqlQuery (E.SqlExpr (E.Value a)))
-> ExamUserTableExpr
-> E.SqlExpr (E.Value a)
queryExamPart epId cont inp = E.sub_select . E.from $ \(examPart `E.LeftOuterJoin` examPartResult) -> flip runReaderT inp $ do
examRegistration <- asks queryExamRegistration
lift $ do
E.on $ E.just (examPart E.^. ExamPartId) E.==. examPartResult E.?. ExamPartResultExamPart
E.&&. examPartResult E.?. ExamPartResultUser E.==. E.just (examRegistration E.^. ExamRegistrationUser)
E.where_ $ examPart E.^. ExamPartExam E.==. examRegistration E.^. ExamRegistrationExam
E.&&. examPart E.^. ExamPartId E.==. E.val epId
cont examPart examPartResult
resultExamRegistration :: Lens' ExamUserTableData (Entity ExamRegistration) resultExamRegistration :: Lens' ExamUserTableData (Entity ExamRegistration)
resultExamRegistration = _dbrOutput . _1 resultExamRegistration = _dbrOutput . _1
@ -88,11 +137,48 @@ resultStudyField = _dbrOutput . _6 . _Just
resultExamOccurrence :: Traversal' ExamUserTableData (Entity ExamOccurrence) resultExamOccurrence :: Traversal' ExamUserTableData (Entity ExamOccurrence)
resultExamOccurrence = _dbrOutput . _3 . _Just resultExamOccurrence = _dbrOutput . _3 . _Just
resultExamBonus :: Traversal' ExamUserTableData (Entity ExamBonus)
resultExamBonus = _dbrOutput . _7 . _Just
resultExamResult :: Traversal' ExamUserTableData (Entity ExamResult) resultExamResult :: Traversal' ExamUserTableData (Entity ExamResult)
resultExamResult = _dbrOutput . _7 . _Just resultExamResult = _dbrOutput . _8 . _Just
resultExamParts :: IndexedTraversal' ExamPartId ExamUserTableData (ExamPart, Maybe (Entity ExamPartResult))
resultExamParts = _dbrOutput . _9 . itraversed
-- resultExamParts' :: Traversal' ExamUserTableData (Entity ExamPart)
-- resultExamParts' = (resultExamParts <. _1) . withIndex . from _Entity
resultExamPartResult :: ExamPartId -> Lens' ExamUserTableData (Maybe (Entity ExamPartResult))
resultExamPartResult epId = _dbrOutput . _9 . unsafeSingular (ix epId) . _2
resultExamPartResults :: IndexedTraversal' ExamPartId ExamUserTableData (Maybe (Entity ExamPartResult))
resultExamPartResults = resultExamParts <. _2
resultCourseNote :: Traversal' ExamUserTableData (Entity CourseUserNote) resultCourseNote :: Traversal' ExamUserTableData (Entity CourseUserNote)
resultCourseNote = _dbrOutput . _8 . _Just resultCourseNote = _dbrOutput . _10 . _Just
resultAutomaticExamBonus :: Exam -> Map UserId SheetTypeSummary -> Fold ExamUserTableData Points
resultAutomaticExamBonus exam examBonus' = resultUser . _entityKey . folding (\uid -> examResultBonus <$> examBonusRule exam <*> pure (examBonusPossible uid examBonus') <*> pure (examBonusAchieved uid examBonus'))
resultAutomaticExamResult :: Exam -> Map UserId SheetTypeSummary -> Fold ExamUserTableData ExamResultGrade
resultAutomaticExamResult exam examBonus' = folding . runReader $ do
parts' <- asks $ sequence . toListOf (resultExamPartResults . to (^? _Just . _entityVal . _examPartResultResult))
bonus <- preview $ resultExamBonus . _entityVal . _examBonusBonus <> resultAutomaticExamBonus exam examBonus'
return $ examGrade exam bonus =<< parts'
csvExamPartHeader :: Prism' Csv.Name ExamPartNumber
csvExamPartHeader = prism' toHeader fromHeader
where
toHeader pName = encodeUtf8 $ partPrefix <> CI.foldedCase (pName ^. _ExamPartNumber)
fromHeader hdr = do
tHdr <- either (const Nothing) Just $ Text.decodeUtf8' hdr
review _ExamPartNumber . CI.mk <$> stripPrefix partPrefix tHdr
partPrefix = "part-"
data ExamUserTableCsv = ExamUserTableCsv data ExamUserTableCsv = ExamUserTableCsv
{ csvEUserSurname :: Maybe Text { csvEUserSurname :: Maybe Text
@ -103,24 +189,46 @@ data ExamUserTableCsv = ExamUserTableCsv
, csvEUserDegree :: Maybe Text , csvEUserDegree :: Maybe Text
, csvEUserSemester :: Maybe Int , csvEUserSemester :: Maybe Int
, csvEUserOccurrence :: Maybe (CI Text) , csvEUserOccurrence :: Maybe (CI Text)
, csvEUserExercisePoints :: Maybe Points , csvEUserExercisePoints :: Maybe (Maybe Points)
, csvEUserExerciseNumPasses :: Maybe Int , csvEUserExerciseNumPasses :: Maybe (Maybe Int)
, csvEUserExercisePointsMax :: Maybe Points , csvEUserExercisePointsMax :: Maybe (Maybe Points)
, csvEUserExerciseNumPassesMax :: Maybe Int , csvEUserExerciseNumPassesMax :: Maybe (Maybe Int)
, csvEUserBonus :: Maybe (Maybe Points)
, csvEUserExamPartResults :: Map ExamPartNumber (Maybe ExamResultPoints)
, csvEUserExamResult :: Maybe ExamResultPassedGrade , csvEUserExamResult :: Maybe ExamResultPassedGrade
, csvEUserCourseNote :: Maybe Html , csvEUserCourseNote :: Maybe Html
} }
deriving (Generic) deriving (Generic)
makeLenses_ ''ExamUserTableCsv makeLenses_ ''ExamUserTableCsv
examUserTableCsvOptions :: Csv.Options
examUserTableCsvOptions = Csv.defaultOptions { Csv.fieldLabelModifier = camelToPathPiece' 3 }
instance ToNamedRecord ExamUserTableCsv where instance ToNamedRecord ExamUserTableCsv where
toNamedRecord = Csv.genericToNamedRecord examUserTableCsvOptions toNamedRecord ExamUserTableCsv{..} = Csv.namedRecord $
[ "surname" Csv..= csvEUserSurname
, "first-name" Csv..= csvEUserFirstName
, "name" Csv..= csvEUserName
, "matriculation" Csv..= csvEUserMatriculation
, "field" Csv..= csvEUserField
, "degree" Csv..= csvEUserDegree
, "semester" Csv..= csvEUserSemester
, "occurrence" Csv..= csvEUserOccurrence
] ++ catMaybes
[ fmap ("exercise-points" Csv..=) csvEUserExercisePoints
, fmap ("exercise-num-passes" Csv..=) csvEUserExerciseNumPasses
, fmap ("exercise-points-max" Csv..=) csvEUserExercisePointsMax
, fmap ("exercise-num-passes-max" Csv..=) csvEUserExerciseNumPassesMax
, fmap ("bonus" Csv..=) csvEUserBonus
]
++ examPartResults ++
[ "exam-result" Csv..= csvEUserExamResult
, "course-note" Csv..= csvEUserCourseNote
]
where
examPartResults
= flip ifoldMap csvEUserExamPartResults $
\pNumber pResult -> pure $ (csvExamPartHeader # pNumber) Csv..= pResult
instance FromNamedRecord ExamUserTableCsv where instance FromNamedRecord ExamUserTableCsv where
parseNamedRecord csv -- Manually defined awaiting issue #427 parseNamedRecord csv
= ExamUserTableCsv = ExamUserTableCsv
<$> csv .:?? "surname" <$> csv .:?? "surname"
<*> csv .:?? "first-name" <*> csv .:?? "first-name"
@ -130,36 +238,66 @@ instance FromNamedRecord ExamUserTableCsv where
<*> csv .:?? "degree" <*> csv .:?? "degree"
<*> csv .:?? "semester" <*> csv .:?? "semester"
<*> csv .:?? "occurrence" <*> csv .:?? "occurrence"
<*> csv .:?? "exercise-points" <*> fmap Just (csv .:?? "exercise-points")
<*> csv .:?? "exercise-num-passes" <*> fmap Just (csv .:?? "exercise-num-passes")
<*> csv .:?? "exercise-points-max" <*> fmap Just (csv .:?? "exercise-points-max")
<*> csv .:?? "exercise-num-passes-max" <*> fmap Just (csv .:?? "exercise-num-passes-max")
<*> fmap Just (csv .:?? "bonus")
<*> examPartResults
<*> csv .:?? "exam-result" <*> csv .:?? "exam-result"
<*> csv .:?? "course-note" <*> csv .:?? "course-note"
where
instance DefaultOrdered ExamUserTableCsv where examPartResults = fmap fold . sequence . flip HashMap.mapMaybeWithKey csv $ \pNumber' _ -> do
headerOrder = Csv.genericHeaderOrder examUserTableCsvOptions pNumber <- pNumber' ^? csvExamPartHeader
return . fmap (singletonMap pNumber ) $ csv .:?? pNumber'
instance CsvColumnsExplained ExamUserTableCsv where instance CsvColumnsExplained ExamUserTableCsv where
csvColumnsExplanations = genericCsvColumnsExplanations examUserTableCsvOptions $ Map.fromList csvColumnsExplanations _ = mconcat
[ ('csvEUserSurname , MsgCsvColumnExamUserSurname ) [ single "surname" MsgCsvColumnExamUserSurname
, ('csvEUserFirstName , MsgCsvColumnExamUserFirstName ) , single "first-name" MsgCsvColumnExamUserFirstName
, ('csvEUserName , MsgCsvColumnExamUserName ) , single "name" MsgCsvColumnExamUserName
, ('csvEUserMatriculation , MsgCsvColumnExamUserMatriculation ) , single "matriculation" MsgCsvColumnExamUserMatriculation
, ('csvEUserField , MsgCsvColumnExamUserField ) , single "field" MsgCsvColumnExamUserField
, ('csvEUserDegree , MsgCsvColumnExamUserDegree ) , single "degree" MsgCsvColumnExamUserDegree
, ('csvEUserSemester , MsgCsvColumnExamUserSemester ) , single "semester" MsgCsvColumnExamUserSemester
, ('csvEUserOccurrence , MsgCsvColumnExamUserOccurrence ) , single "occurrence" MsgCsvColumnExamUserOccurrence
, ('csvEUserExercisePoints , MsgCsvColumnExamUserExercisePoints ) , single "exercise-points" MsgCsvColumnExamUserExercisePoints
, ('csvEUserExerciseNumPasses , MsgCsvColumnExamUserExercisePasses ) , single "exercise-num-passes" MsgCsvColumnExamUserExercisePasses
, ('csvEUserExercisePointsMax , MsgCsvColumnExamUserExercisePointsMax ) , single "exercise-points-max" MsgCsvColumnExamUserExercisePointsMax
, ('csvEUserExerciseNumPassesMax, MsgCsvColumnExamUserExercisePassesMax ) , single "exercise-num-passes-max" MsgCsvColumnExamUserExercisePassesMax
, ('csvEUserExamResult , MsgCsvColumnExamUserResult ) , single "bonus" MsgCsvColumnExamUserBonus
, ('csvEUserCourseNote , MsgCsvColumnExamUserCourseNote ) , single "part-*" MsgCsvColumnExamUserParts
, single "exam-result" MsgCsvColumnExamUserResult
, single "course-note" MsgCsvColumnExamUserCourseNote
] ]
where
single :: RenderMessage UniWorX msg => Csv.Name -> msg -> Map Csv.Name Widget
single k v = singletonMap k [whamlet|_{v}|]
examUserTableCsvHeader :: ( MonoFoldable mono
, Element mono ~ ExamPartNumber
)
=> SheetGradeSummary -> Bool -> mono -> Csv.Header
examUserTableCsvHeader allBoni doBonus pNames = Csv.header $
[ "surname", "first-name", "name"
, "matriculation"
, "field", "degree", "semester"
, "course-note"
, "occurrence"
] ++ bool mempty ["exercise-points", "exercise-points-max"] (doBonus && showPoints)
++ bool mempty ["exercise-num-passes", "exercise-num-passes-max"] (doBonus && showPasses)
++ bool mempty ["bonus"] doBonus
++ map (review csvExamPartHeader) (sort $ otoList pNames) ++
[ "exam-result"
]
where
showPasses = numSheetsPasses allBoni /= 0
showPoints = getSum (numSheetsPoints allBoni) /= 0
data ExamUserAction = ExamUserDeregister data ExamUserAction = ExamUserDeregister
| ExamUserAssignOccurrence | ExamUserAssignOccurrence
| ExamUserAcceptComputedResult
| ExamUserResetToComputedResult
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic, Typeable) deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic, Typeable)
instance Universe ExamUserAction instance Universe ExamUserAction
@ -169,13 +307,21 @@ embedRenderMessage ''UniWorX ''ExamUserAction id
data ExamUserActionData = ExamUserDeregisterData data ExamUserActionData = ExamUserDeregisterData
| ExamUserAssignOccurrenceData (Maybe ExamOccurrenceId) | ExamUserAssignOccurrenceData (Maybe ExamOccurrenceId)
| ExamUserAcceptComputedResultData
| ExamUserResetToComputedResultData
{ examUserResetBonus :: Bool
}
data ExamUserCsvActionClass data ExamUserCsvActionClass
= ExamUserCsvCourseRegister = ExamUserCsvCourseRegister
| ExamUserCsvRegister | ExamUserCsvRegister
| ExamUserCsvAssignOccurrence | ExamUserCsvAssignOccurrence
| ExamUserCsvSetCourseField | ExamUserCsvSetCourseField
| ExamUserCsvSetPartResult
| ExamUserCsvSetBonus
| ExamUserCsvOverrideBonus
| ExamUserCsvSetResult | ExamUserCsvSetResult
| ExamUserCsvOverrideResult
| ExamUserCsvSetCourseNote | ExamUserCsvSetCourseNote
| ExamUserCsvDeregister | ExamUserCsvDeregister
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable) deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable)
@ -202,8 +348,19 @@ data ExamUserCsvAction
| ExamUserCsvDeregisterData | ExamUserCsvDeregisterData
{ examUserCsvActRegistration :: ExamRegistrationId { examUserCsvActRegistration :: ExamRegistrationId
} }
| ExamUserCsvSetResultData | ExamUserCsvSetPartResultData
{ examUserCsvActUser :: UserId { examUserCsvActUser :: UserId
, examUserCsvActExamPart :: ExamPartNumber
, examUserCsvActExamPartResult :: Maybe ExamResultPoints
}
| ExamUserCsvSetBonusData
{ examUserCsvIsBonusOverride :: Bool
, examUserCsvActUser :: UserId
, examUserCsvActExamBonus :: Maybe Points
}
| ExamUserCsvSetResultData
{ examUserCsvIsResultOverride :: Bool
, examUserCsvActUser :: UserId
, examUserCsvActExamResult :: Maybe ExamResultPassedGrade , examUserCsvActExamResult :: Maybe ExamResultPassedGrade
} }
| ExamUserCsvSetCourseNoteData | ExamUserCsvSetCourseNoteData
@ -230,75 +387,148 @@ embedRenderMessage ''UniWorX ''ExamUserCsvException id
getEUsersR, postEUsersR :: TermId -> SchoolId -> CourseShorthand -> ExamName -> Handler Html getEUsersR, postEUsersR :: TermId -> SchoolId -> CourseShorthand -> ExamName -> Handler Html
getEUsersR = postEUsersR getEUsersR = postEUsersR
postEUsersR tid ssh csh examn = do postEUsersR tid ssh csh examn = do
((registrationResult, examUsersTable), Entity eId _) <- runDB $ do (((Any computedValues, registrationResult), examUsersTable), Entity eId examVal, bonus) <- runDB $ do
exam@(Entity eid Exam{..}) <- fetchExam tid ssh csh examn exam@(Entity eid examVal@Exam{..}) <- fetchExam tid ssh csh examn
examParts <- selectList [ExamPartExam ==. eid] [Asc ExamPartName]
bonus <- examBonus exam bonus <- examBonus exam
let let
allBoni :: SheetGradeSummary
allBoni = (mappend <$> normalSummary <*> bonusSummary) $ fold bonus allBoni = (mappend <$> normalSummary <*> bonusSummary) $ fold bonus
showPasses = numSheetsPasses allBoni /= 0
showPoints = getSum (numSheetsPoints allBoni) /= 0 doBonus = is _Just examBonusRule
showPasses = doBonus && numSheetsPasses allBoni /= 0
showPoints = doBonus && getSum (numSheetsPoints allBoni) /= 0
resultView :: ExamResultGrade -> ExamResultPassedGrade resultView :: ExamResultGrade -> ExamResultPassedGrade
resultView = fmap $ bool (Left . view passingGrade) Right examShowGrades resultView = fmap $ bool (Left . view passingGrade) Right examShowGrades
examPartNumbers = examParts ^.. folded . _entityVal . _examPartNumber
resultAutomaticExamBonus' :: Fold ExamUserTableData Points
resultAutomaticExamBonus' = resultAutomaticExamBonus examVal bonus
resultAutomaticExamResult' :: Fold ExamUserTableData ExamResultGrade
resultAutomaticExamResult' = resultAutomaticExamResult examVal bonus
automaticCell :: forall msg m a b r.
( RenderMessage UniWorX msg
, IsDBTable m a
, Eq msg
, Monoid b
, a ~ (Any, b)
)
=> Getting (Endo [Either msg msg]) r (Either msg msg)
-> r
-> DBCell m a
automaticCell l r = case toListOf l r of
[] -> mempty
(Left auto : _)
-> i18nCell auto & cellAttrs <>~ [("class", "table__td--automatic")] & tellCell (Any True, mempty)
(Right man : others)
| all ((== man) . either id id) others
-> i18nCell man
| otherwise
-> i18nCell man & cellAttrs <>~ [("class", "table__td--overriden")] & tellCell (Any True, mempty)
csvName <- getMessageRender <*> pure (MsgExamUserCsvName tid ssh csh examn) csvName <- getMessageRender <*> pure (MsgExamUserCsvName tid ssh csh examn)
let let
examUsersDBTable = DBTable{..} examUsersDBTable = DBTable{..}
where where
dbtSQLQuery ((examRegistration `E.InnerJoin` user) `E.LeftOuterJoin` occurrence `E.LeftOuterJoin` (courseParticipant `E.LeftOuterJoin` (studyFeatures `E.InnerJoin` studyDegree `E.InnerJoin` studyField)) `E.LeftOuterJoin` examResult `E.LeftOuterJoin` courseUserNote) = do dbtSQLQuery = runReaderT $ do
E.on $ courseUserNote E.?. CourseUserNoteUser E.==. E.just (user E.^. UserId) examRegistration <- asks queryExamRegistration
E.&&. courseUserNote E.?. CourseUserNoteCourse E.==. E.just (E.val examCourse) user <- asks queryUser
E.on $ examResult E.?. ExamResultUser E.==. E.just (user E.^. UserId) occurrence <- asks queryExamOccurrence
E.&&. examResult E.?. ExamResultExam E.==. E.just (E.val eid) courseParticipant <- asks queryCourseParticipant
E.on $ studyField E.?. StudyTermsId E.==. studyFeatures E.?. StudyFeaturesField studyFeatures <- asks queryStudyFeatures
E.on $ studyDegree E.?. StudyDegreeId E.==. studyFeatures E.?. StudyFeaturesDegree studyDegree <- asks queryStudyDegree
E.on $ studyFeatures E.?. StudyFeaturesId E.==. E.joinV (courseParticipant E.?. CourseParticipantField) studyField <- asks queryStudyField
E.on $ courseParticipant E.?. CourseParticipantCourse E.==. E.just (E.val examCourse) examBonus' <- asks queryExamBonus
E.&&. courseParticipant E.?. CourseParticipantUser E.==. E.just (user E.^. UserId) examResult <- asks queryExamResult
E.on $ occurrence E.?. ExamOccurrenceExam E.==. E.just (E.val eid) courseUserNote <- asks queryCourseNote
E.&&. occurrence E.?. ExamOccurrenceId E.==. examRegistration E.^. ExamRegistrationOccurrence
E.on $ examRegistration E.^. ExamRegistrationUser E.==. user E.^. UserId lift $ do
E.where_ $ examRegistration E.^. ExamRegistrationExam E.==. E.val eid E.on $ courseUserNote E.?. CourseUserNoteUser E.==. E.just (user E.^. UserId)
return (examRegistration, user, occurrence, studyFeatures, studyDegree, studyField, examResult, courseUserNote) E.&&. courseUserNote E.?. CourseUserNoteCourse E.==. E.just (E.val examCourse)
E.on $ examResult E.?. ExamResultUser E.==. E.just (user E.^. UserId)
E.&&. examResult E.?. ExamResultExam E.==. E.just (E.val eid)
E.on $ examBonus' E.?. ExamBonusUser E.==. E.just (user E.^. UserId)
E.&&. examBonus' E.?. ExamBonusExam E.==. E.just (E.val eid)
E.on $ studyField E.?. StudyTermsId E.==. studyFeatures E.?. StudyFeaturesField
E.on $ studyDegree E.?. StudyDegreeId E.==. studyFeatures E.?. StudyFeaturesDegree
E.on $ studyFeatures E.?. StudyFeaturesId E.==. E.joinV (courseParticipant E.?. CourseParticipantField)
E.on $ courseParticipant E.?. CourseParticipantCourse E.==. E.just (E.val examCourse)
E.&&. courseParticipant E.?. CourseParticipantUser E.==. E.just (user E.^. UserId)
E.on $ occurrence E.?. ExamOccurrenceExam E.==. E.just (E.val eid)
E.&&. occurrence E.?. ExamOccurrenceId E.==. examRegistration E.^. ExamRegistrationOccurrence
E.on $ examRegistration E.^. ExamRegistrationUser E.==. user E.^. UserId
E.where_ $ examRegistration E.^. ExamRegistrationExam E.==. E.val eid
return (examRegistration, user, occurrence, studyFeatures, studyDegree, studyField, examBonus', examResult, courseUserNote)
dbtRowKey = queryExamRegistration >>> (E.^. ExamRegistrationId) dbtRowKey = queryExamRegistration >>> (E.^. ExamRegistrationId)
dbtProj = return dbtProj = runReaderT $ (asks . set _dbrOutput) <=< magnify _dbrOutput $
(,,,,,,,,,)
<$> view _1 <*> view _2 <*> view _3 <*> view _4 <*> view _5 <*> view _6 <*> view _7 <*> view _8
<*> getExamParts
<*> view _9
where
getExamParts :: ReaderT _ (MaybeT (YesodDB UniWorX)) (Map ExamPartId (ExamPart, Maybe (Entity ExamPartResult)))
getExamParts = do
uid <- view $ _2 . _entityKey
rawResults <- lift . lift . E.select . E.from $ \(examPart `E.LeftOuterJoin` examPartResult) -> do
E.on $ examPartResult E.?. ExamPartResultExamPart E.==. E.just (examPart E.^. ExamPartId)
E.&&. examPartResult E.?. ExamPartResultUser E.==. E.just (E.val uid)
E.where_ $ examPart E.^. ExamPartExam E.==. E.val eid
return (examPart, examPartResult)
return $ Map.fromList
[ (epId, (examPart, mbRes))
| (Entity epId examPart, mbRes) <- rawResults
]
dbtColonnade = mconcat $ catMaybes dbtColonnade = mconcat $ catMaybes
[ pure $ dbSelect (applying _2) id $ return . view (resultExamRegistration . _entityKey) [ pure $ dbSelect (_2 . applying _2) _1 $ return . view (resultExamRegistration . _entityKey)
, pure $ colUserNameLink (CourseR tid ssh csh . CUserR) , pure $ colUserNameLink (CourseR tid ssh csh . CUserR)
, pure colUserMatriclenr , pure colUserMatriclenr
, pure $ colField resultStudyField , pure $ colField resultStudyField
, pure $ colDegreeShort resultStudyDegree , pure $ colDegreeShort resultStudyDegree
, pure $ colFeaturesSemester resultStudyFeatures , pure $ colFeaturesSemester resultStudyFeatures
, pure $ sortable (Just "occurrence") (i18nCell MsgExamOccurrence) $ maybe mempty (anchorCell' (\n -> CExamR tid ssh csh examn EShowR :#: [st|exam-occurrence__#{n}|]) id . examOccurrenceName . entityVal) . view _userTableOccurrence , pure $ sortable (Just "occurrence") (i18nCell MsgExamOccurrence) $ maybe mempty (anchorCell' (\n -> CExamR tid ssh csh examn EShowR :#: [st|exam-occurrence__#{n}|]) id . examOccurrenceName . entityVal) . view _userTableOccurrence
, guardOn showPasses $ sortable Nothing (i18nCell MsgAchievedPasses) $ \(view $ resultUser . _entityKey -> uid) -> fromMaybe mempty $ do , guardOn showPasses $ sortable Nothing (i18nCell MsgAchievedPasses) $ \(view $ resultUser . _entityKey -> uid) ->
SheetGradeSummary{achievedPasses} <- examBonusAchieved uid bonus let SheetGradeSummary{achievedPasses} = examBonusAchieved uid bonus
SheetGradeSummary{numSheetsPasses} <- examBonusPossible uid bonus SheetGradeSummary{numSheetsPasses} = examBonusPossible uid bonus
return $ propCell (getSum achievedPasses) (getSum numSheetsPasses) in propCell (getSum achievedPasses) (getSum numSheetsPasses)
, guardOn showPoints $ sortable Nothing (i18nCell MsgAchievedPoints) $ \(view $ resultUser . _entityKey -> uid) -> fromMaybe mempty $ do , guardOn showPoints $ sortable Nothing (i18nCell MsgAchievedPoints) $ \(view $ resultUser . _entityKey -> uid) ->
SheetGradeSummary{achievedPoints} <- examBonusAchieved uid bonus let SheetGradeSummary{achievedPoints} = examBonusAchieved uid bonus
SheetGradeSummary{sumSheetsPoints} <- examBonusPossible uid bonus SheetGradeSummary{sumSheetsPoints} = examBonusPossible uid bonus
return $ propCell (getSum achievedPoints) (getSum sumSheetsPoints) in propCell (getSum achievedPoints) (getSum sumSheetsPoints)
, guardOn examShowGrades $ sortable (Just "result") (i18nCell MsgExamResult) $ maybe mempty i18nCell . preview (resultExamResult . _entityVal . _examResultResult) , guardOn doBonus $ sortable (Just "bonus") (i18nCell MsgExamBonusAchieved) . automaticCell $ resultExamBonus . _entityVal . _examBonusBonus . to Right <> resultAutomaticExamBonus' . to Left
, guardOn (not examShowGrades) $ sortable (Just "result-bool") (i18nCell MsgExamResult) $ maybe mempty i18nCell . preview (resultExamResult . _entityVal . _examResultResult . to (over _examResult $ view passingGrade)) , pure $ mconcat
[ sortable (Just $ fromText [st|part-#{toPathPiece examPartNumber}|]) (i18nCell $ MsgExamPartNumbered examPartNumber) $ maybe mempty i18nCell . preview (resultExamPartResult epId . _Just . _entityVal . _examPartResultResult)
| Entity epId ExamPart{..} <- sortOn (examPartNumber . entityVal) examParts
]
, pure $ sortable (Just $ bool "result-bool" "result" examShowGrades) (i18nCell MsgExamResult) . automaticCell $ (resultExamResult . _entityVal . _examResultResult . to Right <> resultAutomaticExamResult' . to Left) . to (bimap resultView resultView)
, pure . sortable (Just "note") (i18nCell MsgCourseUserNote) $ \((,) <$> view (resultUser . _entityKey) <*> has resultCourseNote -> (uid, hasNote)) , pure . sortable (Just "note") (i18nCell MsgCourseUserNote) $ \((,) <$> view (resultUser . _entityKey) <*> has resultCourseNote -> (uid, hasNote))
-> bool mempty (anchorCellM (CourseR tid ssh csh . CUserR <$> encrypt uid) $ hasComment True) hasNote -> bool mempty (anchorCellM (CourseR tid ssh csh . CUserR <$> encrypt uid) $ hasComment True) hasNote
] ]
dbtSorting = Map.fromList dbtSorting = mconcat
[ sortUserNameLink queryUser [ uncurry singletonMap $ sortUserNameLink queryUser
, sortUserMatriclenr queryUser , uncurry singletonMap $ sortUserMatriclenr queryUser
, sortField queryStudyField , uncurry singletonMap $ sortField queryStudyField
, sortDegreeShort queryStudyDegree , uncurry singletonMap $ sortDegreeShort queryStudyDegree
, sortFeaturesSemester queryStudyFeatures , uncurry singletonMap $ sortFeaturesSemester queryStudyFeatures
, ("occurrence", SortColumn $ queryExamOccurrence >>> (E.?. ExamOccurrenceName)) , mconcat
, ("result", SortColumn $ queryExamResult >>> (E.?. ExamResultResult)) [ singletonMap (fromText [st|part-#{toPathPiece examPartNumber}|]) . SortColumn . queryExamPart epId $ \_ examPartResult -> return $ examPartResult E.?. ExamPartResultResult
, ("result-bool", SortColumn $ queryExamResult >>> (E.?. ExamResultResult) >>> E.orderByList [Just ExamVoided, Just ExamNoShow, Just $ ExamAttended Grade50]) | Entity epId ExamPart{..} <- examParts
, ("note", SortColumn $ queryCourseNote >>> \note -> -- sort by last edit date ]
, singletonMap "occurrence" . SortColumn $ queryExamOccurrence >>> (E.?. ExamOccurrenceName)
, singletonMap "bonus" . SortColumn $ queryExamBonus >>> (E.?. ExamBonusBonus)
, singletonMap "result" . SortColumn $ queryExamResult >>> (E.?. ExamResultResult)
, singletonMap "result-bool" . SortColumn $ queryExamResult >>> (E.?. ExamResultResult) >>> E.orderByList [Just ExamVoided, Just ExamNoShow, Just $ ExamAttended Grade50]
, singletonMap "note" . SortColumn $ queryCourseNote >>> \note -> -- sort by last edit date
E.sub_select . E.from $ \edit -> do E.sub_select . E.from $ \edit -> do
E.where_ $ note E.?. CourseUserNoteId E.==. E.just (edit E.^. CourseUserNoteEditNote) E.where_ $ note E.?. CourseUserNoteId E.==. E.just (edit E.^. CourseUserNoteEditNote)
return . E.max_ $ edit E.^. CourseUserNoteEditTime return . E.max_ $ edit E.^. CourseUserNoteEditTime
)
] ]
dbtFilter = Map.fromList dbtFilter = Map.fromList
[ fltrUserNameEmail queryUser [ fltrUserNameEmail queryUser
@ -341,39 +571,55 @@ postEUsersR tid ssh csh examn = do
, dbParamsFormAdditional = \csrf -> do , dbParamsFormAdditional = \csrf -> do
let let
actionMap :: Map ExamUserAction (AForm Handler ExamUserActionData) actionMap :: Map ExamUserAction (AForm Handler ExamUserActionData)
actionMap = Map.fromList actionMap = mconcat
[ ( ExamUserDeregister [ singletonMap ExamUserDeregister $
, pure ExamUserDeregisterData pure ExamUserDeregisterData
) , singletonMap ExamUserAssignOccurrence $
, ( ExamUserAssignOccurrence ExamUserAssignOccurrenceData
, ExamUserAssignOccurrenceData
<$> aopt (examOccurrenceField eid) (fslI MsgExamOccurrence) (Just Nothing) <$> aopt (examOccurrenceField eid) (fslI MsgExamOccurrence) (Just Nothing)
) , bool mempty computeActionMap $ is _Just examGradingRule
]
computeActionMap = mconcat
[ singletonMap ExamUserAcceptComputedResult $
pure ExamUserAcceptComputedResultData
, singletonMap ExamUserResetToComputedResult $
ExamUserResetToComputedResultData
<$> bool (pure True) (apopt checkBoxField (fslI MsgExamUserResetBonus) (Just True)) (is _Just examBonusRule)
] ]
(res, formWgt) <- multiActionM actionMap (fslI MsgAction) Nothing csrf (res, formWgt) <- multiActionM actionMap (fslI MsgAction) Nothing csrf
let formRes = (, mempty) . First . Just <$> res let formRes = (, mempty) . First . Just <$> res
return (formRes, formWgt) return (formRes, formWgt)
, dbParamsFormEvaluate = liftHandlerT . runFormPost , dbParamsFormEvaluate = liftHandler . runFormPost
, dbParamsFormResult = id , dbParamsFormResult = _2
, dbParamsFormIdent = def , dbParamsFormIdent = def
} }
dbtIdent :: Text dbtIdent :: Text
dbtIdent = "exam-users" dbtIdent = "exam-users"
dbtCsvEncode = simpleCsvEncode csvName $ ExamUserTableCsv dbtCsvEncode = Just DBTCsvEncode
<$> view (resultUser . _entityVal . _userSurname . to Just) { dbtCsvExportForm = pure ()
<*> view (resultUser . _entityVal . _userFirstName . to Just) , dbtCsvDoEncode = \() -> C.map (doEncode' . view _2)
<*> view (resultUser . _entityVal . _userDisplayName . to Just) , dbtCsvName = unpack csvName
<*> view (resultUser . _entityVal . _userMatrikelnummer) , dbtCsvNoExportData = Just id
<*> preview (resultStudyField . _entityVal . to (\StudyTerms{..} -> studyTermsName <|> studyTermsShorthand <|> Just (tshow studyTermsKey)) . _Just) , dbtCsvHeader = const . return . examUserTableCsvHeader allBoni doBonus $ examParts ^.. folded . _entityVal . _examPartNumber
<*> preview (resultStudyDegree . _entityVal . to (\StudyDegree{..} -> studyDegreeName <|> studyDegreeShorthand <|> Just (tshow studyDegreeKey)) . _Just) }
<*> preview (resultStudyFeatures . _entityVal . _studyFeaturesSemester) where
<*> preview (resultExamOccurrence . _entityVal . _examOccurrenceName) doEncode' = ExamUserTableCsv
<*> preview (resultUser . _entityKey . to (examBonusAchieved ?? bonus) . _Just . _achievedPoints . _Wrapped) <$> view (resultUser . _entityVal . _userSurname . to Just)
<*> preview (resultUser . _entityKey . to (examBonusAchieved ?? bonus) . _Just . _achievedPasses . _Wrapped . integral) <*> view (resultUser . _entityVal . _userFirstName . to Just)
<*> preview (resultUser . _entityKey . to (examBonusPossible ?? bonus) . _Just . _sumSheetsPoints . _Wrapped) <*> view (resultUser . _entityVal . _userDisplayName . to Just)
<*> preview (resultUser . _entityKey . to (examBonusPossible ?? bonus) . _Just . _numSheetsPasses . _Wrapped . integral) <*> view (resultUser . _entityVal . _userMatrikelnummer)
<*> preview (resultExamResult . _entityVal . _examResultResult . to resultView) <*> preview (resultStudyField . _entityVal . to (\StudyTerms{..} -> studyTermsName <|> studyTermsShorthand <|> Just (tshow studyTermsKey)) . _Just)
<*> preview (resultCourseNote . _entityVal . _courseUserNoteNote) <*> preview (resultStudyDegree . _entityVal . to (\StudyDegree{..} -> studyDegreeName <|> studyDegreeShorthand <|> Just (tshow studyDegreeKey)) . _Just)
<*> preview (resultStudyFeatures . _entityVal . _studyFeaturesSemester)
<*> preview (resultExamOccurrence . _entityVal . _examOccurrenceName)
<*> previews (resultUser . _entityKey . to (examBonusAchieved ?? bonus) . _achievedPoints . _Wrapped) (bool (const Nothing) Just showPoints)
<*> previews (resultUser . _entityKey . to (examBonusAchieved ?? bonus) . _achievedPasses . _Wrapped . integral) (bool (const Nothing) Just showPasses)
<*> previews (resultUser . _entityKey . to (examBonusPossible ?? bonus) . _sumSheetsPoints . _Wrapped) (bool (const Nothing) Just showPoints)
<*> previews (resultUser . _entityKey . to (examBonusPossible ?? bonus) . _numSheetsPasses . _Wrapped . integral) (bool (const Nothing) Just showPasses)
<*> previews (resultExamBonus . _entityVal . _examBonusBonus <> resultAutomaticExamBonus') (bool (const Nothing) Just doBonus)
<*> (Map.fromList . map (over _1 examPartNumber . over (_2 . _Just) (examPartResultResult . entityVal)) <$> asks (toListOf resultExamParts))
<*> previews (resultExamResult . _entityVal . _examResultResult <> resultAutomaticExamResult') resultView
<*> preview (resultCourseNote . _entityVal . _courseUserNoteNote)
dbtCsvDecode = Just DBTCsvDecode dbtCsvDecode = Just DBTCsvDecode
{ dbtCsvRowKey = \csv -> do { dbtCsvRowKey = \csv -> do
uid <- lift $ view _2 <$> guessUser csv uid <- lift $ view _2 <$> guessUser csv
@ -382,20 +628,28 @@ postEUsersR tid ssh csh examn = do
DBCsvDiffMissing{dbCsvOldKey} DBCsvDiffMissing{dbCsvOldKey}
-> yield . ExamUserCsvDeregisterData $ E.unValue dbCsvOldKey -> yield . ExamUserCsvDeregisterData $ E.unValue dbCsvOldKey
DBCsvDiffNew{dbCsvNewKey = Just _} DBCsvDiffNew{dbCsvNewKey = Just _}
-> fail "An UniqueExamRegistration could be found, but the ExamRegistrationKey is not among the existing keys" -> error "An UniqueExamRegistration could be found, but the ExamRegistrationKey is not among the existing keys"
DBCsvDiffNew{dbCsvNewKey = Nothing, dbCsvNew} -> do DBCsvDiffNew{dbCsvNewKey = Nothing, dbCsvNew} -> do
(isPart, uid) <- lift $ guessUser dbCsvNew (isPart, uid) <- lift $ guessUser dbCsvNew
if if
| isPart -> do | isPart -> do
yieldM $ ExamUserCsvRegisterData <$> pure uid <*> lookupOccurrence dbCsvNew yieldM $ ExamUserCsvRegisterData <$> pure uid <*> lookupOccurrence dbCsvNew
newFeatures <- lift $ lookupStudyFeatures dbCsvNew newFeatures <- lift $ lookupStudyFeatures dbCsvNew
Just (Entity cpId CourseParticipant{ courseParticipantField = oldFeatures }) <- lift . getBy $ UniqueParticipant uid examCourse Entity cpId CourseParticipant{ courseParticipantField = oldFeatures } <- lift . getJustBy $ UniqueParticipant uid examCourse
when (newFeatures /= oldFeatures) $ when (newFeatures /= oldFeatures) $
yield $ ExamUserCsvSetCourseFieldData cpId newFeatures yield $ ExamUserCsvSetCourseFieldData cpId newFeatures
| otherwise -> | otherwise ->
yieldM $ ExamUserCsvCourseRegisterData uid <$> lookupStudyFeatures dbCsvNew <*> lookupOccurrence dbCsvNew yieldM $ ExamUserCsvCourseRegisterData uid <$> lookupStudyFeatures dbCsvNew <*> lookupOccurrence dbCsvNew
iforMOf_ (ifolded <. _Just) (csvEUserExamPartResults dbCsvNew) $ \epNumber epRes ->
when (epNumber `elem` examPartNumbers) $
yield $ ExamUserCsvSetPartResultData uid epNumber (Just epRes)
when (doBonus && is _Just (join $ csvEUserBonus dbCsvNew)) $
yield . ExamUserCsvSetBonusData False uid . join $ csvEUserBonus dbCsvNew
when (is _Just $ csvEUserExamResult dbCsvNew) $ when (is _Just $ csvEUserExamResult dbCsvNew) $
yield . ExamUserCsvSetResultData uid $ csvEUserExamResult dbCsvNew yield . ExamUserCsvSetResultData False uid $ csvEUserExamResult dbCsvNew
note <- lift . getBy $ UniqueCourseUserNote uid examCourse note <- lift . getBy $ UniqueCourseUserNote uid examCourse
when (csvEUserCourseNote dbCsvNew /= note ^? _Just . _entityVal . _courseUserNoteNote) $ when (csvEUserCourseNote dbCsvNew /= note ^? _Just . _entityVal . _courseUserNoteNote) $
@ -407,11 +661,54 @@ postEUsersR tid ssh csh examn = do
newFeatures <- lift $ lookupStudyFeatures dbCsvNew newFeatures <- lift $ lookupStudyFeatures dbCsvNew
when (newFeatures /= dbCsvOld ^? resultStudyFeatures . _entityKey) $ do when (newFeatures /= dbCsvOld ^? resultStudyFeatures . _entityKey) $ do
Just (Entity cpId _) <- lift . getBy . flip UniqueParticipant examCourse $ dbCsvOld ^. resultUser . _entityKey Entity cpId _ <- lift . getJustBy . flip UniqueParticipant examCourse $ dbCsvOld ^. resultUser . _entityKey
yield $ ExamUserCsvSetCourseFieldData cpId newFeatures yield $ ExamUserCsvSetCourseFieldData cpId newFeatures
when (csvEUserExamResult dbCsvNew /= dbCsvOld ^? resultExamResult . _entityVal . _examResultResult . to resultView) $ let uid = dbCsvOld ^. resultUser . _entityKey
yield . ExamUserCsvSetResultData (dbCsvOld ^. resultUser . _entityKey) $ csvEUserExamResult dbCsvNew
forM_ examPartNumbers $ \epNumber ->
let oldPartResult = dbCsvOld ^? resultExamParts . filtered (views (_1 . _examPartNumber) (== epNumber)) . _2 . _Just . _entityVal . _examPartResultResult
in whenIsJust (csvEUserExamPartResults dbCsvNew !? epNumber) $ \epRes ->
when (epRes /= oldPartResult) $
yield $ ExamUserCsvSetPartResultData uid epNumber epRes
let newResults :: Maybe (Map ExamPartNumber ExamResultPoints)
newResults = sequence (csvEUserExamPartResults dbCsvNew)
<|> sequence (toMapOf (resultExamParts .> ito (over _1 $ examPartNumber) <. to (fmap $ examPartResultResult . entityVal)) dbCsvOld)
newBonus, oldBonus :: Maybe Points
newBonus = join (csvEUserBonus dbCsvNew)
oldBonus = dbCsvOld ^? (resultExamBonus . _entityVal . _examBonusBonus <> resultAutomaticExamBonus')
newResult, oldResult :: Maybe ExamResultPassedGrade
newResult = fmap resultView <$> examGrade examVal (newBonus <|> oldBonus) =<< newResults
oldResult = dbCsvOld ^? (resultExamResult . _entityVal . _examResultResult <> resultAutomaticExamResult') . to resultView
when doBonus $
case newBonus of
_ | newBonus == oldBonus
-> return ()
_ | is _Nothing newBonus
-> return ()
Nothing
-> yield $ ExamUserCsvSetBonusData False uid newBonus
Just _
-> yield $ ExamUserCsvSetBonusData True uid newBonus
case newResult of
_ | csvEUserExamResult dbCsvNew == oldResult
-> return ()
_ | is _Nothing $ csvEUserExamResult dbCsvNew
-> return ()
Nothing
-> yield . ExamUserCsvSetResultData False uid $ csvEUserExamResult dbCsvNew
Just _
| csvEUserExamResult dbCsvNew /= newResult
-> yield . ExamUserCsvSetResultData True uid $ csvEUserExamResult dbCsvNew
| oldResult /= newResult
-> yield . ExamUserCsvSetResultData False uid $ csvEUserExamResult dbCsvNew
| otherwise
-> return ()
when (csvEUserCourseNote dbCsvNew /= dbCsvOld ^? resultCourseNote . _entityVal . _courseUserNoteNote) $ when (csvEUserCourseNote dbCsvNew /= dbCsvOld ^? resultCourseNote . _entityVal . _courseUserNoteNote) $
yield . ExamUserCsvSetCourseNoteData (dbCsvOld ^. resultUser . _entityKey) $ csvEUserCourseNote dbCsvNew yield . ExamUserCsvSetCourseNoteData (dbCsvOld ^. resultUser . _entityKey) $ csvEUserCourseNote dbCsvNew
@ -421,7 +718,13 @@ postEUsersR tid ssh csh examn = do
ExamUserCsvDeregisterData{} -> ExamUserCsvDeregister ExamUserCsvDeregisterData{} -> ExamUserCsvDeregister
ExamUserCsvAssignOccurrenceData{} -> ExamUserCsvAssignOccurrence ExamUserCsvAssignOccurrenceData{} -> ExamUserCsvAssignOccurrence
ExamUserCsvSetCourseFieldData{} -> ExamUserCsvSetCourseField ExamUserCsvSetCourseFieldData{} -> ExamUserCsvSetCourseField
ExamUserCsvSetResultData{} -> ExamUserCsvSetResult ExamUserCsvSetPartResultData{} -> ExamUserCsvSetPartResult
ExamUserCsvSetBonusData{..}
| examUserCsvIsBonusOverride -> ExamUserCsvOverrideBonus
| otherwise -> ExamUserCsvSetBonus
ExamUserCsvSetResultData{..}
| examUserCsvIsResultOverride -> ExamUserCsvOverrideResult
| otherwise -> ExamUserCsvSetResult
ExamUserCsvSetCourseNoteData{} -> ExamUserCsvSetCourseNote ExamUserCsvSetCourseNoteData{} -> ExamUserCsvSetCourseNote
, dbtCsvCoarsenActionClass = \case , dbtCsvCoarsenActionClass = \case
ExamUserCsvCourseRegister -> DBCsvActionNew ExamUserCsvCourseRegister -> DBCsvActionNew
@ -462,6 +765,34 @@ postEUsersR tid ssh csh examn = do
update examUserCsvActCourseParticipant [ CourseParticipantField =. examUserCsvActCourseField ] update examUserCsvActCourseParticipant [ CourseParticipantField =. examUserCsvActCourseField ]
CourseParticipant{..} <- getJust examUserCsvActCourseParticipant CourseParticipant{..} <- getJust examUserCsvActCourseParticipant
audit $ TransactionCourseParticipantEdit examCourse courseParticipantUser audit $ TransactionCourseParticipantEdit examCourse courseParticipantUser
ExamUserCsvSetPartResultData{..} -> do
epid <- getKeyJustBy $ UniqueExamPartNumber eid examUserCsvActExamPart
case examUserCsvActExamPartResult of
Nothing -> do
deleteBy $ UniqueExamPartResult epid examUserCsvActUser
audit $ TransactionExamPartResultDeleted epid examUserCsvActUser
Just res -> do
now <- liftIO getCurrentTime
void $ upsertBy
(UniqueExamPartResult epid examUserCsvActUser)
(ExamPartResult epid examUserCsvActUser res now)
[ ExamPartResultResult =. res
, ExamPartResultLastChanged =. now
]
audit $ TransactionExamPartResultEdit epid examUserCsvActUser
ExamUserCsvSetBonusData{..} -> case examUserCsvActExamBonus of
Nothing -> do
deleteBy $ UniqueExamBonus eid examUserCsvActUser
audit $ TransactionExamBonusDeleted eid examUserCsvActUser
Just res -> do
now <- liftIO getCurrentTime
void $ upsertBy
(UniqueExamBonus eid examUserCsvActUser)
(ExamBonus eid examUserCsvActUser res now)
[ ExamBonusBonus =. res
, ExamBonusLastChanged =. now
]
audit $ TransactionExamBonusEdit eid examUserCsvActUser
ExamUserCsvSetResultData{..} -> case examUserCsvActExamResult of ExamUserCsvSetResultData{..} -> case examUserCsvActExamResult of
Nothing -> do Nothing -> do
deleteBy $ UniqueExamResult eid examUserCsvActUser deleteBy $ UniqueExamResult eid examUserCsvActUser
@ -491,13 +822,13 @@ postEUsersR tid ssh csh examn = do
delete nid delete nid
ExamUserCsvSetCourseNoteData{ examUserCsvActCourseNote = Just note, .. } -> do ExamUserCsvSetCourseNoteData{ examUserCsvActCourseNote = Just note, .. } -> do
now <- liftIO getCurrentTime now <- liftIO getCurrentTime
uid <- liftHandlerT requireAuthId uid <- liftHandler requireAuthId
Entity nid _ <- upsert (CourseUserNote examCourse examUserCsvActUser note) [ CourseUserNoteNote =. note ] Entity nid _ <- upsert (CourseUserNote examCourse examUserCsvActUser note) [ CourseUserNoteNote =. note ]
insert_ $ CourseUserNoteEdit uid now nid insert_ $ CourseUserNoteEdit uid now nid
return $ CExamR tid ssh csh examn EUsersR return $ CExamR tid ssh csh examn EUsersR
, dbtCsvRenderKey = \(registeredUserName -> registeredUserName') -> \case , dbtCsvRenderKey = \(registeredUserName -> registeredUserName') -> \case
ExamUserCsvCourseRegisterData{..} -> do ExamUserCsvCourseRegisterData{..} -> do
(User{..}, occ) <- liftHandlerT . runDB $ (,) <$> getJust examUserCsvActUser <*> for examUserCsvActOccurrence getJust (User{..}, occ) <- liftHandler . runDB $ (,) <$> getJust examUserCsvActUser <*> for examUserCsvActOccurrence getJust
[whamlet| [whamlet|
$newline never $newline never
^{nameWidget userDisplayName userSurname} ^{nameWidget userDisplayName userSurname}
@ -511,7 +842,7 @@ postEUsersR tid ssh csh examn = do
\ (_{MsgExamNoOccurrence}) \ (_{MsgExamNoOccurrence})
|] |]
ExamUserCsvRegisterData{..} -> do ExamUserCsvRegisterData{..} -> do
(User{..}, occ) <- liftHandlerT . runDB $ (,) <$> getJust examUserCsvActUser <*> for examUserCsvActOccurrence getJust (User{..}, occ) <- liftHandler . runDB $ (,) <$> getJust examUserCsvActUser <*> for examUserCsvActOccurrence getJust
[whamlet| [whamlet|
$newline never $newline never
^{nameWidget userDisplayName userSurname} ^{nameWidget userDisplayName userSurname}
@ -521,7 +852,7 @@ postEUsersR tid ssh csh examn = do
\ (_{MsgExamNoOccurrence}) \ (_{MsgExamNoOccurrence})
|] |]
ExamUserCsvAssignOccurrenceData{..} -> do ExamUserCsvAssignOccurrenceData{..} -> do
occ <- for examUserCsvActOccurrence $ liftHandlerT . runDB . getJust occ <- for examUserCsvActOccurrence $ liftHandler . runDB . getJust
[whamlet| [whamlet|
$newline never $newline never
^{registeredUserName' examUserCsvActRegistration} ^{registeredUserName' examUserCsvActRegistration}
@ -531,7 +862,7 @@ postEUsersR tid ssh csh examn = do
\ (_{MsgExamNoOccurrence}) \ (_{MsgExamNoOccurrence})
|] |]
ExamUserCsvSetCourseFieldData{..} -> do ExamUserCsvSetCourseFieldData{..} -> do
User{..} <- liftHandlerT . runDB $ getJust . courseParticipantUser =<< getJust examUserCsvActCourseParticipant User{..} <- liftHandler . runDB $ getJust . courseParticipantUser =<< getJust examUserCsvActCourseParticipant
[whamlet| [whamlet|
$newline never $newline never
^{nameWidget userDisplayName userSurname} ^{nameWidget userDisplayName userSurname}
@ -540,8 +871,34 @@ postEUsersR tid ssh csh examn = do
$nothing $nothing
, _{MsgCourseStudyFeatureNone} , _{MsgCourseStudyFeatureNone}
|] |]
ExamUserCsvSetPartResultData{..} -> do
(User{..}, Entity _ ExamPart{..}) <- liftHandler . runDB $
(,) <$> getJust examUserCsvActUser
<*> getJustBy (UniqueExamPartNumber eid examUserCsvActExamPart)
[whamlet|
$newline never
^{nameWidget userDisplayName userSurname}
$maybe pName <- examPartName
, #{pName}
$nothing
, _{MsgExamPartNumbered examPartNumber}
$maybe newResult <- examUserCsvActExamPartResult
, _{newResult}
$nothing
, _{MsgExamResultNone}
|]
ExamUserCsvSetBonusData{..} -> do
User{..} <- liftHandler . runDB $ getJust examUserCsvActUser
[whamlet|
$newline never
^{nameWidget userDisplayName userSurname}
$maybe newBonus <- examUserCsvActExamBonus
, _{newBonus}
$nothing
, _{MsgExamBonusNone}
|]
ExamUserCsvSetResultData{..} -> do ExamUserCsvSetResultData{..} -> do
User{..} <- liftHandlerT . runDB $ getJust examUserCsvActUser User{..} <- liftHandler . runDB $ getJust examUserCsvActUser
[whamlet| [whamlet|
$newline never $newline never
^{nameWidget userDisplayName userSurname} ^{nameWidget userDisplayName userSurname}
@ -551,7 +908,7 @@ postEUsersR tid ssh csh examn = do
, _{MsgExamResultNone} , _{MsgExamResultNone}
|] |]
ExamUserCsvSetCourseNoteData{..} -> do ExamUserCsvSetCourseNoteData{..} -> do
User{..} <- liftHandlerT . runDB $ getJust examUserCsvActUser User{..} <- liftHandler . runDB $ getJust examUserCsvActUser
[whamlet| [whamlet|
$newline never $newline never
^{nameWidget userDisplayName userSurname} ^{nameWidget userDisplayName userSurname}
@ -651,21 +1008,21 @@ postEUsersR tid ssh csh examn = do
examUsersDBTableValidator = def & defaultSorting [SortAscBy "user-name"] examUsersDBTableValidator = def & defaultSorting [SortAscBy "user-name"]
& defaultPagesize PagesizeAll & defaultPagesize PagesizeAll
postprocess :: FormResult (First ExamUserActionData, DBFormResult ExamRegistrationId Bool ExamUserTableData) -> FormResult (ExamUserActionData, Set ExamRegistrationId) postprocess :: FormResult (First ExamUserActionData, DBFormResult ExamRegistrationId (Bool, ExamUserTableData) ExamUserTableData) -> FormResult (ExamUserActionData, Map ExamRegistrationId ExamUserTableData)
postprocess inp = do postprocess inp = do
(First (Just act), regMap) <- inp (First (Just act), regMap) <- inp
let regSet = Map.keysSet . Map.filter id $ getDBFormResult (const False) regMap let regMap' = Map.mapMaybe (uncurry guardOn) $ getDBFormResult (False,) regMap
return (act, regSet) return (act, regMap')
(, exam) . over _1 postprocess <$> dbTable examUsersDBTableValidator examUsersDBTable (, exam, bonus) . over (_1 . _2) postprocess <$> dbTable examUsersDBTableValidator examUsersDBTable
formResult registrationResult $ \case formResult registrationResult $ \case
(ExamUserDeregisterData, selectedRegistrations) -> do (ExamUserDeregisterData, Map.keysSet -> selectedRegistrations) -> do
nrDel <- runDB $ deleteWhereCount nrDel <- runDB $ deleteWhereCount
[ ExamRegistrationId <-. Set.toList selectedRegistrations [ ExamRegistrationId <-. Set.toList selectedRegistrations
] ]
addMessageI Success $ MsgExamUsersDeregistered nrDel addMessageI Success $ MsgExamUsersDeregistered nrDel
redirect $ CExamR tid ssh csh examn EUsersR redirect $ CExamR tid ssh csh examn EUsersR
(ExamUserAssignOccurrenceData occId, selectedRegistrations) -> do (ExamUserAssignOccurrenceData occId, Map.keysSet -> selectedRegistrations) -> do
nrUpdated <- runDB $ updateWhereCount nrUpdated <- runDB $ updateWhereCount
[ ExamRegistrationId <-. Set.toList selectedRegistrations [ ExamRegistrationId <-. Set.toList selectedRegistrations
] ]
@ -673,9 +1030,67 @@ postEUsersR tid ssh csh examn = do
] ]
addMessageI Success $ MsgExamUsersOccurrenceUpdated nrUpdated addMessageI Success $ MsgExamUsersOccurrenceUpdated nrUpdated
redirect $ CExamR tid ssh csh examn EUsersR redirect $ CExamR tid ssh csh examn EUsersR
(ExamUserAcceptComputedResultData, Map.elems -> rows) -> do
nrAccepted <- fmap (getSum . fold) . runDB . forM rows . runReaderT $ do
now <- liftIO getCurrentTime
uid <- view $ resultUser . _entityKey
hasResult <- asks $ has resultExamResult
hasBonus <- asks $ has resultExamBonus
autoResult <- preview $ resultAutomaticExamResult examVal bonus
autoBonus <- preview $ resultAutomaticExamBonus examVal bonus
lift $ if
| not hasResult
, Just examResultResult <- autoResult
-> do
if
| Just examBonusBonus <- autoBonus
, not hasBonus
-> do
insert_ ExamBonus
{ examBonusExam = eId
, examBonusUser = uid
, examBonusLastChanged = now
, ..
}
audit $ TransactionExamBonusEdit eId uid
| otherwise
-> return ()
insert_ ExamResult
{ examResultExam = eId
, examResultUser = uid
, examResultLastChanged = now
, ..
}
audit $ TransactionExamResultEdit eId uid
return $ Sum 1
| otherwise
-> return mempty
addMessageI Success $ MsgExamUsersResultsAccepted nrAccepted
redirect $ CExamR tid ssh csh examn EUsersR
(ExamUserResetToComputedResultData{..}, Map.elems -> rows) -> do
nrReset <- fmap (getSum . fold) . runDB . forM rows . runReaderT $ do
uid <- view $ resultUser . _entityKey
lift $ do
when examUserResetBonus $ do
bonusId' <- getKeyBy $ UniqueExamBonus eId uid
whenIsJust bonusId' $ \bonusId -> do
delete bonusId
audit $ TransactionExamBonusDeleted eId uid
result <- getKeyBy $ UniqueExamResult eId uid
case result of
Just resId -> do
delete resId
audit $ TransactionExamResultDeleted eId uid
return $ Sum 1
Nothing -> return mempty
addMessageI Success $ MsgExamUsersResultsReset nrReset
redirect $ CExamR tid ssh csh examn EUsersR
closeWgt <- examCloseWidget (SomeRoute $ CExamR tid ssh csh examn EUsersR) eId closeWgt <- examCloseWidget (SomeRoute $ CExamR tid ssh csh examn EUsersR) eId
siteLayoutMsg (prependCourseTitle tid ssh csh MsgExamUsersHeading) $ do siteLayoutMsg (prependCourseTitle tid ssh csh MsgExamUsersHeading) $ do
setTitleI $ prependCourseTitle tid ssh csh MsgExamUsersHeading setTitleI $ prependCourseTitle tid ssh csh MsgExamUsersHeading
let computedValuesTip = $(i18nWidgetFile "exam-users/computed-values-tip")
$(widgetFile "exam-users") $(widgetFile "exam-users")

View File

@ -16,7 +16,7 @@ import Handler.Utils
examOfficeOptOutForm :: UserId -> CourseId -> Maybe (Set SchoolId) -> Form (Set SchoolId) examOfficeOptOutForm :: UserId -> CourseId -> Maybe (Set SchoolId) -> Form (Set SchoolId)
-- ^ Deals with sets of _opt outs_ -- ^ Deals with sets of _opt outs_
examOfficeOptOutForm uid cid (fromMaybe Set.empty -> template) = renderWForm FormStandard $ do examOfficeOptOutForm uid cid (fromMaybe Set.empty -> template) = renderWForm FormStandard $ do
schools <- liftHandlerT . runDB . E.select $ courseExamOfficeSchools (E.val uid) (E.val cid) schools <- liftHandler . runDB . E.select $ courseExamOfficeSchools (E.val uid) (E.val cid)
res <- fmap sequence . forM schools $ \(Entity ssh School{..}, E.Value isForced) res <- fmap sequence . forM schools $ \(Entity ssh School{..}, E.Value isForced)
-> fmap (ssh, ) <$> bool wpopt wforcedJust isForced checkBoxField (fslI schoolName) (Just $ ssh `Set.notMember` template) -> fmap (ssh, ) <$> bool wpopt wforcedJust isForced checkBoxField (fslI schoolName) (Just $ ssh `Set.notMember` template)

View File

@ -1,3 +1,5 @@
{-# OPTIONS_GHC -fno-warn-redundant-constraints #-}
module Handler.ExamOffice.Exam module Handler.ExamOffice.Exam
( getEGradesR, postEGradesR ( getEGradesR, postEGradesR
, examCloseWidget , examCloseWidget
@ -84,7 +86,7 @@ type ExamUserTableData = DBRow ( Entity ExamResult
) )
queryExamRegistration :: Getter ExamUserTableExpr (E.SqlExpr (Maybe (Entity ExamRegistration))) queryExamRegistration :: Getter ExamUserTableExpr (E.SqlExpr (Maybe (Entity ExamRegistration)))
queryExamRegistration = to $ $(E.sqlLOJproj 4 2) queryExamRegistration = to $(E.sqlLOJproj 4 2)
queryUser :: Getter ExamUserTableExpr (E.SqlExpr (Entity User)) queryUser :: Getter ExamUserTableExpr (E.SqlExpr (Entity User))
queryUser = to $ $(E.sqlIJproj 2 2) . $(E.sqlLOJproj 4 1) queryUser = to $ $(E.sqlIJproj 2 2) . $(E.sqlLOJproj 4 1)
@ -213,7 +215,7 @@ postEGradesR tid ssh csh examn = do
partAnchor :: Widget partAnchor :: Widget
partAnchor = do partAnchor = do
let partId = x ^. resultUser . _entityKey let partId = x ^. resultUser . _entityKey
cID <- encrypt partId :: WidgetT UniWorX IO CryptoUUIDUser cID <- encrypt partId :: WidgetFor UniWorX CryptoUUIDUser
[whamlet| [whamlet|
$newline never $newline never
<span ##{toPathPiece cID}> <span ##{toPathPiece cID}>
@ -262,6 +264,7 @@ postEGradesR tid ssh csh examn = do
E.on $ occurrence E.?. ExamOccurrenceExam E.==. E.just (E.val eid) E.on $ occurrence E.?. ExamOccurrenceExam E.==. E.just (E.val eid)
E.&&. occurrence E.?. ExamOccurrenceId E.==. E.joinV (examRegistration E.?. ExamRegistrationOccurrence) E.&&. occurrence E.?. ExamOccurrenceId E.==. E.joinV (examRegistration E.?. ExamRegistrationOccurrence)
E.on $ examRegistration E.?. ExamRegistrationUser E.==. E.just (user E.^. UserId) E.on $ examRegistration E.?. ExamRegistrationUser E.==. E.just (user E.^. UserId)
E.&&. examRegistration E.?. ExamRegistrationExam E.==. E.just (E.val eid)
E.on $ examResult E.^. ExamResultUser E.==. user E.^. UserId E.on $ examResult E.^. ExamResultUser E.==. user E.^. UserId
E.&&. examResult E.^. ExamResultExam E.==. E.val eid E.&&. examResult E.^. ExamResultExam E.==. E.val eid
@ -385,7 +388,7 @@ postEGradesR tid ssh csh examn = do
(res, formWgt) <- multiActionM actionMap (fslI MsgAction) Nothing csrf (res, formWgt) <- multiActionM actionMap (fslI MsgAction) Nothing csrf
let formRes = (, mempty) . First . Just <$> res let formRes = (, mempty) . First . Just <$> res
return (formRes, formWgt) return (formRes, formWgt)
, dbParamsFormEvaluate = liftHandlerT . runFormPost , dbParamsFormEvaluate = liftHandler . runFormPost
, dbParamsFormResult = id , dbParamsFormResult = id
, dbParamsFormIdent = def , dbParamsFormIdent = def
} }
@ -408,6 +411,7 @@ postEGradesR tid ssh csh examn = do
(row ^. resultExamResult . _entityVal . _examResultResult . to (fmap $ bool (Left . view passingGrade) Right examShowGrades)) (row ^. resultExamResult . _entityVal . _examResultResult . to (fmap $ bool (Left . view passingGrade) Right examShowGrades))
, dbtCsvName = unpack csvName , dbtCsvName = unpack csvName
, dbtCsvNoExportData = Nothing , dbtCsvNoExportData = Nothing
, dbtCsvHeader = const . return $ Csv.headerOrder (error "headerOrder" :: ExamUserTableCsv)
} }
dbtCsvDecode = Nothing dbtCsvDecode = Nothing

View File

@ -1,3 +1,5 @@
{-# OPTIONS_GHC -fno-warn-redundant-constraints #-}
module Handler.ExamOffice.Exams module Handler.ExamOffice.Exams
( getEOExamsR ( getEOExamsR
) where ) where

View File

@ -45,7 +45,7 @@ eofModeField = Field{..}
makeExamOfficeFieldsForm :: UserId -> Maybe (Map StudyTermsId Bool) -> Form (Map StudyTermsId Bool) makeExamOfficeFieldsForm :: UserId -> Maybe (Map StudyTermsId Bool) -> Form (Map StudyTermsId Bool)
makeExamOfficeFieldsForm uid template = renderWForm FormStandard $ do makeExamOfficeFieldsForm uid template = renderWForm FormStandard $ do
availableFields <- liftHandlerT . runDB . E.select . E.from $ \(terms `E.InnerJoin` schoolTerms) -> do availableFields <- liftHandler . runDB . E.select . E.from $ \(terms `E.InnerJoin` schoolTerms) -> do
E.on $ terms E.^. StudyTermsId E.==. schoolTerms E.^. SchoolTermsTerms E.on $ terms E.^. StudyTermsId E.==. schoolTerms E.^. SchoolTermsTerms
E.where_ . E.exists . E.from $ \userFunction -> E.where_ . E.exists . E.from $ \userFunction ->
E.where_ $ userFunction E.^. UserFunctionUser E.==. E.val uid E.where_ $ userFunction E.^. UserFunctionUser E.==. E.val uid
@ -75,7 +75,7 @@ getEOFieldsR = postEOFieldsR
postEOFieldsR = do postEOFieldsR = do
uid <- requireAuthId uid <- requireAuthId
oldFields <- liftHandlerT . runDB $ do oldFields <- runDB $ do
fields <- E.select . E.from $ \examOfficeField -> do fields <- E.select . E.from $ \examOfficeField -> do
E.where_ $ examOfficeField E.^. ExamOfficeFieldOffice E.==. E.val uid E.where_ $ examOfficeField E.^. ExamOfficeFieldOffice E.==. E.val uid
return $ (examOfficeField E.^. ExamOfficeFieldField, examOfficeField E.^. ExamOfficeFieldForced) return $ (examOfficeField E.^. ExamOfficeFieldField, examOfficeField E.^. ExamOfficeFieldForced)
@ -84,7 +84,7 @@ postEOFieldsR = do
((fieldsRes, fieldsView), fieldsEnc) <- runFormPost . makeExamOfficeFieldsForm uid $ Just oldFields ((fieldsRes, fieldsView), fieldsEnc) <- runFormPost . makeExamOfficeFieldsForm uid $ Just oldFields
formResult fieldsRes $ \newFields -> do formResult fieldsRes $ \newFields -> do
liftHandlerT . runDB . forM_ (Map.keysSet newFields `Set.union` Map.keysSet oldFields) $ \fieldId -> if runDB . forM_ (Map.keysSet newFields `Set.union` Map.keysSet oldFields) $ \fieldId -> if
| Just forced <- Map.lookup fieldId newFields | Just forced <- Map.lookup fieldId newFields
, fieldId `Map.member` oldFields -> do , fieldId `Map.member` oldFields -> do
updateBy (UniqueExamOfficeField uid fieldId) [ ExamOfficeFieldForced =. forced ] updateBy (UniqueExamOfficeField uid fieldId) [ ExamOfficeFieldForced =. forced ]

View File

@ -67,7 +67,7 @@ examOfficeUserInvitationConfig = InvitationConfig{..}
return . SomeMessage $ MsgExamOfficeUserInviteHeading userDisplayName return . SomeMessage $ MsgExamOfficeUserInviteHeading userDisplayName
invitationExplanation _ _ = return [ihamlet|_{SomeMessage MsgExamOfficeUserInviteExplanation}|] invitationExplanation _ _ = return [ihamlet|_{SomeMessage MsgExamOfficeUserInviteExplanation}|]
invitationTokenConfig _ _ = do invitationTokenConfig _ _ = do
itAuthority <- liftHandlerT requireAuthId itAuthority <- liftHandler requireAuthId
let itExpiresAt = Nothing let itExpiresAt = Nothing
itStartsAt = Nothing itStartsAt = Nothing
itAddAuth = Nothing itAddAuth = Nothing
@ -85,7 +85,7 @@ examOfficeUserInvitationConfig = InvitationConfig{..}
makeExamOfficeUsersForm :: Maybe (Set (Either UserEmail UserId)) -> Form (Set (Either UserEmail UserId)) makeExamOfficeUsersForm :: Maybe (Set (Either UserEmail UserId)) -> Form (Set (Either UserEmail UserId))
makeExamOfficeUsersForm template = renderWForm FormStandard $ do makeExamOfficeUsersForm template = renderWForm FormStandard $ do
Just cRoute <- getCurrentRoute cRoute <- fromMaybe (error "makeExamOfficeUsersForm called from 404-handler") <$> getCurrentRoute
let let
miAdd' :: (Text -> Text) miAdd' :: (Text -> Text)
@ -105,7 +105,7 @@ makeExamOfficeUsersForm template = renderWForm FormStandard $ do
miCell' :: Either UserEmail UserId -> Widget miCell' :: Either UserEmail UserId -> Widget
miCell' (Left email) = $(widgetFile "widgets/massinput/examOfficeUsers/cellInvitation") miCell' (Left email) = $(widgetFile "widgets/massinput/examOfficeUsers/cellInvitation")
miCell' (Right uid) = do miCell' (Right uid) = do
User{..} <- liftHandlerT . runDB $ getJust uid User{..} <- liftHandler . runDB $ getJust uid
$(widgetFile "widgets/massinput/examOfficeUsers/cellKnown") $(widgetFile "widgets/massinput/examOfficeUsers/cellKnown")
miButtonAction' :: forall p. PathPiece p => p -> Maybe (SomeRoute UniWorX) miButtonAction' :: forall p. PathPiece p => p -> Maybe (SomeRoute UniWorX)
miButtonAction' frag = Just . SomeRoute $ cRoute :#: frag miButtonAction' frag = Just . SomeRoute $ cRoute :#: frag
@ -119,7 +119,7 @@ makeExamOfficeUsersForm template = renderWForm FormStandard $ do
fRequired :: Bool fRequired :: Bool
fRequired = False fRequired = False
template' <- for template $ \uids -> liftHandlerT . runDB $ do template' <- for template $ \uids -> liftHandler . runDB $ do
let (invitations, knownUsers) = partitionEithers $ Set.toList uids let (invitations, knownUsers) = partitionEithers $ Set.toList uids
knownUsers' <- fmap (map E.unValue) . E.select . E.from $ \user -> do knownUsers' <- fmap (map E.unValue) . E.select . E.from $ \user -> do
E.where_ $ user E.^. UserId `E.in_` E.valList knownUsers E.where_ $ user E.^. UserId `E.in_` E.valList knownUsers
@ -137,7 +137,7 @@ getEOUsersR = postEOUsersR
postEOUsersR = do postEOUsersR = do
uid <- requireAuthId uid <- requireAuthId
oldUsers <- liftHandlerT . runDB $ do oldUsers <- liftHandler . runDB $ do
users <- E.select . E.from $ \(user `E.InnerJoin` examOfficeUser) -> do users <- E.select . E.from $ \(user `E.InnerJoin` examOfficeUser) -> do
E.on $ user E.^. UserId E.==. examOfficeUser E.^. ExamOfficeUserUser E.on $ user E.^. UserId E.==. examOfficeUser E.^. ExamOfficeUserUser
E.&&. examOfficeUser E.^. ExamOfficeUserOffice E.==. E.val uid E.&&. examOfficeUser E.^. ExamOfficeUserOffice E.==. E.val uid
@ -148,7 +148,7 @@ postEOUsersR = do
((usersRes, usersView), usersEnc) <- runFormPost . makeExamOfficeUsersForm $ Just oldUsers ((usersRes, usersView), usersEnc) <- runFormPost . makeExamOfficeUsersForm $ Just oldUsers
formResult usersRes $ \(setSymmDiff oldUsers -> changes) -> do formResult usersRes $ \(setSymmDiff oldUsers -> changes) -> do
liftHandlerT . runDBJobs . forM_ changes $ \change -> if liftHandler . runDBJobs . forM_ changes $ \change -> if
| change `Set.member` oldUsers -> case change of | change `Set.member` oldUsers -> case change of
Right change' -> do Right change' -> do
deleteBy $ UniqueExamOfficeUser uid change' deleteBy $ UniqueExamOfficeUser uid change'

View File

@ -27,7 +27,7 @@ getHealthR = do
waitResult <- atomically $ maybe (pure $ Left False) (fmap (const $ Left True) . waitDelay) delay <|> (fmap Right . assertM (not. Set.null) $ readTVar reportStore) waitResult <- atomically $ maybe (pure $ Left False) (fmap (const $ Left True) . waitDelay) delay <|> (fmap Right . assertM (not. Set.null) $ readTVar reportStore)
case waitResult of case waitResult of
Left False -> sendResponseStatus noContent204 () Left False -> sendResponseStatus noContent204 ()
Left True -> fail "System is not generating HealthReports" Left True -> sendResponseStatus internalServerError500 ("System is not generating HealthReports" :: Text)
Right _ -> redirect HealthR Right _ -> redirect HealthR
Just healthReports -> do Just healthReports -> do
let (Max lastUpdated, Min status) = ofoldMap1 (Max *** Min . healthReportStatus) healthReports let (Max lastUpdated, Min status) = ofoldMap1 (Max *** Min . healthReportStatus) healthReports

View File

@ -58,7 +58,7 @@ homeUpcomingSheets uid = do
, E.Value UTCTime , E.Value UTCTime
, E.Value (Maybe SubmissionId) , E.Value (Maybe SubmissionId)
)) ))
(DBCell (HandlerT UniWorX IO) ()) (DBCell Handler ())
colonnade = mconcat colonnade = mconcat
[ -- dbRow [ -- dbRow
-- TOOD: sortable (Just "term") (textCell MsgTerm) $ \DBRow{ dbrOutput=(view _1 -> E.Value tid) } -> -- TOOD: sortable (Just "term") (textCell MsgTerm) $ \DBRow{ dbrOutput=(view _1 -> E.Value tid) } ->
@ -82,7 +82,7 @@ homeUpcomingSheets uid = do
(hasTickmark True) (hasTickmark True)
] ]
let validator = def & defaultSorting [SortDescBy "done", SortAscBy "deadline"] let validator = def & defaultSorting [SortDescBy "done", SortAscBy "deadline"]
sheetTable <- liftHandlerT . runDB $ dbTableWidget' validator DBTable sheetTable <- liftHandler . runDB $ dbTableWidget' validator DBTable
{ dbtSQLQuery = tableData { dbtSQLQuery = tableData
, dbtRowKey = \((_ `E.InnerJoin` _ `E.InnerJoin` sheet) `E.LeftOuterJoin` _) -> sheet E.^. SheetId , dbtRowKey = \((_ `E.InnerJoin` _ `E.InnerJoin` sheet) `E.LeftOuterJoin` _) -> sheet E.^. SheetId
, dbtColonnade = colonnade , dbtColonnade = colonnade
@ -127,7 +127,7 @@ homeUpcomingSheets uid = do
homeUpcomingExams :: UserId -> Widget homeUpcomingExams :: UserId -> Widget
homeUpcomingExams uid = do homeUpcomingExams uid = do
now <- liftIO getCurrentTime now <- liftIO getCurrentTime
((Any hasExams, examTable), warningDays) <- liftHandlerT . runDB $ do ((Any hasExams, examTable), warningDays) <- liftHandler . runDB $ do
User {userWarningDays} <- get404 uid User {userWarningDays} <- get404 uid
let fortnight = addUTCTime userWarningDays now let fortnight = addUTCTime userWarningDays now
let -- code copied and slightly adapted from Handler.Course.getCShowR: let -- code copied and slightly adapted from Handler.Course.getCShowR:
@ -202,7 +202,7 @@ homeUpcomingExams uid = do
isRegistered <- existsBy $ UniqueExamRegistration eId uid isRegistered <- existsBy $ UniqueExamRegistration eId uid
if if
| mayRegister -> do | mayRegister -> do
(examRegisterForm, examRegisterEnctype) <- liftHandlerT . generateFormPost . buttonForm' $ bool [BtnRegister] [BtnDeregister] isRegistered (examRegisterForm, examRegisterEnctype) <- liftHandler . generateFormPost . buttonForm' $ bool [BtnRegister] [BtnDeregister] isRegistered
return $ wrapForm examRegisterForm def return $ wrapForm examRegisterForm def
{ formAction = Just . SomeRoute $ CExamR courseTerm courseSchool courseShorthand examName ERegisterR { formAction = Just . SomeRoute $ CExamR courseTerm courseSchool courseShorthand examName ERegisterR
, formEncoding = examRegisterEnctype , formEncoding = examRegisterEnctype

View File

@ -28,7 +28,7 @@ data MaterialForm = MaterialForm
, mfType :: Maybe (CI Text) , mfType :: Maybe (CI Text)
, mfDescription :: Maybe Html , mfDescription :: Maybe Html
, mfVisibleFrom :: Maybe UTCTime , mfVisibleFrom :: Maybe UTCTime
, mfFiles :: Maybe (Source Handler (Either FileId File)) , mfFiles :: Maybe (ConduitT () (Either FileId File) Handler ())
} }
makeMaterialForm :: CourseId -> Maybe MaterialForm -> Form MaterialForm makeMaterialForm :: CourseId -> Maybe MaterialForm -> Form MaterialForm
@ -40,7 +40,7 @@ makeMaterialForm cid template = identifyForm FIDmaterial $ \html -> do
| Just source <- template >>= mfFiles | Just source <- template >>= mfFiles
= runConduit $ source .| C.foldMap setIds = runConduit $ source .| C.foldMap setIds
| otherwise = return Set.empty | otherwise = return Set.empty
typeOptions :: HandlerT UniWorX IO (OptionList (CI Text)) typeOptions :: HandlerFor UniWorX (OptionList (CI Text))
typeOptions = do typeOptions = do
let defaults = Set.fromList $ map (CI.mk . mr) [MsgMaterialTypeSlides,MsgMaterialTypeCode,MsgMaterialTypeExample] let defaults = Set.fromList $ map (CI.mk . mr) [MsgMaterialTypeSlides,MsgMaterialTypeCode,MsgMaterialTypeExample]
previouslyUsed <- runDB $ previouslyUsed <- runDB $
@ -77,8 +77,8 @@ getMaterialKeyBy404 tid ssh csh mnm = do
getKeyBy404 $ UniqueMaterial cid mnm getKeyBy404 $ UniqueMaterial cid mnm
fetchMaterial :: TermId -> SchoolId -> CourseShorthand -> MaterialName -> DB (Entity Material) fetchMaterial :: TermId -> SchoolId -> CourseShorthand -> MaterialName -> DB (Entity Material)
fetchMaterial tid ssh csh mnm = do fetchMaterial tid ssh csh mnm =
[matEnt] <- E.select . E.from $ -- uniqueness guaranteed by DB constraints maybe notFound return . listToMaybe <=< E.select . E.from $ -- uniqueness guaranteed by DB constraints
\(course `E.InnerJoin` material) -> do \(course `E.InnerJoin` material) -> do
E.on $ course E.^. CourseId E.==. material E.^. MaterialCourse E.on $ course E.^. CourseId E.==. material E.^. MaterialCourse
E.where_ $ course E.^. CourseTerm E.==. E.val tid E.where_ $ course E.^. CourseTerm E.==. E.val tid
@ -86,7 +86,6 @@ fetchMaterial tid ssh csh mnm = do
E.&&. course E.^. CourseShorthand E.==. E.val csh E.&&. course E.^. CourseShorthand E.==. E.val csh
E.&&. material E.^. MaterialName E.==. E.val mnm E.&&. material E.^. MaterialName E.==. E.val mnm
return material return material
return matEnt
getMaterialListR :: TermId -> SchoolId -> CourseShorthand -> Handler Html getMaterialListR :: TermId -> SchoolId -> CourseShorthand -> Handler Html
@ -245,7 +244,7 @@ postMEditR tid ssh csh mnm = do
E.on $ matFile E.^. MaterialFileFile E.==. file E.^. FileId E.on $ matFile E.^. MaterialFileFile E.==. file E.^. FileId
E.where_ $ matFile E.^. MaterialFileMaterial E.==. E.val (entityKey matEnt) E.where_ $ matFile E.^. MaterialFileMaterial E.==. E.val (entityKey matEnt)
return $ file E.^. FileId return $ file E.^. FileId
return (matEnt, (Left . E.unValue) <$> fileIds) return (matEnt, Left . E.unValue <$> fileIds)
-- let cid = materialCourse -- let cid = materialCourse
let template = Just MaterialForm let template = Just MaterialForm
{ mfName = materialName { mfName = materialName
@ -308,14 +307,14 @@ handleMaterialEdit tid ssh csh cid template dbMaterial = do
when saveOk $ redirect -- redirect must happen outside of runDB when saveOk $ redirect -- redirect must happen outside of runDB
$ CourseR tid ssh csh (MaterialR mfName MShowR) $ CourseR tid ssh csh (MaterialR mfName MShowR)
insertMaterialFile' :: MaterialId -> Source Handler (Either FileId File) -> DB () insertMaterialFile' :: MaterialId -> ConduitT () (Either FileId File) Handler () -> DB ()
insertMaterialFile' mid fs = do insertMaterialFile' mid fs = do
oldFileIdVals <- E.select . E.from $ \(file `E.InnerJoin` materialFile) -> do oldFileIdVals <- E.select . E.from $ \(file `E.InnerJoin` materialFile) -> do
E.on $ materialFile E.^. MaterialFileFile E.==. file E.^. FileId E.on $ materialFile E.^. MaterialFileFile E.==. file E.^. FileId
E.where_ $ materialFile E.^. MaterialFileMaterial E.==. E.val mid E.where_ $ materialFile E.^. MaterialFileMaterial E.==. E.val mid
return $ file E.^. FileId return $ file E.^. FileId
let oldFileIds = setFromList $ map E.unValue oldFileIdVals let oldFileIds = setFromList $ map E.unValue oldFileIdVals
keep <- execWriterT . runConduit $ transPipe (lift . lift) fs =$= C.mapM_ finsert keep <- execWriterT . runConduit $ transPipe (lift . lift) fs .| C.mapM_ finsert
mapM_ deleteCascade (oldFileIds \\ keep :: Set FileId) mapM_ deleteCascade (oldFileIds \\ keep :: Set FileId)
where where
finsert (Left fileId) = tell $ singleton fileId finsert (Left fileId) = tell $ singleton fileId

View File

@ -94,7 +94,7 @@ schoolsForm template = formToAForm $ schoolsFormView =<< renderWForm FormStandar
where where
schoolsForm' :: WForm Handler (FormResult (Set SchoolId)) schoolsForm' :: WForm Handler (FormResult (Set SchoolId))
schoolsForm' = do schoolsForm' = do
allSchools <- liftHandlerT . runDB $ selectList [] [Asc SchoolName] allSchools <- liftHandler . runDB $ selectList [] [Asc SchoolName]
let let
schoolForm (Entity ssh School{schoolName}) schoolForm (Entity ssh School{schoolName})
@ -116,7 +116,7 @@ schoolsForm template = formToAForm $ schoolsFormView =<< renderWForm FormStandar
notificationForm :: Maybe NotificationSettings -> AForm Handler NotificationSettings notificationForm :: Maybe NotificationSettings -> AForm Handler NotificationSettings
notificationForm template = wFormToAForm $ do notificationForm template = wFormToAForm $ do
mbUid <- liftHandlerT maybeAuthId mbUid <- liftHandler maybeAuthId
isAdmin <- hasReadAccessTo AdminR isAdmin <- hasReadAccessTo AdminR
let let
@ -144,7 +144,7 @@ notificationForm template = wFormToAForm $ do
| otherwise | otherwise
= return False = return False
ntHidden <- liftHandlerT . runDB ntHidden <- liftHandler . runDB
$ Set.fromList universeF $ Set.fromList universeF
& Map.fromSet sectionIsHidden & Map.fromSet sectionIsHidden
& sequenceA & sequenceA

View File

@ -70,7 +70,7 @@ mkSchoolForm mSsh template = renderAForm FormStandard $ SchoolForm
<*> areq (textField & cfStrip & cfCI) (fslI MsgSchoolName) (sfName <$> template) <*> areq (textField & cfStrip & cfCI) (fslI MsgSchoolName) (sfName <$> template)
<*> (Set.fromList . mapMaybe (fmap CI.mk . assertM' (not . Text.null) . Text.strip . CI.original) <$> massInputListA (ciField & addDatalist ldapOrgs) (const "") (const Nothing) ("ldap-organisations" :: Text) (fslI MsgSchoolLdapOrganisations & setTooltip MsgSchoolLdapOrganisationsTip) False (Set.toList . sfOrgUnits <$> template)) <*> (Set.fromList . mapMaybe (fmap CI.mk . assertM' (not . Text.null) . Text.strip . CI.original) <$> massInputListA (ciField & addDatalist ldapOrgs) (const "") (const Nothing) ("ldap-organisations" :: Text) (fslI MsgSchoolLdapOrganisations & setTooltip MsgSchoolLdapOrganisationsTip) False (Set.toList . sfOrgUnits <$> template))
where where
ldapOrgs :: HandlerT UniWorX IO (OptionList (CI Text)) ldapOrgs :: HandlerFor UniWorX (OptionList (CI Text))
ldapOrgs = fmap (mkOptionList . map (\t -> Option (CI.original t) t (CI.original t)) . Set.toAscList) . runDB $ ldapOrgs = fmap (mkOptionList . map (\t -> Option (CI.original t) t (CI.original t)) . Set.toAscList) . runDB $
setOf (folded . _entityVal . _schoolLdapOrgUnit) <$> selectList [] [] setOf (folded . _entityVal . _schoolLdapOrgUnit) <$> selectList [] []

View File

@ -2,7 +2,7 @@
module Handler.Sheet where module Handler.Sheet where
import Import import Import hiding (link)
import Jobs.Queue import Jobs.Queue
@ -69,10 +69,7 @@ data SheetForm = SheetForm
, sfActiveTo :: UTCTime , sfActiveTo :: UTCTime
, sfHintFrom :: Maybe UTCTime , sfHintFrom :: Maybe UTCTime
, sfSolutionFrom :: Maybe UTCTime , sfSolutionFrom :: Maybe UTCTime
, sfSheetF :: Maybe (Source Handler (Either FileId File)) , sfSheetF, sfHintF, sfSolutionF, sfMarkingF :: Maybe (ConduitT () (Either FileId File) Handler ())
, sfHintF :: Maybe (Source Handler (Either FileId File))
, sfSolutionF :: Maybe (Source Handler (Either FileId File))
, sfMarkingF :: Maybe (Source Handler (Either FileId File))
, sfType :: SheetType , sfType :: SheetType
, sfGrouping :: SheetGroup , sfGrouping :: SheetGroup
, sfSubmissionMode :: SubmissionMode , sfSubmissionMode :: SubmissionMode
@ -93,7 +90,7 @@ makeSheetForm :: Maybe SheetId -> Maybe SheetForm -> Form SheetForm
makeSheetForm msId template = identifyForm FIDsheet $ \html -> do makeSheetForm msId template = identifyForm FIDsheet $ \html -> do
oldFileIds <- (return.) <$> case msId of oldFileIds <- (return.) <$> case msId of
Nothing -> return $ partitionFileType mempty Nothing -> return $ partitionFileType mempty
(Just sId) -> liftHandlerT $ runDB $ getFtIdMap sId (Just sId) -> liftHandler $ runDB $ getFtIdMap sId
mr <- getMsgRenderer mr <- getMsgRenderer
ctime <- ceilingQuarterHour <$> liftIO getCurrentTime ctime <- ceilingQuarterHour <$> liftIO getCurrentTime
(result, widget) <- flip (renderAForm FormStandard) html $ SheetForm (result, widget) <- flip (renderAForm FormStandard) html $ SheetForm
@ -637,20 +634,20 @@ postSDelR tid ssh csh shn = do
insertSheetFile :: SheetId -> SheetFileType -> FileInfo -> YesodDB UniWorX () insertSheetFile :: SheetId -> SheetFileType -> FileInfo -> YesodDB UniWorX ()
insertSheetFile sid ftype finfo = do insertSheetFile sid ftype finfo = do
runConduit $ (sourceFiles finfo) =$= C.mapM_ finsert runConduit $ sourceFiles finfo .| C.mapM_ finsert
where where
finsert file = do finsert file = do
fid <- insert file fid <- insert file
void . insert $ SheetFile sid fid ftype -- cannot fail due to uniqueness, since we generated a fresh FileId in the previous step void . insert $ SheetFile sid fid ftype -- cannot fail due to uniqueness, since we generated a fresh FileId in the previous step
insertSheetFile' :: SheetId -> SheetFileType -> Source Handler (Either FileId File) -> YesodDB UniWorX () insertSheetFile' :: SheetId -> SheetFileType -> ConduitT () (Either FileId File) Handler () -> YesodDB UniWorX ()
insertSheetFile' sid ftype fs = do insertSheetFile' sid ftype fs = do
oldFileIds <- fmap setFromList . fmap (map E.unValue) . E.select . E.from $ \(file `E.InnerJoin` sheetFile) -> do oldFileIds <- fmap setFromList . fmap (map E.unValue) . E.select . E.from $ \(file `E.InnerJoin` sheetFile) -> do
E.on $ file E.^. FileId E.==. sheetFile E.^. SheetFileFile E.on $ file E.^. FileId E.==. sheetFile E.^. SheetFileFile
E.where_ $ sheetFile E.^. SheetFileSheet E.==. E.val sid E.where_ $ sheetFile E.^. SheetFileSheet E.==. E.val sid
E.&&. sheetFile E.^. SheetFileType E.==. E.val ftype E.&&. sheetFile E.^. SheetFileType E.==. E.val ftype
return (file E.^. FileId) return (file E.^. FileId)
keep <- execWriterT . runConduit $ transPipe (lift . lift) fs =$= C.mapM_ finsert keep <- execWriterT . runConduit $ transPipe (lift . lift) fs .| C.mapM_ finsert
mapM_ deleteCascade $ (oldFileIds \\ keep :: Set FileId) mapM_ deleteCascade $ (oldFileIds \\ keep :: Set FileId)
where where
finsert (Left fileId) = tell $ singleton fileId finsert (Left fileId) = tell $ singleton fileId
@ -689,13 +686,13 @@ defaultLoads shid = do
return (sheetCorrector E.^. SheetCorrectorUser, sheetCorrector E.^. SheetCorrectorLoad, sheetCorrector E.^. SheetCorrectorState) return (sheetCorrector E.^. SheetCorrectorUser, sheetCorrector E.^. SheetCorrectorLoad, sheetCorrector E.^. SheetCorrectorState)
where where
toMap :: [(E.Value UserId, E.Value Load, E.Value CorrectorState)] -> Loads toMap :: [(E.Value UserId, E.Value Load, E.Value CorrectorState)] -> Loads
toMap = foldMap $ \(E.Value uid, E.Value load, E.Value state) -> Map.singleton (Right uid) (state, load) toMap = foldMap $ \(E.Value uid, E.Value cLoad, E.Value cState) -> Map.singleton (Right uid) (cState, cLoad)
correctorForm :: SheetId -> AForm Handler (Set (Either (Invitation' SheetCorrector) SheetCorrector)) correctorForm :: SheetId -> AForm Handler (Set (Either (Invitation' SheetCorrector) SheetCorrector))
correctorForm shid = wFormToAForm $ do correctorForm shid = wFormToAForm $ do
Just currentRoute <- liftHandlerT getCurrentRoute currentRoute <- fromMaybe (error "correctorForm called from 404-handler") <$> liftHandler getCurrentRoute
userId <- liftHandlerT requireAuthId userId <- liftHandler requireAuthId
MsgRenderer mr <- getMsgRenderer MsgRenderer mr <- getMsgRenderer
let let
@ -703,9 +700,9 @@ correctorForm shid = wFormToAForm $ do
currentLoads = Map.union currentLoads = Map.union
<$> fmap (foldMap $ \(Entity _ SheetCorrector{..}) -> Map.singleton (Right sheetCorrectorUser) (sheetCorrectorState, sheetCorrectorLoad)) (selectList [ SheetCorrectorSheet ==. shid ] []) <$> fmap (foldMap $ \(Entity _ SheetCorrector{..}) -> Map.singleton (Right sheetCorrectorUser) (sheetCorrectorState, sheetCorrectorLoad)) (selectList [ SheetCorrectorSheet ==. shid ] [])
<*> fmap (fmap ((,) <$> invDBSheetCorrectorState <*> invDBSheetCorrectorLoad) . Map.mapKeysMonotonic Left) (sourceInvitationsF shid) <*> fmap (fmap ((,) <$> invDBSheetCorrectorState <*> invDBSheetCorrectorLoad) . Map.mapKeysMonotonic Left) (sourceInvitationsF shid)
(defaultLoads', currentLoads') <- liftHandlerT . runDB $ (,) <$> defaultLoads shid <*> currentLoads (defaultLoads', currentLoads') <- liftHandler . runDB $ (,) <$> defaultLoads shid <*> currentLoads
isWrite <- liftHandlerT $ isWriteRequest currentRoute isWrite <- liftHandler $ isWriteRequest currentRoute
let let
applyDefaultLoads = Map.null currentLoads' && not isWrite applyDefaultLoads = Map.null currentLoads' && not isWrite
@ -766,7 +763,7 @@ correctorForm shid = wFormToAForm $ do
identWidget <- case userIdent of identWidget <- case userIdent of
Left email -> return . toWidget $ mailtoHtml email Left email -> return . toWidget $ mailtoHtml email
Right uid -> do Right uid -> do
User{userEmail, userDisplayName, userSurname} <- liftHandlerT . runDB $ getJust uid User{userEmail, userDisplayName, userSurname} <- liftHandler . runDB $ getJust uid
return $ nameEmailWidget userEmail userDisplayName userSurname return $ nameEmailWidget userEmail userDisplayName userSurname
return (res, $(widgetFile "sheetCorrectors/cell")) return (res, $(widgetFile "sheetCorrectors/cell"))
@ -812,7 +809,7 @@ correctorForm shid = wFormToAForm $ do
postProcess' :: (Either UserEmail UserId, (CorrectorState, Load)) -> Either (Invitation' SheetCorrector) SheetCorrector postProcess' :: (Either UserEmail UserId, (CorrectorState, Load)) -> Either (Invitation' SheetCorrector) SheetCorrector
postProcess' (Right sheetCorrectorUser, (sheetCorrectorState, sheetCorrectorLoad)) = Right SheetCorrector{..} postProcess' (Right sheetCorrectorUser, (sheetCorrectorState, sheetCorrectorLoad)) = Right SheetCorrector{..}
postProcess' (Left email, (state, load)) = Left (email, shid, (InvDBDataSheetCorrector load state, InvTokenDataSheetCorrector)) postProcess' (Left email, (cState, load)) = Left (email, shid, (InvDBDataSheetCorrector load cState, InvTokenDataSheetCorrector))
filledData :: Maybe (Map ListPosition (Either UserEmail UserId, (CorrectorState, Load))) filledData :: Maybe (Map ListPosition (Either UserEmail UserId, (CorrectorState, Load)))
filledData = Just . Map.fromList . zip [0..] $ Map.toList loads -- TODO orderBy Name?! filledData = Just . Map.fromList . zip [0..] $ Map.toList loads -- TODO orderBy Name?!
@ -894,18 +891,22 @@ correctorInvitationConfig = InvitationConfig{..}
Course{..} <- get404 sheetCourse Course{..} <- get404 sheetCourse
return $ CSheetR courseTerm courseSchool courseShorthand sheetName SCorrInviteR return $ CSheetR courseTerm courseSchool courseShorthand sheetName SCorrInviteR
invitationResolveFor _ = do invitationResolveFor _ = do
Just (CSheetR tid csh ssh shn SCorrInviteR) <- getCurrentRoute cRoute <- getCurrentRoute
fetchSheetId tid csh ssh shn case cRoute of
Just (CSheetR tid csh ssh shn SCorrInviteR) ->
fetchSheetId tid csh ssh shn
_other ->
error "correctorInvitationConfig called from unsupported route"
invitationSubject (Entity _ Sheet{..}) _ = do invitationSubject (Entity _ Sheet{..}) _ = do
Course{..} <- get404 sheetCourse Course{..} <- get404 sheetCourse
return . SomeMessage $ MsgMailSubjectCorrectorInvitation courseTerm courseSchool courseShorthand sheetName return . SomeMessage $ MsgMailSubjectCorrectorInvitation courseTerm courseSchool courseShorthand sheetName
invitationHeading (Entity _ Sheet{..}) _ = return . SomeMessage $ MsgSheetCorrInviteHeading sheetName invitationHeading (Entity _ Sheet{..}) _ = return . SomeMessage $ MsgSheetCorrInviteHeading sheetName
invitationExplanation _ _ = return [ihamlet|_{SomeMessage MsgSheetCorrInviteExplanation}|] invitationExplanation _ _ = return [ihamlet|_{SomeMessage MsgSheetCorrInviteExplanation}|]
invitationTokenConfig _ _ = do invitationTokenConfig _ _ = do
itAuthority <- liftHandlerT requireAuthId itAuthority <- liftHandler requireAuthId
return $ InvitationTokenConfig itAuthority Nothing Nothing Nothing return $ InvitationTokenConfig itAuthority Nothing Nothing Nothing
invitationRestriction _ _ = return Authorized invitationRestriction _ _ = return Authorized
invitationForm _ (InvDBDataSheetCorrector load state, _) _ = pure $ (JunctionSheetCorrector load state, ()) invitationForm _ (InvDBDataSheetCorrector cLoad cState, _) _ = pure $ (JunctionSheetCorrector cLoad cState, ())
invitationInsertHook _ _ _ _ = id invitationInsertHook _ _ _ _ = id
invitationSuccessMsg (Entity _ Sheet{..}) _ = return . SomeMessage $ MsgCorrectorInvitationAccepted sheetName invitationSuccessMsg (Entity _ Sheet{..}) _ = return . SomeMessage $ MsgCorrectorInvitationAccepted sheetName
invitationUltDest (Entity _ Sheet{..}) _ = do invitationUltDest (Entity _ Sheet{..}) _ = do

View File

@ -89,9 +89,13 @@ submissionUserInvitationConfig = InvitationConfig{..}
cID <- encrypt subId cID <- encrypt subId
return $ CSubmissionR courseTerm courseSchool courseShorthand sheetName cID SInviteR return $ CSubmissionR courseTerm courseSchool courseShorthand sheetName cID SInviteR
invitationResolveFor _ = do invitationResolveFor _ = do
Just (CSubmissionR _tid _ssh _csh _shn cID SInviteR) <- getCurrentRoute cRoute <- getCurrentRoute
subId <- decrypt cID case cRoute of
bool notFound (return subId) =<< existsKey subId Just (CSubmissionR _tid _ssh _csh _shn cID SInviteR) -> do
subId <- decrypt cID
bool notFound (return subId) =<< existsKey subId
_other ->
error "submissionUserInvitationConfig called from unsupported route"
invitationSubject (Entity _ Submission{..}) _ = do invitationSubject (Entity _ Submission{..}) _ = do
Sheet{..} <- getJust submissionSheet Sheet{..} <- getJust submissionSheet
Course{..} <- getJust sheetCourse Course{..} <- getJust sheetCourse
@ -103,7 +107,7 @@ submissionUserInvitationConfig = InvitationConfig{..}
invitationTokenConfig (Entity _ Submission{..}) _ = do invitationTokenConfig (Entity _ Submission{..}) _ = do
Sheet{..} <- getJust submissionSheet Sheet{..} <- getJust submissionSheet
Course{..} <- getJust sheetCourse Course{..} <- getJust sheetCourse
itAuthority <- liftHandlerT requireAuthId itAuthority <- liftHandler requireAuthId
itAddAuth <- either throwM (return . Just) $ routeAuthTags (CSheetR courseTerm courseSchool courseShorthand sheetName SubmissionNewR) itAddAuth <- either throwM (return . Just) $ routeAuthTags (CSheetR courseTerm courseSchool courseShorthand sheetName SubmissionNewR)
let itExpiresAt = Nothing let itExpiresAt = Nothing
itStartsAt = Nothing itStartsAt = Nothing
@ -121,7 +125,7 @@ submissionUserInvitationConfig = InvitationConfig{..}
return . SomeRoute $ CSubmissionR courseTerm courseSchool courseShorthand sheetName cID SubShowR return . SomeRoute $ CSubmissionR courseTerm courseSchool courseShorthand sheetName cID SubShowR
makeSubmissionForm :: CourseId -> Maybe SubmissionId -> UploadMode -> SheetGroup -> Bool -> Set (Either UserEmail UserId) -> Form (Maybe (Source Handler File), Set (Either UserEmail UserId)) makeSubmissionForm :: CourseId -> Maybe SubmissionId -> UploadMode -> SheetGroup -> Bool -> Set (Either UserEmail UserId) -> Form (Maybe (ConduitT () File Handler ()), Set (Either UserEmail UserId))
makeSubmissionForm cid msmid uploadMode grouping isLecturer prefillUsers = identifyForm FIDsubmission . renderAForm FormStandard $ (,) makeSubmissionForm cid msmid uploadMode grouping isLecturer prefillUsers = identifyForm FIDsubmission . renderAForm FormStandard $ (,)
<$> fileUploadForm (not isLecturer && is _Nothing msmid) (fslI . bool MsgSubmissionFile MsgSubmissionArchive) uploadMode <$> fileUploadForm (not isLecturer && is _Nothing msmid) (fslI . bool MsgSubmissionFile MsgSubmissionArchive) uploadMode
<*> wFormToAForm submittorsForm <*> wFormToAForm submittorsForm
@ -129,7 +133,7 @@ makeSubmissionForm cid msmid uploadMode grouping isLecturer prefillUsers = ident
miCell' :: Markup -> Either UserEmail UserId -> Widget miCell' :: Markup -> Either UserEmail UserId -> Widget
miCell' csrf (Left email) = $(widgetFile "widgets/massinput/submissionUsers/cellInvitation") miCell' csrf (Left email) = $(widgetFile "widgets/massinput/submissionUsers/cellInvitation")
miCell' csrf (Right uid) = do miCell' csrf (Right uid) = do
User{..} <- liftHandlerT . runDB $ getJust uid User{..} <- liftHandler . runDB $ getJust uid
$(widgetFile "widgets/massinput/submissionUsers/cellKnown") $(widgetFile "widgets/massinput/submissionUsers/cellKnown")
miLayout :: ListLength miLayout :: ListLength
@ -191,7 +195,7 @@ makeSubmissionForm cid msmid uploadMode grouping isLecturer prefillUsers = ident
| null submittors' -> FormFailure [mr MsgSubmissionUsersEmpty] | null submittors' -> FormFailure [mr MsgSubmissionUsersEmpty]
| otherwise -> FormSuccess $ Set.fromList submittors' | otherwise -> FormSuccess $ Set.fromList submittors'
| otherwise = do | otherwise = do
uid <- liftHandlerT requireAuthId uid <- liftHandler requireAuthId
mRoute <- getCurrentRoute mRoute <- getCurrentRoute
let let
@ -275,7 +279,7 @@ submissionHelper :: TermId -> SchoolId -> CourseShorthand -> SheetName -> Maybe
submissionHelper tid ssh csh shn mcid = do submissionHelper tid ssh csh shn mcid = do
uid <- requireAuthId uid <- requireAuthId
msmid <- traverse decrypt mcid msmid <- traverse decrypt mcid
Just actionUrl <- getCurrentRoute actionUrl <- fromMaybe (error "submissionHelper called from 404-handler") <$> getCurrentRoute
(Entity shid Sheet{..}, buddies, lastEdits, maySubmit, isLecturer, isOwner) <- runDB $ do (Entity shid Sheet{..}, buddies, lastEdits, maySubmit, isLecturer, isOwner) <- runDB $ do
csheet@(Entity shid Sheet{..}) <- fetchSheet tid ssh csh shn csheet@(Entity shid Sheet{..}) <- fetchSheet tid ssh csh shn
@ -478,7 +482,7 @@ submissionHelper tid ssh csh shn mcid = do
Nothing -> return () Nothing -> return ()
-- Maybe construct a table to display uploaded archive files -- Maybe construct a table to display uploaded archive files
let colonnadeFiles :: _ -> Colonnade Sortable _ (DBCell (HandlerT UniWorX IO) ()) let colonnadeFiles :: _ -> Colonnade Sortable _ (DBCell (HandlerFor UniWorX) ())
colonnadeFiles cid = mconcat colonnadeFiles cid = mconcat
[ sortable (Just "path") (i18nCell MsgFileTitle) $ \(coalesce -> (mOrig, mCorr)) -> let [ sortable (Just "path") (i18nCell MsgFileTitle) $ \(coalesce -> (mOrig, mCorr)) -> let
Just fileTitle' = fileTitle . entityVal . snd <$> (mOrig <|> mCorr) Just fileTitle' = fileTitle . entityVal . snd <$> (mOrig <|> mCorr)

View File

@ -179,7 +179,7 @@ postMessageListR = do
{ dbrOutput = (smE, smT) { dbrOutput = (smE, smT)
, .. , ..
} }
psValidator = def :: PSValidator (MForm (HandlerT UniWorX IO)) (FormResult (Last ActionSystemMessageData, DBFormResult CryptoUUIDSystemMessage Bool MessageListData)) psValidator = def :: PSValidator (MForm Handler) (FormResult (Last ActionSystemMessageData, DBFormResult CryptoUUIDSystemMessage Bool MessageListData))
(tableRes', tableView) <- runDB $ dbTable psValidator DBTable (tableRes', tableView) <- runDB $ dbTable psValidator DBTable
{ dbtSQLQuery { dbtSQLQuery
, dbtRowKey = (E.^. SystemMessageId) , dbtRowKey = (E.^. SystemMessageId)
@ -216,7 +216,7 @@ postMessageListR = do
] ]
(actionRes, action) <- multiActionM actions "" (Just SMActivate) mempty (actionRes, action) <- multiActionM actions "" (Just SMActivate) mempty
return ((, mempty) . Last . Just <$> actionRes, toWidget frag <> action) return ((, mempty) . Last . Just <$> actionRes, toWidget frag <> action)
, dbParamsFormEvaluate = liftHandlerT . runFormPost , dbParamsFormEvaluate = liftHandler . runFormPost
, dbParamsFormResult = id , dbParamsFormResult = id
, dbParamsFormIdent = def , dbParamsFormIdent = def
} }
@ -225,8 +225,8 @@ postMessageListR = do
, dbtCsvDecode = Nothing , dbtCsvDecode = Nothing
} }
let tableRes = tableRes' & mapped._2 %~ Map.keysSet . Map.filter id . getDBFormResult (const False) let tableRes = tableRes' <&> _2 %~ Map.keysSet . Map.filter id . getDBFormResult (const False)
& mapped._1 %~ fromMaybe (error "By construction the form should always return an action") . getLast <&> _1 %~ fromMaybe (error "By construction the form should always return an action") . getLast
case tableRes of case tableRes of
FormMissing -> return () FormMissing -> return ()

View File

@ -176,7 +176,7 @@ postTermEditExistR tid = do
termEditHandler :: TermFormTemplate -> Handler Html termEditHandler :: TermFormTemplate -> Handler Html
termEditHandler term = do termEditHandler term = do
Just eHandler <- getCurrentRoute eHandler <- fromMaybe (error "termEditHandler called from 404-handler") <$> getCurrentRoute
((result, formWidget), formEnctype) <- runFormPost $ newTermForm term ((result, formWidget), formEnctype) <- runFormPost $ newTermForm term
case result of case result of
(FormSuccess res) -> do (FormSuccess res) -> do

View File

@ -14,6 +14,7 @@ import Handler.Utils.Invitations
import Jobs.Queue import Jobs.Queue
import qualified Database.Esqueleto as E import qualified Database.Esqueleto as E
import qualified Database.Esqueleto.Utils as E
import Database.Esqueleto.Utils.TH import Database.Esqueleto.Utils.TH
import Data.Map ((!)) import Data.Map ((!))
@ -198,7 +199,7 @@ postTCommR tid ssh csh tutn = do
) )
] ]
, crRecipientAuth = Just $ \uid -> do , crRecipientAuth = Just $ \uid -> do
[E.Value isTutorialUser] <- E.select . return . E.exists . E.from $ \tutorialUser -> isTutorialUser <- E.selectExists . E.from $ \tutorialUser ->
E.where_ $ tutorialUser E.^. TutorialParticipantUser E.==. E.val uid E.where_ $ tutorialUser E.^. TutorialParticipantUser E.==. E.val uid
E.&&. tutorialUser E.^. TutorialParticipantTutorial E.==. E.val tutid E.&&. tutorialUser E.^. TutorialParticipantTutorial E.==. E.val tutid
@ -250,15 +251,19 @@ tutorInvitationConfig = InvitationConfig{..}
Course{..} <- get404 tutorialCourse Course{..} <- get404 tutorialCourse
return $ CTutorialR courseTerm courseSchool courseShorthand tutorialName TInviteR return $ CTutorialR courseTerm courseSchool courseShorthand tutorialName TInviteR
invitationResolveFor _ = do invitationResolveFor _ = do
Just (CTutorialR tid csh ssh tutn TInviteR) <- getCurrentRoute cRoute <- getCurrentRoute
fetchTutorialId tid csh ssh tutn case cRoute of
Just (CTutorialR tid csh ssh tutn TInviteR) ->
fetchTutorialId tid csh ssh tutn
_other ->
error "tutorInvitationConfig called from unsupported route"
invitationSubject (Entity _ Tutorial{..}) _ = do invitationSubject (Entity _ Tutorial{..}) _ = do
Course{..} <- get404 tutorialCourse Course{..} <- get404 tutorialCourse
return . SomeMessage $ MsgMailSubjectTutorInvitation courseTerm courseSchool courseShorthand tutorialName return . SomeMessage $ MsgMailSubjectTutorInvitation courseTerm courseSchool courseShorthand tutorialName
invitationHeading (Entity _ Tutorial{..}) _ = return . SomeMessage $ MsgTutorInviteHeading tutorialName invitationHeading (Entity _ Tutorial{..}) _ = return . SomeMessage $ MsgTutorInviteHeading tutorialName
invitationExplanation _ _ = return [ihamlet|_{SomeMessage MsgTutorInviteExplanation}|] invitationExplanation _ _ = return [ihamlet|_{SomeMessage MsgTutorInviteExplanation}|]
invitationTokenConfig _ _ = do invitationTokenConfig _ _ = do
itAuthority <- liftHandlerT requireAuthId itAuthority <- liftHandler requireAuthId
return $ InvitationTokenConfig itAuthority Nothing Nothing Nothing return $ InvitationTokenConfig itAuthority Nothing Nothing Nothing
invitationRestriction _ _ = return Authorized invitationRestriction _ _ = return Authorized
invitationForm _ _ _ = pure (JunctionTutor, ()) invitationForm _ _ _ = pure (JunctionTutor, ())
@ -289,8 +294,8 @@ data TutorialForm = TutorialForm
tutorialForm :: CourseId -> Maybe TutorialForm -> Form TutorialForm tutorialForm :: CourseId -> Maybe TutorialForm -> Form TutorialForm
tutorialForm cid template html = do tutorialForm cid template html = do
MsgRenderer mr <- getMsgRenderer MsgRenderer mr <- getMsgRenderer
Just cRoute <- getCurrentRoute cRoute <- fromMaybe (error "tutorialForm called from 404-Handler") <$> getCurrentRoute
uid <- liftHandlerT requireAuthId uid <- liftHandler requireAuthId
let let
tutorForm = Set.fromList <$> massInputAccumA miAdd' miCell' (\p -> Just . SomeRoute $ cRoute :#: p) miLayout' ("tutors" :: Text) (fslI MsgTutorialTutors & setTooltip MsgMassInputTip) True (Set.toList . tfTutors <$> template) tutorForm = Set.fromList <$> massInputAccumA miAdd' miCell' (\p -> Just . SomeRoute $ cRoute :#: p) miLayout' ("tutors" :: Text) (fslI MsgTutorialTutors & setTooltip MsgMassInputTip) True (Set.toList . tfTutors <$> template)
@ -314,7 +319,7 @@ tutorialForm cid template html = do
miCell' (Left email) = miCell' (Left email) =
$(widgetFile "tutorial/tutorMassInput/cellInvitation") $(widgetFile "tutorial/tutorMassInput/cellInvitation")
miCell' (Right userId) = do miCell' (Right userId) = do
User{..} <- liftHandlerT . runDB $ get404 userId User{..} <- liftHandler . runDB $ get404 userId
$(widgetFile "tutorial/tutorMassInput/cellKnown") $(widgetFile "tutorial/tutorMassInput/cellKnown")
miLayout' :: MassInputLayout ListLength (Either UserEmail UserId) () miLayout' :: MassInputLayout ListLength (Either UserEmail UserId) ()
@ -338,7 +343,7 @@ tutorialForm cid template html = do
) (tfDeregisterUntil <$> template) ) (tfDeregisterUntil <$> template)
<*> tutorForm <*> tutorForm
where where
tutTypeDatalist :: HandlerT UniWorX IO (OptionList (CI Text)) tutTypeDatalist :: HandlerFor UniWorX (OptionList (CI Text))
tutTypeDatalist = fmap (mkOptionList . map (\t -> Option (CI.original t) t (CI.original t)) . Set.toAscList) . runDB $ tutTypeDatalist = fmap (mkOptionList . map (\t -> Option (CI.original t) t (CI.original t)) . Set.toAscList) . runDB $
fmap (setOf $ folded . _Value) . E.select . E.from $ \tutorial -> do fmap (setOf $ folded . _Value) . E.select . E.from $ \tutorial -> do
E.where_ $ tutorial E.^. TutorialCourse E.==. E.val cid E.where_ $ tutorial E.^. TutorialCourse E.==. E.val cid

View File

@ -74,7 +74,7 @@ postUsersR = do
, sortable (Just "ldap-sync") (i18nCell MsgLdapSynced) $ \DBRow{ dbrOutput = Entity _ User{..} } -> maybe mempty dateTimeCell userLastLdapSynchronisation , sortable (Just "ldap-sync") (i18nCell MsgLdapSynced) $ \DBRow{ dbrOutput = Entity _ User{..} } -> maybe mempty dateTimeCell userLastLdapSynchronisation
, flip foldMap universeF $ \function -> , flip foldMap universeF $ \function ->
sortable Nothing (i18nCell function) $ \DBRow{ dbrOutput = Entity uid _ } -> flip (set' cellContents) mempty $ do sortable Nothing (i18nCell function) $ \DBRow{ dbrOutput = Entity uid _ } -> flip (set' cellContents) mempty $ do
schools <- liftHandlerT . runDB . E.select . E.from $ \(school `E.InnerJoin` userFunction) -> do schools <- liftHandler . runDB . E.select . E.from $ \(school `E.InnerJoin` userFunction) -> do
E.on $ school E.^. SchoolId E.==. userFunction E.^. UserFunctionSchool E.on $ school E.^. SchoolId E.==. userFunction E.^. UserFunctionSchool
E.where_ $ userFunction E.^. UserFunctionUser E.==. E.val uid E.where_ $ userFunction E.^. UserFunctionUser E.==. E.val uid
E.&&. userFunction E.^. UserFunctionFunction E.==. E.val function E.&&. userFunction E.^. UserFunctionFunction E.==. E.val function
@ -92,7 +92,7 @@ postUsersR = do
, formCellContents = do , formCellContents = do
cID <- encrypt uid cID <- encrypt uid
mayHijack <- (== Authorized) <$> evalAccess (AdminHijackUserR cID) True mayHijack <- (== Authorized) <$> evalAccess (AdminHijackUserR cID) True
myUid <- liftHandlerT maybeAuthId myUid <- liftHandler maybeAuthId
if if
| mayHijack | mayHijack
, Just uid /= myUid , Just uid /= myUid
@ -191,7 +191,7 @@ postUsersR = do
= renderAForm FormStandard = renderAForm FormStandard
$ (, mempty) . First . Just $ (, mempty) . First . Just
<$> areq (selectField $ optionsF [UserLdapSync]) (fslI MsgAction) Nothing <$> areq (selectField $ optionsF [UserLdapSync]) (fslI MsgAction) Nothing
, dbParamsFormEvaluate = liftHandlerT . runFormPost , dbParamsFormEvaluate = liftHandler . runFormPost
, dbParamsFormResult = id , dbParamsFormResult = id
, dbParamsFormIdent = def , dbParamsFormIdent = def
} }
@ -309,7 +309,7 @@ postAdminUserR uuid = do
campusHandler _ = mzero campusHandler _ = mzero
campusResult <- runMaybeT . handle campusHandler $ do campusResult <- runMaybeT . handle campusHandler $ do
(Just pool, Just conf) <- getsYesod $ (,) <$> view _appLdapPool <*> view _appLdapConf (Just pool, Just conf) <- getsYesod $ (,) <$> view _appLdapPool <*> view _appLdapConf
void . Auth.campusUser conf pool $ Creds Auth.apLdap (CI.original userIdent) [] void . lift . Auth.campusUser conf pool $ Creds Auth.apLdap (CI.original userIdent) []
case campusResult of case campusResult of
Nothing -> addMessageI Error MsgAuthLDAPInvalidLookup Nothing -> addMessageI Error MsgAuthLDAPInvalidLookup
_other _other
@ -475,7 +475,7 @@ postUserPasswordR cID = do
formResultModal passResult (bool ProfileR (UserPasswordR cID) isAdmin) $ \newPass -> do formResultModal passResult (bool ProfileR (UserPasswordR cID) isAdmin) $ \newPass -> do
newHash <- fmap decodeUtf8 . liftIO $ PWStore.makePasswordWith pwHashAlgorithm newPass pwHashStrength newHash <- fmap decodeUtf8 . liftIO $ PWStore.makePasswordWith pwHashAlgorithm newPass pwHashStrength
liftHandlerT . runDB $ update tUid [ UserAuthentication =. AuthPWHash newHash ] liftHandler . runDB $ update tUid [ UserAuthentication =. AuthPWHash newHash ]
tell . pure =<< messageI Success MsgPasswordChangedSuccess tell . pure =<< messageI Success MsgPasswordChangedSuccess
siteLayout [whamlet|_{MsgUserPasswordHeadingFor} ^{nameEmailWidget userEmail userDisplayName userSurname}|] $ siteLayout [whamlet|_{MsgUserPasswordHeadingFor} ^{nameEmailWidget userEmail userDisplayName userSurname}|] $
@ -545,7 +545,7 @@ functionInvitationConfig = InvitationConfig{..}
MsgRenderer mr <- getMsgRenderer MsgRenderer mr <- getMsgRenderer
return [ihamlet|_{SomeMessage $ MsgSchoolFunctionInviteExplanation (mr $ SomeMessage invTokenUserFunctionFunction)}|] return [ihamlet|_{SomeMessage $ MsgSchoolFunctionInviteExplanation (mr $ SomeMessage invTokenUserFunctionFunction)}|]
invitationTokenConfig _ (InvDBDataUserFunction{..}, _) = do invitationTokenConfig _ (InvDBDataUserFunction{..}, _) = do
itAuthority <- liftHandlerT requireAuthId itAuthority <- liftHandler requireAuthId
let itExpiresAt = Just $ Just invDBUserFunctionDeadline let itExpiresAt = Just $ Just invDBUserFunctionDeadline
itAddAuth = Nothing itAddAuth = Nothing
itStartsAt = Nothing itStartsAt = Nothing

View File

@ -2,7 +2,7 @@ module Handler.Utils
( module Handler.Utils ( module Handler.Utils
) where ) where
import Import import Import hiding (link)
import qualified Data.Text.Encoding as T import qualified Data.Text.Encoding as T
import Data.Map ((!)) import Data.Map ((!))
@ -38,7 +38,7 @@ sendThisFile File{..}
| otherwise = sendResponseStatus noContent204 () | otherwise = sendResponseStatus noContent204 ()
-- | Serve a single file, identified through a given DB query -- | Serve a single file, identified through a given DB query
serveOneFile :: Source (YesodDB UniWorX) File -> Handler TypedContent serveOneFile :: ConduitT () File (YesodDB UniWorX) () -> Handler TypedContent
serveOneFile source = do serveOneFile source = do
results <- runDB . runConduit $ source .| Conduit.take 2 -- We don't need more than two files to make a decision below results <- runDB . runConduit $ source .| Conduit.take 2 -- We don't need more than two files to make a decision below
case results of case results of
@ -51,7 +51,7 @@ serveOneFile source = do
-- | Serve one file directly or a zip-archive of files, identified through a given DB query -- | Serve one file directly or a zip-archive of files, identified through a given DB query
-- --
-- Like `serveOneFile`, but sends a zip-archive if multiple results are returned -- Like `serveOneFile`, but sends a zip-archive if multiple results are returned
serveSomeFiles :: FilePath -> Source (YesodDB UniWorX) File -> Handler TypedContent serveSomeFiles :: FilePath -> ConduitT () File (YesodDB UniWorX) () -> Handler TypedContent
serveSomeFiles archiveName source = do serveSomeFiles archiveName source = do
results <- runDB . runConduit $ source .| peekN 2 results <- runDB . runConduit $ source .| peekN 2
@ -69,7 +69,7 @@ serveSomeFiles archiveName source = do
-- | Serve any number of files as a zip-archive of files, identified through a given DB query -- | Serve any number of files as a zip-archive of files, identified through a given DB query
-- --
-- Like `serveSomeFiles`, but always sends a zip-archive, even if a single file is returned -- Like `serveSomeFiles`, but always sends a zip-archive, even if a single file is returned
serveZipArchive :: FilePath -> Source (YesodDB UniWorX) File -> Handler TypedContent serveZipArchive :: FilePath -> ConduitT () File (YesodDB UniWorX) () -> Handler TypedContent
serveZipArchive archiveName source = do serveZipArchive archiveName source = do
results <- runDB . runConduit $ source .| peekN 2 results <- runDB . runConduit $ source .| peekN 2
@ -122,7 +122,7 @@ warnTermDays tid timeNames = do
-- | return a value only if the current user ist authorized for a given route -- | return a value only if the current user ist authorized for a given route
guardAuthorizedFor :: ( HandlerSite h ~ UniWorX, MonadHandler h, MonadLogger h guardAuthorizedFor :: ( HandlerSite h ~ UniWorX, MonadHandler h, MonadThrow h
, MonadTrans m, MonadPlus (m (ReaderT SqlBackend h))) , MonadTrans m, MonadPlus (m (ReaderT SqlBackend h)))
=> Route UniWorX -> a -> m (ReaderT SqlBackend h) a => Route UniWorX -> a -> m (ReaderT SqlBackend h) a
guardAuthorizedFor link val = guardAuthorizedFor link val =
@ -138,7 +138,7 @@ runAppLoggingT app@(appLogger -> (_, loggerTVar)) = flip runLoggingT logFunc
studyFeaturesWidget :: StudyFeaturesId -> Widget studyFeaturesWidget :: StudyFeaturesId -> Widget
studyFeaturesWidget featId = do studyFeaturesWidget featId = do
(StudyFeatures{studyFeaturesSemester}, (degree, terms)) <- liftHandlerT . runDB . ($ featId) . runKleisli $ Kleisli getJust >>> Kleisli return &&& Kleisli (getJust . studyFeaturesDegree) &&& Kleisli (getJust . studyFeaturesField) (StudyFeatures{studyFeaturesSemester}, (degree, terms)) <- liftHandler . runDB . ($ featId) . runKleisli $ Kleisli getJust >>> Kleisli return &&& Kleisli (getJust . studyFeaturesDegree) &&& Kleisli (getJust . studyFeaturesField)
[whamlet| [whamlet|
$newline never $newline never
_{StudyDegreeTerm degree terms}, _{MsgStudyFeatureAge} #{studyFeaturesSemester} _{StudyDegreeTerm degree terms}, _{MsgStudyFeatureAge} #{studyFeaturesSemester}

View File

@ -71,7 +71,7 @@ instance RenderMessage UniWorX RecipientCategory where
data CommunicationRoute = CommunicationRoute data CommunicationRoute = CommunicationRoute
{ crRecipients :: Map RecipientGroup (E.SqlQuery (E.SqlExpr (Entity User))) { crRecipients :: Map RecipientGroup (E.SqlQuery (E.SqlExpr (Entity User)))
, crRecipientAuth :: Maybe (UserId -> DB AuthResult) -- ^ Only resolve userids given as GET-Parameter if they fulfil this criterion , crRecipientAuth :: Maybe (UserId -> DB AuthResult) -- ^ Only resolve userids given as GET-Parameter if they fulfil this criterion
, crJobs :: Communication -> Source (YesodDB UniWorX) Job , crJobs :: Communication -> ConduitT () Job (YesodDB UniWorX) ()
, crHeading :: SomeMessage UniWorX , crHeading :: SomeMessage UniWorX
, crUltDest :: SomeRoute UniWorX , crUltDest :: SomeRoute UniWorX
} }
@ -150,9 +150,9 @@ commR CommunicationRoute{..} = do
-> Map (EnumPosition RecipientCategory, ListPosition) (FieldView UniWorX) -> Map (EnumPosition RecipientCategory, ListPosition) (FieldView UniWorX)
-> Map (Natural, (EnumPosition RecipientCategory, ListPosition)) Widget -> Map (Natural, (EnumPosition RecipientCategory, ListPosition)) Widget
-> Widget -> Widget
miLayout liveliness state cellWdgts _delButtons addWdgts = do miLayout liveliness cState cellWdgts _delButtons addWdgts = do
checkedIdentBase <- newIdent checkedIdentBase <- newIdent
let checkedCategories = Set.mapMonotonic (unEnumPosition . fst) . Set.filter (\k' -> Map.foldrWithKey (\k (_, checkState) -> (||) $ k == k' && checkState /= FormSuccess False && (checkState /= FormMissing || maybe True snd (chosenRecipients' !? k))) False state) $ Map.keysSet state let checkedCategories = Set.mapMonotonic (unEnumPosition . fst) . Set.filter (\k' -> Map.foldrWithKey (\k (_, checkState) -> (||) $ k == k' && checkState /= FormSuccess False && (checkState /= FormMissing || maybe True snd (chosenRecipients' !? k))) False cState) $ Map.keysSet cState
checkedIdent c = checkedIdentBase <> "-" <> toPathPiece c checkedIdent c = checkedIdentBase <> "-" <> toPathPiece c
hasContent c = not (null $ categoryIndices c) || Map.member (1, (EnumPosition c, 0)) addWdgts hasContent c = not (null $ categoryIndices c) || Map.member (1, (EnumPosition c, 0)) addWdgts
categoryIndices c = Set.filter ((== c) . unEnumPosition . fst) $ review liveCoords liveliness categoryIndices c = Set.filter ((== c) . unEnumPosition . fst) $ review liveCoords liveliness
@ -165,12 +165,15 @@ commR CommunicationRoute{..} = do
postProcess :: Map (EnumPosition RecipientCategory, ListPosition) (Either UserEmail UserId, Bool) -> Set (Either UserEmail UserId) postProcess :: Map (EnumPosition RecipientCategory, ListPosition) (Either UserEmail UserId, Bool) -> Set (Either UserEmail UserId)
postProcess = Set.fromList . map fst . filter snd . Map.elems postProcess = Set.fromList . map fst . filter snd . Map.elems
recipientsListMsg <- messageI Info MsgCommRecipientsList
((commRes,commWdgt),commEncoding) <- runFormPost . identifyForm FIDCommunication . renderAForm FormStandard $ Communication ((commRes,commWdgt),commEncoding) <- runFormPost . identifyForm FIDCommunication . renderAForm FormStandard $ Communication
<$> recipientAForm <$> recipientAForm
<* aformMessage recipientsListMsg
<*> aopt textField (fslI MsgCommSubject) Nothing <*> aopt textField (fslI MsgCommSubject) Nothing
<*> areq htmlField (fslpI MsgCommBody "Html") Nothing <*> areq htmlField (fslpI MsgCommBody "Html" & setTooltip MsgCommBodyTip) Nothing
formResult commRes $ \comm -> do formResult commRes $ \comm -> do
runDBJobs . runConduit $ hoist (mapReaderT lift) (crJobs comm) .| sinkDBJobs runDBJobs . runConduit $ transPipe (mapReaderT lift) (crJobs comm) .| sinkDBJobs
addMessageI Success . MsgCommSuccess . Set.size $ cRecipients comm addMessageI Success . MsgCommSuccess . Set.size $ cRecipients comm
redirect crUltDest redirect crUltDest
@ -183,4 +186,3 @@ commR CommunicationRoute{..} = do
siteLayoutMsg crHeading $ do siteLayoutMsg crHeading $ do
setTitleI crHeading setTitleI crHeading
formWdgt formWdgt
$(i18nWidgetFile "html-input")

View File

@ -8,7 +8,7 @@ import Import
-- | Check whether the user's preference for files is inline-viewing or downloading -- | Check whether the user's preference for files is inline-viewing or downloading
downloadFiles :: (MonadHandler m, HandlerSite m ~ UniWorX) => m Bool downloadFiles :: (MonadHandler m, HandlerSite m ~ UniWorX) => m Bool
downloadFiles = do downloadFiles = do
mauth <- liftHandlerT maybeAuth mauth <- liftHandler maybeAuth
case mauth of case mauth of
Just (Entity _ User{..}) -> return userDownloadFiles Just (Entity _ User{..}) -> return userDownloadFiles
Nothing -> do Nothing -> do

View File

@ -1,17 +1,16 @@
{-# OPTIONS_GHC -fno-warn-orphans #-} {-# OPTIONS_GHC -fno-warn-orphans #-}
module Handler.Utils.Csv module Handler.Utils.Csv
( typeCsv, extensionCsv ( decodeCsv
, decodeCsv
, encodeCsv , encodeCsv
, encodeDefaultOrderedCsv
, respondCsv, respondCsvDB , respondCsv, respondCsvDB
, respondDefaultOrderedCsv, respondDefaultOrderedCsvDB
, fileSourceCsv , fileSourceCsv
, CsvParseError(..) , CsvParseError(..)
, ToNamedRecord(..), FromNamedRecord(..) , ToNamedRecord(..), FromNamedRecord(..)
, DefaultOrdered(..) , DefaultOrdered(..)
, ToField(..), FromField(..) , ToField(..), FromField(..)
, CsvRendered(..)
, toCsvRendered
) where ) where
import Import hiding (Header, mapM_) import Import hiding (Header, mapM_)
@ -37,19 +36,7 @@ import qualified Data.ByteString.Lazy as LBS
import qualified Data.Attoparsec.ByteString.Lazy as A import qualified Data.Attoparsec.ByteString.Lazy as A
deriving instance Typeable CsvParseError decodeCsv :: (MonadThrow m, FromNamedRecord csv, MonadLogger m) => ConduitT ByteString csv m ()
instance Exception CsvParseError
typeCsv, typeCsv' :: ContentType
typeCsv = simpleContentType typeCsv'
typeCsv' = "text/csv; charset=UTF-8; header=present"
extensionCsv :: Extension
extensionCsv = fromMaybe "csv" $ listToMaybe [ ext | (ext, mime) <- Map.toList mimeMap, mime == typeCsv ]
decodeCsv :: (MonadThrow m, FromNamedRecord csv, MonadLogger m) => Conduit ByteString m csv
decodeCsv = transPipe throwExceptT $ do decodeCsv = transPipe throwExceptT $ do
testBuffer <- accumTestBuffer LBS.empty testBuffer <- accumTestBuffer LBS.empty
mapM_ leftover $ LBS.toChunks testBuffer mapM_ leftover $ LBS.toChunks testBuffer
@ -111,47 +98,67 @@ decodeCsv = transPipe throwExceptT $ do
encodeCsv :: ( ToNamedRecord csv encodeCsv :: ( ToNamedRecord csv
, DefaultOrdered csv
, Monad m , Monad m
) )
=> Conduit csv m ByteString => Header
-> ConduitT csv ByteString m ()
-- ^ Encode a stream of records -- ^ Encode a stream of records
-- --
-- Currently not streaming -- Currently not streaming
encodeCsv = fmap encodeDefaultOrderedByName (C.foldMap pure) >>= C.sourceLazy encodeCsv hdr = fmap (encodeByName hdr) (C.foldMap pure) >>= C.sourceLazy
encodeDefaultOrderedCsv :: forall csv m.
( ToNamedRecord csv
, DefaultOrdered csv
, Monad m
)
=> ConduitT csv ByteString m ()
encodeDefaultOrderedCsv = encodeCsv $ headerOrder (error "headerOrder" :: csv)
respondCsv :: ( ToNamedRecord csv respondCsv :: ToNamedRecord csv
, DefaultOrdered csv => Header
) -> ConduitT () csv (HandlerFor site) ()
=> Source (HandlerT site IO) csv -> HandlerFor site TypedContent
-> HandlerT site IO TypedContent respondCsv hdr src = respondSource typeCsv' $ src .| encodeCsv hdr .| awaitForever sendChunk
respondCsv src = respondSource typeCsv' $ src .| encodeCsv .| awaitForever sendChunk
respondDefaultOrderedCsv :: forall csv site.
( ToNamedRecord csv
, DefaultOrdered csv
)
=> ConduitT () csv (HandlerFor site) ()
-> HandlerFor site TypedContent
respondDefaultOrderedCsv = respondCsv $ headerOrder (error "headerOrder" :: csv)
respondCsvDB :: ( ToNamedRecord csv respondCsvDB :: ( ToNamedRecord csv
, DefaultOrdered csv
, YesodPersistRunner site , YesodPersistRunner site
) )
=> Source (YesodDB site) csv => Header
-> HandlerT site IO TypedContent -> ConduitT () csv (YesodDB site) ()
respondCsvDB src = respondSourceDB typeCsv' $ src .| encodeCsv .| awaitForever sendChunk -> HandlerFor site TypedContent
respondCsvDB hdr src = respondSourceDB typeCsv' $ src .| encodeCsv hdr .| awaitForever sendChunk
respondDefaultOrderedCsvDB :: forall csv site.
( ToNamedRecord csv
, DefaultOrdered csv
, YesodPersistRunner site
)
=> ConduitT () csv (YesodDB site) ()
-> HandlerFor site TypedContent
respondDefaultOrderedCsvDB = respondCsvDB $ headerOrder (error "headerOrder" :: csv)
fileSourceCsv :: ( FromNamedRecord csv fileSourceCsv :: ( FromNamedRecord csv
, MonadResource m , MonadResource m
, MonadLogger m , MonadLogger m
, MonadThrow m
) )
=> FileInfo => FileInfo
-> Source m csv -> ConduitT () csv m ()
fileSourceCsv = (.| decodeCsv) . fileSource fileSourceCsv = (.| decodeCsv) . fileSource
data CsvRendered = CsvRendered
{ csvRenderedHeader :: Header
, csvRenderedData :: [NamedRecord]
} deriving (Eq, Read, Show, Generic, Typeable)
instance ToWidget UniWorX CsvRendered where instance ToWidget UniWorX CsvRendered where
toWidget CsvRendered{..} = liftWidgetT $(widgetFile "widgets/csvRendered") toWidget CsvRendered{..} = liftWidget $(widgetFile "widgets/csvRendered")
where where
csvData = [ [ decodeUtf8 <$> HashMap.lookup columnKey row csvData = [ [ decodeUtf8 <$> HashMap.lookup columnKey row
| columnKey <- Vector.toList csvRenderedHeader | columnKey <- Vector.toList csvRenderedHeader
@ -160,14 +167,3 @@ instance ToWidget UniWorX CsvRendered where
] ]
headers = decodeUtf8 <$> Vector.toList csvRenderedHeader headers = decodeUtf8 <$> Vector.toList csvRenderedHeader
toCsvRendered :: forall mono.
( ToNamedRecord (Element mono)
, DefaultOrdered (Element mono)
, MonoFoldable mono
)
=> mono -> CsvRendered
toCsvRendered (otoList -> csvs) = CsvRendered{..}
where
csvRenderedHeader = headerOrder (error "not forced" :: Element mono)
csvRenderedData = map toNamedRecord csvs

View File

@ -34,10 +34,10 @@ getSchoolsOf uid uschool uuser = fmap (Import.map E.unValue) $ E.select $ E.from
-- | Sub-Query to retrieve StudyFeatures with their human-readable names -- | Sub-Query to retrieve StudyFeatures with their human-readable names
studyFeaturesQuery :: E.Esqueleto query expr backend studyFeaturesQuery
=> expr (E.Value StudyFeaturesId) -- ^ query is joined on this @StudyFeaturesId@ :: E.SqlExpr (E.Value StudyFeaturesId) -- ^ query is joined on this @StudyFeaturesId@
-> expr (Entity StudyFeatures) `E.InnerJoin` expr (Entity StudyDegree) `E.InnerJoin` expr (Entity StudyTerms) -> E.SqlExpr (Entity StudyFeatures) `E.InnerJoin` E.SqlExpr (Entity StudyDegree) `E.InnerJoin` E.SqlExpr (Entity StudyTerms)
-> query (expr (Entity StudyFeatures), expr (Entity StudyDegree), expr (Entity StudyTerms)) -> E.SqlQuery (E.SqlExpr (Entity StudyFeatures), E.SqlExpr (Entity StudyDegree), E.SqlExpr (Entity StudyTerms))
studyFeaturesQuery studyFeaturesId (features `E.InnerJoin` degree `E.InnerJoin` terms) = do studyFeaturesQuery studyFeaturesId (features `E.InnerJoin` degree `E.InnerJoin` terms) = do
E.on $ terms E.^. StudyTermsId E.==. features E.^. StudyFeaturesField E.on $ terms E.^. StudyTermsId E.==. features E.^. StudyFeaturesField
E.on $ degree E.^. StudyDegreeId E.==. features E.^. StudyFeaturesDegree E.on $ degree E.^. StudyDegreeId E.==. features E.^. StudyFeaturesDegree

View File

@ -75,7 +75,7 @@ instance HasLocalTime UTCTime where
instance HasLocalTime TimeOfDay where instance HasLocalTime TimeOfDay where
toLocalTime = LocalTime systemEpochDay toLocalTime = LocalTime systemEpochDay
formatTime' :: (HasLocalTime t, MonadHandler m, HandlerSite m ~ UniWorX) => String -> t -> m Text formatTime' :: (HasLocalTime t, MonadHandler m) => String -> t -> m Text
formatTime' fmtStr t = fmap fromString $ Time.formatTime <$> getTimeLocale <*> pure fmtStr <*> pure (toLocalTime t) formatTime' fmtStr t = fmap fromString $ Time.formatTime <$> getTimeLocale <*> pure fmtStr <*> pure (toLocalTime t)
-- formatTime :: (FormatTime t, MonadHandler m, HandlerSite m ~ UniWorX, IsString str) => (DateTimeFormat -> String) -> t -> m str -- formatTime :: (FormatTime t, MonadHandler m, HandlerSite m ~ UniWorX, IsString str) => (DateTimeFormat -> String) -> t -> m str
@ -92,12 +92,12 @@ formatTimeW s t = toWidget =<< formatTime s t
formatTimeMail :: (MonadMail m, HasLocalTime t) => SelDateTimeFormat -> t -> m Text formatTimeMail :: (MonadMail m, HasLocalTime t) => SelDateTimeFormat -> t -> m Text
formatTimeMail sel t = fmap fromString $ Time.formatTime <$> (getTimeLocale' . mailLanguages <$> askMailLanguages) <*> (unDateTimeFormat <$> askMailDateTimeFormat sel) <*> pure (toLocalTime t) formatTimeMail sel t = fmap fromString $ Time.formatTime <$> (getTimeLocale' . mailLanguages <$> askMailLanguages) <*> (unDateTimeFormat <$> askMailDateTimeFormat sel) <*> pure (toLocalTime t)
getTimeLocale :: (MonadHandler m, HandlerSite m ~ UniWorX) => m TimeLocale getTimeLocale :: MonadHandler m => m TimeLocale
getTimeLocale = getTimeLocale' <$> languages getTimeLocale = getTimeLocale' <$> languages
getDateTimeFormat :: (MonadHandler m, HandlerSite m ~ UniWorX) => SelDateTimeFormat -> m DateTimeFormat getDateTimeFormat :: (MonadHandler m, HandlerSite m ~ UniWorX) => SelDateTimeFormat -> m DateTimeFormat
getDateTimeFormat sel = do getDateTimeFormat sel = do
mauth <- liftHandlerT maybeAuth mauth <- liftHandler maybeAuth
UserDefaultConf{..} <- getsYesod $ view _appUserDefaults UserDefaultConf{..} <- getsYesod $ view _appUserDefaults
let let
fmt fmt

View File

@ -29,12 +29,12 @@ import qualified Database.Esqueleto.Internal.Sql as E (SqlSelect)
import qualified Database.Esqueleto.Internal.Language as E (From) import qualified Database.Esqueleto.Internal.Language as E (From)
data DeleteRoute record = forall tables infoExpr info. (E.SqlSelect infoExpr info, E.From E.SqlQuery E.SqlExpr SqlBackend tables) => DeleteRoute data DeleteRoute record = forall tables infoExpr info. (E.SqlSelect infoExpr info, E.From tables) => DeleteRoute
{ drRecords :: Set (Key record) -- ^ Records to be deleted { drRecords :: Set (Key record) -- ^ Records to be deleted
, drGetInfo :: tables -> E.SqlQuery infoExpr -- ^ SQL-Query to get necessary information to render identifing information about records to the user (`drRenderRecord`, `drRecordConfirmString`); @tables@ is an arbitrary join, see `E.from`; @infoExpr@ gets converted to @info@ by esqueleto , drGetInfo :: tables -> E.SqlQuery infoExpr -- ^ SQL-Query to get necessary information to render identifing information about records to the user (`drRenderRecord`, `drRecordConfirmString`); @tables@ is an arbitrary join, see `E.from`; @infoExpr@ gets converted to @info@ by esqueleto
, drUnjoin :: tables -> E.SqlExpr (Entity record) -- ^ `E.SqlExpr` of @Key record@ extracted from @tables@, `deleteR` restricts `drGetInfo` to `drRecords` automatically , drUnjoin :: tables -> E.SqlExpr (Entity record) -- ^ `E.SqlExpr` of @Key record@ extracted from @tables@, `deleteR` restricts `drGetInfo` to `drRecords` automatically
, drRenderRecord :: info -> ReaderT SqlBackend (HandlerT UniWorX IO) Widget -- ^ Present a single record, to be deleted, to the user for inspection prior to deletion , drRenderRecord :: info -> DB Widget -- ^ Present a single record, to be deleted, to the user for inspection prior to deletion
, drRecordConfirmString :: info -> ReaderT SqlBackend (HandlerT UniWorX IO) Text -- ^ Text for the user to copy to confirm deletion; should probably contain all information from `drRenderRecord` so user gets prompted to think about what they're deleting , drRecordConfirmString :: info -> DB Text -- ^ Text for the user to copy to confirm deletion; should probably contain all information from `drRenderRecord` so user gets prompted to think about what they're deleting
, drCaption , drCaption
, drSuccessMessage :: SomeMessage UniWorX , drSuccessMessage :: SomeMessage UniWorX
, drAbort , drAbort
@ -98,7 +98,7 @@ getDeleteR DeleteRoute{..} = do
(deleteFormWdgt, deleteFormEnctype) <- generateFormPost $ confirmForm' drRecords confirmString (deleteFormWdgt, deleteFormEnctype) <- generateFormPost $ confirmForm' drRecords confirmString
Just targetRoute <- getCurrentRoute targetRoute <- fromMaybe (error "getDeleteR called from 404-handler") <$> getCurrentRoute
let deleteForm = wrapForm deleteFormWdgt def let deleteForm = wrapForm deleteFormWdgt def
{ formAction = Just $ SomeRoute targetRoute { formAction = Just $ SomeRoute targetRoute
, formEncoding = deleteFormEnctype , formEncoding = deleteFormEnctype

View File

@ -2,6 +2,7 @@ module Handler.Utils.Exam
( fetchExamAux ( fetchExamAux
, fetchExam, fetchExamId, fetchCourseIdExamId, fetchCourseIdExam , fetchExam, fetchExamId, fetchCourseIdExamId, fetchCourseIdExam
, examBonus, examBonusPossible, examBonusAchieved , examBonus, examBonusPossible, examBonusAchieved
, examResultBonus, examGrade
) where ) where
import Import.NoFoundation import Import.NoFoundation
@ -16,6 +17,8 @@ import qualified Data.Conduit.List as C
import qualified Data.Map as Map import qualified Data.Map as Map
import Data.Fixed (Fixed(..))
fetchExamAux :: ( SqlBackendCanRead backend fetchExamAux :: ( SqlBackendCanRead backend
, E.SqlSelect b a , E.SqlSelect b a
@ -75,9 +78,86 @@ examBonus (Entity eId Exam{..}) = runConduit $
) )
return (examRegistration E.^. ExamRegistrationUser, sheet E.^. SheetType, submission) return (examRegistration E.^. ExamRegistrationUser, sheet E.^. SheetType, submission)
accum = C.fold ?? Map.empty $ \acc (E.Value uid, E.Value sheetType, fmap entityVal -> sub) -> accum = C.fold ?? Map.empty $ \acc (E.Value uid, E.Value sheetType, fmap entityVal -> sub) ->
Map.unionWith mappend acc . Map.singleton uid . sheetTypeSum sheetType . (>>= submissionRatingPoints) $ assertM submissionRatingDone sub flip (Map.insertWith mappend uid) acc . sheetTypeSum sheetType $ assertM submissionRatingDone sub >>= submissionRatingPoints
in rawData .| accum in rawData .| accum
examBonusPossible, examBonusAchieved :: UserId -> Map UserId SheetTypeSummary -> Maybe SheetGradeSummary examBonusPossible, examBonusAchieved :: UserId -> Map UserId SheetTypeSummary -> SheetGradeSummary
examBonusPossible uid bonusMap = normalSummary <$> Map.lookup uid bonusMap examBonusPossible uid bonusMap = normalSummary $ Map.findWithDefault mempty uid bonusMap
examBonusAchieved uid bonusMap = (mappend <$> normalSummary <*> bonusSummary) <$> Map.lookup uid bonusMap examBonusAchieved uid bonusMap = mappend <$> normalSummary <*> bonusSummary $ Map.findWithDefault mempty uid bonusMap
examResultBonus :: ExamBonusRule
-> SheetGradeSummary -- ^ `examBonusPossible`
-> SheetGradeSummary -- ^ `examBonusAchieved`
-> Points
examResultBonus bonusRule bonusPossible bonusAchieved = case bonusRule of
ExamBonusManual{}
-> 0
ExamBonusPoints{..}
-> roundToPoints bonusRound $ toRational bonusMaxPoints * bonusProp
where
bonusProp :: Rational
bonusProp
| possible <= 0 = 1
| otherwise = achieved / possible
where
achieved = toRational (getSum $ achievedPoints bonusAchieved) + scalePasses (getSum $ achievedPasses bonusAchieved)
possible = toRational (getSum $ sumSheetsPoints bonusPossible) + scalePasses (getSum $ numSheetsPasses bonusPossible)
scalePasses :: Integer -> Rational
-- ^ Rescale passes so count of all sheets with pass is worth as many points as sum of all sheets with points
scalePasses passes
| passesPossible <= 0 = 0
| otherwise = fromInteger passes / fromInteger passesPossible * toRational pointsPossible
where
passesPossible = getSum $ numSheetsPasses bonusPossible
pointsPossible = getSum $ sumSheetsPoints bonusPossible
roundToPoints :: forall a. HasResolution a => Fixed a -> Rational -> Fixed a
-- ^ 'round-to-nearest' whole multiple
roundToPoints (MkFixed mult'@(fromInteger -> mult)) ((* toRational (resolution (Proxy @a))) -> raw)
= MkFixed . (* mult') $
let (whole, frac) = raw `divMod'` mult
in if | abs frac < abs (mult / 2)
-> whole
| raw >= 0
-> succ whole
| otherwise
-> pred whole
examGrade :: ( MonoFoldable mono
, Element mono ~ ExamResultPoints
)
=> Exam
-> Maybe Points -- ^ Bonus
-> mono -- ^ `ExamPartResult`s
-> Maybe ExamResultGrade
examGrade Exam{..} mBonus (otoList -> results)
= traverse pointsToGrade achievedPoints'
where
achievedPoints' :: ExamResultPoints
achievedPoints' = withBonus . getSum <$> foldMap (fmap Sum) results
withBonus :: Points -> Points
withBonus ps
| Just bonusRule <- examBonusRule
= if
| maybe True not (bonusRule ^? _bonusOnlyPassed)
|| fmap (view passingGrade) (pointsToGrade ps) == Just (_Wrapped # True)
-> maybe id (+) mBonus ps
| otherwise
-> ps
| otherwise
= ps
pointsToGrade :: Points -> Maybe ExamGrade
pointsToGrade ps = examGradingRule <&> \case
ExamGradingKey{..}
-> gradeFromKey examGradingKey
where
gradeFromKey :: [Points] -> ExamGrade
gradeFromKey examGradingKey' = maximum $ Grade50 `ncons` [ g | (g, b) <- lowerBounds, b <= ps ]
where
lowerBounds :: [(ExamGrade, Points)]
lowerBounds = zip [Grade40, Grade37 ..] examGradingKey'

View File

@ -214,7 +214,7 @@ optionalActionW' minp justAct fs defAction = aFormToWForm $ optionalActionA' min
multiAction :: forall action a. multiAction :: forall action a.
( RenderMessage UniWorX action, PathPiece action, Ord action, Eq action ) ( RenderMessage UniWorX action, PathPiece action, Ord action )
=> Map action (AForm Handler a) => Map action (AForm Handler a)
-> FieldSettings UniWorX -> FieldSettings UniWorX
-> Maybe action -> Maybe action
@ -235,22 +235,22 @@ multiAction acts fs@FieldSettings{..} defAction csrf = do
return ((actionResults Map.!) =<< actionRes, over _fvInput (mappend $ toWidget csrf) actionView : actionViews) return ((actionResults Map.!) =<< actionRes, over _fvInput (mappend $ toWidget csrf) actionView : actionViews)
multiActionA :: (RenderMessage UniWorX action, PathPiece action, Ord action, Eq action) multiActionA :: (RenderMessage UniWorX action, PathPiece action, Ord action)
=> Map action (AForm (HandlerT UniWorX IO) a) => Map action (AForm Handler a)
-> FieldSettings UniWorX -> FieldSettings UniWorX
-> Maybe action -> Maybe action
-> AForm Handler a -> AForm Handler a
multiActionA acts fSettings defAction = formToAForm $ multiAction acts fSettings defAction mempty multiActionA acts fSettings defAction = formToAForm $ multiAction acts fSettings defAction mempty
multiActionW :: (RenderMessage UniWorX action, PathPiece action, Ord action, Eq action) multiActionW :: (RenderMessage UniWorX action, PathPiece action, Ord action)
=> Map action (AForm Handler a) => Map action (AForm Handler a)
-> FieldSettings UniWorX -> FieldSettings UniWorX
-> Maybe action -> Maybe action
-> WForm Handler (FormResult a) -> WForm Handler (FormResult a)
multiActionW acts fSettings defAction = aFormToWForm $ multiActionA acts fSettings defAction multiActionW acts fSettings defAction = aFormToWForm $ multiActionA acts fSettings defAction
multiActionM :: (RenderMessage UniWorX action, PathPiece action, Ord action, Eq action) multiActionM :: (RenderMessage UniWorX action, PathPiece action, Ord action)
=> Map action (AForm (HandlerT UniWorX IO) a) => Map action (AForm Handler a)
-> FieldSettings UniWorX -> FieldSettings UniWorX
-> Maybe action -> Maybe action
-> (Html -> MForm Handler (FormResult a, Widget)) -> (Html -> MForm Handler (FormResult a, Widget))
@ -279,7 +279,7 @@ routeField :: ( Monad m
routeField = checkMMap (return . maybe (Left MsgInvalidRoute) Right . fromPathPiece) toPathPiece textField routeField = checkMMap (return . maybe (Left MsgInvalidRoute) Right . fromPathPiece) toPathPiece textField
-- | Variant that simply removes leading and trailing white space -- | Variant that simply removes leading and trailing white space
htmlField' :: Field (HandlerT UniWorX IO) Html htmlField' :: Field Handler Html
htmlField' = htmlField htmlField' = htmlField
{ fieldParse = \vs fis -> fieldParse htmlField (map Text.strip vs) fis { fieldParse = \vs fis -> fieldParse htmlField (map Text.strip vs) fis
} }
@ -444,9 +444,11 @@ uploadModeForm prev = multiActionA actions (fslI MsgSheetUploadMode) (classifyUp
specificFileForm :: AForm Handler (NonNull (Set UploadSpecificFile)) specificFileForm :: AForm Handler (NonNull (Set UploadSpecificFile))
specificFileForm = wFormToAForm $ do specificFileForm = wFormToAForm $ do
Just currentRoute <- getCurrentRoute currentRoute' <- getCurrentRoute
let miButtonAction :: forall p. PathPiece p => p -> Maybe (SomeRoute UniWorX) let miButtonAction :: forall p. PathPiece p => p -> Maybe (SomeRoute UniWorX)
miButtonAction frag = Just . SomeRoute $ currentRoute :#: frag miButtonAction frag = do
currentRoute <- currentRoute'
return . SomeRoute $ currentRoute :#: frag
miIdent <- ("specific-files--" <>) <$> newIdent miIdent <- ("specific-files--" <>) <$> newIdent
postProcess =<< massInputW MassInput{..} (fslI MsgUploadSpecificFiles & setTooltip MsgMassInputTip) True (preProcess <$> prev ^? _Just . _specificFiles) postProcess =<< massInputW MassInput{..} (fslI MsgUploadSpecificFiles & setTooltip MsgMassInputTip) True (preProcess <$> prev ^? _Just . _specificFiles)
where where
@ -518,7 +520,7 @@ submissionModeForm prev = multiActionA actions (fslI MsgSheetSubmissionMode) $ c
) )
] ]
data ExamBonusRule' = ExamNoBonus' data ExamBonusRule' = ExamBonusManual'
| ExamBonusPoints' | ExamBonusPoints'
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable) deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable)
instance Universe ExamBonusRule' instance Universe ExamBonusRule'
@ -529,7 +531,7 @@ embedRenderMessage ''UniWorX ''ExamBonusRule' id
classifyBonusRule :: ExamBonusRule -> ExamBonusRule' classifyBonusRule :: ExamBonusRule -> ExamBonusRule'
classifyBonusRule = \case classifyBonusRule = \case
ExamNoBonus -> ExamNoBonus' ExamBonusManual{} -> ExamBonusManual'
ExamBonusPoints{} -> ExamBonusPoints' ExamBonusPoints{} -> ExamBonusPoints'
examBonusRuleForm :: Maybe ExamBonusRule -> AForm Handler ExamBonusRule examBonusRuleForm :: Maybe ExamBonusRule -> AForm Handler ExamBonusRule
@ -537,18 +539,19 @@ examBonusRuleForm prev = multiActionA actions (fslI MsgExamBonusRule) $ classify
where where
actions :: Map ExamBonusRule' (AForm Handler ExamBonusRule) actions :: Map ExamBonusRule' (AForm Handler ExamBonusRule)
actions = Map.fromList actions = Map.fromList
[ ( ExamNoBonus' [ ( ExamBonusManual'
, pure ExamNoBonus , ExamBonusManual
<$> (fromMaybe False <$> aopt checkBoxField (fslI MsgExamBonusOnlyPassed) (Just <$> preview _bonusOnlyPassed =<< prev))
) )
, ( ExamBonusPoints' , ( ExamBonusPoints'
, ExamBonusPoints , ExamBonusPoints
<$> apreq (checkBool (> 0) MsgExamBonusMaxPointsNonPositive pointsField) (fslI MsgExamBonusMaxPoints) (preview _bonusMaxPoints =<< prev) <$> apreq (checkBool (> 0) MsgExamBonusMaxPointsNonPositive pointsField) (fslI MsgExamBonusMaxPoints & setTooltip MsgExamBonusMaxPointsTip) (preview _bonusMaxPoints =<< prev)
<*> (fromMaybe False <$> aopt checkBoxField (fslI MsgExamBonusOnlyPassed) (Just <$> preview _bonusOnlyPassed =<< prev)) <*> (fromMaybe False <$> aopt checkBoxField (fslI MsgExamBonusOnlyPassed) (Just <$> preview _bonusOnlyPassed =<< prev))
<*> areq (checkBool (> 0) MsgExamBonusRoundNonPositive pointsField) (fslI MsgExamBonusRound & setTooltip MsgExamBonusRoundTip) (preview _bonusRound =<< prev)
) )
] ]
data ExamOccurrenceRule' = ExamRoomManual' data ExamOccurrenceRule' = ExamRoomSurname'
| ExamRoomSurname'
| ExamRoomMatriculation' | ExamRoomMatriculation'
| ExamRoomRandom' | ExamRoomRandom'
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable) deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable)
@ -560,7 +563,6 @@ embedRenderMessage ''UniWorX ''ExamOccurrenceRule' id
classifyExamOccurrenceRule :: ExamOccurrenceRule -> ExamOccurrenceRule' classifyExamOccurrenceRule :: ExamOccurrenceRule -> ExamOccurrenceRule'
classifyExamOccurrenceRule = \case classifyExamOccurrenceRule = \case
ExamRoomManual -> ExamRoomManual'
ExamRoomSurname -> ExamRoomSurname' ExamRoomSurname -> ExamRoomSurname'
ExamRoomMatriculation -> ExamRoomMatriculation' ExamRoomMatriculation -> ExamRoomMatriculation'
ExamRoomRandom -> ExamRoomRandom' ExamRoomRandom -> ExamRoomRandom'
@ -569,13 +571,11 @@ examOccurrenceRuleForm :: Maybe ExamOccurrenceRule -> AForm Handler ExamOccurren
examOccurrenceRuleForm = fmap reverseClassify . areq (selectField optionsFinite) (fslI MsgExamOccurrenceRule) . fmap classifyExamOccurrenceRule examOccurrenceRuleForm = fmap reverseClassify . areq (selectField optionsFinite) (fslI MsgExamOccurrenceRule) . fmap classifyExamOccurrenceRule
where where
reverseClassify = \case reverseClassify = \case
ExamRoomManual' -> ExamRoomManual
ExamRoomSurname' -> ExamRoomSurname ExamRoomSurname' -> ExamRoomSurname
ExamRoomMatriculation' -> ExamRoomMatriculation ExamRoomMatriculation' -> ExamRoomMatriculation
ExamRoomRandom' -> ExamRoomRandom ExamRoomRandom' -> ExamRoomRandom
data ExamGradingRule' = ExamGradingManual' data ExamGradingRule' = ExamGradingKey'
| ExamGradingKey'
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable) deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable)
instance Universe ExamGradingRule' instance Universe ExamGradingRule'
instance Finite ExamGradingRule' instance Finite ExamGradingRule'
@ -585,7 +585,6 @@ embedRenderMessage ''UniWorX ''ExamGradingRule' id
classifyExamGradingRule :: ExamGradingRule -> ExamGradingRule' classifyExamGradingRule :: ExamGradingRule -> ExamGradingRule'
classifyExamGradingRule = \case classifyExamGradingRule = \case
ExamGradingManual -> ExamGradingManual'
ExamGradingKey{} -> ExamGradingKey' ExamGradingKey{} -> ExamGradingKey'
examGradingRuleForm :: Maybe ExamGradingRule -> AForm Handler ExamGradingRule examGradingRuleForm :: Maybe ExamGradingRule -> AForm Handler ExamGradingRule
@ -593,10 +592,7 @@ examGradingRuleForm prev = multiActionA actions (fslI MsgExamGradingRule) $ clas
where where
actions :: Map ExamGradingRule' (AForm Handler ExamGradingRule) actions :: Map ExamGradingRule' (AForm Handler ExamGradingRule)
actions = Map.fromList actions = Map.fromList
[ ( ExamGradingManual' [ ( ExamGradingKey'
, pure ExamGradingManual
)
, ( ExamGradingKey'
, ExamGradingKey <$> gradingKeyForm (fslI MsgExamGradingKey & setTooltip MsgExamGradingKeyTip) (preview _examGradingKey =<< prev) , ExamGradingKey <$> gradingKeyForm (fslI MsgExamGradingKey & setTooltip MsgExamGradingKeyTip) (preview _examGradingKey =<< prev)
) )
] ]
@ -659,7 +655,7 @@ pseudonymWordField = checkMMap doCheck id $ ciField & addDatalist (return $ mkOp
| otherwise | otherwise
= return . Left $ MsgUnknownPseudonymWord (CI.original w) = return . Left $ MsgUnknownPseudonymWord (CI.original w)
specificFileField :: UploadSpecificFile -> Field Handler (Source Handler File) specificFileField :: UploadSpecificFile -> Field Handler (ConduitT () File Handler ())
specificFileField UploadSpecificFile{..} = Field{..} specificFileField UploadSpecificFile{..} = Field{..}
where where
fieldEnctype = Multipart fieldEnctype = Multipart
@ -677,7 +673,7 @@ specificFileField UploadSpecificFile{..} = Field{..}
zipFileField :: Bool -- ^ Unpack zips? zipFileField :: Bool -- ^ Unpack zips?
-> Maybe (NonNull (Set Extension)) -- ^ Restrictions on file extensions -> Maybe (NonNull (Set Extension)) -- ^ Restrictions on file extensions
-> Field Handler (Source Handler File) -> Field Handler (ConduitT () File Handler ())
zipFileField doUnpack permittedExtensions = Field{..} zipFileField doUnpack permittedExtensions = Field{..}
where where
fieldEnctype = Multipart fieldEnctype = Multipart
@ -696,7 +692,7 @@ zipFileField doUnpack permittedExtensions = Field{..}
fileUploadForm :: Bool -- ^ Required? fileUploadForm :: Bool -- ^ Required?
-> (Bool -> FieldSettings UniWorX) -- ^ given @unpackZips@ generate `FieldSettings` in the case of `UploadAny` -> (Bool -> FieldSettings UniWorX) -- ^ given @unpackZips@ generate `FieldSettings` in the case of `UploadAny`
-> UploadMode -> AForm Handler (Maybe (Source Handler File)) -> UploadMode -> AForm Handler (Maybe (ConduitT () File Handler ()))
fileUploadForm isReq mkFs = \case fileUploadForm isReq mkFs = \case
NoUpload NoUpload
-> pure Nothing -> pure Nothing
@ -705,21 +701,21 @@ fileUploadForm isReq mkFs = \case
UploadSpecific{..} UploadSpecific{..}
-> mergeFileSources <$> sequenceA (map specificFileForm . Set.toList $ toNullable specificFiles) -> mergeFileSources <$> sequenceA (map specificFileForm . Set.toList $ toNullable specificFiles)
where where
specificFileForm :: UploadSpecificFile -> AForm Handler (Maybe (Source Handler File)) specificFileForm :: UploadSpecificFile -> AForm Handler (Maybe (ConduitT () File Handler ()))
specificFileForm spec@UploadSpecificFile{..} specificFileForm spec@UploadSpecificFile{..}
= bool (\f fs d -> aopt f fs $ fmap Just d) (\f fs d -> Just <$> areq f fs d) specificFileRequired (specificFileField spec) (fsl specificFileLabel) Nothing = bool (\f fs d -> aopt f fs $ fmap Just d) (\f fs d -> Just <$> areq f fs d) specificFileRequired (specificFileField spec) (fsl specificFileLabel) Nothing
mergeFileSources :: [Maybe (Source Handler File)] -> Maybe (Source Handler File) mergeFileSources :: [Maybe (ConduitT () File Handler ())] -> Maybe (ConduitT () File Handler ())
mergeFileSources (catMaybes -> sources) = case sources of mergeFileSources (catMaybes -> sources) = case sources of
[] -> Nothing [] -> Nothing
fs -> Just $ sequence_ fs fs -> Just $ sequence_ fs
multiFileField' :: Source Handler (Either FileId File) -- ^ Permitted files in same format as produced by `multiFileField` multiFileField' :: ConduitT () (Either FileId File) Handler () -- ^ Permitted files in same format as produced by `multiFileField`
-> Field Handler (Source Handler (Either FileId File)) -> Field Handler (ConduitT () (Either FileId File) Handler ())
multiFileField' permittedFiles = multiFileField . runConduit $ permittedFiles .| C.mapMaybe (preview _Left) .| C.foldMap Set.singleton multiFileField' permittedFiles = multiFileField . runConduit $ permittedFiles .| C.mapMaybe (preview _Left) .| C.foldMap Set.singleton
multiFileField :: Handler (Set FileId) -- ^ Set of files that may be submitted by id-reference multiFileField :: Handler (Set FileId) -- ^ Set of files that may be submitted by id-reference
-> Field Handler (Source Handler (Either FileId File)) -> Field Handler (ConduitT () (Either FileId File) Handler ())
multiFileField permittedFiles' = Field{..} multiFileField permittedFiles' = Field{..}
where where
fieldEnctype = Multipart fieldEnctype = Multipart
@ -735,7 +731,7 @@ multiFileField permittedFiles' = Field{..}
.| C.filter (`elem` pVals) .| C.filter (`elem` pVals)
.| C.map Left .| C.map Left
let let
handleFile :: FileInfo -> Source Handler File handleFile :: FileInfo -> ConduitT () File Handler ()
handleFile handleFile
| doUnpack = sourceFiles | doUnpack = sourceFiles
| otherwise = yieldM . acceptFile | otherwise = yieldM . acceptFile
@ -899,7 +895,7 @@ utcTimeField = checkMMap (return . localTimeToUTC') utcToLocalTime localTimeFiel
langField :: Bool -- ^ Only allow values from `appLanguages` langField :: Bool -- ^ Only allow values from `appLanguages`
-> Field (HandlerT UniWorX IO) Lang -> Field Handler Lang
langField False = checkBool langCheck MsgInvalidLangFormat $ textField & addDatalist appLanguagesOpts langField False = checkBool langCheck MsgInvalidLangFormat $ textField & addDatalist appLanguagesOpts
where langCheck (T.splitOn "-" -> lParts) where langCheck (T.splitOn "-" -> lParts)
= all ((&&) <$> not . null <*> T.all Char.isAlpha) lParts = all ((&&) <$> not . null <*> T.all Char.isAlpha) lParts
@ -922,7 +918,7 @@ jsonField hide = Field{..}
fieldParse [encodeUtf8 -> v] [] = return . bimap (SomeMessage . MsgJSONFieldDecodeFailure) Just $ eitherDecodeStrict' v <|> eitherDecodeStrict' (urlDecode True v) fieldParse [encodeUtf8 -> v] [] = return . bimap (SomeMessage . MsgJSONFieldDecodeFailure) Just $ eitherDecodeStrict' v <|> eitherDecodeStrict' (urlDecode True v)
fieldParse [] [] = return $ Right Nothing fieldParse [] [] = return $ Right Nothing
fieldParse _ _ = return . Left $ SomeMessage MsgValueRequired fieldParse _ _ = return . Left $ SomeMessage MsgValueRequired
fieldView theId name attrs val isReq = liftWidgetT [whamlet| fieldView theId name attrs val isReq = liftWidget [whamlet|
<input id=#{theId} name=#{name} *{attrs} type=#{inputType} :isReq:required value=#{either fromStrict encodeToLazyText val}> <input id=#{theId} name=#{name} *{attrs} type=#{inputType} :isReq:required value=#{either fromStrict encodeToLazyText val}>
|] |]
fieldEnctype = UrlEncoded fieldEnctype = UrlEncoded
@ -1019,7 +1015,7 @@ fsUniq mkUnique seed = "" { fsName = Just $ mkUnique seed }
optionsPersistCryptoId :: forall site backend a msg. optionsPersistCryptoId :: forall site backend a msg.
( YesodPersist site ( YesodPersist site
, PersistQueryRead backend , PersistQueryRead backend
, HasCryptoUUID (Key a) (HandlerT site IO) , HasCryptoUUID (Key a) (HandlerFor site)
, RenderMessage site msg , RenderMessage site msg
, YesodPersistBackend site ~ backend , YesodPersistBackend site ~ backend
, PersistRecordBackend a backend , PersistRecordBackend a backend
@ -1027,7 +1023,7 @@ optionsPersistCryptoId :: forall site backend a msg.
=> [Filter a] => [Filter a]
-> [SelectOpt a] -> [SelectOpt a]
-> (a -> msg) -> (a -> msg)
-> HandlerT site IO (OptionList (Entity a)) -> HandlerFor site (OptionList (Entity a))
optionsPersistCryptoId filts ords toDisplay = fmap mkOptionList $ do optionsPersistCryptoId filts ords toDisplay = fmap mkOptionList $ do
mr <- getMessageRender mr <- getMessageRender
pairs <- runDB $ selectList filts ords pairs <- runDB $ selectList filts ords
@ -1044,7 +1040,7 @@ examOccurrenceField :: ( MonadHandler m
=> ExamId => ExamId
-> Field m ExamOccurrenceId -> Field m ExamOccurrenceId
examOccurrenceField eid examOccurrenceField eid
= hoistField liftHandlerT . selectField . (fmap $ fmap entityKey) = hoistField liftHandler . selectField . (fmap $ fmap entityKey)
$ optionsPersistCryptoId [ ExamOccurrenceExam ==. eid ] [ Asc ExamOccurrenceName ] examOccurrenceName $ optionsPersistCryptoId [ ExamOccurrenceExam ==. eid ] [ Asc ExamOccurrenceName ] examOccurrenceName
@ -1080,7 +1076,7 @@ userMatriculationField = Field{..}
fieldParse ts _ = runExceptT . fmap Just $ do fieldParse ts _ = runExceptT . fmap Just $ do
let ts' = concatMap (Text.splitOn ",") ts let ts' = concatMap (Text.splitOn ",") ts
forM ts' $ \matr -> do forM ts' $ \matr -> do
dbRes <- liftHandlerT . runDB . E.select . E.from $ \user -> do dbRes <- liftHandler . runDB . E.select . E.from $ \user -> do
E.where_ $ E.strip (user E.^. UserMatrikelnummer) `E.ciEq` E.just (E.val $ Text.strip matr) E.where_ $ E.strip (user E.^. UserMatrikelnummer) `E.ciEq` E.just (E.val $ Text.strip matr)
return user return user
case dbRes of case dbRes of
@ -1114,7 +1110,7 @@ multiUserField onlySuggested suggestions = Field{..}
rEmails <- case lookupExpr of rEmails <- case lookupExpr of
Nothing -> return [] Nothing -> return []
Just lookupExpr' -> fmap concat . forM uids $ \uid -> do Just lookupExpr' -> fmap concat . forM uids $ \uid -> do
dbRes <- liftHandlerT . runDB . E.select $ do dbRes <- liftHandler . runDB . E.select $ do
user <- lookupExpr' user <- lookupExpr'
E.where_ $ user E.^. UserId E.==. E.val uid E.where_ $ user E.^. UserId E.==. E.val uid
return $ user E.^. UserEmail return $ user E.^. UserEmail
@ -1131,7 +1127,7 @@ multiUserField onlySuggested suggestions = Field{..}
|] |]
whenIsJust suggestions $ \suggestions' -> do whenIsJust suggestions $ \suggestions' -> do
suggestedEmails <- fmap (Set.fromList . map E.unValue) . liftHandlerT . runDB . E.select $ do suggestedEmails <- fmap (Set.fromList . map E.unValue) . liftHandler . runDB . E.select $ do
user <- suggestions' user <- suggestions'
return $ user E.^. UserEmail return $ user E.^. UserEmail
[whamlet| [whamlet|
@ -1147,14 +1143,14 @@ multiUserField onlySuggested suggestions = Field{..}
fmap Set.fromList . forM emails $ \(CI.mk -> email) -> case lookupExpr of fmap Set.fromList . forM emails $ \(CI.mk -> email) -> case lookupExpr of
Nothing -> return $ Left email Nothing -> return $ Left email
Just lookupExpr' -> do Just lookupExpr' -> do
dbRes <- liftHandlerT . runDB . E.select $ do dbRes <- liftHandler . runDB . E.select $ do
user <- lookupExpr' user <- lookupExpr'
E.where_ $ user E.^. UserEmail E.==. E.val email E.where_ $ user E.^. UserEmail E.==. E.val email
return $ user E.^. UserId return $ user E.^. UserId
case dbRes of case dbRes of
[] -> return $ Left email [] -> return $ Left email
[E.Value uid] -> return $ Right uid [E.Value uid] -> return $ Right uid
_other -> fail "Ambiguous e-mail addr" _other -> throwE $ SomeMessage ("Ambiguous e-mail addr" :: Text)
examResultField :: forall m res. examResultField :: forall m res.
( MonadHandler m ( MonadHandler m
@ -1195,11 +1191,11 @@ examGradeField :: forall m.
, HandlerSite m ~ UniWorX , HandlerSite m ~ UniWorX
) )
=> Field m ExamGrade => Field m ExamGrade
examGradeField = hoistField liftHandlerT $ selectField optionsFinite examGradeField = hoistField liftHandler $ selectField optionsFinite
examPassedField :: forall m. examPassedField :: forall m.
( MonadHandler m ( MonadHandler m
, HandlerSite m ~ UniWorX , HandlerSite m ~ UniWorX
) )
=> Field m ExamPassed => Field m ExamPassed
examPassedField = hoistField liftHandlerT $ selectField optionsFinite examPassedField = hoistField liftHandler $ selectField optionsFinite

View File

@ -1,5 +1,5 @@
{-# OPTIONS_GHC -fno-warn-orphans #-} -- tupleBoxCoord {-# OPTIONS_GHC -fno-warn-orphans #-} -- tupleBoxCoord
{-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE GeneralizedNewtypeDeriving, UndecidableInstances #-}
module Handler.Utils.Form.MassInput module Handler.Utils.Form.MassInput
( MassInput(..), MassInputLayout ( MassInput(..), MassInputLayout
@ -271,7 +271,7 @@ massInput :: forall handler cellData cellResult liveliness.
( MonadHandler handler, HandlerSite handler ~ UniWorX ( MonadHandler handler, HandlerSite handler ~ UniWorX
, ToJSON cellData, FromJSON cellData , ToJSON cellData, FromJSON cellData
, Liveliness liveliness , Liveliness liveliness
, MonadLogger handler , MonadThrow handler
) )
=> MassInput handler liveliness cellData cellResult => MassInput handler liveliness cellData cellResult
-> FieldSettings UniWorX -> FieldSettings UniWorX
@ -414,7 +414,7 @@ massInput MassInput{ miIdent = toPathPiece -> miIdent, ..} FieldSettings{..} fvR
MsgRenderer mr <- getMsgRenderer MsgRenderer mr <- getMsgRenderer
whenM ((== Just miIdent) <$> lookupCustomHeader HeaderMassInputShortcircuit) . liftHandlerT $ do whenM ((== Just miIdent) <$> lookupCustomHeader HeaderMassInputShortcircuit) . liftHandler $ do
PageContent{..} <- widgetToPageContent $(widgetFile "widgets/massinput/massinput-standalone") PageContent{..} <- widgetToPageContent $(widgetFile "widgets/massinput/massinput-standalone")
ur <- getUrlRenderParams ur <- getUrlRenderParams
@ -459,7 +459,7 @@ listMiLayout lLength _ cellWdgts delButtons addWdgts = $(widgetFile "widgets/mas
-- | Wrapper around `massInput` for the common case, that we just want an arbitrary list of single fields without any constraints -- | Wrapper around `massInput` for the common case, that we just want an arbitrary list of single fields without any constraints
massInputList :: forall handler cellResult ident. massInputList :: forall handler cellResult ident.
( MonadHandler handler, HandlerSite handler ~ UniWorX ( MonadHandler handler, HandlerSite handler ~ UniWorX
, MonadLogger handler , MonadThrow handler
, PathPiece ident , PathPiece ident
) )
=> Field handler cellResult => Field handler cellResult
@ -488,7 +488,7 @@ massInputList field fieldSettings miButtonAction miIdent miSettings miRequired m
massInputListA :: forall handler cellResult ident. massInputListA :: forall handler cellResult ident.
( MonadHandler handler, HandlerSite handler ~ UniWorX ( MonadHandler handler, HandlerSite handler ~ UniWorX
, MonadLogger handler , MonadThrow handler
, PathPiece ident , PathPiece ident
) )
=> Field handler cellResult => Field handler cellResult
@ -505,7 +505,7 @@ massInputListA field fieldSettings miButtonAction miIdent miSettings miRequired
-- | Wrapper around `massInput` for the common case, that we just want a list of data with no option to modify it except deletion and addition -- | Wrapper around `massInput` for the common case, that we just want a list of data with no option to modify it except deletion and addition
massInputAccum :: forall handler cellData ident. massInputAccum :: forall handler cellData ident.
( MonadHandler handler, HandlerSite handler ~ UniWorX ( MonadHandler handler, HandlerSite handler ~ UniWorX
, MonadLogger handler , MonadThrow handler
, ToJSON cellData, FromJSON cellData , ToJSON cellData, FromJSON cellData
, PathPiece ident , PathPiece ident
) )
@ -544,7 +544,7 @@ massInputAccum miAdd' miCell' miButtonAction miLayout miIdent fSettings fRequire
massInputAccumA :: forall handler cellData ident. massInputAccumA :: forall handler cellData ident.
( MonadHandler handler, HandlerSite handler ~ UniWorX ( MonadHandler handler, HandlerSite handler ~ UniWorX
, MonadLogger handler , MonadThrow handler
, ToJSON cellData, FromJSON cellData , ToJSON cellData, FromJSON cellData
, PathPiece ident , PathPiece ident
) )
@ -562,7 +562,7 @@ massInputAccumA miAdd' miCell' miButtonAction' miLayout' miIdent' fSettings fReq
massInputAccumW :: forall handler cellData ident. massInputAccumW :: forall handler cellData ident.
( MonadHandler handler, HandlerSite handler ~ UniWorX ( MonadHandler handler, HandlerSite handler ~ UniWorX
, MonadLogger handler , MonadThrow handler
, ToJSON cellData, FromJSON cellData , ToJSON cellData, FromJSON cellData
, PathPiece ident , PathPiece ident
) )
@ -582,7 +582,7 @@ massInputAccumW miAdd' miCell' miButtonAction' miLayout' miIdent' fSettings fReq
-- | Wrapper around `massInput` for the common case, that we just want a list of data with existing data modified the same way as new data is added -- | Wrapper around `massInput` for the common case, that we just want a list of data with existing data modified the same way as new data is added
massInputAccumEdit :: forall handler cellData ident. massInputAccumEdit :: forall handler cellData ident.
( MonadHandler handler, HandlerSite handler ~ UniWorX ( MonadHandler handler, HandlerSite handler ~ UniWorX
, MonadLogger handler , MonadThrow handler
, ToJSON cellData, FromJSON cellData , ToJSON cellData, FromJSON cellData
, PathPiece ident , PathPiece ident
) )
@ -621,7 +621,7 @@ massInputAccumEdit miAdd' miCell' miButtonAction miLayout miIdent fSettings fReq
massInputAccumEditA :: forall handler cellData ident. massInputAccumEditA :: forall handler cellData ident.
( MonadHandler handler, HandlerSite handler ~ UniWorX ( MonadHandler handler, HandlerSite handler ~ UniWorX
, MonadLogger handler , MonadThrow handler
, ToJSON cellData, FromJSON cellData , ToJSON cellData, FromJSON cellData
, PathPiece ident , PathPiece ident
) )
@ -639,7 +639,7 @@ massInputAccumEditA miAdd' miCell' miButtonAction' miLayout' miIdent' fSettings
massInputAccumEditW :: forall handler cellData ident. massInputAccumEditW :: forall handler cellData ident.
( MonadHandler handler, HandlerSite handler ~ UniWorX ( MonadHandler handler, HandlerSite handler ~ UniWorX
, MonadLogger handler , MonadThrow handler
, ToJSON cellData, FromJSON cellData , ToJSON cellData, FromJSON cellData
, PathPiece ident , PathPiece ident
) )
@ -660,7 +660,7 @@ massInputA :: forall handler cellData cellResult liveliness.
( MonadHandler handler, HandlerSite handler ~ UniWorX ( MonadHandler handler, HandlerSite handler ~ UniWorX
, ToJSON cellData, FromJSON cellData , ToJSON cellData, FromJSON cellData
, Liveliness liveliness , Liveliness liveliness
, MonadLogger handler , MonadThrow handler
) )
=> MassInput handler liveliness cellData cellResult => MassInput handler liveliness cellData cellResult
-> FieldSettings UniWorX -> FieldSettings UniWorX
@ -674,7 +674,7 @@ massInputW :: forall handler cellData cellResult liveliness.
( MonadHandler handler, HandlerSite handler ~ UniWorX ( MonadHandler handler, HandlerSite handler ~ UniWorX
, ToJSON cellData, FromJSON cellData , ToJSON cellData, FromJSON cellData
, Liveliness liveliness , Liveliness liveliness
, MonadLogger handler , MonadThrow handler
) )
=> MassInput handler liveliness cellData cellResult => MassInput handler liveliness cellData cellResult
-> FieldSettings UniWorX -> FieldSettings UniWorX

View File

@ -31,9 +31,10 @@ nullaryPathPiece ''OccurrenceExceptionKind $ camelToPathPiece' 2
embedRenderMessage ''UniWorX ''OccurrenceExceptionKind id embedRenderMessage ''UniWorX ''OccurrenceExceptionKind id
{-# ANN occurrencesAForm ("HLint: ignore Use const" :: String) #-}
occurrencesAForm :: PathPiece ident => ident -> Maybe Occurrences -> AForm Handler Occurrences occurrencesAForm :: PathPiece ident => ident -> Maybe Occurrences -> AForm Handler Occurrences
occurrencesAForm (toPathPiece -> miIdent') mPrev = wFormToAForm $ do occurrencesAForm (toPathPiece -> miIdent') mPrev = wFormToAForm $ do
Just cRoute <- getCurrentRoute cRoute <- fromMaybe (error "occurrencesAForm called from 404-handler") <$> getCurrentRoute
let let
scheduled :: AForm Handler (Set OccurrenceSchedule) scheduled :: AForm Handler (Set OccurrenceSchedule)

View File

@ -1,5 +1,6 @@
{-# LANGUAGE UndecidableInstances #-} {-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE AllowAmbiguousTypes #-} {-# LANGUAGE AllowAmbiguousTypes #-}
{-# OPTIONS_GHC -fno-warn-redundant-constraints #-}
module Handler.Utils.Invitations module Handler.Utils.Invitations
( -- * Procedure ( -- * Procedure
@ -38,7 +39,7 @@ import qualified Data.Aeson as JSON
import Data.Proxy (Proxy(..)) import Data.Proxy (Proxy(..))
import Data.Typeable import Data.Typeable
import Database.Persist.Sql (SqlBackendCanWrite, SqlBackendCanRead) import Database.Persist.Sql (SqlBackendCanWrite)
class ( PersistRecordBackend junction (YesodPersistBackend UniWorX) class ( PersistRecordBackend junction (YesodPersistBackend UniWorX)
@ -169,11 +170,13 @@ instance IsInvitableJunction junction => FromJSON (InvitationTokenRestriction ju
sinkInvitations :: forall junction m backend. sinkInvitations :: forall junction m backend.
( IsInvitableJunction junction ( IsInvitableJunction junction
, MonadHandler m, SqlBackendCanWrite backend , MonadHandler m
, PersistRecordBackend Invitation backend, SqlBackendCanWrite backend
, HasPersistBackend backend
, HandlerSite m ~ UniWorX , HandlerSite m ~ UniWorX
) )
=> InvitationConfig junction => InvitationConfig junction
-> Sink (Invitation' junction) (ReaderT backend (WriterT (Set QueuedJobId) m)) () -> ConduitT (Invitation' junction) Void (ReaderT backend (WriterT (Set QueuedJobId) m)) ()
-- | Register invitations in the database and send them by email -- | Register invitations in the database and send them by email
-- --
-- When an invitation for a certain junction (i.e. an `UserEmail`, `Key -- When an invitation for a certain junction (i.e. an `UserEmail`, `Key
@ -181,9 +184,10 @@ sinkInvitations :: forall junction m backend.
-- (because the token-data may have changed) -- (because the token-data may have changed)
sinkInvitations InvitationConfig{..} = determineExists .| sinkInvitations' sinkInvitations InvitationConfig{..} = determineExists .| sinkInvitations'
where where
determineExists :: Conduit (Invitation' junction) determineExists :: ConduitT (Invitation' junction)
(ReaderT backend (WriterT (Set QueuedJobId) m)) (Invitation' junction)
(Invitation' junction) (ReaderT backend (WriterT (Set QueuedJobId) m))
()
determineExists determineExists
| is _Just (ephemeralInvitation @junction) | is _Just (ephemeralInvitation @junction)
= C.map id = C.map id
@ -203,10 +207,10 @@ sinkInvitations InvitationConfig{..} = determineExists .| sinkInvitations'
decode invData decode invData
= case fromJSON invData of = case fromJSON invData of
JSON.Success dbData -> return dbData JSON.Success dbData -> return dbData
JSON.Error str -> fail $ "Could not decode invitationData: " <> str JSON.Error str -> throwM . PersistMarshalError . pack $ "Could not decode invitationData: " <> str
sinkInvitations' :: Sink (Invitation' junction) (ReaderT backend (WriterT (Set QueuedJobId) m)) () sinkInvitations' :: ConduitT (Invitation' junction) Void (ReaderT backend (WriterT (Set QueuedJobId) m)) ()
sinkInvitations' = transPipe (hoist (hoist liftHandlerT) . withReaderT persistBackend) $ do sinkInvitations' = transPipe (hoist (hoist liftHandler) . withReaderT persistBackend) $ do
C.mapM_ $ \(jInvitee, fid, dat) -> do C.mapM_ $ \(jInvitee, fid, dat) -> do
app <- getYesod app <- getYesod
let mr = renderMessage app $ NonEmpty.toList appLanguages let mr = renderMessage app $ NonEmpty.toList appLanguages
@ -214,15 +218,15 @@ sinkInvitations InvitationConfig{..} = determineExists .| sinkInvitations'
fEnt <- Entity fid <$> get404 fid fEnt <- Entity fid <$> get404 fid
jInviter <- liftHandlerT requireAuthId jInviter <- liftHandler requireAuthId
route <- mapReaderT liftHandlerT $ invitationRoute fEnt dat route <- mapReaderT liftHandler $ invitationRoute fEnt dat
InvitationTokenConfig{..} <- mapReaderT liftHandlerT $ invitationTokenConfig fEnt dat InvitationTokenConfig{..} <- mapReaderT liftHandler $ invitationTokenConfig fEnt dat
protoToken <- bearerToken itAuthority (Just . HashSet.singleton $ urlRoute route) itAddAuth itExpiresAt itStartsAt protoToken <- bearerToken itAuthority (Just . HashSet.singleton $ urlRoute route) itAddAuth itExpiresAt itStartsAt
let token = protoToken & tokenRestrict (urlRoute route) (InvitationTokenRestriction jInvitee $ dat ^. _invitationTokenData) let token = protoToken & tokenRestrict (urlRoute route) (InvitationTokenRestriction jInvitee $ dat ^. _invitationTokenData)
jwt <- encodeToken token jwt <- encodeToken token
jInvitationUrl <- toTextUrl (route, [(toPathPiece GetBearer, toPathPiece jwt)]) jInvitationUrl <- toTextUrl (route, [(toPathPiece GetBearer, toPathPiece jwt)])
jInvitationSubject <- fmap mr . mapReaderT liftHandlerT $ invitationSubject fEnt dat jInvitationSubject <- fmap mr . mapReaderT liftHandler $ invitationSubject fEnt dat
jInvitationExplanation <- (\ihtml -> ihtml (toHtml . mr) ur) <$> mapReaderT liftHandlerT (invitationExplanation fEnt dat) jInvitationExplanation <- (\ihtml -> ihtml (toHtml . mr) ur) <$> mapReaderT liftHandler (invitationExplanation fEnt dat)
when (is _Nothing (ephemeralInvitation @junction)) $ insert_ $ Invitation when (is _Nothing (ephemeralInvitation @junction)) $ insert_ $ Invitation
{ invitationEmail = jInvitee { invitationEmail = jInvitee
@ -237,7 +241,10 @@ sinkInvitationsF :: forall junction mono m backend.
( IsInvitableJunction junction ( IsInvitableJunction junction
, MonoFoldable mono , MonoFoldable mono
, Element mono ~ Invitation' junction , Element mono ~ Invitation' junction
, MonadHandler m, SqlBackendCanWrite backend , MonadHandler m
, MonadThrow m
, PersistRecordBackend Invitation backend, SqlBackendCanWrite backend
, HasPersistBackend backend
, HandlerSite m ~ UniWorX , HandlerSite m ~ UniWorX
) )
=> InvitationConfig junction => InvitationConfig junction
@ -248,7 +255,10 @@ sinkInvitationsF cfg invs = runConduit $ mapM_ yield invs .| sinkInvitations cfg
sinkInvitation :: forall junction m backend. sinkInvitation :: forall junction m backend.
( IsInvitableJunction junction ( IsInvitableJunction junction
, MonadHandler m, SqlBackendCanWrite backend , MonadHandler m
, MonadThrow m
, PersistRecordBackend Invitation backend, SqlBackendCanWrite backend
, HasPersistBackend backend
, HandlerSite m ~ UniWorX , HandlerSite m ~ UniWorX
) )
=> InvitationConfig junction => InvitationConfig junction
@ -260,23 +270,29 @@ sinkInvitation cfg = sinkInvitationsF cfg . Identity
sourceInvitations :: forall junction m backend. sourceInvitations :: forall junction m backend.
( IsInvitableJunction junction ( IsInvitableJunction junction
, MonadResource m, SqlBackendCanRead backend , MonadResource m
, MonadThrow m
, PersistRecordBackend Invitation backend
, HasPersistBackend backend
) )
=> Key (InvitationFor junction) => Key (InvitationFor junction)
-> Source (ReaderT backend m) (UserEmail, InvitationDBData junction) -> ConduitT () (UserEmail, InvitationDBData junction) (ReaderT backend m) ()
sourceInvitations forKey = selectSource [InvitationFor ==. invRef @junction forKey] [] .| C.mapM decode sourceInvitations forKey = selectSource [InvitationFor ==. invRef @junction forKey] [] .| C.mapM decode
where where
decode (Entity _ (Invitation{invitationEmail, invitationData})) decode (Entity _ (Invitation{invitationEmail, invitationData}))
= case fromJSON invitationData of = case fromJSON invitationData of
JSON.Success dbData -> return (invitationEmail, dbData) JSON.Success dbData -> return (invitationEmail, dbData)
JSON.Error str -> fail $ "Could not decode invitationData: " <> str JSON.Error str -> throwM . PersistMarshalError . pack $ "Could not decode invitationData: " <> str
sourceInvitationsF :: forall junction map m backend. sourceInvitationsF :: forall junction map m backend.
( IsInvitableJunction junction ( IsInvitableJunction junction
, IsMap map , IsMap map
, ContainerKey map ~ UserEmail , ContainerKey map ~ UserEmail
, MapValue map ~ InvitationDBData junction , MapValue map ~ InvitationDBData junction
, MonadResource m, SqlBackendCanRead backend , MonadResource m
, MonadThrow m
, PersistRecordBackend Invitation backend
, HasPersistBackend backend
) )
=> Key (InvitationFor junction) => Key (InvitationFor junction)
-> ReaderT backend m map -> ReaderT backend m map
@ -291,15 +307,17 @@ sourceInvitationsF forKey = runConduit $ sourceInvitations forKey .| C.foldMap (
-- > runConduit $ yield userEmail .| deleteInvitations @SubmissionUser submissionId -- > runConduit $ yield userEmail .| deleteInvitations @SubmissionUser submissionId
deleteInvitations :: forall junction m backend. deleteInvitations :: forall junction m backend.
( IsInvitableJunction junction ( IsInvitableJunction junction
, MonadIO m, SqlBackendCanWrite backend , MonadIO m
, PersistRecordBackend Invitation backend, SqlBackendCanWrite backend
) )
=> Key (InvitationFor junction) => Key (InvitationFor junction)
-> Sink UserEmail (ReaderT backend m) () -> ConduitT UserEmail Void (ReaderT backend m) ()
deleteInvitations k = C.foldMap Set.singleton >>= lift . deleteInvitationsF @junction k deleteInvitations k = C.foldMap Set.singleton >>= lift . deleteInvitationsF @junction k
deleteInvitationsF :: forall junction m mono backend. deleteInvitationsF :: forall junction m mono backend.
( IsInvitableJunction junction ( IsInvitableJunction junction
, MonadIO m, SqlBackendCanWrite backend , MonadIO m
, PersistRecordBackend Invitation backend, SqlBackendCanWrite backend
, MonoFoldable mono , MonoFoldable mono
, Element mono ~ UserEmail , Element mono ~ UserEmail
) )
@ -312,7 +330,8 @@ deleteInvitationsF invitationFor (otoList -> emailList)
deleteInvitation :: forall junction m backend. deleteInvitation :: forall junction m backend.
( IsInvitableJunction junction ( IsInvitableJunction junction
, MonadIO m, SqlBackendCanWrite backend , MonadIO m
, PersistRecordBackend Invitation backend, SqlBackendCanWrite backend
) )
=> Key (InvitationFor junction) => Key (InvitationFor junction)
-> UserEmail -> UserEmail
@ -344,10 +363,10 @@ invitationR' :: forall junction m.
=> InvitationConfig junction => InvitationConfig junction
-> m Html -> m Html
-- | Generic handler for incoming invitations -- | Generic handler for incoming invitations
invitationR' InvitationConfig{..} = liftHandlerT $ do invitationR' InvitationConfig{..} = liftHandler $ do
InvitationTokenRestriction{..} <- maybeM (invalidArgsI [MsgInvitationMissingRestrictions]) return requireCurrentTokenRestrictions :: Handler (InvitationTokenRestriction junction) InvitationTokenRestriction{..} <- maybeM (invalidArgsI [MsgInvitationMissingRestrictions]) return requireCurrentTokenRestrictions :: Handler (InvitationTokenRestriction junction)
invitee <- requireAuthId invitee <- requireAuthId
Just cRoute <- getCurrentRoute cRoute <- fromMaybe (error "invitationR' called from 404-handler") <$> getCurrentRoute
(tRoute, (dataWidget, dataEnctype), heading, explanation) <- runDB $ do (tRoute, (dataWidget, dataEnctype), heading, explanation) <- runDB $ do
fEnt@(Entity fid _) <- invitationResolveFor itData >>= (\k -> Entity k <$> get404 k) fEnt@(Entity fid _) <- invitationResolveFor itData >>= (\k -> Entity k <$> get404 k)
@ -356,7 +375,7 @@ invitationR' InvitationConfig{..} = liftHandlerT $ do
Invitation{..} <- entityVal <$> getBy404 (UniqueInvitation itEmail $ invRef @junction fid) Invitation{..} <- entityVal <$> getBy404 (UniqueInvitation itEmail $ invRef @junction fid)
case fromJSON invitationData of case fromJSON invitationData of
JSON.Success dbData -> return dbData JSON.Success dbData -> return dbData
JSON.Error str -> fail $ "Could not decode invitationData: " <> str JSON.Error str -> throwM . PersistMarshalError $ "Could not decode invitationData: " <> pack str
Just (cloneIso -> _DBData) -> return $ view _DBData () Just (cloneIso -> _DBData) -> return $ view _DBData ()
let let
iData :: InvitationData junction iData :: InvitationData junction

View File

@ -19,11 +19,10 @@ import Control.Monad.Trans.State (StateT)
addRecipientsDB :: ( MonadMail m addRecipientsDB :: ( MonadMail m
, MonadHandler m
, HandlerSite m ~ UniWorX , HandlerSite m ~ UniWorX
) => [Filter User] -> m () ) => [Filter User] -> m ()
-- ^ @setRecipientId uid@ throws an exception if @uid@ does not refer to an existing user -- ^ @setRecipientId uid@ throws an exception if @uid@ does not refer to an existing user
addRecipientsDB uFilter = runConduit $ transPipe (liftHandlerT . runDB) (selectSource uFilter [Asc UserDisplayName]) .| C.mapM_ addRecipient addRecipientsDB uFilter = runConduit $ transPipe (liftHandler . runDB) (selectSource uFilter [Asc UserDisplayName]) .| C.mapM_ addRecipient
where where
addRecipient (Entity _ User{userEmail, userDisplayName}) = do addRecipient (Entity _ User{userEmail, userDisplayName}) = do
let addr = Address (Just userDisplayName) $ CI.original userEmail let addr = Address (Just userDisplayName) $ CI.original userEmail
@ -34,8 +33,8 @@ userAddress User{userEmail, userDisplayName} = Address (Just userDisplayName) $
userMailT :: ( MonadHandler m userMailT :: ( MonadHandler m
, HandlerSite m ~ UniWorX , HandlerSite m ~ UniWorX
, MonadBaseControl IO m , MonadThrow m
, MonadLogger m , MonadUnliftIO m
) => UserId -> MailT m a -> m a ) => UserId -> MailT m a -> m a
userMailT uid mAct = do userMailT uid mAct = do
user@User user@User
@ -43,7 +42,7 @@ userMailT uid mAct = do
, userDateTimeFormat , userDateTimeFormat
, userDateFormat , userDateFormat
, userTimeFormat , userTimeFormat
} <- liftHandlerT . runDB $ getJust uid } <- liftHandler . runDB $ getJust uid
let let
ctx = MailContext ctx = MailContext
{ mcLanguages = userMailLanguages { mcLanguages = userMailLanguages
@ -57,14 +56,13 @@ userMailT uid mAct = do
mAct mAct
addFileDB :: ( MonadMail m addFileDB :: ( MonadMail m
, MonadHandler m
, HandlerSite m ~ UniWorX , HandlerSite m ~ UniWorX
) => FileId -> m MailObjectId ) => FileId -> m (Maybe MailObjectId)
addFileDB fId = do addFileDB fId = runMaybeT $ do
File{fileTitle = pack . takeBaseName -> fileName, fileContent = Just fileContent} <- liftHandlerT . runDB $ getJust fId File{fileTitle = pack . takeBaseName -> fileName, fileContent = Just fileContent} <- MaybeT . liftHandler . runDB $ get fId
addPart $ do lift . addPart $ do
_partType .= decodeUtf8 (mimeLookup fileName) _partType .= decodeUtf8 (mimeLookup fileName)
_partEncoding .= Base64 _partEncoding .= Base64
_partFilename .= Just fileName _partFilename .= Just fileName
_partContent .= LBS.fromStrict fileContent _partContent .= LBS.fromStrict fileContent
setMailObjectIdCrypto fId :: StateT Part (HandlerT UniWorX IO) MailObjectId setMailObjectIdCrypto fId :: StateT Part (HandlerFor UniWorX) MailObjectId

View File

@ -133,7 +133,7 @@ ratingFile cID rating@Rating{ ratingValues = Rating'{..}, .. } = do
parseRating :: MonadThrow m => File -> m Rating' parseRating :: MonadThrow m => File -> m Rating'
parseRating File{ fileContent = Just input, .. } = do parseRating File{ fileContent = Just input, .. } = do
inputText <- either (throw . RatingNotUnicode) return $ Text.decodeUtf8' input inputText <- either (throwM . RatingNotUnicode) return $ Text.decodeUtf8' input
let let
(headerLines', commentLines) = break (commentSep `Text.isInfixOf`) $ Text.lines inputText (headerLines', commentLines) = break (commentSep `Text.isInfixOf`) $ Text.lines inputText
(reverse -> ratingLines, reverse -> _headerLines) = break (sep' `Text.isInfixOf`) $ reverse headerLines' (reverse -> ratingLines, reverse -> _headerLines) = break (sep' `Text.isInfixOf`) $ reverse headerLines'
@ -143,20 +143,20 @@ parseRating File{ fileContent = Just input, .. } = do
rating = "Bewertung:" rating = "Bewertung:"
comment' <- case commentLines of comment' <- case commentLines of
(_:commentLines') -> return . Text.strip $ Text.unlines commentLines' (_:commentLines') -> return . Text.strip $ Text.unlines commentLines'
_ -> throw RatingMissingSeparator _ -> throwM RatingMissingSeparator
let let
ratingComment ratingComment
| Text.null comment' = Nothing | Text.null comment' = Nothing
| otherwise = Just comment' | otherwise = Just comment'
ratingLine' <- case ratingLines' of ratingLine' <- case ratingLines' of
[l] -> return l [l] -> return l
_ -> throw RatingMultiple _ -> throwM RatingMultiple
let let
(_, ratingLine) = Text.breakOnEnd rating ratingLine' (_, ratingLine) = Text.breakOnEnd rating ratingLine'
ratingStr = Text.unpack $ Text.strip ratingLine ratingStr = Text.unpack $ Text.strip ratingLine
ratingPoints <- case () of ratingPoints <- case () of
_ | null ratingStr -> return Nothing _ | null ratingStr -> return Nothing
| otherwise -> either (throw . RatingInvalid . pack) return $ Just <$> readEither ratingStr | otherwise -> either (throwM . RatingInvalid . pack) return $ Just <$> readEither ratingStr
return Rating'{ ratingTime = Just fileModified, .. } return Rating'{ ratingTime = Just fileModified, .. }
parseRating _ = throwM RatingFileIsDirectory parseRating _ = throwM RatingFileIsDirectory
@ -166,7 +166,7 @@ type SubmissionContent = Either File (SubmissionId, Rating')
extractRatings :: ( MonadHandler m extractRatings :: ( MonadHandler m
, HandlerSite m ~ UniWorX , HandlerSite m ~ UniWorX
, MonadCatch m , MonadCatch m
) => Conduit File m SubmissionContent ) => ConduitT File SubmissionContent m ()
extractRatings = Conduit.mapM $ \f@File{..} -> do extractRatings = Conduit.mapM $ \f@File{..} -> do
msId <- isRatingFile fileTitle msId <- isRatingFile fileTitle
case () of case () of

View File

@ -16,13 +16,11 @@ sheetFileTypeDates Sheet{..} = \case
SheetMarking -> Nothing SheetMarking -> Nothing
fetchSheetAux :: ( BaseBackend backend ~ SqlBackend fetchSheetAux :: ( E.SqlSelect b a
, E.SqlSelect b a , Typeable a, MonadHandler m
, Typeable a, MonadHandler m, IsPersistBackend backend
, PersistQueryRead backend, PersistUniqueRead backend
) )
=> (E.SqlExpr (Entity Sheet) -> E.SqlExpr (Entity Course) -> b) => (E.SqlExpr (Entity Sheet) -> E.SqlExpr (Entity Course) -> b)
-> TermId -> SchoolId -> CourseShorthand -> SheetName -> ReaderT backend m a -> TermId -> SchoolId -> CourseShorthand -> SheetName -> SqlReadT m a
fetchSheetAux prj tid ssh csh shn = fetchSheetAux prj tid ssh csh shn =
let cachId = encodeUtf8 $ tshow (tid, ssh, csh, shn) let cachId = encodeUtf8 $ tshow (tid, ssh, csh, shn)
in cachedBy cachId $ do in cachedBy cachId $ do

View File

@ -252,7 +252,7 @@ planSubmissions sid restriction = do
unstableSortBy cmp = fmap concat . mapM Rand.shuffleM . groupBy (\a b -> cmp a b == EQ) . sortBy cmp unstableSortBy cmp = fmap concat . mapM Rand.shuffleM . groupBy (\a b -> cmp a b == EQ) . sortBy cmp
submissionFileSource :: SubmissionId -> Source (YesodDB UniWorX) (Entity File) submissionFileSource :: SubmissionId -> ConduitT () (Entity File) (YesodDB UniWorX) ()
submissionFileSource = E.selectSource . fmap snd . E.from . submissionFileQuery submissionFileSource = E.selectSource . fmap snd . E.from . submissionFileQuery
submissionFileQuery :: SubmissionId -> E.SqlExpr (Entity SubmissionFile) `E.InnerJoin` E.SqlExpr (Entity File) submissionFileQuery :: SubmissionId -> E.SqlExpr (Entity SubmissionFile) `E.InnerJoin` E.SqlExpr (Entity File)
@ -285,7 +285,7 @@ submissionMultiArchive (Set.toList -> ids) = do
setContentDisposition' $ Just ((addExtension `on` unpack) archiveName extensionZip) setContentDisposition' $ Just ((addExtension `on` unpack) archiveName extensionZip)
(<* cleanup) . respondSource typeZip . transPipe (runDBRunner dbrunner) $ do (<* cleanup) . respondSource typeZip . transPipe (runDBRunner dbrunner) $ do
let let
fileEntitySource' :: (Rating, Entity Submission, (SheetName,CourseShorthand,SchoolId,TermId)) -> Source (YesodDB UniWorX) File fileEntitySource' :: (Rating, Entity Submission, (SheetName,CourseShorthand,SchoolId,TermId)) -> ConduitT () File (YesodDB UniWorX) ()
fileEntitySource' (rating, Entity submissionID Submission{..},(shn,csh,ssh,tid)) = do fileEntitySource' (rating, Entity submissionID Submission{..},(shn,csh,ssh,tid)) = do
cID <- encrypt submissionID cID <- encrypt submissionID
@ -301,7 +301,7 @@ submissionMultiArchive (Set.toList -> ids) = do
| otherwise = submissionDirectory | otherwise = submissionDirectory
fileEntitySource = do fileEntitySource = do
submissionFileSource submissionID =$= Conduit.map entityVal submissionFileSource submissionID .| Conduit.map entityVal
yieldM (ratingFile cID rating) yieldM (ratingFile cID rating)
withinDirectory f@File{..} = f { fileTitle = directoryName </> fileTitle } withinDirectory f@File{..} = f { fileTitle = directoryName </> fileTitle }
@ -316,9 +316,9 @@ submissionMultiArchive (Set.toList -> ids) = do
, fileContent = Nothing , fileContent = Nothing
} }
fileEntitySource =$= mapC withinDirectory fileEntitySource .| mapC withinDirectory
mapM_ fileEntitySource' ratedSubmissions =$= produceZip def =$= Conduit.map toFlushBuilder mapM_ fileEntitySource' ratedSubmissions .| produceZip def .| Conduit.map toFlushBuilder
@ -331,9 +331,12 @@ data SubmissionSinkState = SubmissionSinkState
, sinkFilenames :: Set FilePath , sinkFilenames :: Set FilePath
} deriving (Show, Eq, Generic, Typeable) } deriving (Show, Eq, Generic, Typeable)
instance Semigroup SubmissionSinkState where
(<>) = mappenddefault
instance Monoid SubmissionSinkState where instance Monoid SubmissionSinkState where
mempty = memptydefault mempty = memptydefault
mappend = mappenddefault mappend = (<>)
filterSubmission :: MonadLogger m => ConduitM File File m (Set FilePath) filterSubmission :: MonadLogger m => ConduitM File File m (Set FilePath)
-- ^ Removes all `File`s matching `submissionBlacklist`, returning their `fileTitle`s -- ^ Removes all `File`s matching `submissionBlacklist`, returning their `fileTitle`s
@ -351,15 +354,13 @@ filterSubmission = do
extractRatings :: ( MonadHandler m extractRatings :: ( MonadHandler m
, HandlerSite m ~ UniWorX , HandlerSite m ~ UniWorX
, MonadCatch m , MonadCatch m
, MonadLogger m
) => ConduitM File SubmissionContent m (Set FilePath) ) => ConduitM File SubmissionContent m (Set FilePath)
extractRatings = filterSubmission `fuseUpstream` Rating.extractRatings extractRatings = filterSubmission `fuseUpstream` Rating.extractRatings
extractRatingsMsg :: ( MonadHandler m extractRatingsMsg :: ( MonadHandler m
, HandlerSite m ~ UniWorX , HandlerSite m ~ UniWorX
, MonadCatch m , MonadCatch m
, MonadLogger m ) => ConduitT File SubmissionContent m ()
) => Conduit File m SubmissionContent
extractRatingsMsg = do extractRatingsMsg = do
ignored' <- filterSubmission `fuseUpstream` Rating.extractRatings ignored' <- filterSubmission `fuseUpstream` Rating.extractRatings
let ignoredFiles :: Set (Either CryptoFileNameSubmission FilePath) let ignoredFiles :: Set (Either CryptoFileNameSubmission FilePath)
@ -385,7 +386,7 @@ msgSubmissionErrors = flip catches
sinkSubmission :: UserId sinkSubmission :: UserId
-> Either SheetId SubmissionId -> Either SheetId SubmissionId
-> Bool -- ^ Is this a correction -> Bool -- ^ Is this a correction
-> Sink SubmissionContent (YesodJobDB UniWorX) SubmissionId -> ConduitT SubmissionContent Void (YesodJobDB UniWorX) SubmissionId
-- ^ Replace the currently saved files for the given submission (either -- ^ Replace the currently saved files for the given submission (either
-- corrected files or original ones, depending on arguments) with the supplied -- corrected files or original ones, depending on arguments) with the supplied
-- 'SubmissionContent'. -- 'SubmissionContent'.
@ -420,7 +421,7 @@ sinkSubmission userId mExists isUpdate = do
where where
tellSt = modify . mappend tellSt = modify . mappend
guardFileTitles :: MonadThrow m => SubmissionMode -> Conduit SubmissionContent m SubmissionContent guardFileTitles :: MonadThrow m => SubmissionMode -> ConduitT SubmissionContent SubmissionContent m ()
guardFileTitles SubmissionMode{..} guardFileTitles SubmissionMode{..}
| Just UploadAny{..} <- submissionModeUser | Just UploadAny{..} <- submissionModeUser
, not isUpdate , not isUpdate
@ -435,7 +436,7 @@ sinkSubmission userId mExists isUpdate = do
| otherwise = Conduit.map id | otherwise = Conduit.map id
sinkSubmission' :: SubmissionId sinkSubmission' :: SubmissionId
-> Sink SubmissionContent (YesodJobDB UniWorX) () -> ConduitT SubmissionContent Void (YesodJobDB UniWorX) ()
sinkSubmission' submissionId = lift . finalize <=< execStateLC mempty . Conduit.mapM_ $ \case sinkSubmission' submissionId = lift . finalize <=< execStateLC mempty . Conduit.mapM_ $ \case
Left file@(File{..}) -> do Left file@(File{..}) -> do
$logDebugS "sinkSubmission" . tshow $ (submissionId, fileTitle) $logDebugS "sinkSubmission" . tshow $ (submissionId, fileTitle)
@ -628,7 +629,7 @@ sinkSubmission userId mExists isUpdate = do
sinkMultiSubmission :: UserId sinkMultiSubmission :: UserId
-> Bool {-^ Are these corrections -} -> Bool {-^ Are these corrections -}
-> Sink SubmissionContent (YesodJobDB UniWorX) (Set SubmissionId) -> ConduitT SubmissionContent Void (YesodJobDB UniWorX) (Set SubmissionId)
-- ^ Expects all supplied 'SubmissionContent' to contain an encrypted 'SubmissionId' and replaces the currently saved files for the respective submissions (either corrected files or original ones, depending on arguments) with the supplied 'SubmissionContent'. -- ^ Expects all supplied 'SubmissionContent' to contain an encrypted 'SubmissionId' and replaces the currently saved files for the respective submissions (either corrected files or original ones, depending on arguments) with the supplied 'SubmissionContent'.
-- --
@ -666,7 +667,7 @@ sinkMultiSubmission userId isUpdate = do
v@(Right (sId, _)) -> do v@(Right (sId, _)) -> do
cID <- encrypt sId cID <- encrypt sId
$logDebugS "sinkMultiSubmission" $ "Feeding rating for " <> toPathPiece cID $logDebugS "sinkMultiSubmission" $ "Feeding rating for " <> toPathPiece cID
lift (feed sId v) `catches` [ E.Handler (throwM . SubmissionSinkException cID Nothing), E.Handler (void . handleHCError (Left cID)) ] lift (feed sId v `catches` [ E.Handler (throwM . SubmissionSinkException cID Nothing), E.Handler (void . handleHCError (Left cID)) ])
(Left f@File{..}) -> do (Left f@File{..}) -> do
let let
acc :: (Maybe SubmissionId, [FilePath]) -> FilePath -> _ (Maybe SubmissionId, [FilePath]) acc :: (Maybe SubmissionId, [FilePath]) -> FilePath -> _ (Maybe SubmissionId, [FilePath])
@ -678,7 +679,7 @@ sinkMultiSubmission userId isUpdate = do
sId <- decrypt (cID :: CryptoFileNameSubmission) sId <- decrypt (cID :: CryptoFileNameSubmission)
Just sId <$ get404 sId Just sId <$ get404 sId
| otherwise = return Nothing | otherwise = return Nothing
msId <- lift (lift $ tryDecrypt segment) `catches` [ E.Handler handleCryptoID, E.Handler (handleHCError $ Right fileTitle) ] msId <- lift (lift (tryDecrypt segment) `catches` [ E.Handler handleCryptoID, E.Handler (handleHCError $ Right fileTitle) ])
return (msId, fp) return (msId, fp)
(msId, (joinPath -> fileTitle')) <- foldM acc (Nothing, []) $ splitDirectories fileTitle (msId, (joinPath -> fileTitle')) <- foldM acc (Nothing, []) $ splitDirectories fileTitle
case msId of case msId of
@ -687,8 +688,8 @@ sinkMultiSubmission userId isUpdate = do
Just sId -> do Just sId -> do
$logDebugS "sinkMultiSubmission" $ "Feeding " <> tshow (splitDirectories fileTitle, msId, fileTitle') $logDebugS "sinkMultiSubmission" $ "Feeding " <> tshow (splitDirectories fileTitle, msId, fileTitle')
cID <- encrypt sId cID <- encrypt sId
handle (throwM . SubmissionSinkException cID (Just fileTitle)) $ lift . handle (throwM . SubmissionSinkException cID (Just fileTitle)) $
lift . feed sId $ Left f{ fileTitle = fileTitle' } feed sId $ Left f{ fileTitle = fileTitle' }
when (not $ null ignoredFiles) $ do when (not $ null ignoredFiles) $ do
mr <- (toHtml .) <$> getMessageRender mr <- (toHtml .) <$> getMessageRender
addMessage Warning =<< withUrlRenderer ($(ihamletFile "templates/messages/submissionFilesIgnored.hamlet") mr) addMessage Warning =<< withUrlRenderer ($(ihamletFile "templates/messages/submissionFilesIgnored.hamlet") mr)

View File

@ -1,100 +1,7 @@
module Handler.Utils.Table module Handler.Utils.Table
( module Handler.Utils.Table ( module Handler.Utils.Table
) where ) where
-- General Utilities for Tables
import Import
import Control.Monad.Except
import Text.Blaze as B
import Colonnade
import Yesod.Colonnade as Yesod
import Data.List ((!!))
import Data.Either
import Handler.Utils.Table.Pagination as Handler.Utils.Table import Handler.Utils.Table.Pagination as Handler.Utils.Table
import Handler.Utils.Table.Columns as Handler.Utils.Table import Handler.Utils.Table.Columns as Handler.Utils.Table
import Handler.Utils.Table.Cells as Handler.Utils.Table import Handler.Utils.Table.Cells as Handler.Utils.Table
-- Table design
{-# DEPRECATED tableDefault, tableSortable "Use dbTable" #-}
tableDefault :: Attribute
tableDefault = customAttribute "class" "table table-striped table-hover"
tableSortable :: Attribute
tableSortable = customAttribute "class" "js-sortable"
-- Colonnade Tools
{-# DEPRECATED numberColonnade, pairColonnade "Use dbTable" #-}
numberColonnade :: (IsString c) => Colonnade Headed Int c
numberColonnade = headed "Nr" (fromString.show)
pairColonnade :: (Functor h) => Colonnade h a c -> Colonnade h b c -> Colonnade h (a,b) c
pairColonnade a b = mconcat [ lmap fst a, lmap snd b]
-- Table Modification
{-# DEPRECATED encodeHeadedWidgetTableNumbered, headedRowSelector "Use dbTable" #-}
encodeHeadedWidgetTableNumbered :: Attribute -> Colonnade Headed a (WidgetT site IO ()) -> [a] -> WidgetT site IO ()
encodeHeadedWidgetTableNumbered attrs colo tdata =
encodeWidgetTable attrs (mconcat [numberCol, lmap snd colo]) (zip [1..] tdata)
where
numberCol :: Colonnade Headed (Int,a) (WidgetT site IO ())
numberCol = headed "Nr" (fromString.show.fst)
headedRowSelector :: ( PathPiece b
, Eq b
)
=> (a -> Handler b)
-> (b -> Handler c)
-> Attribute
-> Colonnade Headed a (Cell UniWorX)
-> [a]
-> MForm Handler (FormResult [c], Widget)
headedRowSelector toExternal fromExternal attrs colonnade tdata = do
externalIds <- mapM (lift . toExternal) tdata
let
checkbox extId = Field{..}
where
fieldEnctype = UrlEncoded
fieldParse [] _ = return $ Right Nothing
fieldParse optlist _ = runExceptT $ do
extIds <- maybe (throwError "Error parsing values") return $ mapM fromPathPiece optlist
case () of
_ | extId `elem` extIds
-> Just <$> lift (fromExternal extId)
| otherwise
-> return Nothing
fieldView theId name attributes val _ =
-- TODO: move this to a *.hamlet file
[whamlet|
<label style="display: block">
<input ##{theId} type=checkbox name=#{name} value=#{toPathPiece extId} *{attributes} :isRight val:checked>
|]
selectionIdent <- newFormIdent
(selectionResults, selectionBoxes) <- fmap unzip . forM externalIds $ \ident -> mopt (checkbox ident) ("" { fsName = Just selectionIdent }) Nothing
let
selColonnade :: Colonnade Headed Int (Cell UniWorX)
selColonnade = headed "Markiert" $ Yesod.cell . fvInput . (selectionBoxes !!)
collectResult :: [FormResult a] -> FormResult [a]
collectResult [] = FormSuccess []
collectResult (FormFailure errs : _) = FormFailure errs
collectResult (FormMissing:rs) = collectResult rs
collectResult (FormSuccess x:rs) = (x :) <$> collectResult rs
return ( catMaybes <$> collectResult selectionResults
, encodeCellTable attrs (pairColonnade selColonnade colonnade) (zip [0..] tdata)
)

View File

@ -1,8 +1,6 @@
module Handler.Utils.Table.Cells where module Handler.Utils.Table.Cells where
import Import import Import hiding (link)
import qualified Control.Monad.Trans.RWS.Lazy
import Data.CaseInsensitive (CI) import Data.CaseInsensitive (CI)
-- import qualified Data.CaseInsensitive as CI -- import qualified Data.CaseInsensitive as CI
@ -28,37 +26,17 @@ type CourseLink = (TermId, SchoolId, CourseShorthand) -- TODO: Refactor with Wit
-- Some basic cells are defined in Handler.Utils.Table.Pagination -- Some basic cells are defined in Handler.Utils.Table.Pagination
-- such as: i18nCell, cellTooltip, anchorCell for links, etc. -- such as: i18nCell, cellTooltip, anchorCell for links, etc.
----------------
-- Cell transformation
-- | Add cell attributes
addCellAttrs :: [(Text, Text)]
-> DBCell (Control.Monad.Trans.RWS.Lazy.RWST
(Maybe (Env, FileEnv), UniWorX, [Lang])
Enctype
Ints
(HandlerT UniWorX IO))
x
-> DBCell (Control.Monad.Trans.RWS.Lazy.RWST
(Maybe (Env, FileEnv), UniWorX, [Lang])
Enctype
Ints
(HandlerT UniWorX IO))
x
addCellAttrs newAttrs fcell = fcell { formCellAttrs = newAttrs <> formCellAttrs fcell } -- Isn't there already a lens for that?
---------------- ----------------
-- Special cells -- Special cells
-- | Display a breakable space -- | Display a breakable space
spacerCell :: (IsDBTable m a) => DBCell m a spacerCell :: IsDBTable m a => DBCell m a
spacerCell = cell [whamlet|&emsp;|] spacerCell = cell [whamlet|&emsp;|]
tellCell :: (Monoid a, IsDBTable m a) => a -> DBCell m a -> DBCell m a tellCell :: IsDBTable m a => a -> DBCell m a -> DBCell m a
tellCell = flip mappend . writerCell . tell tellCell = flip mappend . writerCell . tell
cellTell :: (Monoid a, IsDBTable m a) => DBCell m a -> a -> DBCell m a cellTell :: IsDBTable m a => DBCell m a -> a -> DBCell m a
cellTell = flip tellCell cellTell = flip tellCell
indicatorCell :: IsDBTable m Any => DBCell m Any -- For dbTables that return a Bool to indicate content indicatorCell :: IsDBTable m Any => DBCell m Any -- For dbTables that return a Bool to indicate content
@ -67,7 +45,7 @@ indicatorCell = writerCell . tell $ Any True
writerCell :: IsDBTable m w => WriterT w m () -> DBCell m w writerCell :: IsDBTable m w => WriterT w m () -> DBCell m w
writerCell act = mempty & cellContents %~ (<* act) writerCell act = mempty & cellContents %~ (<* act)
maybeCell :: (IsDBTable m a) => Maybe a -> (a -> DBCell m a) -> DBCell m a maybeCell :: IsDBTable m a => Maybe a -> (a -> DBCell m a) -> DBCell m a
maybeCell = flip foldMap maybeCell = flip foldMap
htmlCell :: (IsDBTable m a, ToMarkup c) => c -> DBCell m a htmlCell :: (IsDBTable m a, ToMarkup c) => c -> DBCell m a
@ -81,12 +59,12 @@ sqlCell :: (IsDBTable (YesodDB UniWorX) a) => YesodDB UniWorX Widget -> DBCell (
sqlCell act = mempty & cellContents .~ lift act sqlCell act = mempty & cellContents .~ lift act
-- | Highlight table cells with warning: Is not yet implemented in frontend. -- | Highlight table cells with warning: Is not yet implemented in frontend.
markCell :: (IsDBTable m a) => MessageStatus -> (t -> Bool) -> (t -> DBCell m a) -> (t -> DBCell m a) markCell :: IsDBTable m a => MessageStatus -> (t -> Bool) -> (t -> DBCell m a) -> (t -> DBCell m a)
markCell status condition normal x markCell status condition normal x
| condition x = normal x & over cellAttrs (insertAttr "class" $ statusToUrgencyClass status) | condition x = normal x & addCellClass (statusToUrgencyClass status)
| otherwise = normal x | otherwise = normal x
ifCell :: (IsDBTable m a) => (t -> Bool) -> (t -> DBCell m a) -> (t -> DBCell m a) -> (t -> DBCell m a) ifCell :: (t -> Bool) -> (t -> DBCell m a) -> (t -> DBCell m a) -> (t -> DBCell m a)
ifCell decision cTrue cFalse x ifCell decision cTrue cFalse x
| decision x = cTrue x | decision x = cTrue x
| otherwise = cFalse x | otherwise = cFalse x
@ -105,22 +83,22 @@ msgCell = textCell . toMessage
iconCell :: IsDBTable m a => Icon -> DBCell m a iconCell :: IsDBTable m a => Icon -> DBCell m a
iconCell = cell . toWidget . icon iconCell = cell . toWidget . icon
addIconFixedWidth :: (IsDBTable m a) => DBCell m a -> DBCell m a addIconFixedWidth :: IsDBTable m a => DBCell m a -> DBCell m a
addIconFixedWidth = over cellAttrs $ insertClass "icon-fixed-width" addIconFixedWidth = addCellClass ("icon-fixed-width" :: Text)
iconSpacerCell :: (IsDBTable m a) => DBCell m a iconSpacerCell :: IsDBTable m a => DBCell m a
iconSpacerCell = mempty & addIconFixedWidth iconSpacerCell = mempty & addIconFixedWidth
-- | Maybe display a tickmark/checkmark icon -- | Maybe display a tickmark/checkmark icon
tickmarkCell :: (IsDBTable m a) => Bool -> DBCell m a tickmarkCell :: IsDBTable m a => Bool -> DBCell m a
tickmarkCell = cell . toWidget . hasTickmark tickmarkCell = cell . toWidget . hasTickmark
-- | Maybe display an icon for tainted rows -- | Maybe display an icon for tainted rows
isBadCell :: (IsDBTable m a) => Bool -> DBCell m a isBadCell :: IsDBTable m a => Bool -> DBCell m a
isBadCell = cell . toWidget . isBad isBadCell = cell . toWidget . isBad
-- | Maybe display a exclamation icon -- | Maybe display a exclamation icon
isNewCell :: (IsDBTable m a) => Bool -> DBCell m a isNewCell :: IsDBTable m a => Bool -> DBCell m a
isNewCell = cell . toWidget . isNew isNewCell = cell . toWidget . isNew
-- | Maybe display comment icon linking a given URL or show nothing at all -- | Maybe display comment icon linking a given URL or show nothing at all
@ -129,11 +107,11 @@ commentCell Nothing = mempty
commentCell (Just link) = anchorCell link $ hasComment True commentCell (Just link) = anchorCell link $ hasComment True
-- | whether something is visible or hidden -- | whether something is visible or hidden
isVisibleCell :: (IsDBTable m a) => Bool -> DBCell m a isVisibleCell :: IsDBTable m a => Bool -> DBCell m a
isVisibleCell True = cell . toWidget $ isVisible True isVisibleCell True = cell . toWidget $ isVisible True
isVisibleCell False = (cell . toWidget $ isVisible False) & addUrgencyClass isVisibleCell False = (cell . toWidget $ isVisible False) & addUrgencyClass
where where
addUrgencyClass = over cellAttrs $ insertClass $ statusToUrgencyClass Warning addUrgencyClass = addCellClass $ statusToUrgencyClass Warning
-- | for simple file downloads -- | for simple file downloads
fileCell :: IsDBTable m a => Route UniWorX -> DBCell m a fileCell :: IsDBTable m a => Route UniWorX -> DBCell m a
@ -171,7 +149,7 @@ dateTimeCellVisible watershed t
| otherwise = cell timeStampWgt | otherwise = cell timeStampWgt
where where
timeStampWgt = formatTimeW SelFormatDateTime t timeStampWgt = formatTimeW SelFormatDateTime t
addUrgencyClass = over cellAttrs $ insertClass $ statusToUrgencyClass Warning addUrgencyClass = addCellClass $ statusToUrgencyClass Warning
userCell :: IsDBTable m a => Text -> Text -> DBCell m a userCell :: IsDBTable m a => Text -> Text -> DBCell m a
userCell displayName surname = cell $ nameWidget displayName surname userCell displayName surname = cell $ nameWidget displayName surname
@ -218,13 +196,13 @@ cellHasDegreeShort x = maybe (numCell $ x ^. _studyDegreeKey) textCell $ x ^. _s
maybeDateTimeCell :: IsDBTable m a => Maybe UTCTime -> DBCell m a maybeDateTimeCell :: IsDBTable m a => Maybe UTCTime -> DBCell m a
maybeDateTimeCell = maybe mempty dateTimeCell maybeDateTimeCell = maybe mempty dateTimeCell
numCell :: (IsDBTable m a, Num b, ToMessage b) => b -> DBCell m a numCell :: (IsDBTable m a, ToMessage b) => b -> DBCell m a
numCell = textCell . toMessage numCell = textCell . toMessage
propCell :: (IsDBTable m a, Real b, ToMessage b) => b -> b -> DBCell m a propCell :: (IsDBTable m a, Real b, ToMessage b) => b -> b -> DBCell m a
propCell curr max' = i18nCell $ MsgProportion (toMessage curr) (toMessage max') (toRational curr / toRational max') propCell curr max' = i18nCell $ MsgProportion (toMessage curr) (toMessage max') (toRational curr / toRational max')
int64Cell :: (IsDBTable m a) => Int64-> DBCell m a int64Cell :: IsDBTable m a => Int64-> DBCell m a
int64Cell = numCell int64Cell = numCell
termCell :: IsDBTable m a => TermId -> DBCell m a termCell :: IsDBTable m a => TermId -> DBCell m a
@ -269,7 +247,7 @@ sheetCell crse shn =
let tid = crse ^. _1 let tid = crse ^. _1
ssh = crse ^. _2 ssh = crse ^. _2
csh = crse ^. _3 csh = crse ^. _3
link= CSheetR tid ssh csh shn SShowR link = CSheetR tid ssh csh shn SShowR
in anchorCell link $ toWgt shn in anchorCell link $ toWgt shn
submissionCell :: IsDBTable m a => CourseLink -> SheetName -> SubmissionId -> DBCell m a submissionCell :: IsDBTable m a => CourseLink -> SheetName -> SubmissionId -> DBCell m a

View File

@ -1,6 +1,8 @@
{-# OPTIONS_GHC -fno-warn-redundant-constraints #-}
module Handler.Utils.Table.Columns where module Handler.Utils.Table.Columns where
import Import import Import hiding (link)
-- import Data.CaseInsensitive (CI) -- import Data.CaseInsensitive (CI)
-- import qualified Data.CaseInsensitive as CI -- import qualified Data.CaseInsensitive as CI
@ -323,7 +325,7 @@ colApplicationId :: OpticColonnade CourseApplicationId
colApplicationId resultId = Colonnade.singleton (fromSortable header) body colApplicationId resultId = Colonnade.singleton (fromSortable header) body
where where
header = Sortable Nothing (i18nCell MsgCourseApplicationId) header = Sortable Nothing (i18nCell MsgCourseApplicationId)
body = views resultId $ cell . (toWidget . toMarkup =<<) . (encrypt :: CourseApplicationId -> WidgetT UniWorX IO CryptoFileNameCourseApplication) body = views resultId $ cell . (toWidget . toMarkup =<<) . (encrypt :: CourseApplicationId -> WidgetFor UniWorX CryptoFileNameCourseApplication)
colApplicationRatingPoints :: OpticColonnade (Maybe ExamGrade) colApplicationRatingPoints :: OpticColonnade (Maybe ExamGrade)
colApplicationRatingPoints resultPoints = Colonnade.singleton (fromSortable header) body colApplicationRatingPoints resultPoints = Colonnade.singleton (fromSortable header) body
@ -813,7 +815,7 @@ anchorColonnadeM :: forall h r' m a url.
, IsDBTable m a , IsDBTable m a
, HandlerSite m ~ UniWorX , HandlerSite m ~ UniWorX
) )
=> (r' -> WidgetT UniWorX IO url) => (r' -> WidgetFor UniWorX url)
-> Colonnade h r' (DBCell m a) -> Colonnade h r' (DBCell m a)
-> Colonnade h r' (DBCell m a) -> Colonnade h r' (DBCell m a)
anchorColonnadeM mkUrl = imapColonnade anchorColonnade' anchorColonnadeM mkUrl = imapColonnade anchorColonnade'

View File

@ -1,3 +1,5 @@
{-# LANGUAGE UndecidableInstances #-}
module Handler.Utils.Table.Pagination module Handler.Utils.Table.Pagination
( module Handler.Utils.Table.Pagination.Types ( module Handler.Utils.Table.Pagination.Types
, dbFilterKey , dbFilterKey
@ -17,6 +19,7 @@ module Handler.Utils.Table.Pagination
, singletonFilter , singletonFilter
, DBParams(..) , DBParams(..)
, cellAttrs, cellContents , cellAttrs, cellContents
, addCellClass
, PagesizeLimit(..) , PagesizeLimit(..)
, PaginationSettings(..), PaginationInput(..), piIsUnset , PaginationSettings(..), PaginationInput(..), piIsUnset
, PSValidator(..) , PSValidator(..)
@ -50,6 +53,9 @@ import Utils
import Utils.Lens import Utils.Lens
import Import hiding (pi) import Import hiding (pi)
import qualified Yesod.Form.Functions as Yesod
import qualified Database.Esqueleto as E import qualified Database.Esqueleto as E
import qualified Database.Esqueleto.Utils as E import qualified Database.Esqueleto.Utils as E
import qualified Database.Esqueleto.Internal.Sql as E (SqlSelect,unsafeSqlValue) import qualified Database.Esqueleto.Internal.Sql as E (SqlSelect,unsafeSqlValue)
@ -111,6 +117,8 @@ import qualified Control.Monad.Catch as Catch
import Data.Dynamic import Data.Dynamic
import qualified Data.Csv as Csv
#if MIN_VERSION_base(4,11,0) #if MIN_VERSION_base(4,11,0)
type Monoid' = Monoid type Monoid' = Monoid
@ -513,17 +521,18 @@ singletonFilter key = prism' fromInner (fmap Just . fromOuter)
data DBTCsvEncode r' k' csv = forall exportData. data DBTCsvEncode r' k' csv = forall exportData.
( ToNamedRecord csv, DefaultOrdered csv, CsvColumnsExplained csv ( ToNamedRecord csv, CsvColumnsExplained csv
, DBTableKey k' , DBTableKey k'
, Typeable exportData , Typeable exportData
) => DBTCsvEncode ) => DBTCsvEncode
{ dbtCsvExportForm :: AForm (YesodDB UniWorX) exportData { dbtCsvExportForm :: AForm (YesodDB UniWorX) exportData
, dbtCsvDoEncode :: exportData -> Conduit (k', r') (YesodDB UniWorX) csv , dbtCsvHeader :: Maybe exportData -> YesodDB UniWorX Csv.Header -- ^ @exportData@ is @Nothing@, if we're reporting an error
, dbtCsvDoEncode :: exportData -> ConduitT (k', r') csv (YesodDB UniWorX) ()
, dbtCsvName :: FilePath , dbtCsvName :: FilePath
, dbtCsvNoExportData :: Maybe (AnIso' exportData ()) , dbtCsvNoExportData :: Maybe (AnIso' exportData ())
} }
data DBTCsvDecode r' k' csv = forall route csvAction csvActionClass csvException. data DBTCsvDecode r' k' csv = forall route csvAction csvActionClass csvException.
( FromNamedRecord csv, ToNamedRecord csv, DefaultOrdered csv ( FromNamedRecord csv, ToNamedRecord csv
, DBTableKey k' , DBTableKey k'
, RedirectUrl UniWorX route , RedirectUrl UniWorX route
, Typeable csv , Typeable csv
@ -532,10 +541,10 @@ data DBTCsvDecode r' k' csv = forall route csvAction csvActionClass csvException
, Exception csvException , Exception csvException
) => DBTCsvDecode ) => DBTCsvDecode
{ dbtCsvRowKey :: csv -> MaybeT (YesodDB UniWorX) k' { dbtCsvRowKey :: csv -> MaybeT (YesodDB UniWorX) k'
, dbtCsvComputeActions :: DBCsvDiff r' csv k' -> Source (YesodDB UniWorX) csvAction , dbtCsvComputeActions :: DBCsvDiff r' csv k' -> ConduitT () csvAction (YesodDB UniWorX) ()
, dbtCsvClassifyAction :: csvAction -> csvActionClass , dbtCsvClassifyAction :: csvAction -> csvActionClass
, dbtCsvCoarsenActionClass :: csvActionClass -> DBCsvActionMode , dbtCsvCoarsenActionClass :: csvActionClass -> DBCsvActionMode
, dbtCsvExecuteActions :: Sink csvAction (YesodDB UniWorX) route , dbtCsvExecuteActions :: ConduitT csvAction Void (YesodDB UniWorX) route
, dbtCsvRenderKey :: Map k' r' -> csvAction -> Widget , dbtCsvRenderKey :: Map k' r' -> csvAction -> Widget
, dbtCsvRenderActionClass :: csvActionClass -> Widget , dbtCsvRenderActionClass :: csvActionClass -> Widget
, dbtCsvRenderException :: csvException -> YesodDB UniWorX Text , dbtCsvRenderException :: csvException -> YesodDB UniWorX Text
@ -545,7 +554,7 @@ data DBTable m x = forall a r r' h i t k k' csv.
( ToSortable h, Functor h ( ToSortable h, Functor h
, E.SqlSelect a r, E.SqlIn k k', DBTableKey k' , E.SqlSelect a r, E.SqlIn k k', DBTableKey k'
, PathPiece i, Eq i , PathPiece i, Eq i
, E.From E.SqlQuery E.SqlExpr E.SqlBackend t , E.From t
) => DBTable ) => DBTable
{ dbtSQLQuery :: t -> E.SqlQuery a { dbtSQLQuery :: t -> E.SqlQuery a
, dbtRowKey :: t -> k -- ^ required for table forms; always same key for repeated requests. For joins: return unique tuples. , dbtRowKey :: t -> k -- ^ required for table forms; always same key for repeated requests. For joins: return unique tuples.
@ -566,7 +575,8 @@ type DBFilterUI = Maybe (Map FilterKey [Text]) -> AForm (YesodDB UniWorX) (Map F
noCsvEncode :: Maybe (DBTCsvEncode r' k' Void) noCsvEncode :: Maybe (DBTCsvEncode r' k' Void)
noCsvEncode = Nothing noCsvEncode = Nothing
simpleCsvEncode :: ( ToNamedRecord csv, DefaultOrdered csv, CsvColumnsExplained csv simpleCsvEncode :: forall fp r' k' csv.
( ToNamedRecord csv, DefaultOrdered csv, CsvColumnsExplained csv
, DBTableKey k' , DBTableKey k'
, Textual fp , Textual fp
) )
@ -576,9 +586,11 @@ simpleCsvEncode fName f = Just DBTCsvEncode
, dbtCsvDoEncode = \() -> C.map (f . view _2) , dbtCsvDoEncode = \() -> C.map (f . view _2)
, dbtCsvName = unpack fName , dbtCsvName = unpack fName
, dbtCsvNoExportData = Just id , dbtCsvNoExportData = Just id
, dbtCsvHeader = const . return $ headerOrder (error "headerOrder" :: csv)
} }
simpleCsvEncodeM :: ( ToNamedRecord csv, DefaultOrdered csv, CsvColumnsExplained csv simpleCsvEncodeM :: forall fp r' k' csv.
( ToNamedRecord csv, DefaultOrdered csv, CsvColumnsExplained csv
, DBTableKey k' , DBTableKey k'
, Textual fp , Textual fp
) )
@ -588,6 +600,7 @@ simpleCsvEncodeM fName f = Just DBTCsvEncode
, dbtCsvDoEncode = \() -> C.mapM (runReaderT f . view _2) , dbtCsvDoEncode = \() -> C.mapM (runReaderT f . view _2)
, dbtCsvName = unpack fName , dbtCsvName = unpack fName
, dbtCsvNoExportData = Just id , dbtCsvNoExportData = Just id
, dbtCsvHeader = const . return $ headerOrder (error "headerOrder" :: csv)
} }
@ -615,14 +628,17 @@ cellAttrs = dbCell . _1
cellContents :: IsDBTable m x => Lens' (DBCell m x) (WriterT x m Widget) cellContents :: IsDBTable m x => Lens' (DBCell m x) (WriterT x m Widget)
cellContents = dbCell . _2 cellContents = dbCell . _2
instance Monoid' x => IsDBTable (HandlerT UniWorX IO) x where addCellClass :: (IsDBTable m x, PathPiece t) => t -> DBCell m x -> DBCell m x
data DBParams (HandlerT UniWorX IO) x = DBParamsWidget addCellClass = over cellAttrs . Yesod.addClass . toPathPiece
type DBResult (HandlerT UniWorX IO) x = (x, Widget)
-- type DBResult' (WidgetT UniWorX IO) () = ()
data DBCell (HandlerT UniWorX IO) x = WidgetCell instance Monoid' x => IsDBTable (HandlerFor UniWorX) x where
data DBParams (HandlerFor UniWorX) x = DBParamsWidget
type DBResult (HandlerFor UniWorX) x = (x, Widget)
-- type DBResult' (WidgetFor UniWorX) () = ()
data DBCell (HandlerFor UniWorX) x = WidgetCell
{ wgtCellAttrs :: [(Text, Text)] { wgtCellAttrs :: [(Text, Text)]
, wgtCellContents :: WriterT x (HandlerT UniWorX IO) Widget , wgtCellContents :: WriterT x (HandlerFor UniWorX) Widget
} }
dbCell = iso dbCell = iso
@ -632,25 +648,25 @@ instance Monoid' x => IsDBTable (HandlerT UniWorX IO) x where
-- dbWidget Proxy Proxy = iso (, ()) $ view _1 -- dbWidget Proxy Proxy = iso (, ()) $ view _1
dbWidget _ _ = return . snd dbWidget _ _ = return . snd
dbHandler _ _ f = return . over _2 f dbHandler _ _ f = return . over _2 f
runDBTable _ _ _ = liftHandlerT runDBTable _ _ _ = liftHandler
instance Monoid' x => Sem.Semigroup (DBCell (HandlerT UniWorX IO) x) where instance Monoid' x => Sem.Semigroup (DBCell (HandlerFor UniWorX) x) where
(WidgetCell a c) <> (WidgetCell a' c') = WidgetCell (a <> a') ((<>) <$> c <*> c') (WidgetCell a c) <> (WidgetCell a' c') = WidgetCell (a <> a') ((<>) <$> c <*> c')
instance Monoid' x => Monoid (DBCell (HandlerT UniWorX IO) x) where instance Monoid' x => Monoid (DBCell (HandlerFor UniWorX) x) where
mempty = WidgetCell mempty $ return mempty mempty = WidgetCell mempty $ return mempty
mappend = (<>) mappend = (<>)
instance Default (DBParams (HandlerT UniWorX IO) x) where instance Default (DBParams (HandlerFor UniWorX) x) where
def = DBParamsWidget def = DBParamsWidget
instance Monoid' x => IsDBTable (ReaderT SqlBackend (HandlerT UniWorX IO)) x where instance Monoid' x => IsDBTable (ReaderT SqlBackend (HandlerFor UniWorX)) x where
data DBParams (ReaderT SqlBackend (HandlerT UniWorX IO)) x = DBParamsDB data DBParams (ReaderT SqlBackend (HandlerFor UniWorX)) x = DBParamsDB
type DBResult (ReaderT SqlBackend (HandlerT UniWorX IO)) x = (x, Widget) type DBResult (ReaderT SqlBackend (HandlerFor UniWorX)) x = (x, Widget)
data DBCell (ReaderT SqlBackend (HandlerT UniWorX IO)) x = DBCell data DBCell (ReaderT SqlBackend (HandlerFor UniWorX)) x = DBCell
{ dbCellAttrs :: [(Text, Text)] { dbCellAttrs :: [(Text, Text)]
, dbCellContents :: WriterT x (ReaderT SqlBackend (HandlerT UniWorX IO)) Widget , dbCellContents :: WriterT x (ReaderT SqlBackend (HandlerFor UniWorX)) Widget
} }
dbCell = iso dbCell = iso
@ -659,17 +675,17 @@ instance Monoid' x => IsDBTable (ReaderT SqlBackend (HandlerT UniWorX IO)) x whe
dbWidget _ _ = return . snd dbWidget _ _ = return . snd
dbHandler _ _ f = return . over _2 f dbHandler _ _ f = return . over _2 f
-- runDBTable :: forall m. (MonadHandler m, HandlerSite m ~ UniWorX) => ReaderT SqlBackend (HandlerT UniWorX IO) ((), Widget) -> m (Widget) -- runDBTable :: forall m. (MonadHandler m, HandlerSite m ~ UniWorX) => ReaderT SqlBackend (HandlerFor UniWorX) ((), Widget) -> m (Widget)
runDBTable _ _ _ = mapReaderT liftHandlerT runDBTable _ _ _ = mapReaderT liftHandler
instance Monoid' x => Sem.Semigroup (DBCell (ReaderT SqlBackend (HandlerT UniWorX IO)) x) where instance Monoid' x => Sem.Semigroup (DBCell (ReaderT SqlBackend (HandlerFor UniWorX)) x) where
(DBCell a c) <> (DBCell a' c') = DBCell (a <> a') ((<>) <$> c <*> c') (DBCell a c) <> (DBCell a' c') = DBCell (a <> a') ((<>) <$> c <*> c')
instance Monoid' x => Monoid (DBCell (ReaderT SqlBackend (HandlerT UniWorX IO)) x) where instance Monoid' x => Monoid (DBCell (ReaderT SqlBackend (HandlerFor UniWorX)) x) where
mempty = DBCell mempty $ return mempty mempty = DBCell mempty $ return mempty
mappend = (<>) mappend = (<>)
instance Default (DBParams (ReaderT SqlBackend (HandlerT UniWorX IO)) x) where instance Default (DBParams (ReaderT SqlBackend (HandlerFor UniWorX)) x) where
def = DBParamsDB def = DBParamsDB
data DBParamsFormIdent where data DBParamsFormIdent where
@ -685,29 +701,29 @@ unDBParamsFormIdent DBTable{dbtIdent} DBParamsFormTableIdent = Just $ toP
unDBParamsFormIdent _ (DBParamsFormOverrideIdent x) = Just $ toPathPiece x unDBParamsFormIdent _ (DBParamsFormOverrideIdent x) = Just $ toPathPiece x
unDBParamsFormIdent _ DBParamsFormNoIdent = Nothing unDBParamsFormIdent _ DBParamsFormNoIdent = Nothing
instance Monoid' x => IsDBTable (RWST (Maybe (Env, FileEnv), UniWorX, [Lang]) Enctype Ints (HandlerT UniWorX IO)) x where instance Monoid' x => IsDBTable (RWST (Maybe (Env, FileEnv), UniWorX, [Lang]) Enctype Ints (HandlerFor UniWorX)) x where
data DBParams (RWST (Maybe (Env, FileEnv), UniWorX, [Lang]) Enctype Ints (HandlerT UniWorX IO)) x = forall a. DBParamsForm data DBParams (RWST (Maybe (Env, FileEnv), UniWorX, [Lang]) Enctype Ints (HandlerFor UniWorX)) x = forall a. DBParamsForm
{ dbParamsFormMethod :: StdMethod { dbParamsFormMethod :: StdMethod
, dbParamsFormAction :: Maybe (SomeRoute UniWorX) , dbParamsFormAction :: Maybe (SomeRoute UniWorX)
, dbParamsFormAttrs :: [(Text, Text)] , dbParamsFormAttrs :: [(Text, Text)]
, dbParamsFormSubmit :: FormSubmitType , dbParamsFormSubmit :: FormSubmitType
, dbParamsFormAdditional :: Form a , dbParamsFormAdditional :: Form a
, dbParamsFormEvaluate :: forall m' a' x'. (MonadHandler m', HandlerSite m' ~ UniWorX, MonadResource m') => (Html -> MForm (HandlerT UniWorX IO) (FormResult a', x')) -> m' ((FormResult a', x'), Enctype) , dbParamsFormEvaluate :: forall m' a' x'. (MonadHandler m', HandlerSite m' ~ UniWorX, MonadResource m') => (Html -> MForm (HandlerFor UniWorX) (FormResult a', x')) -> m' ((FormResult a', x'), Enctype)
, dbParamsFormResult :: Lens' x (FormResult a) , dbParamsFormResult :: Lens' x (FormResult a)
, dbParamsFormIdent :: DBParamsFormIdent , dbParamsFormIdent :: DBParamsFormIdent
} }
type DBResult (RWST (Maybe (Env, FileEnv), UniWorX, [Lang]) Enctype Ints (HandlerT UniWorX IO)) x = (x, Widget) type DBResult (RWST (Maybe (Env, FileEnv), UniWorX, [Lang]) Enctype Ints (HandlerFor UniWorX)) x = (x, Widget)
-- type DBResult' (RWST (Maybe (Env, FileEnv), UniWorX, [Lang]) Enctype Ints (HandlerT UniWorX IO)) (FormResult a) = (FormResult a, Enctype) -- type DBResult' (RWST (Maybe (Env, FileEnv), UniWorX, [Lang]) Enctype Ints (HandlerFor UniWorX)) (FormResult a) = (FormResult a, Enctype)
data DBCell (RWST (Maybe (Env, FileEnv), UniWorX, [Lang]) Enctype Ints (HandlerT UniWorX IO)) x = forall a. FormCell data DBCell (RWST (Maybe (Env, FileEnv), UniWorX, [Lang]) Enctype Ints (HandlerFor UniWorX)) x = forall a. FormCell
{ formCellAttrs :: [(Text, Text)] { formCellAttrs :: [(Text, Text)]
, formCellContents :: WriterT x (MForm (HandlerT UniWorX IO)) (FormResult a, Widget) , formCellContents :: WriterT x (MForm (HandlerFor UniWorX)) (FormResult a, Widget)
, formCellLens :: Lens' x (FormResult a) , formCellLens :: Lens' x (FormResult a)
} }
-- dbCell :: Iso' -- dbCell :: Iso'
-- (DBCell (RWST ... ... ... (HandlerT UniWorX IO)) x) -- (DBCell (RWST ... ... ... (HandlerFor UniWorX)) x)
-- ([(Text, Text)], WriterT x (RWST ... ... ... (HandlerT UniWorX IO)) Widget) -- ([(Text, Text)], WriterT x (RWST ... ... ... (HandlerFor UniWorX)) Widget)
dbCell = iso dbCell = iso
(\FormCell{..} -> (formCellAttrs, formCellContents >>= uncurry ($>) . over _1 (tell . (flip $ set formCellLens) mempty))) (\FormCell{..} -> (formCellAttrs, formCellContents >>= uncurry ($>) . over _1 (tell . (flip $ set formCellLens) mempty)))
(\(attrs, mkWidget) -> FormCell attrs ((pure (), ) <$> mkWidget) $ lens (\_ -> pure ()) (\s _ -> s)) (\(attrs, mkWidget) -> FormCell attrs ((pure (), ) <$> mkWidget) $ lens (\_ -> pure ()) (\s _ -> s))
@ -716,7 +732,7 @@ instance Monoid' x => IsDBTable (RWST (Maybe (Env, FileEnv), UniWorX, [Lang]) En
-- ((,) <$> ((,) <$> view (_2._1) <*> view _1) <*> view (_2._2)) -- ((,) <$> ((,) <$> view (_2._1) <*> view _1) <*> view (_2._2))
dbWidget _ _ = return . snd dbWidget _ _ = return . snd
dbHandler _ _ f = return . over _2 f dbHandler _ _ f = return . over _2 f
-- runDBTable :: forall m' k'. (MonadHandler m', HandlerSite m' ~ UniWorX, ToJSON k') => DBTable (MForm (HandlerT UniWorX IO)) x -> PaginationInput -> [k'] -> (MForm (HandlerT UniWorX IO)) (x, Widget) -> ReaderT SqlBackend m' (x, Widget) -- runDBTable :: forall m' k'. (MonadHandler m', HandlerSite m' ~ UniWorX, ToJSON k') => DBTable (MForm (HandlerFor UniWorX)) x -> PaginationInput -> [k'] -> (MForm (HandlerFor UniWorX)) (x, Widget) -> ReaderT SqlBackend m' (x, Widget)
runDBTable dbtable@(DBTable{ dbtParams = dbtParams@DBParamsForm{..} }) pi pKeys runDBTable dbtable@(DBTable{ dbtParams = dbtParams@DBParamsForm{..} }) pi pKeys
= fmap ((\(res, (wdgt, x)) -> (x & dbParamsFormResult .~ res, wdgt)) . view _1) = fmap ((\(res, (wdgt, x)) -> (x & dbParamsFormResult .~ res, wdgt)) . view _1)
. dbParamsFormEvaluate . dbParamsFormEvaluate
@ -734,19 +750,19 @@ instance Monoid' x => IsDBTable (RWST (Maybe (Env, FileEnv), UniWorX, [Lang]) En
adjResult _ = FormFailure $ pure reasonTxt adjResult _ = FormFailure $ pure reasonTxt
return $ over (_1 . dbParamsFormResult) adjResult result return $ over (_1 . dbParamsFormResult) adjResult result
instance Monoid' x => Default (DBParams (RWST (Maybe (Env, FileEnv), UniWorX, [Lang]) Enctype Ints (HandlerT UniWorX IO)) x) where instance Default (DBParams (RWST (Maybe (Env, FileEnv), UniWorX, [Lang]) Enctype Ints (HandlerFor UniWorX)) x) where
def = DBParamsForm def = DBParamsForm
{ dbParamsFormMethod = POST { dbParamsFormMethod = POST
, dbParamsFormAction = Nothing , dbParamsFormAction = Nothing
, dbParamsFormAttrs = [] , dbParamsFormAttrs = []
, dbParamsFormSubmit = FormSubmit , dbParamsFormSubmit = FormSubmit
, dbParamsFormAdditional = \_ -> return (pure (), mempty) , dbParamsFormAdditional = \_ -> return (pure (), mempty)
, dbParamsFormEvaluate = liftHandlerT . runFormPost , dbParamsFormEvaluate = liftHandler . runFormPost
, dbParamsFormResult = lens (\_ -> pure ()) (\s _ -> s) , dbParamsFormResult = lens (\_ -> pure ()) (\s _ -> s)
, dbParamsFormIdent = def , dbParamsFormIdent = def
} }
dbParamsFormWrap :: Monoid' x => DBTable (MForm (HandlerT UniWorX IO)) x -> DBParams (MForm (HandlerT UniWorX IO)) x -> (Html -> MForm (HandlerT UniWorX IO) (x, Widget)) -> (Html -> MForm (HandlerT UniWorX IO) (x, Widget)) dbParamsFormWrap :: Monoid' x => DBTable (MForm (HandlerFor UniWorX)) x -> DBParams (MForm (HandlerFor UniWorX)) x -> (Html -> MForm (HandlerFor UniWorX) (x, Widget)) -> (Html -> MForm (HandlerFor UniWorX) (x, Widget))
dbParamsFormWrap DBTable{ dbtIdent } DBParamsForm{..} tableForm frag = do dbParamsFormWrap DBTable{ dbtIdent } DBParamsForm{..} tableForm frag = do
let form = mappend <$> tableForm frag <*> (fmap (over _1 $ (flip $ set dbParamsFormResult) mempty) $ dbParamsFormAdditional mempty) let form = mappend <$> tableForm frag <*> (fmap (over _1 $ (flip $ set dbParamsFormResult) mempty) $ dbParamsFormAdditional mempty)
((res, fWidget), enctype) <- listen form ((res, fWidget), enctype) <- listen form
@ -781,10 +797,10 @@ addPreviousHiddenField DBTable{ dbtIdent } pKeys form fragment = do
wIdent :: Text -> Text wIdent :: Text -> Text
wIdent = toPathPiece . WithIdent dbtIdent wIdent = toPathPiece . WithIdent dbtIdent
instance Monoid' x => Sem.Semigroup (DBCell (RWST (Maybe (Env, FileEnv), UniWorX, [Lang]) Enctype Ints (HandlerT UniWorX IO)) x) where instance Monoid' x => Sem.Semigroup (DBCell (RWST (Maybe (Env, FileEnv), UniWorX, [Lang]) Enctype Ints (HandlerFor UniWorX)) x) where
(FormCell attrs c l) <> (FormCell attrs' c' l') = FormCell (attrs <> attrs') ((\(a, w) (a', w') -> ((,) <$> a <*> a', w <> w')) <$> c <*> c') (lens (liftA2 (,) <$> view l <*> view l') (\s as -> s & l .~ (fst <$> as) & l' .~ (snd <$> as))) (FormCell attrs c l) <> (FormCell attrs' c' l') = FormCell (attrs <> attrs') ((\(a, w) (a', w') -> ((,) <$> a <*> a', w <> w')) <$> c <*> c') (lens (liftA2 (,) <$> view l <*> view l') (\s as -> s & l .~ (fst <$> as) & l' .~ (snd <$> as)))
instance Monoid' x => Monoid (DBCell (RWST (Maybe (Env, FileEnv), UniWorX, [Lang]) Enctype Ints (HandlerT UniWorX IO)) x) where instance Monoid' x => Monoid (DBCell (RWST (Maybe (Env, FileEnv), UniWorX, [Lang]) Enctype Ints (HandlerFor UniWorX)) x) where
mempty = FormCell mempty (return mempty) $ lens (\_ -> pure ()) (\s _ -> s) mempty = FormCell mempty (return mempty) $ lens (\_ -> pure ()) (\s _ -> s)
mappend = (<>) mappend = (<>)
@ -839,7 +855,7 @@ dbTable PSValidator{..} dbtable@DBTable{ dbtIdent = dbtIdent'@(toPathPiece -> db
(filterRes'@((filterRes, _), _)) <- runFormGet . identifyForm FIDDBTableFilter . addPIHiddenField dbtable (prevPi & _piFilter .~ Nothing & _piPage .~ Nothing & _piLimit .~ (formResult' pagesizeRes <|> piLimit prevPi)) . renderAForm FormDBTableFilter $ dbtFilterUI (piFilter prevPi) (filterRes'@((filterRes, _), _)) <- runFormGet . identifyForm FIDDBTableFilter . addPIHiddenField dbtable (prevPi & _piFilter .~ Nothing & _piPage .~ Nothing & _piLimit .~ (formResult' pagesizeRes <|> piLimit prevPi)) . renderAForm FormDBTableFilter $ dbtFilterUI (piFilter prevPi)
(pagesizeRes'@((pagesizeRes, _), _)) <- lift . runFormGet . identifyForm FIDDBTablePagesize . addPIHiddenField dbtable (prevPi & _piPage .~ Nothing & _piLimit .~ Nothing & _piFilter .~ (formResult' filterRes <|> piFilter prevPi)) . renderAForm FormDBTablePagesize $ (pagesizeRes'@((pagesizeRes, _), _)) <- lift . runFormGet . identifyForm FIDDBTablePagesize . addPIHiddenField dbtable (prevPi & _piPage .~ Nothing & _piLimit .~ Nothing & _piFilter .~ (formResult' filterRes <|> piFilter prevPi)) . renderAForm FormDBTablePagesize $
areq (pagesizeField referencePagesize) (fslI MsgDBTablePagesize & addName (wIdent "pagesize") & addClass "select--pagesize") (Just referencePagesize) areq (pagesizeField referencePagesize) (fslI MsgDBTablePagesize & addName (wIdent "pagesize") & addClass ("select--pagesize" :: Text)) (Just referencePagesize)
return (filterRes', pagesizeRes') return (filterRes', pagesizeRes')
let let
@ -863,8 +879,8 @@ dbTable PSValidator{..} dbtable@DBTable{ dbtIdent = dbtIdent'@(toPathPiece -> db
mapM_ (addMessageI Warning) errs mapM_ (addMessageI Warning) errs
Just currentRoute <- getCurrentRoute -- `dbTable` should never be called from a 404-handler currentRoute <- fromMaybe (error "dbTable called from 404-handler") <$> getCurrentRoute
getParams <- liftHandlerT $ queryToQueryText . Wai.queryString . reqWaiRequest <$> getRequest getParams <- liftHandler $ queryToQueryText . Wai.queryString . reqWaiRequest <$> getRequest
let let
tblLink :: (QueryText -> QueryText) -> SomeRoute UniWorX tblLink :: (QueryText -> QueryText) -> SomeRoute UniWorX
tblLink f = SomeRoute . (currentRoute, ) . over (mapped . _2) (fromMaybe Text.empty) $ (f . substPi . setParam "_hasdata" Nothing) getParams tblLink f = SomeRoute . (currentRoute, ) . over (mapped . _2) (fromMaybe Text.empty) $ (f . substPi . setParam "_hasdata" Nothing) getParams
@ -964,15 +980,17 @@ dbTable PSValidator{..} dbtable@DBTable{ dbtIdent = dbtIdent'@(toPathPiece -> db
DBCsvExport{..} DBCsvExport{..}
| Just DBTCsvEncode{..} <- dbtCsvEncode | Just DBTCsvEncode{..} <- dbtCsvEncode
, Just exportData <- fromDynamic dbCsvExportData -> do , Just exportData <- fromDynamic dbCsvExportData -> do
hdr <- dbtCsvHeader $ Just exportData
let ensureExtension ext fName = bool (addExtension ext) id (ext `isExtensionOf` fName) fName let ensureExtension ext fName = bool (addExtension ext) id (ext `isExtensionOf` fName) fName
setContentDisposition' . Just $ ensureExtension (unpack extensionCsv) dbtCsvName setContentDisposition' . Just $ ensureExtension (unpack extensionCsv) dbtCsvName
sendResponse <=< liftHandlerT . respondCsvDB $ C.sourceList (zip currentKeys rows) .| dbtCsvDoEncode exportData >> lift E.transactionSave sendResponse <=< liftHandler . respondCsvDB hdr $ C.sourceList (zip currentKeys rows) .| dbtCsvDoEncode exportData >> lift E.transactionSave
DBCsvImport{..} DBCsvImport{..}
| Just (DBTCsvDecode{ dbtCsvClassifyAction = dbtCsvClassifyAction :: csvAction -> csvActionClass | Just DBTCsvEncode{..} <- dbtCsvEncode
, Just (DBTCsvDecode{ dbtCsvClassifyAction = dbtCsvClassifyAction :: csvAction -> csvActionClass
, .. , ..
} :: DBTCsvDecode r' k' csv) <- dbtCsvDecode -> do } :: DBTCsvDecode r' k' csv) <- dbtCsvDecode -> do
let existing = Map.fromList $ zip currentKeys rows let existing = Map.fromList $ zip currentKeys rows
sourceDiff :: Source (StateT (Map k' csv) (YesodDB UniWorX)) (DBCsvDiff r' csv k') sourceDiff :: ConduitT () (DBCsvDiff r' csv k') (StateT (Map k' csv) (YesodDB UniWorX)) ()
sourceDiff = do sourceDiff = do
let let
toDiff :: csv -> StateT (Map k' csv) (YesodDB UniWorX) (DBCsvDiff r' csv k') toDiff :: csv -> StateT (Map k' csv) (YesodDB UniWorX) (DBCsvDiff r' csv k')
@ -1001,7 +1019,7 @@ dbTable PSValidator{..} dbtable@DBTable{ dbtIdent = dbtIdent'@(toPathPiece -> db
importCsv = do importCsv = do
let let
dbtCsvComputeActions' :: Sink (DBCsvDiff r' csv k') (YesodDB UniWorX) (Map csvActionClass (Set csvAction)) dbtCsvComputeActions' :: ConduitT (DBCsvDiff r' csv k') Void (YesodDB UniWorX) (Map csvActionClass (Set csvAction))
dbtCsvComputeActions' = do dbtCsvComputeActions' = do
let innerAct = awaitForever $ \x let innerAct = awaitForever $ \x
-> let doHandle -> let doHandle
@ -1017,7 +1035,7 @@ dbTable PSValidator{..} dbtable@DBTable{ dbtIdent = dbtIdent'@(toPathPiece -> db
addMessageI Info MsgCsvImportUnnecessary addMessageI Info MsgCsvImportUnnecessary
redirect $ tblLink id redirect $ tblLink id
liftHandlerT . (>>= sendResponse) $ liftHandler . (>>= sendResponse) $
siteLayoutMsg MsgCsvImportConfirmationHeading $ do siteLayoutMsg MsgCsvImportConfirmationHeading $ do
setTitleI MsgCsvImportConfirmationHeading setTitleI MsgCsvImportConfirmationHeading
@ -1033,14 +1051,14 @@ dbTable PSValidator{..} dbtable@DBTable{ dbtIdent = dbtIdent'@(toPathPiece -> db
csvActionCheckBox :: [(Text, Text)] -> csvAction -> Widget csvActionCheckBox :: [(Text, Text)] -> csvAction -> Widget
csvActionCheckBox vAttrs act = do csvActionCheckBox vAttrs act = do
let let
sJsonField :: Field (HandlerT UniWorX IO) csvAction sJsonField :: Field (HandlerFor UniWorX) csvAction
sJsonField = secretJsonField' $ \theId name attrs val _isReq -> sJsonField = secretJsonField' $ \theId name attrs val _isReq ->
[whamlet| [whamlet|
$newline never $newline never
<input id=#{theId} *{attrs} type=checkbox name=#{name} value=#{either id id val} :defaultChecked (dbtCsvClassifyAction act):checked> <input id=#{theId} *{attrs} type=checkbox name=#{name} value=#{either id id val} :defaultChecked (dbtCsvClassifyAction act):checked>
|] |]
fieldView sJsonField (actionIdent act) (toPathPiece PostDBCsvImportAction) vAttrs (Right act) False fieldView sJsonField (actionIdent act) (toPathPiece PostDBCsvImportAction) vAttrs (Right act) False
(csvImportConfirmForm', csvImportConfirmEnctype) <- liftHandlerT . generateFormPost . identifyForm FIDDBTableCsvImportConfirm $ \csrf -> return (error "No meaningful FormResult", $(widgetFile "csv-import-confirmation")) (csvImportConfirmForm', csvImportConfirmEnctype) <- liftHandler . generateFormPost . identifyForm FIDDBTableCsvImportConfirm $ \csrf -> return (error "No meaningful FormResult", $(widgetFile "csv-import-confirmation"))
let csvImportConfirmForm = wrapForm' BtnCsvImportConfirm csvImportConfirmForm' FormSettings let csvImportConfirmForm = wrapForm' BtnCsvImportConfirm csvImportConfirmForm' FormSettings
{ formMethod = POST { formMethod = POST
, formAction = Just $ tblLink id , formAction = Just $ tblLink id
@ -1052,14 +1070,14 @@ dbTable PSValidator{..} dbtable@DBTable{ dbtIdent = dbtIdent'@(toPathPiece -> db
$(widgetFile "csv-import-confirmation-wrapper") $(widgetFile "csv-import-confirmation-wrapper")
let defaultHeaderOrder = headerOrder (error "not to be forced" :: csv) hdr <- dbtCsvHeader Nothing
catches importCsv catches importCsv
[ Catch.Handler $ \case [ Catch.Handler $ \case
(DBCsvDuplicateKey{..} :: DBCsvException k') (DBCsvDuplicateKey{..} :: DBCsvException k')
-> liftHandlerT $ sendResponseStatus badRequest400 =<< do -> liftHandler $ sendResponseStatus badRequest400 =<< do
mr <- getMessageRender mr <- getMessageRender
let offendingCsv = CsvRendered defaultHeaderOrder [ dbCsvDuplicateKeyRowA, dbCsvDuplicateKeyRowB ] let offendingCsv = CsvRendered hdr [ dbCsvDuplicateKeyRowA, dbCsvDuplicateKeyRowB ]
heading = ErrorResponseTitle $ InvalidArgs [mr MsgDBCsvDuplicateKey] heading = ErrorResponseTitle $ InvalidArgs [mr MsgDBCsvDuplicateKey]
siteLayoutMsg heading $ do siteLayoutMsg heading $ do
@ -1070,10 +1088,10 @@ dbTable PSValidator{..} dbtable@DBTable{ dbtIdent = dbtIdent'@(toPathPiece -> db
^{offendingCsv} ^{offendingCsv}
|] |]
(DBCsvException{..} :: DBCsvException k') (DBCsvException{..} :: DBCsvException k')
-> liftHandlerT $ sendResponseStatus badRequest400 =<< do -> liftHandler $ sendResponseStatus badRequest400 =<< do
mr <- getMessageRender mr <- getMessageRender
let offendingCsv = CsvRendered defaultHeaderOrder [ dbCsvExceptionRow ] let offendingCsv = CsvRendered hdr [ dbCsvExceptionRow ]
heading = ErrorResponseTitle $ InvalidArgs [mr MsgDBCsvException] heading = ErrorResponseTitle $ InvalidArgs [mr MsgDBCsvException]
siteLayoutMsg heading $ do siteLayoutMsg heading $ do
@ -1098,7 +1116,7 @@ dbTable PSValidator{..} dbtable@DBTable{ dbtIdent = dbtIdent'@(toPathPiece -> db
. setParam (wIdent "page") Nothing . setParam (wIdent "page") Nothing
. setParam (wIdent "pagination") Nothing . setParam (wIdent "pagination") Nothing
table' :: HandlerSite m ~ UniWorX => WriterT x m Widget table' :: WriterT x m Widget
table' = let table' = let
columnCount :: Int64 columnCount :: Int64
columnCount = olength64 $ getColonnade dbtColonnade columnCount = olength64 $ getColonnade dbtColonnade
@ -1182,7 +1200,7 @@ dbTable PSValidator{..} dbtable@DBTable{ dbtIdent = dbtIdent'@(toPathPiece -> db
where where
tblLayout :: forall m'. (MonadHandler m', HandlerSite m' ~ UniWorX) => Widget -> m' Html tblLayout :: forall m'. (MonadHandler m', HandlerSite m' ~ UniWorX) => Widget -> m' Html
tblLayout tbl' = do tblLayout tbl' = do
tbl <- liftHandlerT $ widgetToPageContent tbl' tbl <- liftHandler $ widgetToPageContent tbl'
withUrlRenderer $(hamletFile "templates/table/layout-standalone.hamlet") withUrlRenderer $(hamletFile "templates/table/layout-standalone.hamlet")
setParams :: Text -> [Text] -> QueryText -> QueryText setParams :: Text -> [Text] -> QueryText -> QueryText
@ -1191,26 +1209,27 @@ dbTable PSValidator{..} dbtable@DBTable{ dbtIdent = dbtIdent'@(toPathPiece -> db
setParam :: Text -> Maybe Text -> QueryText -> QueryText setParam :: Text -> Maybe Text -> QueryText -> QueryText
setParam key = setParams key . maybeToList setParam key = setParams key . maybeToList
dbTableWidget :: Monoid' x => PSValidator (HandlerT UniWorX IO) x -> DBTable (HandlerT UniWorX IO) x dbTableWidget :: Monoid x
-> DB (DBResult (HandlerT UniWorX IO) x) => PSValidator (HandlerFor UniWorX) x
-> DBTable (HandlerFor UniWorX) x
-> DB (DBResult (HandlerFor UniWorX) x)
dbTableWidget = dbTable dbTableWidget = dbTable
dbTableWidget' :: PSValidator (HandlerT UniWorX IO) () -> DBTable (HandlerT UniWorX IO) () -> DB Widget dbTableWidget' :: PSValidator (HandlerFor UniWorX) ()
-> DBTable (HandlerFor UniWorX) ()
-> DB Widget
dbTableWidget' = fmap (fmap snd) . dbTable dbTableWidget' = fmap (fmap snd) . dbTable
widgetColonnade :: (Headedness h, Monoid' x) widgetColonnade :: Colonnade h r (DBCell (HandlerFor UniWorX) x)
=> Colonnade h r (DBCell (HandlerT UniWorX IO) x) -> Colonnade h r (DBCell (HandlerFor UniWorX) x)
-> Colonnade h r (DBCell (HandlerT UniWorX IO) x)
widgetColonnade = id widgetColonnade = id
formColonnade :: (Headedness h, Monoid' a) formColonnade :: Colonnade h r (DBCell (RWST (Maybe (Env, FileEnv), UniWorX, [Lang]) Enctype Ints (HandlerFor UniWorX)) (FormResult a))
=> Colonnade h r (DBCell (RWST (Maybe (Env, FileEnv), UniWorX, [Lang]) Enctype Ints (HandlerT UniWorX IO)) (FormResult a)) -> Colonnade h r (DBCell (RWST (Maybe (Env, FileEnv), UniWorX, [Lang]) Enctype Ints (HandlerFor UniWorX)) (FormResult a))
-> Colonnade h r (DBCell (RWST (Maybe (Env, FileEnv), UniWorX, [Lang]) Enctype Ints (HandlerT UniWorX IO)) (FormResult a))
formColonnade = id formColonnade = id
dbColonnade :: (Headedness h, Monoid' x) dbColonnade :: Colonnade h r (DBCell (ReaderT SqlBackend (HandlerFor UniWorX)) x)
=> Colonnade h r (DBCell (ReaderT SqlBackend (HandlerT UniWorX IO)) x) -> Colonnade h r (DBCell (ReaderT SqlBackend (HandlerFor UniWorX)) x)
-> Colonnade h r (DBCell (ReaderT SqlBackend (HandlerT UniWorX IO)) x)
dbColonnade = id dbColonnade = id
pagesizeOptions :: PagesizeLimit -- ^ Current/previous value pagesizeOptions :: PagesizeLimit -- ^ Current/previous value
@ -1271,17 +1290,17 @@ anchorCell' :: ( IsDBTable m a
-> (r -> DBCell m a) -> (r -> DBCell m a)
anchorCell' mkRoute mkWidget val = anchorCell (mkRoute val) (mkWidget val) anchorCell' mkRoute mkWidget val = anchorCell (mkRoute val) (mkWidget val)
anchorCellM :: (HasRoute UniWorX url, ToWidget UniWorX wgt, IsDBTable m a, HandlerSite m ~ UniWorX) => WidgetT UniWorX IO url -> wgt -> DBCell m a anchorCellM :: (HasRoute UniWorX url, ToWidget UniWorX wgt, IsDBTable m a) => WidgetFor UniWorX url -> wgt -> DBCell m a
anchorCellM routeM widget = anchorCellM' routeM id (const widget) anchorCellM routeM widget = anchorCellM' routeM id (const widget)
anchorCellM' :: (HasRoute UniWorX url, ToWidget UniWorX wgt, IsDBTable m a, HandlerSite m ~ UniWorX) => WidgetT UniWorX IO x -> (x -> url) -> (x -> wgt) -> DBCell m a anchorCellM' :: (HasRoute UniWorX url, ToWidget UniWorX wgt, IsDBTable m a) => WidgetFor UniWorX x -> (x -> url) -> (x -> wgt) -> DBCell m a
anchorCellM' xM x2route x2widget = linkEitherCellM' xM x2route (x2widget, x2widget) anchorCellM' xM x2route x2widget = linkEitherCellM' xM x2route (x2widget, x2widget)
-- | Variant of `anchorCell` that displays different widgets depending whether the route is authorized for current user -- | Variant of `anchorCell` that displays different widgets depending whether the route is authorized for current user
linkEitherCell :: (HasRoute UniWorX url, ToWidget UniWorX wgt, ToWidget UniWorX wgt', IsDBTable m a, HandlerSite m ~ UniWorX) => url -> (wgt, wgt') -> DBCell m a linkEitherCell :: (HasRoute UniWorX url, ToWidget UniWorX wgt, ToWidget UniWorX wgt', IsDBTable m a) => url -> (wgt, wgt') -> DBCell m a
linkEitherCell = linkEitherCellM . return linkEitherCell = linkEitherCellM . return
linkEitherCellM :: (HasRoute UniWorX url, ToWidget UniWorX wgt, ToWidget UniWorX wgt', IsDBTable m a, HandlerSite m ~ UniWorX) => WidgetT UniWorX IO url -> (wgt, wgt') -> DBCell m a linkEitherCellM :: (HasRoute UniWorX url, ToWidget UniWorX wgt, ToWidget UniWorX wgt', IsDBTable m a) => WidgetFor UniWorX url -> (wgt, wgt') -> DBCell m a
linkEitherCellM routeM (widgetAuth,widgetUnauth) = linkEitherCellM' routeM id (const widgetAuth, const widgetUnauth) linkEitherCellM routeM (widgetAuth,widgetUnauth) = linkEitherCellM' routeM id (const widgetAuth, const widgetUnauth)
linkEitherCellM' :: forall m url wgt wgt' a x. linkEitherCellM' :: forall m url wgt wgt' a x.
@ -1289,16 +1308,15 @@ linkEitherCellM' :: forall m url wgt wgt' a x.
, ToWidget UniWorX wgt , ToWidget UniWorX wgt
, ToWidget UniWorX wgt' , ToWidget UniWorX wgt'
, IsDBTable m a , IsDBTable m a
, HandlerSite m ~ UniWorX
) )
=> WidgetT UniWorX IO x -> (x -> url) -> (x -> wgt, x -> wgt') -> DBCell m a => WidgetFor UniWorX x -> (x -> url) -> (x -> wgt, x -> wgt') -> DBCell m a
linkEitherCellM' xM x2route (x2widgetAuth,x2widgetUnauth) = cell $ do linkEitherCellM' xM x2route (x2widgetAuth,x2widgetUnauth) = cell $ do
x <- xM x <- xM
let route = x2route x let route = x2route x
widget, widgetUnauth :: WidgetT UniWorX IO () widget, widgetUnauth :: Widget
widget = toWidget $ x2widgetAuth x widget = toWidget $ x2widgetAuth x
widgetUnauth = toWidget $ x2widgetUnauth x widgetUnauth = toWidget $ x2widgetUnauth x
authResult <- liftHandlerT $ isAuthorized (urlRoute route) False authResult <- liftHandler $ isAuthorized (urlRoute route) False
linkUrl <- toTextUrl route linkUrl <- toTextUrl route
case authResult of case authResult of
Authorized -> $(widgetFile "table/cell/link") -- show allowed link Authorized -> $(widgetFile "table/cell/link") -- show allowed link
@ -1324,17 +1342,17 @@ instance Ord i => Monoid (DBFormResult i a r) where
mempty = DBFormResult Map.empty mempty = DBFormResult Map.empty
mappend = (<>) mappend = (<>)
getDBFormResult :: forall r i a. Ord i => (r -> a) -> DBFormResult i a r -> Map i a getDBFormResult :: forall r i a. (r -> a) -> DBFormResult i a r -> Map i a
getDBFormResult initial (DBFormResult m) = Map.map (\(r, f) -> f $ initial r) m getDBFormResult initial (DBFormResult m) = Map.map (\(r, f) -> f $ initial r) m
formCell :: forall x r i a. (Ord i, Monoid x) formCell :: forall x r i a. Monoid x
=> Lens' x (FormResult (DBFormResult i a (DBRow r))) -- ^ lens focussing on the form result within the larger DBResult; @id@ iff the form delivers the only result of the table => Lens' x (FormResult (DBFormResult i a (DBRow r))) -- ^ lens focussing on the form result within the larger DBResult; @id@ iff the form delivers the only result of the table
-> (DBRow r -> MForm (HandlerT UniWorX IO) i) -- ^ generate row identfifiers for use in form result -> (DBRow r -> MForm (HandlerFor UniWorX) i) -- ^ generate row identfifiers for use in form result
-> (DBRow r -> (forall p. PathPiece p => p -> Text) -> MForm (HandlerT UniWorX IO) (FormResult (a -> a), Widget)) -- ^ Given the row data and a callback to make an input name suitably unique generate the `MForm` -> (DBRow r -> (forall p. PathPiece p => p -> Text) -> MForm (HandlerFor UniWorX) (FormResult (a -> a), Widget)) -- ^ Given the row data and a callback to make an input name suitably unique generate the `MForm`
-> (DBRow r -> DBCell (MForm (HandlerT UniWorX IO)) x) -> (DBRow r -> DBCell (MForm (HandlerFor UniWorX)) x)
formCell formCellLens genIndex genForm input@(DBRow{dbrKey}) = FormCell formCell formCellLens genIndex genForm input@(DBRow{dbrKey}) = FormCell
{ formCellAttrs = [] { formCellAttrs = []
, formCellContents = do -- MForm (HandlerT UniWorX IO) (FormResult (Map i (Endo a)), Widget) , formCellContents = do -- MForm (HandlerFor UniWorX) (FormResult (Map i (Endo a)), Widget)
i <- lift $ genIndex input i <- lift $ genIndex input
hashKey <- LBS.toStrict . B.encode <$> cryptoIDKey return hashKey <- LBS.toStrict . B.encode <$> cryptoIDKey return
let let
@ -1354,11 +1372,11 @@ formCell formCellLens genIndex genForm input@(DBRow{dbrKey}) = FormCell
dbRow :: forall h r m a. (Headedness h, IsDBTable m a) => Colonnade h (DBRow r) (DBCell m a) dbRow :: forall h r m a. (Headedness h, IsDBTable m a) => Colonnade h (DBRow r) (DBCell m a)
dbRow = Colonnade.singleton (headednessPure $ i18nCell MsgNrColumn) $ \DBRow{ dbrIndex } -> textCell $ tshow dbrIndex dbRow = Colonnade.singleton (headednessPure $ i18nCell MsgNrColumn) $ \DBRow{ dbrIndex } -> textCell $ tshow dbrIndex
dbSelect :: forall x h r i a. (Headedness h, Ord i, PathPiece i, Monoid' x) dbSelect :: forall x h r i a. (Headedness h, Monoid' x)
=> Lens' x (FormResult (DBFormResult i a (DBRow r))) => Lens' x (FormResult (DBFormResult i a (DBRow r)))
-> Setter' a Bool -> Setter' a Bool
-> (DBRow r -> MForm (HandlerT UniWorX IO) i) -> (DBRow r -> MForm (HandlerFor UniWorX) i)
-> Colonnade h (DBRow r) (DBCell (MForm (HandlerT UniWorX IO)) x) -> Colonnade h (DBRow r) (DBCell (MForm (HandlerFor UniWorX)) x)
-- dbSelect resLens selLens genIndex = Colonnade.singleton (headednessPure $ i18nCell MsgSelectColumn) $ formCell resLens genIndex genForm -- dbSelect resLens selLens genIndex = Colonnade.singleton (headednessPure $ i18nCell MsgSelectColumn) $ formCell resLens genIndex genForm
dbSelect resLens selLens genIndex = Colonnade.singleton (headednessPure $ mempty) $ formCell resLens genIndex genForm dbSelect resLens selLens genIndex = Colonnade.singleton (headednessPure $ mempty) $ formCell resLens genIndex genForm
where where

View File

@ -57,8 +57,7 @@ instance GCsvColumnsExplained a => GCsvColumnsExplained (M1 C c a) where
-- | Instance to ensure that you cannot derive DefaultOrdered for -- | Instance to ensure that you cannot derive DefaultOrdered for
-- constructors without selectors. -- constructors without selectors.
instance CsvColumnsExplained (M1 S ('MetaSel 'Nothing srcpk srcstr decstr) a ()) instance GCsvColumnsExplained (M1 S ('MetaSel 'Nothing srcpk srcstr decstr) a)
=> GCsvColumnsExplained (M1 S ('MetaSel 'Nothing srcpk srcstr decstr) a)
where where
gCsvColumnsExplanations _ _ = gCsvColumnsExplanations _ _ =
error "You cannot derive CsvColumnsExplanations for constructors without selectors." error "You cannot derive CsvColumnsExplanations for constructors without selectors."

View File

@ -18,6 +18,7 @@ import Data.CaseInsensitive (CI)
import Data.Aeson (FromJSON, ToJSON, FromJSONKey, ToJSONKey) import Data.Aeson (FromJSON, ToJSON, FromJSONKey, ToJSONKey)
{-# ANN module ("HLint: ignore Use newtype instead of data" :: String) #-}
newtype FilterKey = FilterKey { _unFilterKey :: CI Text } newtype FilterKey = FilterKey { _unFilterKey :: CI Text }
deriving (Show, Read, Generic) deriving (Show, Read, Generic)

View File

@ -17,7 +17,7 @@ maybeBearerToken = runMaybeT $ catchIfMaybeT cPred requireBearerToken
] ]
requireBearerToken :: (MonadHandler m, HandlerSite m ~ UniWorX) => m (BearerToken UniWorX) requireBearerToken :: (MonadHandler m, HandlerSite m ~ UniWorX) => m (BearerToken UniWorX)
requireBearerToken = liftHandlerT $ do requireBearerToken = liftHandler $ do
token <- exceptT (guardAuthResult >=> error "askToken should not throw `Authorized`") return askTokenUnsafe token <- exceptT (guardAuthResult >=> error "askToken should not throw `Authorized`") return askTokenUnsafe
mAuthId <- maybeAuthId mAuthId <- maybeAuthId
currentRoute <- maybe (permissionDeniedI MsgUnauthorizedToken404) return =<< getCurrentRoute currentRoute <- maybe (permissionDeniedI MsgUnauthorizedToken404) return =<< getCurrentRoute
@ -25,18 +25,24 @@ requireBearerToken = liftHandlerT $ do
guardAuthResult <=< runDB $ validateToken mAuthId currentRoute isWrite token guardAuthResult <=< runDB $ validateToken mAuthId currentRoute isWrite token
return token return token
maybeCurrentTokenRestrictions, requireCurrentTokenRestrictions :: ( MonadHandler m requireCurrentTokenRestrictions :: ( MonadHandler m
, HandlerSite m ~ UniWorX , HandlerSite m ~ UniWorX
, MonadCatch m , FromJSON a
, MonadLogger m , ToJSON a
, FromJSON a )
, ToJSON a => m (Maybe a)
)
=> m (Maybe a)
requireCurrentTokenRestrictions = runMaybeT $ do requireCurrentTokenRestrictions = runMaybeT $ do
token <- requireBearerToken token <- requireBearerToken
route <- MaybeT getCurrentRoute route <- MaybeT getCurrentRoute
hoistMaybe $ token ^? _tokenRestrictionIx route hoistMaybe $ token ^? _tokenRestrictionIx route
maybeCurrentTokenRestrictions :: ( MonadHandler m
, HandlerSite m ~ UniWorX
, MonadCatch m
, FromJSON a
, ToJSON a
)
=> m (Maybe a)
maybeCurrentTokenRestrictions = runMaybeT $ do maybeCurrentTokenRestrictions = runMaybeT $ do
token <- MaybeT maybeBearerToken token <- MaybeT maybeBearerToken
route <- MaybeT getCurrentRoute route <- MaybeT getCurrentRoute

View File

@ -30,6 +30,15 @@ import Data.List (dropWhileEnd)
import qualified Data.Map as Map import qualified Data.Map as Map
import qualified Data.Conduit.List as C
import Data.Encoding ( decodeStrictByteStringExplicit
, encodeStrictByteStringExplicit
)
import Data.Encoding.CP437
import qualified Data.Char as Char
typeZip :: ContentType typeZip :: ContentType
typeZip = "application/zip" typeZip = "application/zip"
@ -45,30 +54,33 @@ instance Default ZipInfo where
} }
consumeZip :: ( MonadBase b m consumeZip :: forall b m.
, PrimMonad b ( MonadThrow b
, MonadThrow m , MonadThrow m
) => ConduitM ByteString File m ZipInfo , MonadBase b m
consumeZip = unZipStream `fuseUpstream` consumeZip' , PrimMonad b
)
=> ConduitT ByteString File m ZipInfo
consumeZip = transPipe liftBase unZipStream `fuseUpstream` consumeZip'
where where
consumeZip' :: ( MonadThrow m consumeZip' :: ConduitT (Either ZipEntry ByteString) File m ()
) => Conduit (Either ZipEntry ByteString) m File
consumeZip' = do consumeZip' = do
input <- await input <- await
case input of case input of
Nothing -> return () Nothing -> return ()
Just (Right _) -> throw $ userError "Data chunk in unexpected place when parsing ZIP" Just (Right _) -> throwM $ userError "Data chunk in unexpected place when parsing ZIP"
Just (Left ZipEntry{..}) -> do Just (Left ZipEntry{..}) -> do
contentChunks <- toConsumer accContents contentChunks <- toConsumer accContents
zipEntryName' <- decodeZipEntryName zipEntryName
let let
fileTitle = dropWhile isPathSeparator . dropTrailingPathSeparator . normalise $ makeValid zipEntryName fileTitle = dropWhile isPathSeparator . dropTrailingPathSeparator . normalise $ makeValid zipEntryName'
fileModified = localTimeToUTC utc zipEntryTime fileModified = localTimeToUTC utc zipEntryTime
fileContent fileContent
| hasTrailingPathSeparator zipEntryName = Nothing | hasTrailingPathSeparator zipEntryName' = Nothing
| otherwise = Just $ mconcat contentChunks | otherwise = Just $ mconcat contentChunks
yield File{..} yield File{..}
consumeZip' consumeZip'
accContents :: Monad m => Sink (Either a b) m [b] accContents :: ConduitT (Either a b') Void m [b']
accContents = do accContents = do
input <- await input <- await
case input of case input of
@ -76,12 +88,15 @@ consumeZip = unZipStream `fuseUpstream` consumeZip'
Just (Left x) -> [] <$ leftover (Left x) Just (Left x) -> [] <$ leftover (Left x)
_ -> return [] _ -> return []
produceZip :: ( MonadBase b m produceZip :: forall b m.
, PrimMonad b ( MonadThrow b
, MonadThrow m , MonadThrow m
) => ZipInfo , MonadBase b m
-> Conduit File m ByteString , PrimMonad b
produceZip info = mapC toZipData =$= void (zipStream zipOptions) )
=> ZipInfo
-> ConduitT File ByteString m ()
produceZip info = C.map toZipData .| transPipe liftBase (void $ zipStream zipOptions)
where where
zipOptions = ZipOptions zipOptions = ZipOptions
{ zipOpt64 = True { zipOpt64 = True
@ -89,35 +104,60 @@ produceZip info = mapC toZipData =$= void (zipStream zipOptions)
, zipOptInfo = info , zipOptInfo = info
} }
toZipData :: Monad m => File -> (ZipEntry, ZipData m) toZipData :: File -> (ZipEntry, ZipData b)
toZipData f@File{..} = ((toZipEntry f){ zipEntrySize = fromIntegral . ByteString.length <$> fileContent }, maybe mempty (ZipDataByteString . Lazy.ByteString.fromStrict) fileContent) toZipData f@File{..} =
let zData = maybe mempty (ZipDataByteString . Lazy.ByteString.fromStrict) fileContent
zEntry = (toZipEntry f){ zipEntrySize = fromIntegral . ByteString.length <$> fileContent }
in (zEntry, zData)
toZipEntry :: File -> ZipEntry toZipEntry :: File -> ZipEntry
toZipEntry File{..} = ZipEntry toZipEntry File{..} = ZipEntry{..}
{ zipEntryName = bool (dropWhileEnd isPathSeparator) addTrailingPathSeparator isDir . normalise . makeValid $ fileTitle
, zipEntryTime = utcToLocalTime utc fileModified
}
where where
isDir = isNothing fileContent isDir = isNothing fileContent
zipEntryName = encodeZipEntryName . bool (dropWhileEnd isPathSeparator) addTrailingPathSeparator isDir . normalise $ makeValid fileTitle
zipEntryTime = utcToLocalTime utc fileModified
zipEntrySize = Nothing
zipEntryExternalAttributes = Nothing
modifyFileTitle :: Monad m => (FilePath -> FilePath) -> Conduit File m File modifyFileTitle :: Monad m => (FilePath -> FilePath) -> ConduitT File File m ()
modifyFileTitle f = mapC $ \x@File{..} -> x{ fileTitle = f fileTitle } modifyFileTitle f = mapC $ \x@File{..} -> x{ fileTitle = f fileTitle }
-- Takes FileInfo and if it is a ZIP-Archive, extract files, otherwiese yield fileinfo -- Takes FileInfo and if it is a ZIP-Archive, extract files, otherwiese yield fileinfo
sourceFiles :: (MonadLogger m, MonadResource m, MonadThrow m, MonadIO m) => FileInfo -> Source m File sourceFiles :: (MonadLogger m, MonadResource m, MonadThrow m, MonadBase IO m) => FileInfo -> ConduitT () File m ()
sourceFiles fInfo sourceFiles fInfo
| ((==) `on` simpleContentType) mimeType typeZip = do | ((==) `on` simpleContentType) mimeType typeZip = do
$logInfoS "sourceFiles" "Unpacking ZIP" $logInfoS "sourceFiles" "Unpacking ZIP"
fileSource fInfo =$= void consumeZip fileSource fInfo .| void consumeZip
| otherwise = do | otherwise = do
$logDebugS "sourceFiles" [st|Not unpacking file of type #{decodeUtf8 mimeType}|] $logDebugS "sourceFiles" [st|Not unpacking file of type #{decodeUtf8 mimeType}|]
yieldM $ acceptFile fInfo yieldM $ acceptFile fInfo
where where
mimeType = mimeLookup $ fileName fInfo mimeType = mimeLookup $ fileName fInfo
acceptFile :: (MonadResource m, MonadThrow m, MonadIO m) => FileInfo -> m File acceptFile :: MonadResource m => FileInfo -> m File
acceptFile fInfo = do acceptFile fInfo = do
let fileTitle = dropWhile isPathSeparator . dropTrailingPathSeparator . normalise . unpack $ fileName fInfo let fileTitle = dropWhile isPathSeparator . dropTrailingPathSeparator . normalise . unpack $ fileName fInfo
fileModified <- liftIO getCurrentTime fileModified <- liftIO getCurrentTime
fileContent <- Just <$> runConduit (fileSource fInfo =$= foldC) fileContent <- fmap Just . runConduit $ fileSource fInfo .| foldC
return File{..} return File{..}
decodeZipEntryName :: MonadThrow m => Either Text ByteString -> m FilePath
-- ^ Extract the filename from a 'ZipEntry' doing decoding along the way.
--
-- Throws 'Data.Encoding.Exception.DecodingException's.
decodeZipEntryName = \case
Left t -> return $ unpack t
Right cp437 -> either throwM return $ decodeStrictByteStringExplicit CP437 cp437
encodeZipEntryName :: FilePath -> Either Text ByteString
-- ^ Encode a filename for use in a 'ZipEntry', encodes as
-- 'Data.Encoding.UTF8.UTF8' iff the given path contains non-ascii characters.
--
-- Does not do any normalisation (in particular this function does not ensure
-- that the 'FilePath' does not start with a slash).
encodeZipEntryName path = fromMaybe (Left $ pack path) $ do
guard $ all Char.isAscii path
either (const mzero) (return . Right) $ encodeStrictByteStringExplicit CP437 path

View File

@ -12,11 +12,15 @@ import ClassyPrelude.Yesod as Import
, Proxy , Proxy
, foldlM , foldlM
, static , static
, boolField, identifyForm , boolField, identifyForm, addClass
, HasHttpManager(..) , HasHttpManager(..)
, embed , embed
, try, embed, catches, handle, catch, bracket, bracketOnError, bracket_, catchJust, finally, handleJust, mask, mask_, onException, tryJust, uninterruptibleMask, uninterruptibleMask_
, fail
) )
import UnliftIO.Async.Utils as Import
import Model.Types.TH.JSON as Import import Model.Types.TH.JSON as Import
import Model.Types.TH.Wordlist as Import import Model.Types.TH.Wordlist as Import
@ -44,7 +48,7 @@ import Text.Shakespeare.Text as Import hiding (text, stext)
import Data.Universe as Import import Data.Universe as Import
import Data.Universe.TH as Import import Data.Universe.TH as Import
import Data.Pool as Import (Pool) import UnliftIO.Pool as Import (Pool)
import Network.HaskellNet.SMTP as Import (SMTPConnection) import Network.HaskellNet.SMTP as Import (SMTPConnection)
import Data.Data as Import (Data) import Data.Data as Import (Data)
@ -80,6 +84,14 @@ import Control.Monad.Trans.Reader as Import
( reader, Reader, runReader, mapReader, withReader ( reader, Reader, runReader, mapReader, withReader
, ReaderT(..), mapReaderT, withReaderT , ReaderT(..), mapReaderT, withReaderT
) )
import Control.Monad.Trans.State as Import
( state, State, runState, mapState, withState
, StateT(..), mapStateT, withStateT
)
import Control.Monad.Base as Import
import Control.Monad.Catch as Import hiding (Handler(..))
import Control.Monad.Trans.Control as Import hiding (embed)
import Control.Monad.Fail as Import
import Jose.Jwt as Import (Jwt) import Jose.Jwt as Import (Jwt)
@ -101,7 +113,6 @@ import Algebra.Lattice as Import hiding (meet, join)
import Data.Proxy as Import (Proxy(..)) import Data.Proxy as Import (Proxy(..))
import Language.Haskell.TH.Instances as Import () import Language.Haskell.TH.Instances as Import ()
import Data.List.NonEmpty.Instances as Import ()
import Data.NonNull.Instances as Import () import Data.NonNull.Instances as Import ()
import Data.Monoid.Instances as Import () import Data.Monoid.Instances as Import ()
import Data.Maybe.Instances as Import () import Data.Maybe.Instances as Import ()
@ -141,7 +152,7 @@ import Control.Lens as Import
hiding ( (<.>) hiding ( (<.>)
, universe , universe
, cons, uncons, snoc, unsnoc, (<|) , cons, uncons, snoc, unsnoc, (<|)
, Index, index, (<.) , Index, index, (<.), (.>)
) )
import Control.Lens.Extras as Import (is) import Control.Lens.Extras as Import (is)
import Data.Set.Lens as Import import Data.Set.Lens as Import

View File

@ -44,7 +44,7 @@ import Control.Monad.Trans.RWS.Lazy (RWST, mapRWST, evalRWST)
import qualified Control.Monad.State.Class as State import qualified Control.Monad.State.Class as State
import Control.Monad.Writer.Class (MonadWriter(..)) import Control.Monad.Writer.Class (MonadWriter(..))
import Control.Monad.Reader.Class (MonadReader(..)) import Control.Monad.Reader.Class (MonadReader(..))
import Control.Monad.Trans.Resource (MonadResourceBase, runResourceT) import Control.Monad.Trans.Resource (runResourceT)
import Control.Monad.Trans.Maybe (MaybeT(..)) import Control.Monad.Trans.Maybe (MaybeT(..))
import Control.Monad.Trans.Cont (ContT(..), callCC) import Control.Monad.Trans.Cont (ContT(..), callCC)
import Control.Monad.Random.Lazy (evalRandTIO, mapRandT) import Control.Monad.Random.Lazy (evalRandTIO, mapRandT)
@ -55,6 +55,8 @@ import Data.Time.Zones
import Control.Concurrent.STM (retry) import Control.Concurrent.STM (retry)
import Control.Concurrent.STM.Delay import Control.Concurrent.STM.Delay
import UnliftIO.Concurrent (forkIO)
import Jobs.Handler.SendNotification import Jobs.Handler.SendNotification
import Jobs.Handler.SendTestEmail import Jobs.Handler.SendTestEmail
@ -82,7 +84,8 @@ instance Exception JobQueueException
handleJobs :: ( MonadResource m handleJobs :: ( MonadResource m
, MonadLoggerIO m , MonadLogger m
, MonadUnliftIO m
) )
=> UniWorX -> m () => UniWorX -> m ()
-- | Spawn a set of workers that read control commands from `appJobCtl` and address them as they come in -- | Spawn a set of workers that read control commands from `appJobCtl` and address them as they come in
@ -92,12 +95,11 @@ handleJobs :: ( MonadResource m
handleJobs foundation@UniWorX{..} handleJobs foundation@UniWorX{..}
| foundation ^. _appJobWorkers == 0 = return () | foundation ^. _appJobWorkers == 0 = return ()
| otherwise = do | otherwise = do
logger <- askLoggerIO UnliftIO{..} <- askUnliftIO
let runInIO = flip runLoggingT logger . runResourceT
jobPoolManager <- allocateLinkedAsync . runInIO $ manageJobPool foundation jobPoolManager <- allocateLinkedAsync . unliftIO $ manageJobPool foundation
jobCron <- allocateLinkedAsync . runInIO $ manageCrontab foundation jobCron <- allocateLinkedAsync . unliftIO $ manageCrontab foundation
let jobWorkers = Map.empty let jobWorkers = Map.empty
jobWorkerName = const $ error "Unknown worker" jobWorkerName = const $ error "Unknown worker"
@ -109,11 +111,9 @@ handleJobs foundation@UniWorX{..}
, .. , ..
} }
manageJobPool, manageCrontab :: forall m. manageCrontab :: forall m.
( MonadResource m MonadResource m
, MonadLogger m => UniWorX -> m ()
)
=> UniWorX -> m ()
manageCrontab foundation@UniWorX{..} = do manageCrontab foundation@UniWorX{..} = do
context <- atomically . fmap jobContext $ readTMVar appJobState context <- atomically . fmap jobContext $ readTMVar appJobState
let awaitTermination = atomically $ do let awaitTermination = atomically $ do
@ -125,7 +125,12 @@ manageCrontab foundation@UniWorX{..} = do
writeJobCtlBlock JobCtlDetermineCrontab writeJobCtlBlock JobCtlDetermineCrontab
evalRWST (forever execCrontab) context HashMap.empty evalRWST (forever execCrontab) context HashMap.empty
manageJobPool :: forall m.
( MonadResource m
, MonadLogger m
, MonadUnliftIO m
)
=> UniWorX -> m ()
manageJobPool foundation@UniWorX{..} manageJobPool foundation@UniWorX{..}
= flip runContT return . forever . join . atomically $ asum = flip runContT return . forever . join . atomically $ asum
[ spawnMissingWorkers [ spawnMissingWorkers
@ -138,6 +143,9 @@ manageJobPool foundation@UniWorX{..}
spawnMissingWorkers, reapDeadWorkers, terminateGracefully :: STM (ContT () m ()) spawnMissingWorkers, reapDeadWorkers, terminateGracefully :: STM (ContT () m ())
spawnMissingWorkers = do spawnMissingWorkers = do
shouldTerminate' <- readTMVar appJobState >>= fmap not . isEmptyTMVar . jobShutdown
guard $ not shouldTerminate'
oldState <- takeTMVar appJobState oldState <- takeTMVar appJobState
let missing = num - Map.size (jobWorkers oldState) let missing = num - Map.size (jobWorkers oldState)
guard $ missing > 0 guard $ missing > 0
@ -163,7 +171,7 @@ manageJobPool foundation@UniWorX{..}
$logInfoS logIdent "Started" $logInfoS logIdent "Started"
runConduit $ streamChan .| handleJobs' workerId runConduit $ streamChan .| handleJobs' workerId
$logInfoS logIdent "Stopped" $logInfoS logIdent "Stopped"
worker <- allocateLinkedAsync runWorker worker <- lift . lift $ allocateLinkedAsync runWorker
tell . Endo $ \cSt -> cSt tell . Endo $ \cSt -> cSt
{ jobWorkers = Map.insert worker chan $ jobWorkers cSt { jobWorkers = Map.insert worker chan $ jobWorkers cSt
@ -199,28 +207,32 @@ manageJobPool foundation@UniWorX{..}
terminateGracefully = do terminateGracefully = do
shouldTerminate <- readTMVar appJobState >>= fmap not . isEmptyTMVar . jobShutdown shouldTerminate <- readTMVar appJobState >>= fmap not . isEmptyTMVar . jobShutdown
guard shouldTerminate guard shouldTerminate
oldState <- takeTMVar appJobState
guard $ 0 == Map.size (jobWorkers oldState)
return . callCC $ \terminate -> do return . callCC $ \terminate -> do
$logInfoS "JobPoolManager" "Shutting down" $logInfoS "JobPoolManager" "Shutting down"
terminate () terminate ()
stopJobCtl :: (MonadIO m, MonadBaseControl IO m) => UniWorX -> m () stopJobCtl :: MonadUnliftIO m => UniWorX -> m ()
-- ^ Stop all worker threads currently running -- ^ Stop all worker threads currently running
stopJobCtl UniWorX{appJobState} = do stopJobCtl UniWorX{appJobState} = do
didStop <- atomically $ do didStop <- atomically $ do
jState <- tryReadTMVar appJobState jState <- tryReadTMVar appJobState
for jState $ \jSt'@JobState{jobShutdown} -> jSt' <$ tryPutTMVar jobShutdown () for jState $ \jSt'@JobState{jobShutdown} -> jSt' <$ tryPutTMVar jobShutdown ()
whenIsJust didStop $ \jSt' -> void . fork . atomically $ do whenIsJust didStop $ \jSt' -> void . forkIO . atomically $ do
workers <- maybe [] (Map.keys . jobWorkers) <$> tryTakeTMVar appJobState workers <- maybe [] (Map.keys . jobWorkers) <$> tryTakeTMVar appJobState
mapM_ (void . waitCatchSTM) $ mapM_ (void . waitCatchSTM) $
[ jobPoolManager jSt' [ jobPoolManager jSt'
, jobCron jSt' , jobCron jSt'
] ++ workers ] ++ workers
execCrontab :: RWST JobContext () (HashMap JobCtl (Max UTCTime)) (HandlerT UniWorX IO) () execCrontab :: RWST JobContext () (HashMap JobCtl (Max UTCTime)) (HandlerFor UniWorX) ()
-- ^ Keeping a `HashMap` of the latest execution times of `JobCtl`s we have -- ^ Keeping a `HashMap` of the latest execution times of `JobCtl`s we have
-- seen, wait for the time of the next job and fire it -- seen, wait for the time of the next job and fire it
execCrontab = do execCrontab = do
mapRWST (liftHandlerT . runDB . setSerializable) $ do mapRWST (liftHandler . runDB . setSerializable) $ do
let let
mergeLastExec (Entity _leId CronLastExec{..}) mergeLastExec (Entity _leId CronLastExec{..})
| Just job <- Aeson.parseMaybe parseJSON cronLastExecJob | Just job <- Aeson.parseMaybe parseJSON cronLastExecJob
@ -251,7 +263,7 @@ execCrontab = do
-- now <- liftIO getCurrentTime -- now <- liftIO getCurrentTime
-- $logDebugS "Crontab" . intercalate "\n" . map tshow . sortOn fst . flip map (HashMap.toList currentCrontab) $ \(job, cron) -> (,job) $ nextCronMatch appTZ (getMax <$> HashMap.lookup job lastTimes) (debouncingAcc settings job) now cron -- $logDebugS "Crontab" . intercalate "\n" . map tshow . sortOn fst . flip map (HashMap.toList currentCrontab) $ \(job, cron) -> (,job) $ nextCronMatch appTZ (getMax <$> HashMap.lookup job lastTimes) (debouncingAcc settings job) now cron
let doJob = mapRWST (liftHandlerT . runDBJobs . setSerializable) $ do let doJob = mapRWST (liftHandler . runDBJobs . setSerializable) $ do
newCrontab <- lift . hoist lift $ determineCrontab' newCrontab <- lift . hoist lift $ determineCrontab'
if if
| ((==) `on` HashMap.lookup jobCtl) newCrontab currentCrontab | ((==) `on` HashMap.lookup jobCtl) newCrontab currentCrontab
@ -305,7 +317,7 @@ execCrontab = do
where where
t = nextCronMatch appTZ (getMax <$> HashMap.lookup jobCtl lastTimes) (debouncingAcc settings jobCtl) now cron t = nextCronMatch appTZ (getMax <$> HashMap.lookup jobCtl lastTimes) (debouncingAcc settings jobCtl) now cron
waitUntil :: (Eq a, MonadResourceBase m, MonadLogger m) => TVar a -> a -> UTCTime -> m Bool waitUntil :: (Eq a, MonadUnliftIO m, MonadLogger m) => TVar a -> a -> UTCTime -> m Bool
waitUntil crontabTV crontab nextTime = runResourceT $ do waitUntil crontabTV crontab nextTime = runResourceT $ do
diffT <- diffUTCTime nextTime <$> liftIO getCurrentTime diffT <- diffUTCTime nextTime <$> liftIO getCurrentTime
let waitTime = fromInteger (truncate $ diffT / acc) * toRational acc let waitTime = fromInteger (truncate $ diffT / acc) * toRational acc
@ -328,7 +340,7 @@ execCrontab = do
mkLogIdent :: JobWorkerId -> Text mkLogIdent :: JobWorkerId -> Text
mkLogIdent wId = "Job-Executor " <> showWorkerId wId mkLogIdent wId = "Job-Executor " <> showWorkerId wId
handleJobs' :: JobWorkerId -> Sink JobCtl (ReaderT JobContext Handler) () handleJobs' :: JobWorkerId -> ConduitT JobCtl Void (ReaderT JobContext Handler) ()
handleJobs' wNum = C.mapM_ $ \jctl -> do handleJobs' wNum = C.mapM_ $ \jctl -> do
$logDebugS logIdent $ tshow jctl $logDebugS logIdent $ tshow jctl
res <- fmap (either Just $ const Nothing) . try $ handleCmd jctl res <- fmap (either Just $ const Nothing) . try $ handleCmd jctl
@ -348,7 +360,7 @@ handleJobs' wNum = C.mapM_ $ \jctl -> do
handleQueueException (JLocked jId lInstance lTime) = $logDebugS logIdent $ "Saw locked QueuedJob: " ++ tshow (fromSqlKey jId, lInstance, lTime) handleQueueException (JLocked jId lInstance lTime) = $logDebugS logIdent $ "Saw locked QueuedJob: " ++ tshow (fromSqlKey jId, lInstance, lTime)
handleCmd JobCtlTest = return () handleCmd JobCtlTest = return ()
handleCmd JobCtlFlush = void . lift . runDB . runConduit $ selectKeys [] [ Asc QueuedJobCreationTime ] .| C.mapM_ (lift . writeJobCtl . JobCtlPerform) handleCmd JobCtlFlush = void . lift . runDB . runConduit $ selectKeys [] [ Asc QueuedJobCreationTime ] .| C.mapM_ (\j -> lift $ runReaderT (writeJobCtl $ JobCtlPerform j) =<< getYesod)
handleCmd (JobCtlQueue job) = lift $ queueJob' job handleCmd (JobCtlQueue job) = lift $ queueJob' job
handleCmd (JobCtlPerform jId) = lift . handle handleQueueException . jLocked jId $ \j@QueuedJob{..} -> do handleCmd (JobCtlPerform jId) = lift . handle handleQueueException . jLocked jId $ \j@QueuedJob{..} -> do
content <- case fromJSON queuedJobContent of content <- case fromJSON queuedJobContent of
@ -379,7 +391,7 @@ handleJobs' wNum = C.mapM_ $ \jctl -> do
] ]
delete jId delete jId
handleCmd JobCtlDetermineCrontab = do handleCmd JobCtlDetermineCrontab = do
newCTab <- liftHandlerT . runDB $ setSerializable determineCrontab' newCTab <- liftHandler . runDB $ setSerializable determineCrontab'
-- logDebugS logIdent $ tshow newCTab -- logDebugS logIdent $ tshow newCTab
mapReaderT (liftIO . atomically) $ mapReaderT (liftIO . atomically) $
lift . void . flip swapTVar newCTab =<< asks jobCrontab lift . void . flip swapTVar newCTab =<< asks jobCrontab
@ -454,5 +466,5 @@ determineCrontab' :: DB (Crontab JobCtl)
determineCrontab' = (\ct -> ct <$ pruneLastExecs ct) =<< determineCrontab determineCrontab' = (\ct -> ct <$ pruneLastExecs ct) =<< determineCrontab
performJob :: Job -> HandlerT UniWorX IO () performJob :: Job -> HandlerFor UniWorX ()
performJob = $(dispatchTH ''Job) performJob = $(dispatchTH ''Job)

View File

@ -19,7 +19,7 @@ dispatchJobHelpRequest :: Either (Maybe Address) UserId
-> Maybe Text -- ^ Referer -> Maybe Text -- ^ Referer
-> Handler () -> Handler ()
dispatchJobHelpRequest jSender jRequestTime jHelpSubject jHelpRequest jReferer = do dispatchJobHelpRequest jSender jRequestTime jHelpSubject jHelpRequest jReferer = do
supportAddress <- view _appMailSupport supportAddress <- getsYesod $ view _appMailSupport
userInfo <- bitraverse return (runDB . getEntity) jSender userInfo <- bitraverse return (runDB . getEntity) jSender
let senderAddress = either let senderAddress = either
id id

View File

@ -6,8 +6,6 @@ import Import
import Handler.Utils import Handler.Utils
import qualified Data.Set as Set
import qualified Data.CaseInsensitive as CI import qualified Data.CaseInsensitive as CI
@ -26,11 +24,11 @@ dispatchJobSendCourseCommunication jRecipientEmail jAllRecipientAddresses jCours
either (\email -> mailT def . (assign _mailTo (pure . Address Nothing $ CI.original email) *>)) userMailT jRecipientEmail $ do either (\email -> mailT def . (assign _mailTo (pure . Address Nothing $ CI.original email) *>)) userMailT jRecipientEmail $ do
void $ setMailObjectUUID jMailObjectUUID void $ setMailObjectUUID jMailObjectUUID
_mailFrom .= userAddress sender _mailFrom .= userAddress sender
if -- Use `addMailHeader` instead of `_mailCc` to make `mailT` ignore the additional recipients addMailHeader "Cc" "Undisclosed Recipients:;"
| jRecipientEmail == Right jSender
-> addMailHeader "Cc" . intercalate ", " . map renderAddress $ Set.toAscList (Set.delete (userAddress sender) jAllRecipientAddresses)
| otherwise
-> addMailHeader "Cc" "Undisclosed Recipients:;"
addMailHeader "Auto-Submitted" "no" addMailHeader "Auto-Submitted" "no"
setSubjectI . prependCourseTitle courseTerm courseSchool courseShorthand $ maybe (SomeMessage MsgCommCourseSubject) SomeMessage jSubject setSubjectI . prependCourseTitle courseTerm courseSchool courseShorthand $ maybe (SomeMessage MsgCommCourseSubject) SomeMessage jSubject
void $ addPart jMailContent void $ addPart jMailContent
when (jRecipientEmail == Right jSender) $
addPart' $ do
partIsAttachment $ "all-recipients" `addExtension` unpack extensionCsv
toMailPart $ toDefaultOrderedCsvRendered jAllRecipientAddresses

View File

@ -21,7 +21,7 @@ import qualified Database.Esqueleto.Utils as E
dispatchNotificationAllocationStaffRegister :: AllocationId -> UserId -> Handler () dispatchNotificationAllocationStaffRegister :: AllocationId -> UserId -> Handler ()
dispatchNotificationAllocationStaffRegister nAllocation jRecipient = userMailT jRecipient $ do dispatchNotificationAllocationStaffRegister nAllocation jRecipient = userMailT jRecipient $ do
Allocation{..} <- liftHandlerT . runDB $ getJust nAllocation Allocation{..} <- liftHandler . runDB $ getJust nAllocation
replaceMailHeader "Auto-Submitted" $ Just "auto-generated" replaceMailHeader "Auto-Submitted" $ Just "auto-generated"
setSubjectI $ MsgMailSubjectAllocationStaffRegister allocationName setSubjectI $ MsgMailSubjectAllocationStaffRegister allocationName
@ -32,7 +32,7 @@ dispatchNotificationAllocationStaffRegister nAllocation jRecipient = userMailT j
dispatchNotificationAllocationRegister :: AllocationId -> UserId -> Handler () dispatchNotificationAllocationRegister :: AllocationId -> UserId -> Handler ()
dispatchNotificationAllocationRegister nAllocation jRecipient = userMailT jRecipient $ do dispatchNotificationAllocationRegister nAllocation jRecipient = userMailT jRecipient $ do
Allocation{..} <- liftHandlerT . runDB $ getJust nAllocation Allocation{..} <- liftHandler . runDB $ getJust nAllocation
replaceMailHeader "Auto-Submitted" $ Just "auto-generated" replaceMailHeader "Auto-Submitted" $ Just "auto-generated"
setSubjectI $ MsgMailSubjectAllocationRegister allocationName setSubjectI $ MsgMailSubjectAllocationRegister allocationName
@ -43,7 +43,7 @@ dispatchNotificationAllocationRegister nAllocation jRecipient = userMailT jRecip
dispatchNotificationAllocationAllocation :: AllocationId -> UserId -> Handler () dispatchNotificationAllocationAllocation :: AllocationId -> UserId -> Handler ()
dispatchNotificationAllocationAllocation nAllocation jRecipient = do dispatchNotificationAllocationAllocation nAllocation jRecipient = do
(Allocation{..}, courses) <- liftHandlerT . runDB $ do (Allocation{..}, courses) <- runDB $ do
allocation <- getJust nAllocation allocation <- getJust nAllocation
courses <- E.select . E.from $ \(course `E.InnerJoin` allocationCourse `E.InnerJoin` lecturer) -> do courses <- E.select . E.from $ \(course `E.InnerJoin` allocationCourse `E.InnerJoin` lecturer) -> do
E.on $ lecturer E.^. LecturerCourse E.==. course E.^. CourseId E.on $ lecturer E.^. LecturerCourse E.==. course E.^. CourseId
@ -75,7 +75,7 @@ dispatchNotificationAllocationAllocation nAllocation jRecipient = do
dispatchNotificationAllocationUnratedApplications :: AllocationId -> UserId -> Handler () dispatchNotificationAllocationUnratedApplications :: AllocationId -> UserId -> Handler ()
dispatchNotificationAllocationUnratedApplications nAllocation jRecipient = do dispatchNotificationAllocationUnratedApplications nAllocation jRecipient = do
(Allocation{..}, courses) <- liftHandlerT . runDB $ do (Allocation{..}, courses) <- runDB $ do
allocation <- getJust nAllocation allocation <- getJust nAllocation
courses <- E.select . E.from $ \(course `E.InnerJoin` allocationCourse `E.InnerJoin` lecturer) -> do courses <- E.select . E.from $ \(course `E.InnerJoin` allocationCourse `E.InnerJoin` lecturer) -> do
E.on $ lecturer E.^. LecturerCourse E.==. course E.^. CourseId E.on $ lecturer E.^. LecturerCourse E.==. course E.^. CourseId
@ -117,7 +117,7 @@ dispatchNotificationAllocationUnratedApplications nAllocation jRecipient = do
dispatchNotificationAllocationOutdatedRatings :: AllocationId -> UserId -> Handler () dispatchNotificationAllocationOutdatedRatings :: AllocationId -> UserId -> Handler ()
dispatchNotificationAllocationOutdatedRatings nAllocation jRecipient = do dispatchNotificationAllocationOutdatedRatings nAllocation jRecipient = do
(Allocation{..}, courses) <- liftHandlerT . runDB $ do (Allocation{..}, courses) <- runDB $ do
allocation <- getJust nAllocation allocation <- getJust nAllocation
courses <- E.select . E.from $ \(course `E.InnerJoin` allocationCourse `E.InnerJoin` lecturer) -> do courses <- E.select . E.from $ \(course `E.InnerJoin` allocationCourse `E.InnerJoin` lecturer) -> do
E.on $ lecturer E.^. LecturerCourse E.==. course E.^. CourseId E.on $ lecturer E.^. LecturerCourse E.==. course E.^. CourseId

View File

@ -14,7 +14,7 @@ import qualified Data.CaseInsensitive as CI
dispatchNotificationCorrectionsAssigned :: UserId -> SheetId -> UserId -> Handler () dispatchNotificationCorrectionsAssigned :: UserId -> SheetId -> UserId -> Handler ()
dispatchNotificationCorrectionsAssigned nUser nSheet jRecipient = do dispatchNotificationCorrectionsAssigned nUser nSheet jRecipient = do
(Course{..}, Sheet{..}, nbrSubs) <- liftHandlerT . runDB $ do (Course{..}, Sheet{..}, nbrSubs) <- runDB $ do
sheet <- getJust nSheet sheet <- getJust nSheet
course <- belongsToJust sheetCourse sheet course <- belongsToJust sheetCourse sheet
nbrSubs <- count [ SubmissionSheet ==. nSheet nbrSubs <- count [ SubmissionSheet ==. nSheet

View File

@ -11,7 +11,7 @@ import qualified Data.CaseInsensitive as CI
dispatchNotificationCorrectionsNotDistributed :: SheetId -> UserId -> Handler () dispatchNotificationCorrectionsNotDistributed :: SheetId -> UserId -> Handler ()
dispatchNotificationCorrectionsNotDistributed nSheet jRecipient = do dispatchNotificationCorrectionsNotDistributed nSheet jRecipient = do
(Course{..}, Sheet{..}, nbrSubs) <- liftHandlerT . runDB $ do (Course{..}, Sheet{..}, nbrSubs) <- runDB $ do
sheet <- getJust nSheet sheet <- getJust nSheet
course <- belongsToJust sheetCourse sheet course <- belongsToJust sheetCourse sheet
nbrSubs <- count [ SubmissionSheet ==. nSheet nbrSubs <- count [ SubmissionSheet ==. nSheet

View File

@ -16,7 +16,7 @@ import qualified Data.CaseInsensitive as CI
dispatchNotificationExamRegistrationActive :: ExamId -> UserId -> Handler () dispatchNotificationExamRegistrationActive :: ExamId -> UserId -> Handler ()
dispatchNotificationExamRegistrationActive nExam jRecipient = userMailT jRecipient $ do dispatchNotificationExamRegistrationActive nExam jRecipient = userMailT jRecipient $ do
(Course{..}, Exam{..}) <- liftHandlerT . runDB $ do (Course{..}, Exam{..}) <- liftHandler . runDB $ do
exam <- getJust nExam exam <- getJust nExam
course <- belongsToJust examCourse exam course <- belongsToJust examCourse exam
return (course, exam) return (course, exam)
@ -37,7 +37,7 @@ dispatchNotificationExamRegistrationActive nExam jRecipient = userMailT jRecipie
dispatchNotificationExamRegistrationSoonInactive :: ExamId -> UserId -> Handler () dispatchNotificationExamRegistrationSoonInactive :: ExamId -> UserId -> Handler ()
dispatchNotificationExamRegistrationSoonInactive nExam jRecipient = userMailT jRecipient $ do dispatchNotificationExamRegistrationSoonInactive nExam jRecipient = userMailT jRecipient $ do
(Course{..}, Exam{..}) <- liftHandlerT . runDB $ do (Course{..}, Exam{..}) <- liftHandler . runDB $ do
exam <- getJust nExam exam <- getJust nExam
course <- belongsToJust examCourse exam course <- belongsToJust examCourse exam
return (course, exam) return (course, exam)
@ -58,7 +58,7 @@ dispatchNotificationExamRegistrationSoonInactive nExam jRecipient = userMailT jR
dispatchNotificationExamDeregistrationSoonInactive :: ExamId -> UserId -> Handler () dispatchNotificationExamDeregistrationSoonInactive :: ExamId -> UserId -> Handler ()
dispatchNotificationExamDeregistrationSoonInactive nExam jRecipient = userMailT jRecipient $ do dispatchNotificationExamDeregistrationSoonInactive nExam jRecipient = userMailT jRecipient $ do
(Course{..}, Exam{..}) <- liftHandlerT . runDB $ do (Course{..}, Exam{..}) <- liftHandler . runDB $ do
exam <- getJust nExam exam <- getJust nExam
course <- belongsToJust examCourse exam course <- belongsToJust examCourse exam
return (course, exam) return (course, exam)

Some files were not shown because too many files have changed in this diff Show More