diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6b28f4605..6f3d4d314 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,397 @@
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.10.0](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/compare/v7.9.1...v7.10.0) (2019-10-09)
+
+
+### Bug Fixes
+
+* hlint ([c19f427](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/c19f427))
+* **tooltips:** add dark variants of theme independent colors ([e5c7aa0](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/e5c7aa0))
+
+
+### Features
+
+* **course:** csv export of course participants ([9a28dc8](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/9a28dc8))
+* **courses:** add NotificationCourseRegistered ([3750da8](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/3750da8))
+* **info-lecturer:** add expiry time for newFeat ([fa9e6b5](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/fa9e6b5))
+* **info-lecturer:** add inline newU2W icons ([5a49feb](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/5a49feb))
+* **info-lecturer:** add newU2W icons on info page ([9f02ef0](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/9f02ef0))
+* **info-lecturer:** minor adjustments ([64b391a](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/64b391a))
+* **info-lecturer:** more bullhorns ([4a5e7d9](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/4a5e7d9))
+* **info-lecturer:** remove "news" section ([cb1e3a6](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/cb1e3a6))
+* **lecturer-info:** add planned features icon; update info ([a4068b4](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/a4068b4))
+* **lecturer-info:** fix typos, add info (adding tutorial participants) ([5139825](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/5139825))
+* **lecturer-info:** replaced icons with icon-tooltips; edited text ([2ca7085](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/2ca7085))
+* **tooltip:** added test warning to admin test page ([885efd3](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/885efd3))
+* **tooltips:** add auto unzip and multiFileField tooltips ([276dcb6](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/276dcb6))
+* **tooltips:** add option for inline tooltips ([0b2e931](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/0b2e931))
+* **tooltips:** replace tooltips ([3b0e1d5](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/3b0e1d5))
+* **tooltips:** tooltips from messages ([f85ab69](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/f85ab69))
+
+
+
+### [7.9.1](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/compare/v7.9.0...v7.9.1) (2019-10-07)
+
+
+### Bug Fixes
+
+* allow deregistering from full courses ([d7e1e67](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/d7e1e67))
+
+
+
+## [7.9.0](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/compare/v7.8.5...v7.9.0) (2019-10-05)
+
+
+### Features
+
+* **allocations:** show more information ([b7c54df](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/b7c54df))
+
+
+
+### [7.8.5](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/compare/v7.8.4...v7.8.5) (2019-10-05)
+
+
+### Bug Fixes
+
+* fix form-notification styling ([0226593](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/0226593))
+
+
+
+### [7.8.4](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/compare/v7.8.3...v7.8.4) (2019-10-05)
+
+
+### Bug Fixes
+
+* **course-user:** handle allocations when deregistering single users ([ef5bb70](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/ef5bb70))
+
+
+
+### [7.8.3](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/compare/v7.8.2...v7.8.3) (2019-10-05)
+
+
+### Bug Fixes
+
+* typo ([a6e40f1](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/a6e40f1))
+
+
+
+### [7.8.2](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/compare/v7.8.1...v7.8.2) (2019-10-04)
+
+
+### Bug Fixes
+
+* **favourites:** always move current course up ([56d89d7](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/56d89d7))
+
+
+
+### [7.8.1](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/compare/v7.8.0...v7.8.1) (2019-10-04)
+
+
+### Bug Fixes
+
+* **allocation:** fix allocation-results notifications ([ed700a3](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/ed700a3))
+
+
+
+## [7.8.0](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/compare/v7.7.0...v7.8.0) (2019-10-04)
+
+
+### Bug Fixes
+
+* ordinalPriorities ([d4ab6f6](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/d4ab6f6))
+
+
+### Features
+
+* **course:** show direct registration dates ([8f284ac](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/8f284ac))
+
+
+
+## [7.7.0](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/compare/v7.6.0...v7.7.0) (2019-10-04)
+
+
+### Features
+
+* **allocations:** fingerprints & ordinal ratings ([60603cb](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/60603cb))
+
+
+
+## [7.6.0](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/compare/v7.5.0...v7.6.0) (2019-10-04)
+
+
+### Features
+
+* **allocations:** notification about finished allocation ([9323220](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/9323220))
+* **allocations:** properly save allocation-relevant course-deregs ([7a759b1](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/7a759b1))
+* **favourites:** usability improvements ([fccc2ea](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/fccc2ea))
+
+
+
+## [7.5.0](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/compare/v7.4.2...v7.5.0) (2019-10-03)
+
+
+### Features
+
+* **allocations:** auxilliaries for allocation-algo ([47bfd8d](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/47bfd8d))
+* **allocations:** prototype assignment-algorithm ([0fcf48c](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/0fcf48c))
+
+
+
+### [7.4.2](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/compare/v7.4.1...v7.4.2) (2019-10-01)
+
+
+### Bug Fixes
+
+* **course-news:** prevent display of edit-functions unless auth'ed ([89cc9ad](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/89cc9ad))
+
+
+
+### [7.4.1](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/compare/v7.4.0...v7.4.1) (2019-10-01)
+
+
+### Bug Fixes
+
+* **course-news:** fix permissions ([9e5fde9](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/9e5fde9))
+
+
+
+## [7.4.0](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/compare/v7.3.2...v7.4.0) (2019-10-01)
+
+
+### Features
+
+* **course:** introduce CourseNews ([aa93b75](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/aa93b75))
+
+
+
+### [7.3.2](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/compare/v7.3.1...v7.3.2) (2019-10-01)
+
+
+### Bug Fixes
+
+* **exam-users:** make csv import much more lenient ([2ddb566](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/2ddb566))
+* **mail:** honor userCsvOptions and userDisplayEmail ([89adf7f](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/89adf7f))
+
+
+
+### [7.3.1](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/compare/v7.3.0...v7.3.1) (2019-09-30)
+
+
+### Bug Fixes
+
+* **course-edit:** edit courses without being school-wide lecturer ([d7d1f27](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/d7d1f27)), closes [#464](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/issues/464)
+
+
+
+## [7.3.0](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/compare/v7.2.2...v7.3.0) (2019-09-30)
+
+
+### Bug Fixes
+
+* **course-application:** better display of priorities ([64f7715](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/64f7715))
+
+
+### Features
+
+* **csv:** allow customisation of csv-export-options ([95ceedd](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/95ceedd))
+
+
+
+### [7.2.2](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/compare/v7.2.1...v7.2.2) (2019-09-30)
+
+
+### Bug Fixes
+
+* **authorisation:** keep showing allocations (ro) to lecturers ([c8e1d51](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/c8e1d51))
+
+
+
+### [7.2.1](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/compare/v7.2.0...v7.2.1) (2019-09-28)
+
+
+### Bug Fixes
+
+* fix build ([69f4a80](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/69f4a80))
+* fix tutorial registration group applying globally ([d2ba173](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/d2ba173))
+
+
+
+## [7.2.0](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/compare/v7.1.2...v7.2.0) (2019-09-27)
+
+
+### Bug Fixes
+
+* bump changelog ([60a7bb2](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/60a7bb2))
+* don't treat ExamBonusManual as override ([16abcd2](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/16abcd2))
+
+
+### Features
+
+* **course-applications:** automatic acceptance of direct applicants ([620950d](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/620950d))
+
+
+
+### [7.1.2](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/compare/v7.1.1...v7.1.2) (2019-09-26)
+
+
+### Bug Fixes
+
+* **exams:** include bonus points in sum for exam participants ([2bc6894](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/2bc6894))
+
+
+
+### [7.1.1](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/compare/v7.1.0...v7.1.1) (2019-09-26)
+
+
+### Bug Fixes
+
+* fix build ([d13ace4](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/d13ace4))
+
+
+
+## [7.1.0](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/compare/v7.0.0...v7.1.0) (2019-09-26)
+
+
+### Bug Fixes
+
+* **datepicker:** select time from preselected date on edit ([d3375bb](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/d3375bb))
+* **jobs:** cleaner shutdown of job-pool-manager ([adc8d46](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/adc8d46))
+
+
+### Features
+
+* **exams:** re-introduce ExamBonusManual ([54e94a6](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/54e94a6))
+
+
+
+## [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)
+
+
+### Bug Fixes
+
+* **changelog:** update changelog ([fa5358a](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/fa5358a))
+
+
+
+## [6.11.0](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/compare/v6.10.0...v6.11.0) (2019-09-16)
+
+
+### Bug Fixes
+
+* **course:** add links between users & applications ([edaca1b](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/edaca1b))
+* **exam-office:** better logic for isSynced ([cb9ff32](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/cb9ff32))
+* **exams:** make examClosed a button ([530a8c6](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/530a8c6))
+
+
+### Features
+
+* **exam-office:** course/user opt-outs ([484fa1c](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/484fa1c))
+* **exam-office:** exam-office permissions by courseSchool ([5841a7b](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/5841a7b))
+* **exam-office:** exams list ([651f0bc](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/651f0bc))
+* **exam-office:** grade export ([72a7f6e](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/72a7f6e))
+* **exam-office:** notifications ([52e1844](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/52e1844))
+* **exam-office:** show exam(Occurrence) end-time ([b638783](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/b638783))
+* **exam-office:** subscription management for users & fields ([f75cc64](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/f75cc64))
+* **exam-office:** user invitations ([123970a](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/123970a))
+
+
+
+## [6.10.0](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/compare/v6.9.0...v6.10.0) (2019-09-13)
+
+
+### Features
+
+* **exams:** notifications wrt. registration ([ae27ff0](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/ae27ff0))
+
+
+
+## [6.9.0](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/compare/v6.8.0...v6.9.0) (2019-09-12)
+
+
+### Features
+
+* **users:** allow customisation of displayed email address ([2f38278](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/2f38278)), closes [#459](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/issues/459)
+* **users:** allow customisation of userDisplayName ([a85f317](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/a85f317)), closes [#346](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/issues/346)
+
+
+
+## [6.8.0](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/compare/v6.7.0...v6.8.0) (2019-09-12)
+
+
+### Bug Fixes
+
+* **allocations:** better explain capped allocation bounds ([a890e34](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/a890e34))
+
+
+### Features
+
+* **allocations:** allow changing course capacity during allocation ([83e1c94](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/83e1c94))
+* **allocations:** show bounds on assignments due to allocation ([91b249e](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/91b249e))
+
+
+
+## [6.7.0](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/compare/v6.6.0...v6.7.0) (2019-09-12)
+
+
+### Bug Fixes
+
+* **datepicker:** increase datepicker z-index in modals ([593a6a7](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/593a6a7))
+* **datepicker:** quickfix to fix datepicker position in modals ([3f9454a](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/3f9454a))
+* **submission-users:** properly delete old invitations ([91c926b](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/91c926b))
+
+
+### Features
+
+* **admin-users:** allow adding users ([67f1201](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/67f1201))
+* **health:** timeout all health checks ([33338cd](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/33338cd))
+* **invitations:** additional explanation for new users ([bb9c34f](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/bb9c34f))
+
+
+
## [6.6.0](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/compare/v6.5.0...v6.6.0) (2019-09-09)
diff --git a/README.md b/README.md
index f61775faa..685041baa 100644
--- a/README.md
+++ b/README.md
@@ -5,6 +5,14 @@ The following description applies to Ubuntu and similar debian based Linux distr
## Prerequisites
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 this repository and navigate into it
```sh
@@ -41,7 +49,7 @@ You'll get a prompt:
```sh
Enter name of role to add: uniworx
-Shall the new role be a superuser? (y/n) [not exactly sure. Guess not?]
+Shall the new role be a superuser? (y/n) y [user must be superuser to create extensions]
Password: uniworx
...
```
@@ -89,18 +97,6 @@ $ sudo apt-get install pkg-config
$ sudo apt-get install libsodium-dev
```
-Build the app:
-```sh
-$ stack build
-```
-
-This might take a few minutes... if not hours... be prepared.
-
-install yesod:
-```sh
-$ stack install yesod-bin --install-ghc
-```
-
### `Node` & `npm`
Node and Npm are needed to compile the frontend.
@@ -110,6 +106,18 @@ $ curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash -
$ 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
After building the app you can prepare the database and add some dummy data:
```sh
@@ -118,7 +126,7 @@ $ ./db.sh -f
## Run Uni2work
```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.
diff --git a/app/DevelMain.hs b/app/DevelMain.hs
index 0a7a89562..b850b33b2 100644
--- a/app/DevelMain.hs
+++ b/app/DevelMain.hs
@@ -67,7 +67,7 @@ update = do
restartAppInNewThread tidStore = modifyStoredIORef tidStore $ \tid -> do
killThread tid
withStore doneStore takeMVar
- readStore doneStore >>= start
+ withStore doneStore start
-- | Start the server in a separate thread.
@@ -77,10 +77,7 @@ update = do
(port, site, app) <- getApplicationRepl
resourceForkIO $ do
finally (liftIO $ runSettings (setPort port defaultSettings) app)
- -- Note that this implies concurrency
- -- between shutdownApp and the next app that is starting.
- -- Normally this should be fine
- (liftIO $ putMVar done () >> shutdownApp site)
+ (liftIO $ shutdownApp site `finally` putMVar done ())
-- | kill the server
shutdown :: IO ()
diff --git a/clean.sh b/clean.sh
index 2b9f5bfc7..02487e8b2 100755
--- a/clean.sh
+++ b/clean.sh
@@ -10,6 +10,8 @@ case $1 in
;;
*)
target=".stack-work-${1}"
+ shift
+
if [[ ! -d "${target}" ]]; then
printf "%s does not exist or is no directory\n" "${target}" >&2
exit 1
@@ -20,7 +22,11 @@ case $1 in
fi
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
}
@@ -28,6 +34,9 @@ case $1 in
mv -v "${target}" .stack-work
trap move-back EXIT
- stack clean
+ (
+ set -ex
+ stack clean $@
+ )
;;
esac
diff --git a/config/settings.yml b/config/settings.yml
index 8eef1cb7b..0e2dbe810 100644
--- a/config/settings.yml
+++ b/config/settings.yml
@@ -24,8 +24,8 @@ job-flush-interval: "_env:JOB_FLUSH:30"
job-cron-interval: "_env:CRON_INTERVAL:60"
job-stale-threshold: 300
notification-rate-limit: 3600
-notification-collate-delay: 300
-notification-expiration: 259201
+notification-collate-delay: 7200
+notification-expiration: 259200
session-timeout: 7200
jwt-expiration: 604800
jwt-encoding: HS256
@@ -122,13 +122,23 @@ widget-memcached:
expiration: "_env:MEMCACHEDEXPIRATION:3600"
user-defaults:
- max-favourites: 12
- theme: Default
- date-time-format: "%a %d %b %Y %R"
- date-format: "%d.%m.%Y"
- time-format: "%R"
- download-files: false
- warning-days: 1209600
+ max-favourites: 12
+ max-favourite-terms: 2
+ theme: Default
+ date-time-format: "%a %d %b %Y %R"
+ date-format: "%d.%m.%Y"
+ time-format: "%R"
+ download-files: false
+ warning-days: 1209600
+
+# During central allocations lecturer-given ratings of applications (as
+# ExamGrades) are combined with a central priority.
+# This encodes the weight of the lecturer ratings on the same scale as the
+# centrally supplied priorities.
+allocation-grade-scale: 25
+# This encodes how many ordinal places lecturer ratings may move students up or
+# down when central priorities are supplied as ordered list.
+allocation-grade-ordinal-places: 3
instance-id: "_env:INSTANCE_ID:instance"
ribbon: "_env:RIBBON:"
diff --git a/config/test-settings.yml b/config/test-settings.yml
index 23f59aed5..5fb61bedf 100644
--- a/config/test-settings.yml
+++ b/config/test-settings.yml
@@ -8,3 +8,5 @@ log-settings:
destination: "test.log"
auth-dummy-login: true
+
+job-workers: 1
diff --git a/frontend/src/utils/asidenav/asidenav.scss b/frontend/src/utils/asidenav/asidenav.scss
index e7ab3b50d..8ef003c21 100644
--- a/frontend/src/utils/asidenav/asidenav.scss
+++ b/frontend/src/utils/asidenav/asidenav.scss
@@ -56,6 +56,10 @@
font-size: 18px;
padding-left: 10px;
}
+
+ .asidenav__box-subtitle {
+ display: none;
+ }
}
}
@@ -95,6 +99,14 @@
border-bottom: 1px solid var(--color-grey);
}
+.asidenav__box-subtitle {
+ color: var(--color-fontsec);
+ font-size: 0.9rem;
+ font-weight: 600;
+ padding: 0 13px;
+ margin: 3px 0;
+}
+
/* LOGO */
.asidenav__logo {
@@ -170,7 +182,7 @@
position: absolute;
bottom: -40px;
right: 25px;
- opacity: 0.2;
+ opacity: 0.1;
> img {
width: 350px;
@@ -314,8 +326,16 @@
color: var(--color-lightwhite);
&:hover {
- background-color: var(--color-dark);
+ background-color: var(--color-darker);
}
+
+ &::before {
+ display: none;
+ }
+ }
+
+ .asidenav__box-subtitle {
+ display: none;
}
.asidenav__link-shorthand {
diff --git a/frontend/src/utils/form/datepicker.js b/frontend/src/utils/form/datepicker.js
index 9ab2607a2..c20294489 100644
--- a/frontend/src/utils/form/datepicker.js
+++ b/frontend/src/utils/form/datepicker.js
@@ -25,17 +25,6 @@ const FORM_DATE_FORMAT_MOMENT = {
'datetime-local': `${FORM_DATE_FORMAT_DATE_MOMENT} ${FORM_DATE_FORMAT_TIME_MOMENT}`,
};
-/**
- * Takes a string representation of a date and a format string and parses the given date to a Date object.
- * If the date string is not valid (i.e. cannot be parsed with the given format string), returns undefined.
- * @param {*} dateStr string representation of a date
- * @param {*} dateFormat format string of the date
- */
-function parseDateWithFormat(dateStr, dateFormat) {
- const parsedMomentDate = moment(dateStr, dateFormat);
- if (parsedMomentDate.isValid()) return parsedMomentDate.toDate();
-}
-
/**
* Takes a string representation of a date, an input ('previous') format and a desired output format and returns a reformatted date string.
* If the date string is not valid (i.e. cannot be parsed with the given input format string), returns the original date string;
@@ -137,6 +126,9 @@ export class Datepicker {
throw new Error('Datepicker utility called on unsupported element!');
}
+ // format any existing dates to fancy display format on pageload
+ this.formatElementValue(true);
+
// initialize tail.datetime (datepicker) instance
this.datepickerInstance = datetime(this._element, { ...datepickerGlobalConfig, ...datepickerConfig });
@@ -198,9 +190,6 @@ export class Datepicker {
// format the date value of the form input element of this datepicker before form submission
this._element.form.addEventListener('submit', () => this.formatElementValue());
-
- // format any existing dates to fancy display format on pageload
- this.formatElementValue(true);
}
destroy() {
@@ -212,22 +201,21 @@ export class Datepicker {
* @param {*} toFancy optional target format switch (boolean value; default is false). If set to a truthy value, formats the element value to fancy instead of internal date format.
*/
formatElementValue(toFancy) {
- const dp = this.datepickerInstance;
if (this._element.value) {
- if (toFancy) {
- const parsedDate = parseDateWithFormat(this._element.value, FORM_DATE_FORMAT[this.elementType]);
- if (parsedDate) dp.selectDate();
- } else {
- this._element.value = this.unformat();
- }
+ this._element.value = this.unformat(toFancy);
}
}
+
+
/**
* Returns a datestring in internal format from the current state of the input element value.
+ * @param {*} toFancy Format date from internal to fancy or vice versa. When omitted, toFancy is falsy and results in fancy -> internal
*/
- unformat() {
- return reformatDateString(this._element.value, FORM_DATE_FORMAT_MOMENT[this.elementType], FORM_DATE_FORMAT[this.elementType]);
+ unformat(toFancy) {
+ const formatIn = toFancy ? FORM_DATE_FORMAT[this.elementType] : FORM_DATE_FORMAT_MOMENT[this.elementType];
+ const formatOut = toFancy ? FORM_DATE_FORMAT_MOMENT[this.elementType] : FORM_DATE_FORMAT[this.elementType];
+ return reformatDateString(this._element.value, formatIn, formatOut);
}
/**
@@ -256,4 +244,4 @@ export class Datepicker {
// return the (possibly changed) FormData
return formData;
}
-}
\ No newline at end of file
+}
diff --git a/frontend/src/utils/inputs/inputs.scss b/frontend/src/utils/inputs/inputs.scss
index c4cb63373..2d8fb8db5 100644
--- a/frontend/src/utils/inputs/inputs.scss
+++ b/frontend/src/utils/inputs/inputs.scss
@@ -33,11 +33,14 @@
margin: 7px 0;
}
-.form-section-title__hint {
- margin-top: 7px;
+.form-group__hint, .form-section-title__hint {
color: var(--color-fontsec);
font-size: 0.9rem;
font-weight: 600;
+}
+
+.form-section-title__hint {
+ margin-top: 7px;
+ .form-group {
margin-top: 11px;
@@ -58,6 +61,7 @@
.form-group--required .form-group-label__caption::after, .form-group__required-marker::before {
content: ' *';
color: var(--color-error);
+ font-weight: 600;
}
.form-group--optional {
diff --git a/frontend/src/utils/tooltips/tooltips.scss b/frontend/src/utils/tooltips/tooltips.scss
index a19074aee..845c33310 100644
--- a/frontend/src/utils/tooltips/tooltips.scss
+++ b/frontend/src/utils/tooltips/tooltips.scss
@@ -1,8 +1,7 @@
.tooltip {
position: relative;
display: inline-block;
- height: 1.5rem;
- vertical-align: -0.375rem;
+ vertical-align: middle;
&:hover .tooltip__content {
display: inline-block;
@@ -10,13 +9,10 @@
}
.tooltip__handle {
- background-color: var(--color-dark);
- border-radius: 50%;
+ color: var(--color-light);
height: 1.5rem;
- width: 1.5rem;
line-height: 1.5rem;
font-size: 1.2rem;
- color: white;
display: inline-block;
text-align: center;
margin: 0 10px;
@@ -24,27 +20,45 @@
position: relative;
&::before {
- content: '\f128';
position: absolute;
top: 0;
left: 0;
- font-family: 'Font Awesome 5 Free';
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 15px;
}
- &.tooltip__handle--danger::before {
- content: '\f12a';
+ &.tooltip__handle.urgency__success {
+ color: var(--color-success);
}
-
- &.tooltip__handle--danger {
- background-color: var(--color-warning);
+ &.tooltip__handle.urgency__warning {
+ color: var(--color-warning);
+ }
+ &.tooltip__handle.urgency__error {
+ color: var(--color-error);
}
&:hover {
- background-color: var(--color-light);
+ color: var(--color-dark);
+
+ &.tooltip__handle.urgency__success {
+ color: var(--color-success-dark);
+ }
+ &.tooltip__handle.urgency__warning {
+ color: var(--color-warning-dark);
+ }
+ &.tooltip__handle.urgency__error {
+ color: var(--color-error-dark);
+ }
+ }
+}
+
+.tooltip.tooltip__inline {
+ .tooltip__handle {
+ height: 1.0rem;
+ line-height: 1.0rem;
+ font-size: 1.0rem;
}
}
@@ -53,7 +67,7 @@
display: none;
top: -10px;
transform: translateY(-100%);
- right: 3px;
+ left: 3px;
width: 275px;
z-index: 10;
background-color: #fafafa;
@@ -68,7 +82,7 @@
background-color: #fafafa;
transform: rotate(45deg);
position: absolute;
- right: 10px;
+ left: 10px;
bottom: -8px;
}
}
diff --git a/haddock.sh b/haddock.sh
index 13bb626e0..00308065f 100755
--- a/haddock.sh
+++ b/haddock.sh
@@ -15,4 +15,4 @@ if [[ -d .stack-work-doc ]]; then
trap move-back EXIT
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 ${@}
diff --git a/is-clean.sh b/is-clean.sh
index b63b54f46..4bcf4bd7d 100755
--- a/is-clean.sh
+++ b/is-clean.sh
@@ -1,5 +1,7 @@
#!/usr/bin/env bash
+[[ -n "${FORCE_RELEASE}" ]] && exit 0
+
set -e
if [ -n "$(git status --porcelain)" ]; then
diff --git a/messages/uniworx/de.msg b/messages/uniworx/de.msg
index cae00655d..b83eb1cf6 100644
--- a/messages/uniworx/de.msg
+++ b/messages/uniworx/de.msg
@@ -36,6 +36,10 @@ RegisterTo: Anmeldungen bis
DeRegUntil: Abmeldungen bis
RegisterRetry: Sie wurden noch nicht angemeldet. Drücken Sie dazu den Knopf "Anmelden"
+CourseRegistrationInterval: Anmeldung
+CourseDirectRegistrationInterval: Direkte Anmeldung
+CourseDeregisterUntil time@Text: Abmeldung nur bis #{time}
+
GenericKey: Schlüssel
GenericShort: Kürzel
GenericIsNew: Neu
@@ -173,6 +177,7 @@ CourseApplicationTemplateApplication: Bewerbungsvorlage(n)
CourseApplicationTemplateRegistration: Anmeldungsvorlage(n)
CourseApplicationTemplateArchiveName tid@TermId ssh@SchoolId csh@CourseShorthand: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-bewerbungsvorlagen
CourseApplication: Bewerbung
+CourseApplicationIsParticipant: Kursteilnehmer
CourseApplicationExists: Sie haben sich bereits für diesen Kurs beworben
CourseApplicationInvalidAction: Angegeben Aktion kann nicht durchgeführt werden
@@ -365,6 +370,8 @@ UnauthorizedToken404: Authorisierungs-Tokens können nicht auf Fehlerseiten ausg
UnauthorizedSiteAdmin: Sie sind kein System-weiter Administrator.
UnauthorizedSchoolAdmin: Sie sind nicht als Administrator für dieses Institut eingetragen.
UnauthorizedAdminEscalation: Sie sind nicht Administrator für alle Institute, für die dieser Nutzer Administrator oder Veranstalter ist.
+UnauthorizedExamOffice: Sie sind nicht Teil eines Prüfungsamts.
+UnauthorizedExamExamOffice: Es existieren keine Prüfungsergebnisse für Nutzer, für die Sie Teil eines assoziierten Prüfungsamts sind.
UnauthorizedSchoolLecturer: Sie sind nicht als Veranstalter für dieses Institut eingetragen.
UnauthorizedLecturer: Sie sind nicht als Veranstalter für diese Veranstaltung eingetragen.
UnauthorizedAllocationLecturer: Sie sind nicht als Veranstalter für eine Veranstaltung dieser Zentralanmeldung eingetragen.
@@ -375,12 +382,14 @@ UnauthorizedRegistered: Sie sind nicht als Teilnehmer für diese Veranstaltung r
UnauthorizedAllocationRegistered: Sie sind nicht als Teilnehmer für diese Zentralanmeldung registriert.
UnauthorizedExamResult: Sie haben keine Ergebnisse in dieser Prüfung.
UnauthorizedParticipant: Angegebener Benutzer ist nicht als Teilnehmer dieser Veranstaltung registriert.
+UnauthorizedCourseNewsParticipant: Sie sind kein Teilnehmer dieser Veranstaltung.
UnauthorizedCourseTime: Dieses Kurs erlaubt momentan keine Anmeldungen.
UnauthorizedAllocationRegisterTime: Diese Zentralanmeldung erlaubt momentan keine Bewerbungen.
UnauthorizedSheetTime: Dieses Übungsblatt ist momentan nicht freigegeben.
UnauthorizedApplicationTime: Diese Bewerbung ist momentan nicht freigegeben.
UnauthorizedMaterialTime: Dieses Material ist momentan nicht freigegeben.
UnauthorizedTutorialTime: Dieses Tutorium erlaubt momentan keine Anmeldungen.
+UnauthorizedCourseNewsTime: Diese Nachricht ist momentan nicht freigegeben.
UnauthorizedExamTime: Diese Prüfung ist momentan nicht freigegeben.
UnauthorizedSubmissionOwner: Sie sind an dieser Abgabe nicht beteiligt.
UnauthorizedSubmissionRated: Diese Abgabe ist noch nicht korrigiert.
@@ -471,7 +480,9 @@ LdapSynced: LDAP-Synchronisiert
LdapSyncedBefore: Letzte LDAP-Synchronisation vor
NoMatrikelKnown: Keine Matrikelnummer
Theme: Oberflächen Design
-Favoriten: Anzahl gespeicherter Favoriten
+Favourites: Anzahl gespeicherter Favoriten
+FavouritesTip: Betrifft nur automatisch angelegte Favoriten („Kürzlich besucht“)
+FavouriteSemesters: Maximale Anzahl an Semestern in Seitenleiste
Plugin: Plugin
Ident: Identifikation
LastLogin: Letzter Login
@@ -638,6 +649,7 @@ UserSchoolsTip: Sie erhalten nur institutweite Benachrichtigungen für Institute
FormNotifications: Benachrichtigungen
FormBehaviour: Verhalten
FormCosmetics: Oberfläche
+FormPersonalAppearance: Öffentliche Daten
FormFieldRequiredTip: Gekennzeichnete Pflichtfelder sind immer auszufüllen
ActiveAuthTags: Aktivierte Authorisierungsprädikate
@@ -688,6 +700,9 @@ UploadModeSpecific: Upload, vorgegebene Dateinamen
UploadModeUnpackZips: Abgabe mehrerer Dateien
UploadModeUnpackZipsTip: Wenn die Abgabe mehrerer Dateien erlaubt ist, werden auch unterstützte Archiv-Formate zugelassen. Diese werden nach dann beim Hochladen automatisch entpackt.
+AutoUnzip: ZIPs automatisch entpacken
+AutoUnzipInfo: Entpackt hochgeladene ZIP-Dateien (*.zip) automatisch und fügt den Inhalt dem Stamm-Verzeichnis hinzu.
+
UploadModeExtensionRestriction: Zulässige Dateiendungen
UploadModeExtensionRestrictionTip: Komma-separiert. Wenn keine Dateiendungen angegeben werden erfolgt keine Einschränkung.
UploadModeExtensionRestrictionEmpty: Liste von zulässigen Dateiendungen darf nicht leer sein
@@ -762,9 +777,29 @@ MailSubmissionRatedIntro courseName@Text termDesc@Text: Ihre Abgabe im Kurs #{co
MailSubjectSheetActive csh@CourseShorthand sheetName@SheetName: #{sheetName} in #{csh} wurde herausgegeben
MailSheetActiveIntro courseName@Text termDesc@Text sheetName@SheetName: Sie können nun #{sheetName} im Kurs #{courseName} (#{termDesc}) herunterladen.
+MailSubjectCourseRegistered csh@CourseShorthand: Sie wurden zu #{csh} angemeldet
+MailSubjectCourseRegisteredOther displayName@Text csh@CourseShorthand: #{displayName} wurde zu #{csh} angemeldet
+MailCourseRegisteredIntro courseName@Text termDesc@Text: Sie wurden im Kurs #{courseName} (#{termDesc}) angemeldet.
+MailCourseRegisteredIntroOther displayName@Text courseName@Text termDesc@Text: #{displayName} wurde im Kurs #{courseName} (#{termDesc}) angemeldet.
+
MailSubjectExamResult csh@CourseShorthand examn@ExamName: Ergebnisse für #{examn} in #{csh} wurden herausgegeben
MailExamResultIntro courseName@Text termDesc@Text examn@ExamName: Sie können nun Ihr Ergebnis für #{examn} im Kurs #{courseName} (#{termDesc}) einsehen.
+MailSubjectExamOfficeExamResults csh@CourseShorthand examn@ExamName: Ergebnisse für #{examn} in #{csh} sind fertiggestellt
+MailExamOfficeExamResultsIntro courseName@Text termDesc@Text examn@ExamName: Ein Kursverwalter hat die Prüfungsleistungen für #{examn} im Kurs #{courseName} (#{termDesc}) freigegeben.
+
+MailSubjectExamOfficeExamResultsChanged csh@CourseShorthand examn@ExamName: Ergebnisse für #{examn} in #{csh} wurden verändert
+MailExamOfficeExamResultsChangedIntro courseName@Text termDesc@Text examn@ExamName: Ein Kursverwalter hat Prüfungsleistungen für #{examn} im Kurs #{courseName} (#{termDesc}) verändert.
+
+MailSubjectExamRegistrationActive csh@CourseShorthand examn@ExamName: Anmeldung für #{examn} in #{csh} ist möglich
+MailExamRegistrationActiveIntro courseName@Text termDesc@Text examn@ExamName: Sie können sich nun für #{examn} im Kurs #{courseName} (#{termDesc}) anmelden.
+
+MailSubjectExamRegistrationSoonInactive csh@CourseShorthand examn@ExamName: Anmeldung für #{examn} in #{csh} ist nur noch kurze Zeit möglich
+MailExamRegistrationSoonInactiveIntro courseName@Text termDesc@Text examn@ExamName: Sie können sich bald nicht mehr für #{examn} im Kurs #{courseName} (#{termDesc}) anmelden.
+
+MailSubjectExamDeregistrationSoonInactive csh@CourseShorthand examn@ExamName: Abmeldung für #{examn} in #{csh} ist nur noch kurze Zeit möglich
+MailExamDeregistrationSoonInactiveIntro courseName@Text termDesc@Text examn@ExamName: Sie können sich bald nicht mehr von #{examn} im Kurs #{courseName} (#{termDesc}) abmelden.
+
MailSubjectSubmissionsUnassigned csh@CourseShorthand sheetName@SheetName: Abgaben zu #{sheetName} in #{csh} konnten nicht verteilt werden
MailSubmissionsUnassignedIntro n@Int courseName@Text termDesc@Text sheetName@SheetName: #{n} Abgaben zu #{sheetName} im Kurs #{courseName} (#{termDesc}) konnten nicht automatisiert verteilt werden.
@@ -781,9 +816,9 @@ MailUserRightsIntro name@Text email@UserEmail: #{name} <#{email}> hat folgende U
MailNoLecturerRights: Sie haben derzeit keine Dozenten-Rechte.
MailLecturerRights n@Int: Als Dozent dürfen Sie Veranstaltungen innerhalb #{pluralDE n "Ihres Instituts" "Ihrer Institute"} anlegen.
-MailSubjectUserAuthModeUpdate: Änderung Ihres Uni2work-Anmeldemodus
-UserAuthModePWHashChangedToLDAP: Sie melden sich nun mit Ihrer Campus-Kennung an
-UserAuthModeLDAPChangedToPWHash: Sie melden sich nun mit einer Uni2work-internen Kennung an
+MailSubjectUserAuthModeUpdate: Ihr Uni2work-Login
+UserAuthModePWHashChangedToLDAP: Sie können sich nun mit Ihrer Campus-Kennung in Uni2work einloggen
+UserAuthModeLDAPChangedToPWHash: Sie können sich nun mit einer Uni2work-internen Kennung in Uni2work einloggen
NewPasswordLinkTip: Das Passwort Ihrer Uni2work-internen Kennung können Sie auf der folgenden Seite setzen:
NewPasswordLink: Neues Passwort setzen
AuthPWHashTip: Sie müssen nun das mit "Uni2work-Login" beschriftete Login-Formular verwenden. Stellen Sie bitte sicher, dass Sie ein Passwort gesetzt haben, bevor Sie versuchen sich anzumelden.
@@ -796,8 +831,10 @@ MailSubjectSupportCustom customSubject@Text: [Support] #{customSubject}
CommCourseSubject: Kursmitteilung
MailSubjectLecturerInvitation tid@TermId ssh@SchoolId csh@CourseShorthand: [#{tid}-#{ssh}-#{csh}] Einladung zum Kursverwalter
InvitationAcceptDecline: Einladung annehmen/ablehnen
+InvitationFromTip displayName@Text: Sie erhalten diese Einladung, weil #{displayName} ihren Versand in Uni2work ausgelöst hat.
+InvitationUniWorXTip: Uni2work ist ein webbasiertes Lehrverwaltungssystem der LMU München.
-MailSubjectParticipantInvitation tid@TermId ssh@SchoolId csh@CourseShorthand: [#{tid}-#{ssh}-#{csh}] Einladung zum Kursteilname
+MailSubjectParticipantInvitation tid@TermId ssh@SchoolId csh@CourseShorthand: [#{tid}-#{ssh}-#{csh}] Einladung zur Kursteilnahme
MailSubjectCorrectorInvitation tid@TermId ssh@SchoolId csh@CourseShorthand shn@SheetName: [#{tid}-#{ssh}-#{csh}] Einladung zum Korrektor für #{shn}
@@ -809,6 +846,8 @@ MailSubjectExamRegistrationInvitation tid@TermId ssh@SchoolId csh@CourseShorthan
MailSubjectSubmissionUserInvitation tid@TermId ssh@SchoolId csh@CourseShorthand shn@SheetName: [#{tid}-#{ssh}-#{csh}] Einladung zu einer Abgabe für #{shn}
+MailSubjectExamOfficeUserInvitation displayName@Text: Berücksichtigung von Prüfungsleistungen in Uni2work
+
MailSubjectPasswordReset: Uni2work-Passwort ändern bzw. setzen
SheetGrading: Bewertung
@@ -863,22 +902,31 @@ NotificationTriggerCorrectionsAssigned: Mir wurden Abgaben zur Korrektur zugetei
NotificationTriggerCorrectionsNotDistributed: Nicht alle Abgaben eines meiner Übungsblätter konnten einem Korrektor zugeteilt werden
NotificationTriggerUserRightsUpdate: Meine Berechtigungen wurden geändert
NotificationTriggerUserAuthModeUpdate: Mein Anmelde-Modus wurde geändert
+NotificationTriggerExamRegistrationActive: Ich kann mich für eine Prüfung anmelden
+NotificationTriggerExamRegistrationSoonInactive: Ich kann mich bald nicht mehr für eine Prüfung anmelden
+NotificationTriggerExamDeregistrationSoonInactive: Ich kann mich bald nicht mehr von einer Prüfung abmelden
NotificationTriggerExamResult: Ich kann ein neues Prüfungsergebnis einsehen
NotificationTriggerAllocationStaffRegister: Ich kann Kurse bei einer neuen Zentralanmeldung eintragen
NotificationTriggerAllocationAllocation: Ich kann Zentralanmeldung-Bewerbungen für einen meiner Kurse bewerten
NotificationTriggerAllocationRegister: Ich kann mich bei einer neuen Zentralanmeldung bewerben
NotificationTriggerAllocationOutdatedRatings: Zentralanmeldung-Bewerbungen für einen meiner Kurse wurden verändert, nachdem sie bewertet wurden
NotificationTriggerAllocationUnratedApplications: Bewertungen zu Zentralanmeldung-Bewerbungen für einen meiner Kurse stehen aus
+NotificationTriggerAllocationResults: Plätze wurden für eine meiner Zentralanmeldungen verteilt
+NotificationTriggerExamOfficeExamResults: Ich kann neue Prüfungsergebnisse einsehen
+NotificationTriggerExamOfficeExamResultsChanged: Prüfungsergebnisse wurden verändert
+NotificationTriggerCourseRegistered: Ein Kursverwalter hat mich zu einem Kurs angemeldet
NotificationTriggerKindAll: Für alle Benutzer
NotificationTriggerKindCourseParticipant: Für Kursteilnehmer
NotificationTriggerKindExamParticipant: Für Prüfungsteilnehmer
NotificationTriggerKindCorrector: Für Korrektoren
NotificationTriggerKindLecturer: Für Dozenten
+NotificationTriggerKindCourseLecturer: Für Kursverwalter
NotificationTriggerKindAdmin: Für Administratoren
NotificationTriggerKindExamOffice: Für das Prüfungsamt
NotificationTriggerKindEvaluation: Für Vorlesungsumfragen
-NotificationTriggerKindAllocationStaff: Für Zentralanmeldungen
+NotificationTriggerKindAllocationStaff: Für Zentralanmeldungen (Dozenten)
+NotificationTriggerKindAllocationParticipant: Für Zentralanmeldungen
CorrCreate: Abgaben erstellen
UnknownPseudonymWord pseudonymWord@Text: Unbekanntes Pseudonym-Wort "#{pseudonymWord}"
@@ -993,11 +1041,13 @@ MenuAllocationList: Zentralanmeldungen
MenuCourseList: Kurse
MenuCourseMembers: Kursteilnehmer
MenuCourseAddMembers: Kursteilnehmer hinzufügen
-MenuCourseCommunication: Kursmitteilung
+MenuCourseCommunication: Kursmitteilung (E-Mail)
MenuCourseApplications: Bewerbungen
+MenuCourseExamOffice: Prüfungsämter
MenuTermShow: Semester
MenuSubmissionDelete: Abgabe löschen
MenuUsers: Benutzer
+MenuUserAdd: Benutzer anlegen
MenuUserNotifications: Benachrichtigungs-Einstellungen
MenuUserPassword: Passwort
MenuAdminTest: Admin-Demo
@@ -1045,18 +1095,25 @@ MenuExamList: Prüfungen
MenuExamNew: Neue Prüfung anlegen
MenuExamEdit: Bearbeiten
MenuExamUsers: Teilnehmer
+MenuExamGrades: Prüfungsleistungen
MenuExamAddMembers: Prüfungsteilnehmer hinzufügen
+MenuExamOfficeExams: Prüfungen
+MenuExamOfficeFields: Fächer
+MenuExamOfficeUsers: Benutzer
MenuLecturerInvite: Dozenten hinzufügen
MenuAllocationInfo: Hinweise zum Ablauf einer Zentralanmeldung
MenuCourseApplicationsFiles: Dateien aller Bewerbungen
MenuSchoolList: Institute
MenuSchoolNew: Neues Institut anlegen
+MenuCourseNewsNew: Neue Kursnachricht
+MenuCourseNewsEdit: Kursnachricht bearbeiten
AuthPredsInfo: Um eigene Veranstaltungen aus Sicht der Teilnehmer anzusehen, können Veranstalter und Korrektoren hier die Prüfung ihrer erweiterten Berechtigungen temporär deaktivieren. Abgewählte Prädikate schlagen immer fehl. Abgewählte Prädikate werden also nicht geprüft um Zugriffe zu gewähren, welche andernfalls nicht erlaubt wären. Diese Einstellungen gelten nur temporär bis Ihre Sitzung abgelaufen ist, d.h. bis ihr Browser-Cookie abgelaufen ist. Durch Abwahl von Prädikaten kann man sich höchstens temporär aussperren.
AuthPredsActive: Aktive Authorisierungsprädikate
AuthPredsActiveChanged: Authorisierungseinstellungen für aktuelle Sitzung gespeichert
AuthTagFree: Seite ist universell zugänglich
AuthTagAdmin: Nutzer ist Administrator
+AuthTagExamOffice: Nutzer ist Teil eines Prüfungsamts
AuthTagToken: Nutzer präsentiert Authorisierungs-Token
AuthTagNoEscalation: Nutzer-Rechte werden nicht auf fremde Institute ausgeweitet
AuthTagDeprecated: Seite ist nicht überholt
@@ -1089,6 +1146,7 @@ AuthTagRead: Zugriff ist nur lesend
AuthTagWrite: Zugriff ist i.A. schreibend
DeleteCopyStringIfSure n@Int: Wenn Sie sich sicher sind, dass Sie #{pluralDE n "das obige Objekt" "obige Objekte"} unwiderbringlich löschen möchten, schreiben Sie bitte zunächst den angezeigten Text ab.
+DeletePressButtonIfSure n@Int: Wenn Sie sich sicher sind, dass Sie #{pluralDE n "das obige Objekt" "obige Objekte"} unwiderbringlich löschen möchten, bestätigen Sie dies bitte durch Drücken des untigen Knopfes.
DeleteConfirmation: Bestätigung
DeleteConfirmationWrong: Bestätigung muss genau dem angezeigten Text entsprechen.
@@ -1101,10 +1159,14 @@ NavigationFavourites: Favoriten
CommSubject: Betreff
CommBody: Nachricht
+CommBodyTip: Das Eingabefeld akzeptiert derzeit ausschließlich Html. U.A. Zeilumbrüche werden dementsprechend ignoriert und müssen manuell mit
eingefügt werden.
CommRecipients: Empfänger
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
CommSuccess n@Int: Nachricht wurde an #{n} Empfänger versandt
+CommUndisclosedRecipients: Verborgene Empfänger
+CommAllRecipients: alle-empfaenger
CommCourseHeading: Kursmitteilung
CommTutorialHeading: Tutorium-Mitteilung
@@ -1162,6 +1224,10 @@ SubmissionUserInvitationDeclined shn@SheetName: Sie haben die Einladung, Mitabge
SubmissionUserInviteHeading shn@SheetName: Einladung zu einer Abgabe für #{shn}
SubmissionUserInviteExplanation: Sie wurden eingeladen, Mitabgebende(r) bei einer Abgabe zu sein.
+ExamOfficeUserInviteHeading displayName@Text: Zugriff auf Ihre Prüfungsleistungen durch #{displayName}
+ExamOfficeUserInviteExplanation: Um Ihre Prüfungsleistungen ordnungsgemäß anrechnen zu können (z.B. im finalen Transcript of Records für Erasmus-Studierende) werden sie eingeladen der hierfür zuständigen Stelle Einsicht zu gewähren.
+ExamOfficeUserInvitationAccepted: Einsicht erfolgreich gewährt
+
InvitationAction: Aktion
InvitationActionTip: Abgelehnte Einladungen können nicht mehr angenommen werden
InvitationMissingRestrictions: Authorisierungs-Token fehlen benötigte Daten
@@ -1246,6 +1312,9 @@ HealthSMTPConnect: SMTP-Server kann erreicht werden
HealthWidgetMemcached: Memcached-Server liefert Widgets korrekt aus
HealthActiveJobExecutors: Anteil der job-workers, die neue Befehle annehmen
+CourseParticipantsHeading: Kursteilnehmer
+CourseParticipantsCount n@Int: #{n}
+CourseParticipantsCountOf n@Int m@Int: #{n} von #{m}
CourseParticipants n@Int: Derzeit #{n} angemeldete Kursteilnehmer
CourseParticipantsInvited n@Int: #{n} #{pluralDE n "Einladung" "Einladungen"} per E-Mail verschickt
CourseParticipantsAlreadyRegistered n@Int: #{n} Teilnehmer #{pluralDE n "ist" "sind"} bereits angemeldet
@@ -1283,35 +1352,48 @@ ExamPublishOccurrenceAssignments: Termin- bzw. Raumzuteilung den Teilnehmern mit
ExamPublishOccurrenceAssignmentsTip: Ab diesem Zeitpunkt Teilnehmer einsehen zu welcher Teilprüfung bzw. welchen Raum sie angemeldet sind
ExamPublishOccurrenceAssignmentsParticipant: Termin- bzw. Raumzuteilung einsehbar ab
ExamFinished: Bewertung abgeschlossen ab
+ExamFinishedOffice: Noten bekannt gegeben
ExamFinishedParticipant: Bewertung vorrausichtlich abgeschlossen
ExamFinishedTip: Zeitpunkt zu dem Prüfungergebnisse den Teilnehmern gemeldet werden
-ExamClosed: Noten stehen fest ab
-ExamClosedTip: Zeitpunkt ab dem keine Änderungen an den Ergebnissen zulässig sind; Prüfungsämter bekommen Einsicht
-ExamShowGrades: Noten anzeigen
-ExamShowGradesTip: Soll den Teilnehmern ihre genaue Note angezeigt werden, oder sollen sie nur informiert werden, ob sie bestanden haben?
+ExamClosed: Noten gemeldet
+ExamClosedTip: Prüfungsämter, die im System Noten einsehen, werden zu diesem Zeitpunkt benachrichtigt und danach bei Änderungen informiert
+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?
ExamPublicStatistics: Statistik veröffentlichen
-ExamPublicStatisticsTip: Soll die statistische Auswertung auch den Teilnehmer 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
ExamGradingManual': Keine automatische Berechnung
ExamGradingKey': Nach Schlüssel
ExamGradingKey: Notenschlüssel
-ExamGradingKeyTip: Die Grenzen beziehen sich auf die effektive Maximalpunktzahl, nachdem etwaige Bonuspunkte aus dem Übungsbetrieb angerechnet und die Ergebnise der Teilaufgaben mit ihrem Gewicht multipliziert wurden
+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
PointsMustBeNonNegative: Punktegrenzen dürfen nicht negativ sein
PointsMustBeMonotonic: Punktegrenzen müssen aufsteigend sein
GradingFrom: Ab
ExamNew: Neue Prüfung
+ExamBonus: Bonuspunkte-System
ExamBonusRule: Prüfungsbonus aus Übungsbetrieb
ExamNoBonus': Kein automatischer Bonus
ExamBonusPoints': Umrechnung von Übungspunkten
+ExamBonusManual': Manuelle Berechnung
+
+ExamBonusAchieved: Bonuspunkte
ExamEditHeading examn@ExamName: #{examn} bearbeiten
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
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
ExamRoomManual': Keine automatische Zuteilung
ExamRoomSurname': Nach Nachname
@@ -1334,6 +1416,8 @@ ExamRoomDescription: Beschreibung
ExamTimeTip: Nur zur Information der Studierenden, die tatsächliche Zeitangabe erfolgt pro Prüfung
ExamRoomRegistered: Zugeteilt
+ExamOccurrenceStart: Prüfungsbeginn
+
ExamFormTimes: Zeiten
ExamFormOccurrences: Prüfungstermine/Räume
ExamFormAutomaticFunctions: Automatische Funktionen
@@ -1343,12 +1427,17 @@ ExamFormParts: Teile
ExamCorrectors: Korrektoren
ExamCorrectorAlreadyAdded: Ein Korrektor mit dieser E-Mail ist bereits für diese Prüfung eingetragen
-ExamParts: Teilaufgaben
-ExamPartWeightNegative: Gewicht aller Teilaufgaben muss größer oder gleich Null sein
-ExamPartAlreadyExists: Teilaufgabe mit diesem Namen existiert bereits
-ExamPartName: Name
+ExamParts: Teilprüfungen/Aufgaben
+ExamPartWeightNegative: Gewicht aller Teilprüfungen muss größer oder gleich Null sein
+ExamPartAlreadyExists: Teilprüfung mit diesem Namen existiert bereits
+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
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
ExamNameTaken exam@ExamName: Es existiert bereits eine Prüfung mit Namen #{exam}
@@ -1358,6 +1447,7 @@ ExamEdited exam@ExamName: #{exam} erfolgreich bearbeitet
ExamNoShow: Nicht erschienen
ExamVoided: Entwertet
+ExamBonusManualParticipants: Von den Kursverwaltern manuell berechnet
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
@@ -1391,11 +1481,30 @@ VersionHistory: Versionsgeschichte
KnownBugs: Bekannte Bugs
ImplementationDetails: Implementierung
+ExamSynchronised: Synchronisiert
+
ExamUsersHeading: Prüfungsteilnehmer
ExamUserDeregister: Teilnehmer von Prüfung abmelden
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
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
+ExamUserSyncOfficeName: Name
+ExamUserSyncTime: Zeitpunkt
+ExamUserSyncSchools: Institute
+ExamUserSyncLastChange: Zuletzt geändert
+ExamUserMarkSynchronised: Prüfungsleistung als synchronisiert markieren
+
+ExamUserMarkSynchronisedCsv: Prüfungsleistungen beim Export als synchronisiert markieren
+ExamUserMarkedSynchronised n@Int: #{n} #{pluralDE n "Prüfungsleistung" "Prüfungsleistungen"} als synchronisiert markiert
+
+ExamOfficeExamUsersHeading: Prüfungsleistungen
CsvFile: CSV-Datei
CsvModifyExisting: Existierende Einträge angleichen
@@ -1415,8 +1524,12 @@ CsvImportExplanationLabel: Hinweise zum CSV-Import
Proportion c@Text of@Text prop@Rational: #{c}/#{of} (#{rationalToFixed2 (100 * prop)}%)
-CsvColumnsExplanationsLabel: Spalten
-CsvColumnsExplanationsTip: Bedeutung der in der CSV-Datei enthaltenen Spalten
+CourseUserCsvName tid@TermId ssh@SchoolId csh@CourseShorthand: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-teilnehmer
+ExamUserCsvName tid@TermId ssh@SchoolId csh@CourseShorthand examn@ExamName: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-#{foldedCase examn}-teilnehmer
+CourseApplicationsTableCsvName tid@TermId ssh@SchoolId csh@CourseShorthand: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-bewerbungen
+
+CsvColumnsExplanationsLabel: Spalten- & Zellenformat
+CsvColumnsExplanationsTip: Bedeutung und Format der in der CSV-Datei enthaltenen Spalten
CsvColumnExamUserSurname: Nachname(n) des Teilnehmers
CsvColumnExamUserFirstName: Vorname(n) des Teilnehmers
CsvColumnExamUserName: Voller Name des Teilnehmers (gewöhnlicherweise inkl. Vor- und Nachname(n))
@@ -1429,8 +1542,22 @@ 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
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
+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")
CsvColumnExamUserCourseNote: Notizen zum Teilnehmer
+
+CsvColumnUserName: Voller Name des Teilnehmers
+CsvColumnUserMatriculation: Matrikelnummer des Teilnehmers
+CsvColumnUserEmail: E-Mail Addresse des Teilnehmers
+CsvColumnUserField: Studienfach, mit dem der Teilnehmer seine Kursanmeldung assoziiert hat
+CsvColumnUserDegree: Abschluss, den der Teilnehmer im assoziierten Studienfach anstrebt
+CsvColumnUserSemester: Fachsemester des Teilnehmers im assoziierten Studienfach
+CsvColumnUserRegistration: Zeitpunkt der Anmeldung zum Kurs (ISO 8601)
+CsvColumnUserNote: Notizen zum Teilnehmer
+
+CsvColumnExamOfficeExamUserOccurrenceStart: Prüfungstermin (ISO 8601)
+
CsvColumnApplicationsAllocation: Zentralanmeldung über die die Bewerbung eingegangen ist
CsvColumnApplicationsApplication: Eindeutige Nummer der Bewerbung (zur Zuordnung im ZIP-Archiv aller Bewerbungsdateien)
CsvColumnApplicationsName: Voller Name des Bewerbers
@@ -1441,7 +1568,7 @@ CsvColumnApplicationsSemester: Fachsemester des Bewerbes im assoziierten Studien
CsvColumnApplicationsText: Text-Bewerbung
CsvColumnApplicationsHasFiles: Hat der Bewerber Dateien zu seiner Bewerbung eingereicht (siehe ZIP-Archiv aller Bewerbungsdateien)?
CsvColumnApplicationsVeto: Bewerber mit Veto werden garantiert nicht dem Kurs zugeteilt; "veto" oder leer
-CsvColumnApplicationsRating: Bewertung der Bewerbung; "1.0", "1.3", "1.7", ..., "4.0", "5.0"
+CsvColumnApplicationsRating: Bewertung der Bewerbung; "1.0", "1.3", "1.7", ..., "4.0", "5.0" (Leer wird behandelt wie eine Note zwischen 2.3 und 2.7)
CsvColumnApplicationsComment: Kommentar zur Bewerbung; je nach Kurs-Einstellungen entweder nur als Notiz für die Kursverwalter oder Feedback für den Bewerber
Action: Aktion
@@ -1457,8 +1584,13 @@ ExamUserCsvRegister: Kursteilnehmer zur Prüfung anmelden
ExamUserCsvAssignOccurrence: Teilnehmern einen anderen Termin/Raum zuweisen
ExamUserCsvDeregister: Teilnehmer von der Prüfung abmelden
ExamUserCsvSetCourseField: Kurs-assoziiertes Studienfach ändern
+ExamUserCsvOverrideBonus: Bonuspunkte entgegen Bonusregelung überschreiben
+ExamUserCsvOverrideResult: Ergebnis entgegen automatischer Notenberechnung überschreiben
+ExamUserCsvSetBonus: Bonuspunkte eintragen
ExamUserCsvSetResult: Ergebnis eintragen
+ExamUserCsvSetPartResult: Ergebnis einer Teilprüfung eintragen
ExamUserCsvSetCourseNote: Teilnehmer-Notizen anpassen
+ExamBonusNone: Keine Bonuspunkte
ExamUserCsvCourseNoteDeleted: Notiz wird gelöscht
@@ -1530,7 +1662,10 @@ AllocationAppliedCourses: Bewerbungen
AllocationNumCoursesAvailableApplied available@Int applied@Int: Sie haben sich bisher für #{applied}/#{available} #{pluralDE applied "Kurs" "Kursen"} beworben
AllocationTitle termText@Text ssh'@SchoolShorthand allocation@AllocationName: #{termText} - #{ssh'}: #{allocation}
AllocationShortTitle termText@Text ssh'@SchoolShorthand ash@AllocationShorthand: #{termText} - #{ssh'} - #{ash}
+AllocationSchool: Institut
+AllocationSemester: Semester
AllocationDescription: Beschreibung
+AllocationStaffDescription: Beschreibung für Dozenten
AllocationStaffRegisterFrom: Eintragung der Kurse ab
AllocationStaffRegister: Eintragung der Kurse
AllocationRegisterFrom: Bewerbung ab
@@ -1539,6 +1674,13 @@ AllocationRegisterClosed: Die Zentralanmeldung ist aktuell geschlossen.
AllocationRegisterOpensIn difftime@Text: Die Zentralanmeldung öffnet voraussichtlich in #{difftime}
AllocationStaffAllocationFrom: Bewertung der Bewerbungen ab
AllocationStaffAllocation: Bewertung der Bewerbungen
+AllocationRegisterByStaff: An- und Abmeldung durch Kursverwalter
+AllocationRegisterByStaffFrom: An- und Abmeldung durch Kursverwalter ab
+AllocationRegisterByStaffTip: In diesem Zeitraum können Kursverwalter Teilnehmer zu und von ihren Kursen an- und abmelden.
+AllocationRegisterByStaffFromTip: Ab diesem Zeitpunkt können Kursverwalter Teilnehmer zu und von ihren Kursen an- und abmelden.
+AllocationRegisterByCourseFrom: Direkte An- und Abmeldung ab
+AllocationRegisterByCourseFromTip: Frühestens ab diesem Zeitpunkt ist die eigentständige An- und Abmeldung zu und von den Kursen, die an der Zentralanmeldung teilnehmen, möglich. Kontrolle über die genauen Fristen haben die Kursverwalter.
+AllocationOverrideDeregister: Abmeldung von den Kursen nur bis
AllocationProcess: Platzvergabe
AllocationNoApplication: Keine Bewerbung
AllocationPriority: Priorität
@@ -1586,9 +1728,16 @@ CourseApplicationNoVeto: Kein Veto
CourseApplicationNoRatingPoints: Keine Bewertung
CourseApplicationNoRatingComment: Kein Kommentar
-UserDisplayName: Voller Name
+UserDisplayName: Angezeigter Name
+UserDisplayNameInvalid: Angezeigter Name erfüllt nicht die Vorgaben
+UserDisplayNameRules: Vorgaben für den angezeigten Namen
+UserDisplayNameRulesBelow: Vorgaben für den angezeigten Namen finden sich weiter unten auf der Seite
UserMatriculation: Matrikelnummer
+UserDisplayEmail: Angezeigte E-Mail Adresse
+UserDisplayEmailTip: Diese Adresse wird in öffentlich zugänglichen Teilen des Systems im Zusammenhang mit Ihrem Namen angezeigt. Benachrichtigungen und andere Kommunikation von Uni2work und Nutzern mit erweiterten Rechten erhalten sie stets, unabhängig von dieser Einstellung, an die in Ihren Persönlichen Daten hinterlegte primäre Adresse.
+UserDisplayEmailChangeSent displayEmail@UserEmail: Anweisungen zum Ändern der angezeigten E-Mail Adresse wurden an „#{displayEmail}” versandt
+
SchoolShort: Kürzel
SchoolName: Name
SchoolLdapOrganisations: Assoziierte LDAP-Fragmente
@@ -1629,4 +1778,142 @@ MailAllocationUnratedApplications: Für die unten aufgeführten Kurse liegen Bew
MailSubjectAllocationOutdatedRatings allocation@AllocationName: Bereits bewertete Bewerbungen für ihre Kurse in der Zentralanmeldung „#{allocation}“ haben sich geändert
MailAllocationOutdatedRatings: Für die unten aufgeführten Kurse liegen Bewerbungen vor, die im Rahmen der Zentralanmeldung an den jeweiligen Kurs gestellt wurden, die sich verändert haben, seit sie zuletzt bewertet wurden.
-MailAllocationOutdatedRatingsWarning: Bewerbungen deren Bewertung veraltet ist (d.h. die Bewerbung wurde nach der Bewertung verändert) zählen als nicht bewertet.
\ No newline at end of file
+MailAllocationOutdatedRatingsWarning: Bewerbungen deren Bewertung veraltet ist (d.h. die Bewerbung wurde nach der Bewertung verändert) zählen als nicht bewertet.
+
+ExamOfficeSubscribedUsers: Benutzer
+ExamOfficeSubscribedUsersTip: Sie können mehrere Matrikelnummern mit Komma separieren
+
+ExamOfficeSubscribedUsersExplanation: Für hier angegebene Benutzer können Sie (ungeachtet der Fächer des Studierenden) stets sämtliche Prüfungsergebnisse einsehen.
+ExamOfficeSubscribedFieldsExplanation: Sie können für alle Benutzer, die mindestens eines der angegeben Studienfächer studieren, sämtliche Prüfungsergebnisse einsehen. Sie haben zusätzlich die Möglichkeit anzugeben, ob es den Benutzern gestattet sein soll, dieser Einsicht im Einzelfall (pro Kurs) zu widersprechen.
+
+UserMatriculationNotFound matriculation@Text: Es existiert kein Uni2work-Benutzer mit Matrikelnummer „#{matriculation}“
+UserMatriculationAmbiguous matriculation@Text: Matrikelnummer „#{matriculation}“ ist nicht eindeutig
+
+TransactionExamOfficeUsersUpdated nDeleted@Int nAdded@Int: #{nAdded} Benutzer hinzugefügt, #{nDeleted} Benutzer gelöscht
+
+TransactionExamOfficeFieldsUpdated nUpdates@Int: #{nUpdates} #{pluralDE nUpdates "Studienfach" "Studienfächer"} angepasst
+ExamOfficeFieldNotSubscribed: —
+ExamOfficeFieldSubscribed: Einsicht
+ExamOfficeFieldForced: Forcierte Einsicht
+InvalidExamOfficeFieldMode parseErr@Text: Konnte „#{parseErr}“ nicht interpretieren
+
+LdapIdentification: Campus-Kennung
+LdapIdentificationOrEmail: Campus-Kennung/E-Mail Addresse
+AdminUserTitle: Titel
+AdminUserFirstName: Vorname
+AdminUserSurname: Nachname
+AdminUserDisplayName: Anzeige-Name
+AdminUserEmail: E-Mail Addresse
+AdminUserDisplayEmail: Anzeige-E-Mail
+AdminUserIdent: Identifikation
+AdminUserAuth: Authentifizierung
+AdminUserMatriculation: Matrikelnummer
+AuthKindLDAP: Campus-Kennung
+AuthKindPWHash: Uni2work-Kennung
+UserAdded: Benutzer erfolgreich angelegt
+UserCollision: Benutzer konnte wegen Eindeutigkeit nicht angelegt werden
+
+CourseAllocationsBounds n@Int: Voraussichtliche Zuteilungen durch #{pluralDE n "Zentralanmeldung" "Zentralanmeldungen"}
+CourseAllocationsBoundCoincide numFirstChoice@Int: Vstl. #{numFirstChoice} Teilnehmer
+CourseAllocationsBound numApps@Int numFirstChoice@Int: Vstl. zwischen #{numFirstChoice} und #{numApps} Teilnehmer
+CourseAllocationsBoundCapped: Die obige Anzeige wurde durch die aktuell angegebene Kurskapazität reduziert.
+CourseAllocationsBoundWarningOpen: Diese Informationen entsprechen nur dem aktuellen Stand der Bewerbungen und können sich noch ändern.
+
+BtnSetDisplayEmail: E-Mail Adresse setzen
+UserDisplayEmailChanged: Öffentliche E-Mail Adresse erfolgreich gesetzt
+TitleChangeUserDisplayEmail: Öffentliche E-Mail Adresse setzen
+
+MailSubjectChangeUserDisplayEmail: Diese E-Mail Adresse in Uni2work veröffentlichen
+MailIntroChangeUserDisplayEmail displayEmail@UserEmail: Der oben genannte Benutzer möchte „#{displayEmail}“ als öffentliche Adresse, assoziiert mit sich selbst, angeben. Wenn Sie diese Aktion nicht selbst ausgelöst haben, ignorieren Sie diese Mitteilung bitte!
+MailTitleChangeUserDisplayEmail displayName@Text: #{displayName} möchte diese E-Mail Adresse in Uni2work veröffentlichen
+
+ExamOfficeOptOutsChanged: Zuständige Prüfungsämter erfolgreich angepasst
+
+BtnCloseExam: Klausur abschließen
+ExamCloseTip: Wenn eine Klausur abgeschlossen wird, werden Prüfungsämter, die im System Noten einsehen, benachrichtigt und danach bei Änderungen informiert.
+ExamCloseReminder: Bitte schließen Sie die Klausur frühstmöglich, sobald die Prüfungsleistungen sich voraussichtlich nicht mehr ändern werden. Z.B. direkt nach der Klausureinsicht.
+ExamDidClose: Klausur erfolgreich abgeschlossen
+
+ExamClosedSince time@Text: Klausur abgeschlossen seit #{time}
+
+LecturerInfoTooltipNew: Neues Feature
+LecturerInfoTooltipProblem: Feature mit bekannten Problemen
+LecturerInfoTooltipPlanned: Geplantes Feature
+LecturerInfoTooltipNewU2W: Unterschied zu UniWorX
+
+BtnAcceptApplications: Bewerbungen akzeptieren
+BtnAcceptApplicationsTip: Mit dem untigen Knopf können Sie den Kurs (höchstens bis zur angegeben Maximalkapazität, falls eingestellt) mit Bewerbern auffüllen. Die Bewertungen der Bewerbungen werden dabei berücksichtigt (Unbewertet wird behandelt wie eine Note zwischen 2.3 und 2.7). Bewerber mit Veto oder 5.0 werden nicht angemeldet.
+AcceptApplicationsMode: Bewerbungen akzeptieren
+AcceptApplicationsModeTip: Sollen akzeptierte Bewerber direkt als Teilnehmer im Kurs eingetragen werden oder sollen Einladungen per E-Mail verschickt werden?
+AcceptApplicationsDirect: Direkt anmelden
+AcceptApplicationsInvite: Einladungen verschicken
+AcceptApplicationsSecondary: Gleichstände auflösen
+AcceptApplicationsSecondaryTip: Wenn es im Laufe des Verfahrens mehrere Bewerber mit der selben Bewertung für den selben Platz gibt, wie soll der Gleichstand aufgelöst werden?
+AcceptApplicationsSecondaryRandom: Zufällig
+AcceptApplicationsSecondaryTime: Nach Zeitpunkt der Bewerbung
+
+CsvOptions: CSV-Optionen
+CsvOptionsTip: Diese Einstellungen betreffen nur den CSV-Export; beim Import werden die verwendeten Einstellungen automatisch ermittelt.
+CsvPresetRFC: Standard-Konform (RFC 4180)
+CsvPresetExcel: Excel-Kompatibel
+CsvCustom: Benutzerdefiniert
+CsvDelimiter: Trennzeichen
+CsvUseCrLf: Zeilenumbrüche
+CsvQuoting: Quoting
+CsvQuotingTip: Wann sollen Anführungszeichen (") um Felder platziert werden, um Interpretation von im Feld enthaltenen Zeichen als Trennzeichen zu verhindern?
+CsvDelimiterNull: Null-Byte
+CsvDelimiterTab: Tabulator
+CsvDelimiterComma: Komma
+CsvDelimiterColon: Doppelpunkt
+CsvDelimiterBar: Senkrechter Strich
+CsvDelimiterSpace: Leerzeichen
+CsvDelimiterUnitSep: Teilgruppentrennzeichen
+CsvCrLf: DOS (CRLF)
+CsvLf: Unix (LF)
+CsvQuoteNone: Nie
+CsvQuoteMinimal: Nur wenn nötig
+CsvQuoteAll: Immer
+CsvOptionsUpdated: CSV-Optionen erfolgreich angepasst
+CsvChangeOptionsLabel: Export-Optionen
+
+CourseNews: Aktuelles
+CourseNewsArchiveName tid@TermId ssh@SchoolId csh@CourseShorthand newsTitle@Text: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-#{foldCase newsTitle}
+CourseNewsFiles: Dateien
+CourseNewsLastEdited time@Text: Zuletzt verändert: #{time}
+CourseNewsActionEdit: Bearbeiten
+CourseNewsActionDelete: Löschen
+CourseNewsActionCreate: Neue Nachricht
+CourseMaterial: Material
+CourseMaterialFree: Das Kursmaterial ist ohne Anmeldung frei zugänglich
+CourseMaterialNotFree: Eine Anmeldung zum Kurs ist Voraussetzung zum Zugang zu Kursmaterial
+
+CourseNewsVisibleFromEditWarning: Das Datum der Veröffentlichung liegt in der Vergangenheit und sollte nicht mehr verändert werden, da dies die Teilnehmer verwirren könnte.
+CourseNewsVisibleFromTip: Ohne Datum nie sichtbar für Teilnehmer; leer lassen ist nur sinnvoll für noch unfertige Nachrichten
+CourseNewsTitle: Titel
+CourseNewsSummary: Zusammenfassung
+CourseNewsSummaryTip: Wenn angegeben, wird auf der Kursübersichtsseite, platzsparend, nur die Zusammenfassung angezeigt und der Inhalt in ein Popup ausgelagert
+CourseNewsContent: Inhalt
+CourseNewsParticipantsOnly: Nur für Kursteilnehmer
+CourseNewsVisibleFrom: Sichtbar ab
+CourseNewsCreated: Kursnachricht erfolgreich angelegt
+CourseNewsEdited: Kursnachricht erfolgreich editiert
+CourseNewsDeleteQuestion: Wollen Sie die unten aufgeführte Nachricht wirklich löschen?
+CourseNewsDeleted: Kursnachricht erfolgreich gelöscht
+
+CourseDeregistrationAllocationLog: Ihr Platz in diesem Kurs stammt aus einer Zentralanmeldung. Wenn Sie sich vom Kurs abmelden wird dieser Umstand permanent im System gespeichert und kann Sie u.U. bei zukünftigen Zentralanmeldungen benachteiligen. Wenn Sie gute Gründe vorzuweisen haben, warum Ihre Abmeldung nicht selbstverschuldet ist, kontaktieren Sie bitte einen Kursverwalter. Diese haben die Möglichkeit Sie ohne permanente Eintragung im System abzumelden.
+CourseDeregistrationAllocationReason: Grund
+CourseDeregistrationAllocationReasonTip: Der angegebene Grund wird permanent im System hinterlegt und ist i.A. einziger Anhaltspunkt zur Schlichtung etwaiger Konflikte
+CourseDeregistrationAllocationShouldLog: Selbstverschuldet
+CourseDeregistrationAllocationShouldLogTip: Falls der Platz des Studierenden, der abgemeldet wird, aus einer Zentralanmeldung stammt, ist vorgesehen einen permanenten Eintrag im System zu speichern, der den Studierenden u.U. bei zukünftigen Zentralanmeldungen benachteiligt. Als Kursverwalter haben Sie die Möglichkeit dies zu unterbinden, wenn der Studierende gute Gründe vorweisen kann, warum seine Abmeldung nicht selbstverschuldet ist.
+
+MailSubjectAllocationResults allocation@AllocationName: Plätze für Zentralanmeldung „#{allocation}“ wurden verteilt
+AllocationResultsLecturer: Es wurden Plätze zugewiesen, wie folgt:
+AllocationResultLecturer csh@CourseShorthand count@Int64: #{count} Teilnehmer für #{csh}
+AllocationResultsStudent: Sie haben Plätze erhalten in:
+AllocationNoResultsStudent: Sie haben leider keine Plätze erhalten.
+AllocationResultStudent csh@CourseShorthand: Sie haben einen Platz in #{csh} erhalten.
+
+FavouriteVisited: Kürzlich besucht
+FavouriteParticipant: Ihre Kurse
+FavouriteManual: Favoriten
+FavouriteCurrent: Aktueller Kurs
diff --git a/models/allocations b/models/allocations.model
similarity index 95%
rename from models/allocations
rename to models/allocations.model
index 9ddbd59bd..8f0805b34 100644
--- a/models/allocations
+++ b/models/allocations.model
@@ -21,6 +21,8 @@ Allocation -- attributes with prefix staff- affect lecturers only, but are invis
registerByCourse UTCTime Maybe -- course registration dates are ignored until this day has passed or always prohibited
overrideDeregister UTCTime Maybe -- course deregistration enforced to be this date, i.e. students may disenrol from course after or never
-- overrideVisible not needed, since courses are always visible
+ fingerprint AllocationFingerprint Maybe
+ matchingLog FileId Maybe
TermSchoolAllocationShort term school shorthand -- shorthand must be unique within school and semester
TermSchoolAllocationName term school name -- name must be unique within school and semester
deriving Show Eq Ord Generic
@@ -35,6 +37,7 @@ AllocationUser
allocation AllocationId
user UserId
totalCourses Natural -- number of total allocated courses for this user must be <= than this number
+ priority AllocationPriority Maybe
UniqueAllocationUser allocation user
AllocationDeregister -- self-inflicted user-deregistrations from an allocated course
diff --git a/models/audit b/models/audit.model
similarity index 100%
rename from models/audit
rename to models/audit.model
diff --git a/models/config b/models/config.model
similarity index 100%
rename from models/config
rename to models/config.model
diff --git a/models/courses b/models/courses.model
similarity index 73%
rename from models/courses
rename to models/courses.model
index dd1099e55..680d0d7dd 100644
--- a/models/courses
+++ b/models/courses.model
@@ -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
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
- applicationsRequired Bool
+ applicationsRequired Bool default=false
applicationsInstructions Html Maybe
- applicationsText Bool
- applicationsFiles UploadMode
- applicationsRatingsVisible Bool
+ applicationsText Bool default=false
+ applicationsFiles UploadMode "default='{\"mode\": \"no-upload\"}'::jsonb"
+ applicationsRatingsVisible Bool default=false
TermSchoolCourseShort term school shorthand -- shorthand must be unique within school and semester
TermSchoolCourseName term school name -- name must be unique within school and semester
deriving Generic
@@ -35,12 +35,6 @@ CourseEdit -- who edited when a row in table "Course", kept indef
user UserId
time UTCTime
course CourseId
-CourseFavourite -- which user accessed which course when, only displayed to user for convenience;
- user UserId -- max number of rows kept per user is user-defined by column 'maxFavourites' in table "User"
- time UTCTime -- oldest is removed first
- course CourseId
- UniqueCourseFavourite user course
- deriving Show
Lecturer -- course ownership
user UserId
course CourseId
@@ -51,7 +45,7 @@ CourseParticipant -- course enrolement
user UserId
registration UTCTime -- time of last enrolement for this course
field StudyFeaturesId Maybe -- associated degree course, user-defined; required for communicating grades
- allocated Bool default=false -- participant was centrally allocated
+ allocated AllocationId Maybe -- participant was centrally allocated
UniqueParticipant user course
-- Replace the last two by the following, once an audit log is available
-- CourseUserNote -- lecturers of a specific course may share a text note on each enrolled student
@@ -71,19 +65,8 @@ CourseUserNoteEdit -- who edited a participants course note when
time UTCTime
note CourseUserNoteId -- PROBLEM: deleted notes have no modification date any more
-CourseApplication
- course CourseId
- user UserId
- field StudyFeaturesId Maybe -- associated degree course, user-defined; required for communicating grades
- text Text Maybe -- free text entered by user
- ratingVeto Bool default=false
- ratingPoints ExamGrade Maybe
- ratingComment Text Maybe
- allocation AllocationId Maybe
- allocationPriority Natural Maybe
- time UTCTime default=now()
- ratingTime UTCTime Maybe
-CourseApplicationFile
- application CourseApplicationId
- file FileId
- UniqueApplicationFile application file
\ No newline at end of file
+CourseUserExamOfficeOptOut
+ course CourseId
+ user UserId
+ school SchoolId
+ UniqueCourseUserExamOfficeOptOut course user school
diff --git a/models/courses/applications.model b/models/courses/applications.model
new file mode 100644
index 000000000..d1d3a4876
--- /dev/null
+++ b/models/courses/applications.model
@@ -0,0 +1,16 @@
+CourseApplication
+ course CourseId
+ user UserId
+ field StudyFeaturesId Maybe -- associated degree course, user-defined; required for communicating grades
+ text Text Maybe -- free text entered by user
+ ratingVeto Bool default=false
+ ratingPoints ExamGrade Maybe
+ ratingComment Text Maybe
+ allocation AllocationId Maybe
+ allocationPriority Natural Maybe
+ time UTCTime default=now()
+ ratingTime UTCTime Maybe
+CourseApplicationFile
+ application CourseApplicationId
+ file FileId
+ UniqueApplicationFile application file
diff --git a/models/courses/favourite.model b/models/courses/favourite.model
new file mode 100644
index 000000000..1c5077b77
--- /dev/null
+++ b/models/courses/favourite.model
@@ -0,0 +1,10 @@
+CourseFavourite -- which user accessed which course when, only displayed to user for convenience;
+ user UserId
+ course CourseId
+ reason FavouriteReason
+ lastVisit UTCTime
+ UniqueCourseFavourite user course
+CourseNoFavourite
+ user UserId
+ course CourseId
+ UniqueCourseNoFavourite user course
\ No newline at end of file
diff --git a/models/materials b/models/courses/materials.model
similarity index 89%
rename from models/materials
rename to models/courses/materials.model
index 01076a1cf..b8d321fc3 100644
--- a/models/materials
+++ b/models/courses/materials.model
@@ -9,4 +9,5 @@ Material -- course material for disemination to course participants
deriving Generic
MaterialFile -- a file that is part of a material distribution
material MaterialId
- file FileId
\ No newline at end of file
+ file FileId
+ UniqueMaterialFile material file
\ No newline at end of file
diff --git a/models/courses/news.model b/models/courses/news.model
new file mode 100644
index 000000000..8596d0930
--- /dev/null
+++ b/models/courses/news.model
@@ -0,0 +1,12 @@
+CourseNews
+ course CourseId
+ visibleFrom UTCTime Maybe
+ participantsOnly Bool
+ title Text Maybe
+ content Html
+ summary Html Maybe
+ lastEdit UTCTime
+CourseNewsFile
+ news CourseNewsId
+ file FileId
+ UniqueCourseNewsFile news file
\ No newline at end of file
diff --git a/models/exam-office.model b/models/exam-office.model
new file mode 100644
index 000000000..dc952c26f
--- /dev/null
+++ b/models/exam-office.model
@@ -0,0 +1,14 @@
+ExamOfficeField
+ office UserId
+ field StudyTermsId
+ forced Bool
+ UniqueExamOfficeField office field
+ExamOfficeUser
+ office UserId
+ user UserId
+ UniqueExamOfficeUser office user
+ExamOfficeResultSynced
+ school SchoolId Maybe
+ office UserId
+ result ExamResultId
+ time UTCTime
\ No newline at end of file
diff --git a/models/exams b/models/exams.model
similarity index 74%
rename from models/exams
rename to models/exams.model
index 694f1a9bc..7eff47789 100644
--- a/models/exams
+++ b/models/exams.model
@@ -1,9 +1,9 @@
Exam
course CourseId
name ExamName
- gradingRule ExamGradingRule
- bonusRule ExamBonusRule
- occurrenceRule ExamOccurrenceRule
+ gradingRule ExamGradingRule Maybe
+ bonusRule ExamBonusRule Maybe
+ occurrenceRule ExamOccurrenceRule Maybe
visibleFrom UTCTime Maybe
registerFrom UTCTime Maybe
registerTo UTCTime Maybe
@@ -19,10 +19,12 @@ Exam
UniqueExam course name
ExamPart
exam ExamId
- name (CI Text)
+ number ExamPartNumber
+ name ExamPartName Maybe
maxPoints Points Maybe
weight Rational
- UniqueExamPart exam name
+ UniqueExamPartNumber exam number
+ UniqueExamPartName exam name !force
ExamOccurrence
exam ExamId
name ExamOccurrenceName
@@ -42,7 +44,14 @@ ExamPartResult
examPart ExamPartId
user UserId
result ExamResultPoints
+ lastChanged UTCTime default=now()
UniqueExamPartResult examPart user
+ExamBonus
+ exam ExamId
+ user UserId
+ bonus Points
+ lastChanged UTCTime default=now()
+ UniqueExamBonus exam user
ExamResult
exam ExamId
user UserId
diff --git a/models/files b/models/files.model
similarity index 100%
rename from models/files
rename to models/files.model
diff --git a/models/invitations b/models/invitations.model
similarity index 100%
rename from models/invitations
rename to models/invitations.model
diff --git a/models/jobs b/models/jobs.model
similarity index 100%
rename from models/jobs
rename to models/jobs.model
diff --git a/models/schools b/models/schools.model
similarity index 91%
rename from models/schools
rename to models/schools.model
index 2da425cf4..c5bd3d6ac 100644
--- a/models/schools
+++ b/models/schools.model
@@ -13,4 +13,5 @@ SchoolLdap
UniqueOrgUnit orgUnit
SchoolTerms
school SchoolId
- terms StudyTermsId
\ No newline at end of file
+ terms StudyTermsId
+ UniqueSchoolTerms school terms
\ No newline at end of file
diff --git a/models/sheets b/models/sheets.model
similarity index 100%
rename from models/sheets
rename to models/sheets.model
diff --git a/models/submissions b/models/submissions.model
similarity index 100%
rename from models/submissions
rename to models/submissions.model
diff --git a/models/system-messages b/models/system-messages.model
similarity index 100%
rename from models/system-messages
rename to models/system-messages.model
diff --git a/models/terms b/models/terms.model
similarity index 100%
rename from models/terms
rename to models/terms.model
diff --git a/models/tutorials b/models/tutorials.model
similarity index 100%
rename from models/tutorials
rename to models/tutorials.model
diff --git a/models/users b/models/users.model
similarity index 92%
rename from models/users
rename to models/users.model
index 223cd2b8a..86fa7fd9f 100644
--- a/models/users
+++ b/models/users.model
@@ -9,9 +9,10 @@
--
User json -- Each Uni2work user has a corresponding row in this table; created upon first login.
surname UserSurname -- Display user names always through 'nameWidget displayName surname'
- displayName UserDisplayName -- we only show LDAP-DisplayName, and highlight LDAP-Surname within (appended if not contained)
- email (CI Text) -- Case-insensitive eMail address
- ident (CI Text) -- Case-insensitive user-identifier
+ displayName UserDisplayName
+ displayEmail UserEmail
+ email UserEmail -- Case-insensitive eMail address
+ ident UserIdent -- Case-insensitive user-identifier
authentication AuthenticationMode -- 'AuthLDAP' or ('AuthPWHash'+password-hash)
lastAuthentication UTCTime Maybe -- last login date
created UTCTime default=now()
@@ -20,7 +21,8 @@ User json -- Each Uni2work user has a corresponding row in this table; create
matrikelnummer UserMatriculation Maybe -- optional immatriculation-string; usually a number, but not always (e.g. lecturers, pupils, guests,...)
firstName Text -- For export in tables, pre-split firstName from displayName
title Text Maybe -- For upcoming name customisation
- maxFavourites Int default=12 -- max number of rows with this userId in table "CourseFavourite"; for convenience links; user-defined
+ maxFavourites Int default=12 -- max number of non-manual entries in favourites bar (pruned only if below a set importance threshold)
+ maxFavouriteTerms Int default=2 -- max number of term-sections in favourites bar
theme Theme default='Default' -- Color-theme of the frontend; user-defined
dateTimeFormat DateTimeFormat "default='%a %d %b %Y %R'" -- preferred Date+Time display format for user; user-defined
dateFormat DateTimeFormat "default='%d.%m.%Y'" -- preferred Date-only display format for user; user-defined
@@ -29,6 +31,7 @@ User json -- Each Uni2work user has a corresponding row in this table; create
mailLanguages MailLanguages "default='[]'::jsonb" -- Preferred language for eMail; i18n not yet implemented; user-defined
notificationSettings NotificationSettings -- Bit-array for which events email notifications are requested by user; user-defined
warningDays NominalDiffTime default=1209600 -- timedistance to pending deadlines for homepage infos
+ csvOptions CsvOptions "default='{}'::jsonb"
UniqueAuthentication ident -- Column 'ident' can be used as a row-key in this table
UniqueEmail email -- Column 'email' can be used as a row-key in this table
deriving Show Eq Ord Generic -- Haskell-specific settings for runtime-value representing a row in memory
diff --git a/nixpkgs.nix b/nixpkgs.nix
index f21a81350..783ede000 100644
--- a/nixpkgs.nix
+++ b/nixpkgs.nix
@@ -4,6 +4,6 @@
import ((nixpkgs {}).fetchFromGitHub {
owner = "NixOS";
repo = "nixpkgs";
- rev = "19.03";
- sha256 = "0q2m2qhyga9yq29yz90ywgjbn9hdahs7i8wwlq7b55rdbyiwa5dy";
+ rev = "10e61bf5be57736035ec7a804cb0bf3d083bf2cf";
+ sha256 = "0fplfm2zx4vk7gs8bdcxnvzkdmpx2w0llqwf8475z9dz9cl132rm";
})
diff --git a/package-lock.json b/package-lock.json
index daac92fa4..6b03a7be3 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "uni2work",
- "version": "6.6.0",
+ "version": "7.10.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -7702,9 +7702,9 @@
"dev": true
},
"handlebars": {
- "version": "4.1.2",
- "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.2.tgz",
- "integrity": "sha512-nvfrjqvt9xQ8Z/w0ijewdD/vvWDTOweBUm96NTr66Wfvo1mJenBLwcYmPs3TIBP5ruzYGD7Hx/DaM9RmhroGPw==",
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.3.1.tgz",
+ "integrity": "sha512-c0HoNHzDiHpBt4Kqe99N8tdLPKAnGCQ73gYMPWtAYM4PwGnf7xl8PBUHJqh9ijlzt2uQKaSRxbXRt+rZ7M2/kA==",
"dev": true,
"requires": {
"neo-async": "^2.6.0",
@@ -15623,9 +15623,9 @@
"dev": true
},
"uglify-js": {
- "version": "3.5.15",
- "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.5.15.tgz",
- "integrity": "sha512-fe7aYFotptIddkwcm6YuA0HmknBZ52ZzOsUxZEdhhkSsz7RfjHDX2QDxwKTiv4JQ5t5NhfmpgAK+J7LiDhKSqg==",
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.0.tgz",
+ "integrity": "sha512-W+jrUHJr3DXKhrsS7NUVxn3zqMOFn0hL/Ei6v0anCIMoKC93TjcflTagwIHLW7SfMFfiQuktQyFVCFHGUE0+yg==",
"dev": true,
"optional": true,
"requires": {
diff --git a/package.json b/package.json
index 42d9345cf..dd5b75eba 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "uni2work",
- "version": "6.6.0",
+ "version": "7.10.0",
"description": "",
"keywords": [],
"author": "",
@@ -14,7 +14,9 @@
"yesod:start": "./start.sh",
"yesod:lint": "./hlint.sh",
"yesod:test": "./test.sh",
+ "yesod:test:watch": "./test.sh --file-watch",
"yesod:build": "./build.sh",
+ "yesod:build:watch": "./build.sh --file-watch",
"frontend:lint": "eslint frontend/src",
"frontend:test": "karma start --conf karma.conf.js",
"frontend:test:watch": "karma start --conf karma.conf.js --single-run false",
diff --git a/package.yaml b/package.yaml
index e8685cb40..d3d6392ae 100644
--- a/package.yaml
+++ b/package.yaml
@@ -1,41 +1,39 @@
name: uniworx
-version: 6.6.0
+version: 7.10.0
dependencies:
- # Due to a bug in GHC 8.0.1, we block its usage
- # See: https://ghc.haskell.org/trac/ghc/ticket/12130
- - base >=4.8.2.0 && <4.9 || >=4.9.1.0 && <5
- # version 1.0 had a bug in reexporting Handler, causing trouble
- - classy-prelude-yesod >=0.10.2 && <1.0 || >=1.1
- - foreign-store
- - yesod >=1.4.3 && <1.5
- - yesod-core >=1.4.30 && <1.5
- - yesod-auth >=1.4.0 && <1.5
- - yesod-static >=1.4.0.3 && <1.6
- - yesod-form >=1.4.0 && <1.5
- - classy-prelude >=0.10.2
- - classy-prelude-conduit >=0.10.2
- - bytestring >=0.9 && <0.11
+ - base >=4.9.1.0 && <5
+ - yesod >=1.6 && <1.7
+ - yesod-core >=1.6 && <1.7
+ - yesod-auth >=1.6 && <1.7
+ - yesod-static >=1.6 && <1.7
+ - yesod-form >=1.6 && <1.7
+ - classy-prelude >=1.5 && <1.6
+ - classy-prelude-conduit >=1.5 && <1.6
+ - classy-prelude-yesod >=1.5 && <1.6
+ - bytestring >=0.10 && <0.11
- text >=0.11 && <2.0
- - persistent >=2.7.2 && <2.8
- - persistent-postgresql >=2.1.1 && <2.8
- - persistent-template >=2.0 && <2.8
+ - persistent >=2.9 && <2.10
+ - persistent-postgresql >=2.9 && <2.10
+ - persistent-template >=2.5 && <2.9
+ - persistent-qq >=2.9 && <2.10
- template-haskell
- shakespeare >=2.0 && <2.1
- hjsmin >=0.1 && <0.3
- monad-control >=0.3 && <1.1
- wai-extra >=3.0 && <3.1
- - yaml >=0.8 && <0.9
- - http-conduit >=2.1 && <2.3
+ - yaml >=0.11 && <0.12
+ - http-conduit >=2.3 && <2.4
- directory >=1.1 && <1.4
- warp >=3.0 && <3.3
- data-default
- - aeson >=0.6 && <1.3
+ - aeson >=1.4 && <1.5
- conduit >=1.0 && <2.0
- conduit-combinators
- monad-logger >=0.3 && <0.4
- fast-logger >=2.2 && <2.5
- wai-logger >=2.2 && <2.4
+ - foreign-store
- file-embed
- safe
- unordered-containers
@@ -52,11 +50,12 @@ dependencies:
- http-api-data
- profunctors
- colonnade >=1.1.1
- - yesod-colonnade >=1.1.0
- blaze-markup
- zip-stream
+ - encoding
- filepath
- transformers
+ - transformers-base
- wl-pprint-text
- uuid-types
- path-pieces
@@ -100,8 +99,10 @@ dependencies:
- th-abstraction
- HaskellNet
- HaskellNet-SSL
- - network
- - resource-pool
+ - network >=3
+ - network-bsd
+ - unliftio
+ - unliftio-pool
- mime-mail
- hashable
- aeson-pretty
@@ -116,7 +117,6 @@ dependencies:
- pkcs7
- memcached-binary
- directory-tree
- - lifted-base
- lattices
- hsass
- semigroupoids
@@ -126,7 +126,6 @@ dependencies:
- mono-traversable
- lens-aeson
- systemd
- - lifted-async
- streaming-commons
- hourglass
- unix
@@ -137,6 +136,10 @@ dependencies:
- memory
- pqueue
- deepseq
+ - multiset
+ - retry
+ - generic-lens
+ - array
other-extensions:
- GeneralizedNewtypeDeriving
@@ -182,6 +185,7 @@ default-extensions:
- DeriveLift
- DeriveFunctor
- DerivingStrategies
+ - DerivingVia
- DataKinds
- BinaryLiterals
- PolyKinds
@@ -189,14 +193,18 @@ default-extensions:
- TypeApplications
- RecursiveDo
- TypeFamilyDependencies
+ - QuantifiedConstraints
ghc-options:
- -Wall
+ - -Wmissing-home-modules
+ - -Wredundant-constraints
- -fno-warn-type-defaults
- -fno-warn-unrecognised-pragmas
- -fno-warn-partial-type-signatures
- -fno-max-relevant-binds
- -j
+ - -freduction-depth=0
when:
- condition: flag(pedantic)
diff --git a/routes b/routes
index 293577bf9..944e20502 100644
--- a/routes
+++ b/routes
@@ -51,6 +51,7 @@
/users/#CryptoUUIDUser/password UserPasswordR GET POST !selfANDis-pw-hash
!/users/functionary-invite/new AdminNewFunctionaryInviteR GET POST
!/users/functionary-invite AdminFunctionaryInviteR GET POST
+!/users/add AdminUserAddR GET POST
/admin AdminR GET
/admin/features AdminFeaturesR GET POST
/admin/test AdminTestR GET POST
@@ -70,6 +71,14 @@
/user ProfileR GET POST !free
/user/profile ProfileDataR GET !free
/user/authpreds AuthPredsR GET POST !free
+/user/set-display-email SetDisplayEmailR GET POST !free
+/user/csv-options CsvOptionsR GET POST !free
+
+/exam-office ExamOfficeR !exam-office:
+ / EOExamsR GET
+ /fields EOFieldsR GET POST
+ /users EOUsersR GET POST
+ /users/invite EOUsersInviteR GET POST
/term TermShowR GET !free
/term/current TermCurrentR GET !free
@@ -95,7 +104,8 @@
!/course/new CourseNewR GET POST !lecturer
/course/#TermId/#SchoolId/#CourseShorthand CourseR !lecturer:
/ CShowR GET !free
- /register CRegisterR GET POST !timeANDcapacityANDallocation-timeAND¬exam-result !lecturerANDallocation-time
+ /favourite CFavouriteR POST
+ /register CRegisterR GET POST !timeANDcapacityANDallocation-timeAND¬course-registered !timeANDallocation-timeAND¬exam-resultANDcourse-registered !lecturerANDallocation-time
/register-template CRegisterTemplateR GET !free
/edit CEditR GET POST
/lecturer-invite CLecInviteR GET POST
@@ -107,6 +117,7 @@
/correctors CHiWisR GET
/communication CCommR GET POST
/notes CNotesR GET POST !corrector -- THIS route is used to check for overall course corrector access!
+ /exam-office CExamOfficeR GET POST !course-registered
/subs CCorrectionsR GET POST
/subs/assigned CAssignR GET POST
/sheet SheetListR GET !course-registered !materials !corrector
@@ -156,18 +167,26 @@
/exams CExamListR GET !free
/exams/new CExamNewR GET POST
/exams/#ExamName ExamR:
- /show EShowR GET !time
+ /show EShowR GET !time !exam-office
/edit EEditR GET POST
/corrector-invite ECInviteR GET POST
/users EUsersR GET POST
/users/new EAddUserR GET POST
/users/invite EInviteR GET POST
/register ERegisterR POST !timeANDcourse-registeredAND¬exam-registered !timeANDexam-registeredAND¬exam-result
+ /grades EGradesR GET POST !exam-office
/apps CApplicationsR GET POST
!/apps/files CAppsFilesR GET
/apps/#CryptoFileNameCourseApplication CourseApplicationR:
/ CAEditR GET POST !timeANDself !lecturerANDstaff-time !selfANDread
/files CAFilesR GET !self !lecturerANDstaff-time
+ !/news/add CNewsNewR GET POST
+ /news/#CryptoUUIDCourseNews CourseNewsR:
+ / CNShowR GET !timeANDparticipant
+ /edit CNEditR GET POST
+ /delete CNDeleteR GET POST
+ !/download CNArchiveR GET !timeANDparticipant
+ !/download/*FilePath CNFileR GET !timeANDparticipant
/subs CorrectionsR GET POST !corrector !lecturer
/subs/upload CorrectionsUploadR GET POST !corrector !lecturer
diff --git a/shell.nix b/shell.nix
index d65bb65a3..1a285b264 100644
--- a/shell.nix
+++ b/shell.nix
@@ -19,7 +19,7 @@ let
'';
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 = ''
export PROMPT_INFO="${oldAttrs.name}"
@@ -47,6 +47,12 @@ let
set +xe
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}
'';
};
diff --git a/src/Application.hs b/src/Application.hs
index fe1bc98ff..41ca6fed4 100644
--- a/src/Application.hs
+++ b/src/Application.hs
@@ -24,7 +24,7 @@ import Language.Haskell.TH.Syntax (qLocation)
import Network.Wai (Middleware)
import Network.Wai.Handler.Warp (Settings, defaultSettings,
defaultShouldDisplayException,
- runSettingsSocket, setHost,
+ runSettings, runSettingsSocket, setHost,
setBeforeMainLoop,
setOnException, setPort, getPort)
import Data.Streaming.Network (bindPortTCP)
@@ -44,7 +44,6 @@ import qualified Data.UUID as UUID
import qualified Data.UUID.V4 as UUID
import System.Directory
-import System.FilePath
import Jobs
@@ -55,7 +54,9 @@ import qualified Data.ByteString.Lazy as LBS
import Network.HaskellNet.SSL 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
@@ -71,17 +72,17 @@ import System.Exit
import qualified Database.Memcached.Binary.IO as Memcached
import qualified System.Systemd.Daemon as Systemd
-import Control.Concurrent.Async.Lifted.Safe
import System.Environment (lookupEnv)
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 Network (socketPort)
+import Network.Socket (socketPort, Socket, PortNumber)
import qualified Network.Socket as Socket (close)
import Control.Concurrent.STM.Delay
import Control.Monad.STM (retry)
+import Control.Monad.Trans.Cont (runContT, callCC)
import qualified Data.Set as Set
@@ -109,6 +110,7 @@ import Handler.SystemMessage
import Handler.Health
import Handler.Exam
import Handler.Allocation
+import Handler.ExamOffice
-- This line actually creates our YesodDispatch instance. It is the second half
@@ -120,7 +122,7 @@ mkYesodDispatch "UniWorX" resourcesUniWorX
-- performs initialization and returns a foundation datatype value. This is also
-- the place to put your migrate statements to have automatic database
-- 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
-- Some basic initializations: HTTP connection manager, logger, and static
-- subsite.
@@ -146,7 +148,7 @@ makeFoundation appSettings'@AppSettings{..} = do
oldLogger <- atomically . swapTVar tVar =<< mkLogger newSettings
rmLoggerSet $ loggerSet oldLogger
updateLogger newSettings
- (tVar, ) <$> fork (updateLogger initialSettings)
+ (tVar, ) <$> forkIO (updateLogger initialSettings)
appLogger <- over _2 fst <$> allocate mkLogger' (\(tVar, tId) -> killThread tId >> (readTVarIO tVar >>= rmLoggerSet . loggerSet))
let appStatic = embeddedStatic
@@ -250,7 +252,7 @@ readInstanceIDFile idFile = liftIO . handle generateInstead $ LBS.readFile idFil
instanceId <- UUID.nextRandom
LBS.writeFile idFile $ UUID.toByteString instanceId
return instanceId
- | otherwise = throw e
+ | otherwise = throwIO e
createSmtpPool :: MonadLoggerIO m => SmtpConf -> m SMTPPool
createSmtpPool SmtpConf{ smtpPool = ResourcePoolConf{..}, .. } = do
@@ -327,7 +329,7 @@ warpSettings foundation = defaultSettings
void $ liftIO Systemd.notifyReady
if
| foundation ^. _appHealthCheckDelayNotify
- -> void . fork $ do
+ -> void . forkIO $ do
let activeChecks = Set.fromList universeF
& Set.filter (is _Just . (foundation ^. _appHealthCheckInterval))
atomically $ do
@@ -365,11 +367,20 @@ develMain = runResourceT $ do
wsettings <- liftIO . getDevSettings $ warpSettings 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
- liftIO . develMainHelper $ return (wsettings, app)
+ void . liftIO $ awaitTermination `race` runSettings wsettings app
-- | The @main@ function for an executable running this site.
-appMain :: MonadResourceBase m => m ()
+appMain :: forall m. (MonadUnliftIO m, MonadMask m) => m ()
appMain = runResourceT $ do
settings <- getAppSettings
@@ -397,7 +408,7 @@ appMain = runResourceT $ do
$logInfoS "bind" [st|Listening on #{tshow host} port #{tshow port} as per configuration|]
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
liftIO . void . flip (installHandler sigTERM) Nothing . Signals.CatchInfo $ \SignalInfo{..} -> runAppLoggingT foundation $ do
@@ -445,7 +456,7 @@ appMain = runResourceT $ do
_other -> return ()
go status
- in void $ allocate (async notifyWatchdog >>= \a -> a <$ link a) cancel
+ in void $ allocateLinkedAsync notifyWatchdog
_other -> return ()
let runWarp socket = runSettingsSocket (warpSettings foundation) socket app
@@ -461,7 +472,7 @@ appMain = runResourceT $ do
foundationStoreNum :: Word32
foundationStoreNum = 2
-getApplicationRepl :: (MonadResource m, MonadBaseControl IO m) => m (Int, UniWorX, Application)
+getApplicationRepl :: (MonadResource m, MonadUnliftIO m, MonadMask m) => m (Int, UniWorX, Application)
getApplicationRepl = do
settings <- getAppDevSettings
foundation <- makeFoundation settings
@@ -475,7 +486,7 @@ getApplicationRepl = do
return (getPort wsettings, foundation, app1)
-shutdownApp :: (MonadIO m, MonadBaseControl IO m) => UniWorX -> m ()
+shutdownApp :: (MonadIO m, MonadUnliftIO m) => UniWorX -> m ()
shutdownApp app = do
stopJobCtl app
liftIO $ do
@@ -494,7 +505,7 @@ handler :: Handler a -> IO a
handler h = runResourceT $ getAppDevSettings >>= makeFoundation >>= liftIO . flip unsafeHandler h
-- | Run DB queries
-db :: ReaderT SqlBackend (HandlerT UniWorX IO) a -> IO a
+db :: DB a -> IO a
db = handler . runDB
addPWEntry :: User
diff --git a/src/Audit.hs b/src/Audit.hs
index 06d3d8767..0b7890b8c 100644
--- a/src/Audit.hs
+++ b/src/Audit.hs
@@ -22,7 +22,7 @@ import qualified Network.Socket as Wai
import qualified Net.IP as IP
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) #-}
@@ -79,7 +79,6 @@ instance Exception AuditException
audit :: ( AuthId (HandlerSite m) ~ Key User
- , AuthEntity (HandlerSite m) ~ User
, IsSqlBackend (YesodPersistBackend (HandlerSite m))
, SqlBackendCanWrite (YesodPersistBackend (HandlerSite m))
, HasInstanceID (HandlerSite m) InstanceId
@@ -99,7 +98,7 @@ audit (toJSON -> transactionLogInfo) = do
transactionLogTime <- liftIO getCurrentTime
transactionLogInstance <- getsYesod $ view instanceID
- transactionLogInitiator <- liftHandlerT maybeAuthId
+ transactionLogInitiator <- liftHandler maybeAuthId
transactionLogRemote <- handle (throwM . AuditRemoteException) $ Just <$> getRemote
insert_ TransactionLog{..}
diff --git a/src/Audit/Types.hs b/src/Audit/Types.hs
index dec21cdea..5b835a722 100644
--- a/src/Audit/Types.hs
+++ b/src/Audit/Types.hs
@@ -23,6 +23,24 @@ data Transaction
{ transactionExam :: ExamId
, transactionUser :: UserId
}
+
+ | TransactionExamPartResultEdit
+ { transactionExamPart :: ExamPartId
+ , transactionUser :: UserId
+ }
+ | TransactionExamPartResultDeleted
+ { transactionExamPart :: ExamPartId
+ , transactionUser :: UserId
+ }
+
+ | TransactionExamBonusEdit
+ { transactionExam :: ExamId
+ , transactionUser :: UserId
+ }
+ | TransactionExamBonusDeleted
+ { transactionExam :: ExamId
+ , transactionUser :: UserId
+ }
| TransactionExamResultEdit
{ transactionExam :: ExamId
@@ -98,6 +116,23 @@ data Transaction
{ transactionFile :: FileId
}
+ | TransactionExamOfficeUserAdd
+ { transactionOffice :: UserId
+ , transactionUser :: UserId
+ }
+ | TransactionExamOfficeUserDelete
+ { transactionOffice :: UserId
+ , transactionUser :: UserId
+ }
+ | TransactionExamOfficeFieldEdit
+ { transactionOffice :: UserId
+ , transactionField :: StudyTermsId
+ }
+ | TransactionExamOfficeFieldDelete
+ { transactionOffice :: UserId
+ , transactionField :: StudyTermsId
+ }
+
deriving (Eq, Ord, Read, Show, Generic, Typeable)
deriveJSON defaultOptions
diff --git a/src/Auth/Dummy.hs b/src/Auth/Dummy.hs
index 53a10acde..4bfc09d01 100644
--- a/src/Auth/Dummy.hs
+++ b/src/Auth/Dummy.hs
@@ -17,41 +17,47 @@ data DummyMessage = MsgDummyIdent
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic, Typeable)
-dummyForm :: ( RenderMessage site FormMessage
- , RenderMessage site DummyMessage
- , YesodPersist site
- , SqlBackendCanRead (YesodPersistBackend site)
- , Button site ButtonSubmit
- ) => AForm (HandlerT site IO) (CI Text)
+dummyForm :: ( RenderMessage (HandlerSite m) FormMessage
+ , RenderMessage (HandlerSite m) DummyMessage
+ , YesodPersist (HandlerSite m)
+ , SqlBackendCanRead (YesodPersistBackend (HandlerSite m))
+ , Button (HandlerSite m) ButtonSubmit
+ , MonadHandler m
+ ) => AForm m (CI Text)
dummyForm = areq (ciField & addDatalist userList) (fslI MsgDummyIdent & noAutocomplete) Nothing
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)
-dummyLogin :: ( YesodAuth site
+dummyLogin :: forall site.
+ ( YesodAuth site
, YesodPersist site
, SqlBackendCanRead (YesodPersistBackend site)
- , RenderMessage site FormMessage
, RenderMessage site AFormMessage
, RenderMessage site DummyMessage
, Button site ButtonSubmit
) => AuthPlugin site
dummyLogin = AuthPlugin{..}
where
+ apName :: Text
apName = "dummy"
- -- apDispatch :: Text -> [Text] -> HandlerT Auth (HandlerT site IO) TypedContent
- apDispatch "POST" [] = do
- ((loginRes, _), _) <- lift . runFormPost $ renderAForm FormStandard dummyForm
+
+ apDispatch :: forall m. MonadAuthHandler site m => Text -> [Text] -> m TypedContent
+ apDispatch "POST" [] = liftSubHandler $ do
+ ((loginRes, _), _) <- runFormPost $ renderAForm FormStandard dummyForm
+ tp <- getRouteToParent
case loginRes of
FormFailure errs -> do
- lift . forM_ errs $ addMessage Error . toHtml
- redirect LoginR
+ forM_ errs $ addMessage Error . toHtml
+ redirect $ tp LoginR
FormMissing -> do
- lift $ addMessageI Warning MsgDummyNoFormData
- redirect LoginR
+ addMessageI Warning MsgDummyNoFormData
+ redirect $ tp LoginR
FormSuccess ident ->
- lift . setCredsRedirect $ Creds "dummy" (CI.original ident) []
+ setCredsRedirect $ Creds "dummy" (CI.original ident) []
apDispatch _ _ = notFound
+
+ apLogin :: (Route Auth -> Route site) -> WidgetFor site ()
apLogin toMaster = do
(login, loginEnctype) <- handlerToWidget . generateFormPost $ renderAForm FormStandard dummyForm
let loginForm = wrapForm login FormSettings
diff --git a/src/Auth/LDAP.hs b/src/Auth/LDAP.hs
index 320ab6e27..1ba6af9e7 100644
--- a/src/Auth/LDAP.hs
+++ b/src/Auth/LDAP.hs
@@ -84,7 +84,7 @@ instance Exception 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
Ldap.bind ldap ldapDn ldapPassword
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
]
-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}
= runMaybeT . catchIfMaybeT (is _CampusUserNoResult) $ campusUser conf pool (Creds apLdap (CI.original userIdent) [])
-campusForm :: ( RenderMessage site FormMessage
- , RenderMessage site CampusMessage
- , Button site ButtonSubmit
- ) => WForm (HandlerT site IO) (FormResult CampusLogin)
+campusForm :: ( RenderMessage (HandlerSite m) FormMessage
+ , RenderMessage (HandlerSite m) CampusMessage
+ , MonadHandler m
+ ) => WForm m (FormResult CampusLogin)
campusForm = do
MsgRenderer mr <- getMsgRenderer
@@ -133,24 +133,26 @@ apLdap = "LDAP"
campusLogin :: forall site.
( YesodAuth site
- , RenderMessage site FormMessage
, RenderMessage site CampusMessage
, RenderMessage site AFormMessage
, Button site ButtonSubmit
) => LdapConf -> LdapPool -> AuthPlugin site
campusLogin conf@LdapConf{..} pool = AuthPlugin{..}
where
+ apName :: Text
apName = apLdap
- apDispatch :: Text -> [Text] -> HandlerT Auth (HandlerT site IO) TypedContent
- apDispatch "POST" [] = do
- ((loginRes, _), _) <- lift . runFormPost $ renderWForm FormStandard campusForm
+
+ apDispatch :: forall m. MonadAuthHandler site m => Text -> [Text] -> m TypedContent
+ apDispatch "POST" [] = liftSubHandler $ do
+ ((loginRes, _), _) <- runFormPost $ renderWForm FormStandard campusForm
+ tp <- getRouteToParent
case loginRes of
FormFailure errs -> do
forM_ errs $ addMessage Error . toHtml
- redirect LoginR
- FormMissing -> redirect LoginR
+ redirect $ tp LoginR
+ FormMissing -> redirect $ tp LoginR
FormSuccess CampusLogin{ campusIdent = CI.original -> campusIdent, ..} -> do
- ldapResult <- withLdap pool $ \ldap -> do
+ ldapResult <- withLdap pool $ \ldap -> liftIO $ do
Ldap.bind ldap ldapDn ldapPassword
searchResults <- findUser conf ldap campusIdent [ldapUserPrincipalName]
case searchResults of
@@ -169,11 +171,13 @@ campusLogin conf@LdapConf{..} pool = AuthPlugin{..}
$logErrorS "LDAP" $ "Error during login: " <> tshow err
loginErrorMessageI LoginR Msg.AuthError
Right (Right (userDN, credsIdent)) ->
- lift . setCredsRedirect $ Creds apName credsIdent [("DN", userDN)]
+ setCredsRedirect $ Creds apName credsIdent [("DN", userDN)]
Right (Left searchResults) -> do
$logWarnS "LDAP" $ "Could not extract principal name: " <> tshow searchResults
loginErrorMessageI LoginR Msg.AuthError
apDispatch _ _ = notFound
+
+ apLogin :: (Route Auth -> Route site) -> WidgetFor site ()
apLogin toMaster = do
(login, loginEnctype) <- handlerToWidget . generateFormPost $ renderWForm FormStandard campusForm
let loginForm = wrapForm login FormSettings
diff --git a/src/Auth/PWHash.hs b/src/Auth/PWHash.hs
index d6f5bf4e8..b2194bf90 100644
--- a/src/Auth/PWHash.hs
+++ b/src/Auth/PWHash.hs
@@ -26,68 +26,50 @@ data PWHashMessage = MsgPWHashIdent
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic, Typeable)
-hashForm :: ( RenderMessage site FormMessage
- , RenderMessage site PWHashMessage
- , Button site ButtonSubmit
- ) => AForm (HandlerT site IO) HashLogin
+hashForm :: ( RenderMessage (HandlerSite m) FormMessage
+ , RenderMessage (HandlerSite m) PWHashMessage
+ , MonadHandler m
+ ) => AForm m HashLogin
hashForm = HashLogin
<$> areq ciField (fslpI MsgPWHashIdent "Identifikation") Nothing
<*> areq passwordField (fslpI MsgPWHashPassword "Passwort") Nothing
-hashLogin :: ( YesodAuth site
+hashLogin :: forall site.
+ ( YesodAuth site
, YesodPersist site
, SqlBackendCanRead (YesodPersistBackend site)
- , RenderMessage site FormMessage
+ , PersistRecordBackend User (YesodPersistBackend site)
, RenderMessage site PWHashMessage
, RenderMessage site AFormMessage
, Button site ButtonSubmit
) => PWHashAlgorithm -> AuthPlugin site
hashLogin pwHashAlgo = AuthPlugin{..}
where
+ apName :: Text
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
FormFailure errs -> do
forM_ errs $ addMessage Error . toHtml
- redirect LoginR
- FormMissing -> redirect LoginR
+ redirect $ tp LoginR
+ FormMissing -> redirect $ tp LoginR
FormSuccess HashLogin{..} -> do
- user <- lift . runDB . getBy $ UniqueAuthentication hashIdent
+ user <- liftHandler . runDB . getBy $ UniqueAuthentication hashIdent
case user of
Just (Entity _ User{ userAuthentication = AuthPWHash{..}, userIdent = CI.original -> userIdent })
| verifyPasswordWith pwHashAlgo (2^) (encodeUtf8 hashPassword) (encodeUtf8 authPWHash) -> -- (2^) is magic.
- lift . setCredsRedirect $ Creds apName userIdent []
+ setCredsRedirect $ Creds apName userIdent []
other -> do
$logDebugS "PWHash" $ tshow other
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
+
+ apLogin :: (Route Auth -> Route site) -> WidgetFor site ()
apLogin toMaster = do
(login, loginEnctype) <- handlerToWidget . generateFormPost $ renderAForm FormStandard hashForm
let loginForm = wrapForm login FormSettings
diff --git a/src/Control/Concurrent/Async/Lifted/Safe/Utils.hs b/src/Control/Concurrent/Async/Lifted/Safe/Utils.hs
deleted file mode 100644
index 27dc86127..000000000
--- a/src/Control/Concurrent/Async/Lifted/Safe/Utils.hs
+++ /dev/null
@@ -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
diff --git a/src/CryptoID.hs b/src/CryptoID.hs
index 9263ca308..35ec7af50 100644
--- a/src/CryptoID.hs
+++ b/src/CryptoID.hs
@@ -8,11 +8,12 @@ module CryptoID
, module System.FilePath.Cryptographic.ImplicitNamespace
) where
-import CryptoID.TH
-import ClassyPrelude
+import Import.NoModel
import Model
+import CryptoID.TH
+
import qualified Data.CryptoID as E
import Data.CryptoID.Poly.ImplicitNamespace
import Data.UUID.Cryptographic.ImplicitNamespace
@@ -20,9 +21,6 @@ import System.FilePath.Cryptographic.ImplicitNamespace
import qualified Data.Text as Text
--- import Data.UUID.Types
-import Web.PathPieces
-
import Data.CaseInsensitive (CI)
import qualified Data.CaseInsensitive as CI
@@ -50,6 +48,7 @@ decCryptoIDs [ ''SubmissionId
, ''AllocationId
, ''CourseApplicationId
, ''CourseId
+ , ''CourseNewsId
]
-- CryptoIDNamespace (CI FilePath) SubmissionId ~ "Submission"
diff --git a/src/Data/CaseInsensitive/Instances.hs b/src/Data/CaseInsensitive/Instances.hs
index 937fb2c46..f218308f5 100644
--- a/src/Data/CaseInsensitive/Instances.hs
+++ b/src/Data/CaseInsensitive/Instances.hs
@@ -1,5 +1,6 @@
{-# LANGUAGE UndecidableInstances #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
+{-# OPTIONS_GHC -fno-warn-redundant-constraints #-}
module Data.CaseInsensitive.Instances
(
diff --git a/src/Data/CryptoID/Instances.hs b/src/Data/CryptoID/Instances.hs
index 0867f60b5..b48c0df70 100644
--- a/src/Data/CryptoID/Instances.hs
+++ b/src/Data/CryptoID/Instances.hs
@@ -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
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
diff --git a/src/Data/List/NonEmpty/Instances.hs b/src/Data/List/NonEmpty/Instances.hs
deleted file mode 100644
index f151b6c18..000000000
--- a/src/Data/List/NonEmpty/Instances.hs
+++ /dev/null
@@ -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|]
diff --git a/src/Data/Time/Clock/Instances.hs b/src/Data/Time/Clock/Instances.hs
index 88ad3c047..d1b0af22e 100644
--- a/src/Data/Time/Clock/Instances.hs
+++ b/src/Data/Time/Clock/Instances.hs
@@ -1,7 +1,7 @@
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Data.Time.Clock.Instances
- (
+ ( iso8601OutputFormat, iso8601ParseFormat
) where
import ClassyPrelude
@@ -17,6 +17,8 @@ import Data.Time.Clock
import Data.Time.Calendar.Instances ()
import Web.PathPieces
+import qualified Data.Csv as Csv
+
instance Hashable DiffTime where
hashWithSalt s = hashWithSalt s . toRational
@@ -29,12 +31,23 @@ instance PersistFieldSql NominalDiffTime where
sqlType _ = sqlType (Proxy @Rational)
+iso8601OutputFormat, iso8601ParseFormat :: String
+iso8601OutputFormat = "%0Y-%m-%dT%H:%M:%S%Q%z"
+iso8601ParseFormat = "%Y-%m-%dT%H:%M:%S%Q%z"
+
+
deriving instance Generic UTCTime
instance Hashable UTCTime
instance PathPiece UTCTime where
- toPathPiece = pack . formatTime defaultTimeLocale "%0Y-%m-%dT%H:%M:%S%Q%z"
- fromPathPiece = parseTimeM False defaultTimeLocale "%Y-%m-%dT%H:%M:%S%Q%z" . unpack
+ toPathPiece = pack . formatTime defaultTimeLocale iso8601OutputFormat
+ fromPathPiece = parseTimeM False defaultTimeLocale iso8601ParseFormat . unpack
+
+instance Csv.ToField UTCTime where
+ toField = Csv.toField . formatTime defaultTimeLocale iso8601OutputFormat
+
+instance Csv.FromField UTCTime where
+ parseField = parseTimeM False defaultTimeLocale iso8601ParseFormat <=< Csv.parseField
instance Binary DiffTime where
diff --git a/src/Data/Time/LocalTime/Instances.hs b/src/Data/Time/LocalTime/Instances.hs
index 39c0d70f0..6bdf4610d 100644
--- a/src/Data/Time/LocalTime/Instances.hs
+++ b/src/Data/Time/LocalTime/Instances.hs
@@ -12,6 +12,12 @@ import Data.Binary (Binary)
import qualified Language.Haskell.TH.Syntax as TH
+import qualified Data.Csv as Csv
+
+import Data.Time.Clock.Instances
+ ( iso8601OutputFormat, iso8601ParseFormat
+ )
+
deriving instance Generic TimeOfDay
deriving instance Typeable TimeOfDay
@@ -21,3 +27,9 @@ instance Binary TimeOfDay
deriving instance TH.Lift TimeZone
+
+instance Csv.ToField ZonedTime where
+ toField = Csv.toField . formatTime defaultTimeLocale iso8601OutputFormat
+
+instance Csv.FromField ZonedTime where
+ parseField = parseTimeM False defaultTimeLocale iso8601ParseFormat <=< Csv.parseField
diff --git a/src/Database/Esqueleto/Utils.hs b/src/Database/Esqueleto/Utils.hs
index c038f2152..2cdfc69d2 100644
--- a/src/Database/Esqueleto/Utils.hs
+++ b/src/Database/Esqueleto/Utils.hs
@@ -1,4 +1,5 @@
{-# OPTIONS_GHC -fno-warn-orphans #-}
+{-# OPTIONS_GHC -fno-warn-redundant-constraints #-}
module Database.Esqueleto.Utils
( true, false
@@ -6,19 +7,19 @@ module Database.Esqueleto.Utils
, isInfixOf, hasInfix
, or, and
, any, all
- , SqlIn(..)
, mkExactFilter, mkExactFilterWith
, mkContainsFilter, mkContainsFilterWith
, mkExistsFilter
, anyFilter, allFilter
, orderByList
, orderByOrd, orderByEnum
- , lower, ciEq
+ , strip, lower, ciEq
, selectExists
, SqlHashable
, sha256
, maybe
, SqlProject(..)
+ , module Database.Esqueleto.Utils.TH
) where
@@ -61,24 +62,22 @@ false :: E.SqlExpr (E.Value Bool)
false = E.val False
-- | Negation of `isNothing` which is missing
-isJust :: (E.Esqueleto query expr backend, PersistField typ) => expr (E.Value (Maybe typ)) -> expr (E.Value Bool)
+isJust :: PersistField typ => E.SqlExpr (E.Value (Maybe typ)) -> E.SqlExpr (E.Value Bool)
isJust = E.not_ . E.isNothing
infix 4 `isInfixOf`, `hasInfix`
-- | Check if the first string is contained in the text derived from the second argument
-isInfixOf :: ( E.Esqueleto query expr backend
- , E.SqlString s1
+isInfixOf :: ( E.SqlString s1
, 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.%)
-hasInfix :: ( E.Esqueleto query expr backend
- , E.SqlString s1
+hasInfix :: ( E.SqlString s1
, 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
and, or :: Foldable f => f (E.SqlExpr (E.Value Bool)) -> E.SqlExpr (E.Value Bool)
@@ -194,6 +193,9 @@ orderByEnum = orderByList $ List.sortOn fromEnum universeF
lower :: E.SqlString s => E.SqlExpr (E.Value s) -> E.SqlExpr (E.Value s)
lower = E.unsafeSqlFunction "LOWER"
+
+strip :: E.SqlString s => E.SqlExpr (E.Value s) -> E.SqlExpr (E.Value s)
+strip = E.unsafeSqlFunction "TRIM"
ciEq :: E.SqlString s => E.SqlExpr (E.Value s) -> E.SqlExpr (E.Value s) -> E.SqlExpr (E.Value Bool)
ciEq a b = lower a E.==. lower b
diff --git a/src/Database/Persist/Class/Instances.hs b/src/Database/Persist/Class/Instances.hs
index 2dbb2bfb0..8666a2c87 100644
--- a/src/Database/Persist/Class/Instances.hs
+++ b/src/Database/Persist/Class/Instances.hs
@@ -8,11 +8,14 @@ module Database.Persist.Class.Instances
import ClassyPrelude
import Database.Persist.Class
+import Database.Persist.Types (HaskellName, DBName, PersistValue)
import Database.Persist.Types.Instances ()
import Data.Binary (Binary)
import qualified Data.Binary as Binary
+import qualified Data.Map as Map
+
instance PersistEntity record => Hashable (Key record) where
hashWithSalt s = hashWithSalt s . toPersistValue
@@ -24,3 +27,13 @@ instance PersistEntity record => Binary (Key record) where
instance PersistEntity record => NFData (Key record) where
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
diff --git a/src/Foundation.hs b/src/Foundation.hs
index 740fc2f45..a878daacb 100644
--- a/src/Foundation.hs
+++ b/src/Foundation.hs
@@ -5,7 +5,6 @@
module Foundation where
import Import.NoFoundation hiding (embedFile)
-import qualified ClassyPrelude.Yesod as Yesod (getHttpManager)
import Database.Persist.Sql (ConnectionPool, runSqlPool)
import Text.Hamlet (hamletFile)
@@ -48,9 +47,6 @@ import Data.List (nubBy, (!!), findIndex)
import Data.Monoid (Any(..))
-import Data.Pool
-
-import Data.Conduit (($$))
import Data.Conduit.List (sourceList)
import qualified Database.Esqueleto as E
@@ -66,6 +62,9 @@ import qualified Control.Monad.Catch as C
import Handler.Utils.StudyFeatures
import Handler.Utils.SchoolLdap
+import Handler.Utils.ExamOffice.Exam
+import Handler.Utils.ExamOffice.Course
+import Handler.Utils.Profile
import Utils.Form
import Utils.Sheet
import Utils.SystemMessage
@@ -91,6 +90,8 @@ import Data.FileEmbed (embedFile)
import qualified Ldap.Client as Ldap
+import UnliftIO.Pool
+
type SMTPPool = Pool SMTPConnection
@@ -154,13 +155,26 @@ deriving instance Generic ExamR
deriving instance Generic CourseApplicationR
deriving instance Generic AllocationR
deriving instance Generic SchoolR
+deriving instance Generic ExamOfficeR
+deriving instance Generic CourseNewsR
deriving instance Generic (Route UniWorX)
+data RouteChildren
+type instance Children RouteChildren a = ChildrenRouteChildren a
+type family ChildrenRouteChildren a where
+ ChildrenRouteChildren (Route EmbeddedStatic) = '[]
+ ChildrenRouteChildren (Route Auth) = '[]
+ ChildrenRouteChildren UUID = '[]
+ ChildrenRouteChildren (Key a) = '[]
+ ChildrenRouteChildren (CI a) = '[]
+
+ ChildrenRouteChildren a = Children ChGeneric a
+
-- | Convenient Type Synonyms:
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 MailM a = MailT (HandlerT UniWorX IO) a
+type MailM a = MailT (HandlerFor UniWorX) a
-- Pattern Synonyms for convenience
pattern CSheetR :: TermId -> SchoolId -> CourseShorthand -> SheetName -> SheetR -> Route UniWorX
@@ -186,6 +200,10 @@ pattern CSubmissionR tid ssh csh shn cid ptn
pattern CApplicationR :: TermId -> SchoolId -> CourseShorthand -> CryptoFileNameCourseApplication -> CourseApplicationR -> Route UniWorX
pattern CApplicationR tid ssh csh appId ptn
= CourseR tid ssh csh (CourseApplicationR appId ptn)
+
+pattern CNewsR :: TermId -> SchoolId -> CourseShorthand -> CryptoUUIDCourseNews -> CourseNewsR -> Route UniWorX
+pattern CNewsR tid ssh csh nId ptn
+ = CourseR tid ssh csh (CourseNewsR nId ptn)
pluralDE :: (Eq a, Num a)
@@ -313,6 +331,9 @@ embedRenderMessage ''UniWorX ''UploadModeDescr id
embedRenderMessage ''UniWorX ''SecretJSONFieldException id
embedRenderMessage ''UniWorX ''AFormMessage $ concat . drop 2 . splitCamel
embedRenderMessage ''UniWorX ''SchoolFunction id
+embedRenderMessage ''UniWorX ''CsvPreset id
+embedRenderMessage ''UniWorX ''Quoting ("Csv" <>)
+embedRenderMessage ''UniWorX ''FavouriteReason id
embedRenderMessage ''UniWorX ''AuthenticationMode id
@@ -527,13 +548,13 @@ class (MonadHandler m, HandlerSite m ~ UniWorX) => MonadAP m where
evalAccessPred :: AccessPredicate -> Maybe (AuthId UniWorX) -> Route UniWorX -> Bool -> m AuthResult
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
(APHandler p) -> 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
- 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
(APHandler p) -> lift $ p aid r w
(APDB p) -> p aid r w
@@ -569,7 +590,6 @@ falseAP = APPure . const . const . const $ falseAR <$> ask -- included for compl
askTokenUnsafe :: forall m.
( MonadHandler m
, HandlerSite m ~ UniWorX
- , MonadLogger m
, MonadCatch m
)
=> ExceptT AuthResult m (BearerToken (UniWorX))
@@ -652,6 +672,26 @@ tagAccessPredicate AuthAdmin = APDB $ \mAuthId route _ -> case route of
adrights <- lift $ selectFirst [UserFunctionUser ==. authId, UserFunctionFunction ==. SchoolAdmin] []
guardMExceptT (isJust adrights) (unauthorizedI MsgUnauthorizedSiteAdmin)
return Authorized
+tagAccessPredicate AuthExamOffice = APDB $ \mAuthId route _ -> case route of
+ CExamR tid ssh csh examn _ -> $cachedHereBinary (mAuthId, tid, ssh, csh, examn) . exceptT return return $ do
+ authId <- maybeExceptT AuthenticationRequired $ return mAuthId
+ hasUsers <- lift . E.selectExists . E.from $ \(course `E.InnerJoin` exam `E.InnerJoin` examResult) -> do
+ E.on $ examResult E.^. ExamResultExam E.==. exam E.^. ExamId
+ E.on $ exam E.^. ExamCourse E.==. course E.^. CourseId
+
+ E.where_ $ course E.^. CourseTerm E.==. E.val tid
+ E.&&. course E.^. CourseSchool E.==. E.val ssh
+ E.&&. course E.^. CourseShorthand E.==. E.val csh
+ E.&&. exam E.^. ExamName E.==. E.val examn
+
+ E.where_ $ examOfficeExamResultAuth (E.val authId) examResult
+ guardMExceptT hasUsers (unauthorizedI MsgUnauthorizedExamExamOffice)
+ return Authorized
+ _other -> $cachedHereBinary mAuthId . exceptT return return $ do
+ authId <- maybeExceptT AuthenticationRequired $ return mAuthId
+ adrights <- lift $ selectFirst [UserFunctionUser ==. authId, UserFunctionFunction ==. SchoolExamOffice] []
+ guardMExceptT (isJust adrights) (unauthorizedI MsgUnauthorizedExamOffice)
+ return Authorized
tagAccessPredicate AuthToken = APDB $ \mAuthId route isWrite -> exceptT return return $
lift . validateToken mAuthId route isWrite =<< askTokenUnsafe
tagAccessPredicate AuthNoEscalation = APDB $ \mAuthId route _ -> case route of
@@ -666,7 +706,7 @@ tagAccessPredicate AuthNoEscalation = APDB $ \mAuthId route _ -> case route of
tagAccessPredicate AuthDeprecated = APHandler $ \_ r _ -> do
$logWarnS "AccessControl" ("deprecated route: " <> tshow r)
addMessageI Error MsgDeprecatedRoute
- allow <- view _appAllowDeprecated
+ allow <- getsYesod $ view _appAllowDeprecated
return $ bool (Unauthorized "Deprecated Route") Authorized allow
tagAccessPredicate AuthDevelopment = APHandler $ \_ r _ -> do
$logWarnS "AccessControl" ("route in development: " <> tshow r)
@@ -911,8 +951,15 @@ tagAccessPredicate AuthTime = APDB $ \mAuthId route _ -> case route of
&& NTop systemMessageTo >= cTime
return Authorized
+ CNewsR _ _ _ cID _ -> maybeT (unauthorizedI MsgUnauthorizedCourseNewsTime) $ do
+ nId <- catchIfMaybeT (const True :: CryptoIDError -> Bool) $ decrypt cID
+ CourseNews{courseNewsVisibleFrom} <- $cachedHereBinary nId . MaybeT $ get nId
+ cTime <- (NTop . Just) <$> liftIO getCurrentTime
+ guard $ NTop courseNewsVisibleFrom <= cTime
+ return Authorized
+
r -> $unsupportedAuthPredicate AuthTime r
-tagAccessPredicate AuthStaffTime = APDB $ \_ route _ -> case route of
+tagAccessPredicate AuthStaffTime = APDB $ \_ route isWrite -> case route of
CApplicationR tid ssh csh _ _ -> maybeT (unauthorizedI MsgUnauthorizedApplicationTime) $ do
course <- $cachedHereBinary (tid, ssh, csh) . MaybeT . getKeyBy $ TermSchoolCourseShort tid ssh csh
allocationCourse <- $cachedHereBinary course . lift . getBy $ UniqueAllocationCourse course
@@ -923,7 +970,8 @@ tagAccessPredicate AuthStaffTime = APDB $ \_ route _ -> case route of
Just Allocation{..} -> do
cTime <- liftIO getCurrentTime
guard $ NTop allocationStaffAllocationFrom <= NTop (Just cTime)
- guard $ NTop (Just cTime) <= NTop allocationStaffAllocationTo
+ when isWrite $
+ guard $ NTop (Just cTime) <= NTop allocationStaffAllocationTo
return Authorized
@@ -961,8 +1009,8 @@ tagAccessPredicate AuthAllocationTime = APDB $ \mAuthId route _ -> case route of
mba <- mbAllocation tid ssh csh
case mba of
Just (_, Allocation{..})
- | NTop allocationStaffRegisterTo <= NTop (Just now)
- || NTop allocationStaffRegisterFrom >= NTop (Just now)
+ | NTop allocationRegisterByStaffTo <= NTop (Just now)
+ || NTop allocationRegisterByStaffFrom >= NTop (Just now)
-> unauthorizedI MsgUnauthorizedAllocatedCourseRegister
_other -> return Authorized
@@ -971,7 +1019,7 @@ tagAccessPredicate AuthAllocationTime = APDB $ \mAuthId route _ -> case route of
mba <- mbAllocation tid ssh csh
case mba of
Just (_, Allocation{..})
- | NTop allocationRegisterByStaffTo <= NTop (Just now)
+ | NTop allocationRegisterByStaffTo <= NTop (Just now)
|| NTop allocationRegisterByStaffFrom >= NTop (Just now)
-> unauthorizedI MsgUnauthorizedAllocatedCourseDelete
_other -> return Authorized
@@ -1080,81 +1128,96 @@ tagAccessPredicate AuthAllocationRegistered = APDB $ \mAuthId route _ -> case ro
void . MaybeT . $cachedHereBinary (uid, aId) . getKeyBy $ UniqueAllocationUser aId uid
return Authorized
r -> $unsupportedAuthPredicate AuthAllocationRegistered r
-tagAccessPredicate AuthParticipant = APDB $ \_ route _ -> case route of
- CourseR tid ssh csh (CUserR cID) -> exceptT return return $ do
- cTime <- liftIO getCurrentTime
- let authorizedIfExists f = do
- [E.Value ok] <- lift . E.select . return . E.exists $ E.from f
- whenExceptT ok Authorized
- participant <- catchIfMExceptT (const $ unauthorizedI MsgUnauthorizedParticipant) (const True :: CryptoIDError -> Bool) $ decrypt cID
- -- participant is currently registered
- $cachedHereBinary (participant, tid, ssh, csh) . authorizedIfExists $ \(course `E.InnerJoin` courseParticipant) -> do
- E.on $ course E.^. CourseId E.==. courseParticipant E.^. CourseParticipantCourse
- E.where_ $ courseParticipant E.^. CourseParticipantUser E.==. E.val participant
- E.&&. course E.^. CourseTerm E.==. E.val tid
- E.&&. course E.^. CourseSchool E.==. E.val ssh
- E.&&. course E.^. CourseShorthand E.==. E.val csh
- -- participant has at least one submission
- $cachedHereBinary (participant, tid, ssh, csh) . authorizedIfExists $ \(course `E.InnerJoin` sheet `E.InnerJoin` submission `E.InnerJoin` submissionUser) -> do
- E.on $ submission E.^. SubmissionId E.==. submissionUser E.^. SubmissionUserSubmission
- E.on $ sheet E.^. SheetId E.==. submission E.^. SubmissionSheet
- E.on $ course E.^. CourseId E.==. sheet E.^. SheetCourse
- E.where_ $ submissionUser E.^. SubmissionUserUser E.==. E.val participant
- E.&&. course E.^. CourseTerm E.==. E.val tid
- E.&&. course E.^. CourseSchool E.==. E.val ssh
- E.&&. course E.^. CourseShorthand E.==. E.val csh
- -- participant is member of a submissionGroup
- $cachedHereBinary (participant, tid, ssh, csh) . authorizedIfExists $ \(course `E.InnerJoin` submissionGroup `E.InnerJoin` submissionGroupUser) -> do
- E.on $ submissionGroup E.^. SubmissionGroupId E.==. submissionGroupUser E.^. SubmissionGroupUserSubmissionGroup
- E.on $ course E.^. CourseId E.==. submissionGroup E.^. SubmissionGroupCourse
- E.where_ $ submissionGroupUser E.^. SubmissionGroupUserUser E.==. E.val participant
- E.&&. course E.^. CourseTerm E.==. E.val tid
- E.&&. course E.^. CourseSchool E.==. E.val ssh
- E.&&. course E.^. CourseShorthand E.==. E.val csh
- -- participant is a sheet corrector
- $cachedHereBinary (participant, tid, ssh, csh) . authorizedIfExists $ \(course `E.InnerJoin` sheet `E.InnerJoin` sheetCorrector) -> do
- E.on $ sheet E.^. SheetId E.==. sheetCorrector E.^. SheetCorrectorSheet
- E.on $ course E.^. CourseId E.==. sheet E.^. SheetCourse
- E.where_ $ sheetCorrector E.^. SheetCorrectorUser E.==. E.val participant
- E.&&. course E.^. CourseTerm E.==. E.val tid
- E.&&. course E.^. CourseSchool E.==. E.val ssh
- E.&&. course E.^. CourseShorthand E.==. E.val csh
- -- participant is a tutorial user
- $cachedHereBinary (participant, tid, ssh, csh) . authorizedIfExists $ \(course `E.InnerJoin` tutorial `E.InnerJoin` tutorialUser) -> do
- E.on $ tutorial E.^. TutorialId E.==. tutorialUser E.^. TutorialParticipantTutorial
- E.on $ course E.^. CourseId E.==. tutorial E.^. TutorialCourse
- E.where_ $ tutorialUser E.^. TutorialParticipantUser E.==. E.val participant
- E.&&. course E.^. CourseTerm E.==. E.val tid
- E.&&. course E.^. CourseSchool E.==. E.val ssh
- E.&&. course E.^. CourseShorthand E.==. E.val csh
- -- participant is tutor for this course
- $cachedHereBinary (participant, tid, ssh, csh) . authorizedIfExists $ \(course `E.InnerJoin` tutorial `E.InnerJoin` tutor) -> do
- E.on $ tutorial E.^. TutorialId E.==. tutor E.^. TutorTutorial
- E.on $ course E.^. CourseId E.==. tutorial E.^. TutorialCourse
- E.where_ $ tutor E.^. TutorUser E.==. E.val participant
- E.&&. course E.^. CourseTerm E.==. E.val tid
- E.&&. course E.^. CourseSchool E.==. E.val ssh
- E.&&. course E.^. CourseShorthand E.==. E.val csh
- -- participant is lecturer for this course
- $cachedHereBinary (participant, tid, ssh, csh) . authorizedIfExists $ \(course `E.InnerJoin` lecturer) -> do
- E.on $ course E.^. CourseId E.==. lecturer E.^. LecturerCourse
- E.where_ $ lecturer E.^. LecturerUser E.==. E.val participant
- E.&&. course E.^. CourseTerm E.==. E.val tid
- E.&&. course E.^. CourseSchool E.==. E.val ssh
- E.&&. course E.^. CourseShorthand E.==. E.val csh
- -- participant is applicant for this course
- $cachedHereBinary (participant, tid, ssh, csh) . authorizedIfExists $ \((course `E.InnerJoin` courseApplication) `E.LeftOuterJoin` allocation) -> do
- E.on $ allocation E.?. AllocationId E.==. courseApplication E.^. CourseApplicationAllocation
- E.on $ course E.^. CourseId E.==. courseApplication E.^. CourseApplicationCourse
- E.where_ $ courseApplication E.^. CourseApplicationUser E.==. E.val participant
- E.&&. course E.^. CourseTerm E.==. E.val tid
- E.&&. course E.^. CourseSchool E.==. E.val ssh
- E.&&. course E.^. CourseShorthand E.==. E.val csh
- E.where_ $ E.maybe E.true (E.maybe E.false $ \f -> f E.<=. E.val cTime) (allocation E.?. AllocationStaffAllocationFrom)
- E.&&. E.maybe E.true (E.maybe E.true $ \t -> t E.>=. E.val cTime) (allocation E.?. AllocationStaffAllocationTo)
+tagAccessPredicate AuthParticipant = APDB $ \mAuthId route _ -> case route of
+ CNewsR tid ssh csh cID _ -> maybeT (unauthorizedI MsgUnauthorizedCourseNewsParticipant) $ do
+ nId <- catchIfMaybeT (const True :: CryptoIDError -> Bool) $ decrypt cID
+ CourseNews{courseNewsParticipantsOnly} <- $cachedHereBinary nId . MaybeT $ get nId
+ if | courseNewsParticipantsOnly -> do
+ uid <- hoistMaybe mAuthId
+ exceptT return (const mzero) . hoist lift $ isCourseParticipant tid ssh csh uid
+ | otherwise
+ -> return Authorized
+ CourseR tid ssh csh (CUserR cID) -> exceptT return return $ do
+ participant <- catchIfMExceptT (const $ unauthorizedI MsgUnauthorizedParticipant) (const True :: CryptoIDError -> Bool) $ decrypt cID
+ isCourseParticipant tid ssh csh participant
unauthorizedI MsgUnauthorizedParticipant
+
r -> $unsupportedAuthPredicate AuthParticipant r
+
+ where
+ isCourseParticipant tid ssh csh participant = do
+ cTime <- liftIO getCurrentTime
+ let
+ authorizedIfExists :: E.From a => (a -> E.SqlQuery b) -> ExceptT AuthResult DB ()
+ authorizedIfExists = flip whenExceptT Authorized <=< lift . E.selectExists . E.from
+ -- participant is currently registered
+ $cachedHereBinary (participant, tid, ssh, csh) . authorizedIfExists $ \(course `E.InnerJoin` courseParticipant) -> do
+ E.on $ course E.^. CourseId E.==. courseParticipant E.^. CourseParticipantCourse
+ E.where_ $ courseParticipant E.^. CourseParticipantUser E.==. E.val participant
+ E.&&. course E.^. CourseTerm E.==. E.val tid
+ E.&&. course E.^. CourseSchool E.==. E.val ssh
+ E.&&. course E.^. CourseShorthand E.==. E.val csh
+ -- participant has at least one submission
+ $cachedHereBinary (participant, tid, ssh, csh) . authorizedIfExists $ \(course `E.InnerJoin` sheet `E.InnerJoin` submission `E.InnerJoin` submissionUser) -> do
+ E.on $ submission E.^. SubmissionId E.==. submissionUser E.^. SubmissionUserSubmission
+ E.on $ sheet E.^. SheetId E.==. submission E.^. SubmissionSheet
+ E.on $ course E.^. CourseId E.==. sheet E.^. SheetCourse
+ E.where_ $ submissionUser E.^. SubmissionUserUser E.==. E.val participant
+ E.&&. course E.^. CourseTerm E.==. E.val tid
+ E.&&. course E.^. CourseSchool E.==. E.val ssh
+ E.&&. course E.^. CourseShorthand E.==. E.val csh
+ -- participant is member of a submissionGroup
+ $cachedHereBinary (participant, tid, ssh, csh) . authorizedIfExists $ \(course `E.InnerJoin` submissionGroup `E.InnerJoin` submissionGroupUser) -> do
+ E.on $ submissionGroup E.^. SubmissionGroupId E.==. submissionGroupUser E.^. SubmissionGroupUserSubmissionGroup
+ E.on $ course E.^. CourseId E.==. submissionGroup E.^. SubmissionGroupCourse
+ E.where_ $ submissionGroupUser E.^. SubmissionGroupUserUser E.==. E.val participant
+ E.&&. course E.^. CourseTerm E.==. E.val tid
+ E.&&. course E.^. CourseSchool E.==. E.val ssh
+ E.&&. course E.^. CourseShorthand E.==. E.val csh
+ -- participant is a sheet corrector
+ $cachedHereBinary (participant, tid, ssh, csh) . authorizedIfExists $ \(course `E.InnerJoin` sheet `E.InnerJoin` sheetCorrector) -> do
+ E.on $ sheet E.^. SheetId E.==. sheetCorrector E.^. SheetCorrectorSheet
+ E.on $ course E.^. CourseId E.==. sheet E.^. SheetCourse
+ E.where_ $ sheetCorrector E.^. SheetCorrectorUser E.==. E.val participant
+ E.&&. course E.^. CourseTerm E.==. E.val tid
+ E.&&. course E.^. CourseSchool E.==. E.val ssh
+ E.&&. course E.^. CourseShorthand E.==. E.val csh
+ -- participant is a tutorial user
+ $cachedHereBinary (participant, tid, ssh, csh) . authorizedIfExists $ \(course `E.InnerJoin` tutorial `E.InnerJoin` tutorialUser) -> do
+ E.on $ tutorial E.^. TutorialId E.==. tutorialUser E.^. TutorialParticipantTutorial
+ E.on $ course E.^. CourseId E.==. tutorial E.^. TutorialCourse
+ E.where_ $ tutorialUser E.^. TutorialParticipantUser E.==. E.val participant
+ E.&&. course E.^. CourseTerm E.==. E.val tid
+ E.&&. course E.^. CourseSchool E.==. E.val ssh
+ E.&&. course E.^. CourseShorthand E.==. E.val csh
+ -- participant is tutor for this course
+ $cachedHereBinary (participant, tid, ssh, csh) . authorizedIfExists $ \(course `E.InnerJoin` tutorial `E.InnerJoin` tutor) -> do
+ E.on $ tutorial E.^. TutorialId E.==. tutor E.^. TutorTutorial
+ E.on $ course E.^. CourseId E.==. tutorial E.^. TutorialCourse
+ E.where_ $ tutor E.^. TutorUser E.==. E.val participant
+ E.&&. course E.^. CourseTerm E.==. E.val tid
+ E.&&. course E.^. CourseSchool E.==. E.val ssh
+ E.&&. course E.^. CourseShorthand E.==. E.val csh
+ -- participant is lecturer for this course
+ $cachedHereBinary (participant, tid, ssh, csh) . authorizedIfExists $ \(course `E.InnerJoin` lecturer) -> do
+ E.on $ course E.^. CourseId E.==. lecturer E.^. LecturerCourse
+ E.where_ $ lecturer E.^. LecturerUser E.==. E.val participant
+ E.&&. course E.^. CourseTerm E.==. E.val tid
+ E.&&. course E.^. CourseSchool E.==. E.val ssh
+ E.&&. course E.^. CourseShorthand E.==. E.val csh
+ -- participant is applicant for this course
+ $cachedHereBinary (participant, tid, ssh, csh) . authorizedIfExists $ \((course `E.InnerJoin` courseApplication) `E.LeftOuterJoin` allocation) -> do
+ E.on $ allocation E.?. AllocationId E.==. courseApplication E.^. CourseApplicationAllocation
+ E.on $ course E.^. CourseId E.==. courseApplication E.^. CourseApplicationCourse
+ E.where_ $ courseApplication E.^. CourseApplicationUser E.==. E.val participant
+ E.&&. course E.^. CourseTerm E.==. E.val tid
+ E.&&. course E.^. CourseSchool E.==. E.val ssh
+ E.&&. course E.^. CourseShorthand E.==. E.val csh
+ E.where_ $ E.maybe E.true (E.maybe E.false $ \f -> f E.<=. E.val cTime) (allocation E.?. AllocationStaffAllocationFrom)
+ E.&&. E.maybe E.true (E.maybe E.true $ \t -> t E.>=. E.val cTime) (allocation E.?. AllocationStaffAllocationTo)
+
+ return ()
tagAccessPredicate AuthCapacity = APDB $ \_ route _ -> case route of
CTutorialR tid ssh csh tutn _ -> maybeT (unauthorizedI MsgTutorialNoCapacity) $ do
cid <- $cachedHereBinary (tid, ssh, csh) . MaybeT . getKeyBy $ TermSchoolCourseShort tid ssh csh
@@ -1176,10 +1239,11 @@ tagAccessPredicate AuthRegisterGroup = APDB $ \mAuthId route _ -> case route of
(Nothing, _) -> return Authorized
(_, Nothing) -> return AuthenticationRequired
(Just rGroup, Just uid) -> do
- [E.Value hasOther] <- $cachedHereBinary (uid, rGroup) . lift . E.select . return . E.exists . E.from $ \(tutorial `E.InnerJoin` participant) -> do
+ hasOther <- $cachedHereBinary (uid, rGroup) . lift . E.selectExists . E.from $ \(tutorial `E.InnerJoin` participant) -> do
E.on $ tutorial E.^. TutorialId E.==. participant E.^. TutorialParticipantTutorial
- E.where_ $ participant E.^. TutorialParticipantUser E.==. E.val uid
- E.&&. tutorial E.^. TutorialRegGroup E.==. E.just (E.val rGroup)
+ E.&&. tutorial E.^. TutorialCourse E.==. E.val tutorialCourse
+ E.&&. tutorial E.^. TutorialRegGroup E.==. E.just (E.val rGroup)
+ E.&&. participant E.^. TutorialParticipantUser E.==. E.val uid
guard $ not hasOther
return Authorized
r -> $unsupportedAuthPredicate AuthRegisterGroup r
@@ -1371,42 +1435,42 @@ evalAuthTags AuthTagActive{..} (map (Set.toList . toNullable) . Set.toList . dnf
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
dnf <- either throwM return $ routeAuthTags route
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
-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
- mAuthId <- liftHandlerT maybeAuthId
+ mAuthId <- liftHandler maybeAuthId
tagActive <- fromMaybe def <$> lookupSessionJson SessionActiveAuthTags
dnf <- either throwM return $ routeAuthTags route
(result, deactivated) <- runWriterT $ evalAuthTags tagActive dnf mAuthId route isWrite
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
-- | Check whether the current user is authorized by `evalAccess` for the given route
-- 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
-- | Check whether the current user is authorized by `evalAccess` to read from the given route
-- 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
-- | Check whether the current user is authorized by `evalAccess` to rwrite to the given route
-- 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
-- | 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
-- must hide URL if not authorized
access <- evalAccess url False
@@ -1415,7 +1479,7 @@ redirectAccess url = do
_ -> permissionDeniedI MsgUnauthorizedRedirect
-- | 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
evalAccessCorrector tid ssh csh = evalAccess (CourseR tid ssh csh CNotesR) False
@@ -1457,26 +1521,24 @@ instance Yesod UniWorX where
$logDebugS "updateFavourites" "Updating favourites"
now <- liftIO $ getCurrentTime
- uid <- MaybeT $ liftHandlerT maybeAuthId
+ uid <- MaybeT $ liftHandler maybeAuthId
cid <- MaybeT . getKeyBy $ TermSchoolCourseShort tid ssh csh
- user <- MaybeT $ get uid
- let courseFavourite = CourseFavourite uid now cid
+ User{userMaxFavourites} <- MaybeT $ get uid
- $logDebugS "updateFavourites" [st|Updating/Inserting: #{tshow courseFavourite}|]
-- update Favourites
void . lift $ upsertBy
(UniqueCourseFavourite uid cid)
- courseFavourite
- [CourseFavouriteTime =. now]
+ (CourseFavourite uid cid FavouriteVisited now)
+ [CourseFavouriteLastVisit =. now]
-- prune Favourites to user-defined size
- oldFavs <- lift $ selectKeysList
- [ CourseFavouriteUser ==. uid]
- [ Desc CourseFavouriteTime
- , OffsetBy $ userMaxFavourites user
- ]
- lift . forM_ oldFavs $ \fav -> do
- $logDebugS "updateFavourites" "Deleting old favourite."
- delete fav
+ oldFavs <- lift $ selectList [CourseFavouriteUser ==. uid] []
+ let deleteFavs = oldFavs
+ & sortOn ((courseFavouriteReason &&& Down . courseFavouriteLastVisit) . entityVal)
+ & drop userMaxFavourites
+ & filter ((<= FavouriteVisited) . courseFavouriteReason . entityVal)
+ & map entityKey
+ unless (null deleteFavs) $
+ lift $ deleteWhere [CourseFavouriteId <-. deleteFavs]
_other -> return ()
normalizeRouteMiddleware :: Handler a -> Handler a
normalizeRouteMiddleware handler = (*> handler) . runMaybeT $ do
@@ -1509,7 +1571,7 @@ instance Yesod UniWorX where
encrypted :: ToJSON a => a -> Widget -> Widget
encrypted plaintextJson plaintext = do
canDecrypt <- (== Authorized) <$> evalAccess AdminErrMsgR True
- shouldEncrypt <- view _appEncryptErrors
+ shouldEncrypt <- getsYesod $ view _appEncryptErrors
if
| shouldEncrypt
, not canDecrypt -> do
@@ -1572,14 +1634,13 @@ instance Yesod UniWorX where
. decodeUtf8
. Base64.encode
. (convert :: Digest (SHAKE256 144) -> ByteString)
- . runIdentity
- $ sourceList (Lazy.ByteString.toChunks content) $$ sinkHash
+ . runConduitPure
+ $ sourceList (Lazy.ByteString.toChunks content) .| sinkHash
fileUpload _site _length = FileUploadMemory lbsBackEnd
-- What messages should be logged. The following includes all messages when
-- in development, and warnings and errors in production.
- shouldLog _ _ _ = error "Must use shouldLogIO"
shouldLogIO app _source level = do
LogSettings{..} <- readTVarIO $ appLogSettings app
return $ logAll || level >= logMinimumLevel
@@ -1602,7 +1663,7 @@ siteLayout = siteLayout' . Just
siteLayout' :: Maybe Widget -- ^ Optionally override `pageHeading`
-> Widget -> Handler Html
siteLayout' headingOverride widget = do
- AppSettings { appUserDefaults = UserDefaultConf{..}, .. } <- view appSettings
+ AppSettings { appUserDefaults = UserDefaultConf{..}, .. } <- getsYesod $ view appSettings
isModal <- hasCustomHeader HeaderIsModal
@@ -1624,24 +1685,61 @@ siteLayout' headingOverride widget = do
isAuth <- isJust <$> maybeAuthId
- -- Lookup Favourites & Theme if possible -- TODO: cache this info in a cookie?!
- (favourites', currentTheme) <- do
+ -- Lookup Favourites & Theme if possible
+ (favourites', maxFavouriteTerms, currentTheme) <- do
muid <- maybeAuthPair
- case muid of
- Nothing -> return ([],userDefaultTheme)
- (Just (uid,user)) -> do
- favs <- runDB $ E.select . E.from $ \(course `E.InnerJoin` courseFavourite) -> do
- E.on (course E.^. CourseId E.==. courseFavourite E.^. CourseFavouriteCourse)
- E.where_ (courseFavourite E.^. CourseFavouriteUser E.==. E.val uid)
- E.orderBy [ E.asc $ course E.^. CourseShorthand ]
- return course
- return (favs, userTheme user)
- favourites <- forM favourites' $ \(Entity _ c@Course{..})
+
+ favCourses <- runDB . E.select . E.from $ \(course `E.LeftOuterJoin` courseFavourite) -> do
+ E.on $ E.just (course E.^. CourseId) E.==. courseFavourite E.?. CourseFavouriteCourse
+ E.&&. courseFavourite E.?. CourseFavouriteUser E.==. E.val (view _1 <$> muid)
+
+ let isFavourite = E.not_ . E.isNothing $ courseFavourite E.?. CourseFavouriteId
+ isCurrent
+ | Just (CourseR tid ssh csh _) <- mcurrentRoute
+ = course E.^. CourseTerm E.==. E.val tid
+ E.&&. course E.^. CourseSchool E.==. E.val ssh
+ E.&&. course E.^. CourseShorthand E.==. E.val csh
+ | otherwise
+ = E.false
+ notBlacklist = E.not_ . E.exists . E.from $ \courseNoFavourite ->
+ E.where_ $ E.just (courseNoFavourite E.^. CourseNoFavouriteUser) E.==. E.val (view _1 <$> muid)
+ E.&&. courseNoFavourite E.^. CourseNoFavouriteCourse E.==. course E.^. CourseId
+ isParticipant = E.exists . E.from $ \participant ->
+ E.where_ $ participant E.^. CourseParticipantCourse E.==. course E.^. CourseId
+ E.&&. E.just (participant E.^. CourseParticipantUser) E.==. E.val (view _1 <$> muid)
+ isLecturer = E.exists . E.from $ \lecturer ->
+ E.where_ $ lecturer E.^. LecturerCourse E.==. course E.^. CourseId
+ E.&&. E.just (lecturer E.^. LecturerUser) E.==. E.val (view _1 <$> muid)
+ isCorrector = E.exists . E.from $ \(corrector `E.InnerJoin` sheet) -> do
+ E.on $ corrector E.^. SheetCorrectorSheet E.==. sheet E.^. SheetId
+ E.&&. sheet E.^. SheetCourse E.==. course E.^. CourseId
+ E.where_ $ E.just (corrector E.^. SheetCorrectorUser) E.==. E.val (view _1 <$> muid)
+ isTutor = E.exists . E.from $ \(tutor `E.InnerJoin` tutorial) -> do
+ E.on $ tutor E.^. TutorTutorial E.==. tutorial E.^. TutorialId
+ E.&&. tutorial E.^. TutorialCourse E.==. course E.^. CourseId
+ E.where_ $ E.just (tutor E.^. TutorUser) E.==. E.val (view _1 <$> muid)
+ isAssociated = isParticipant E.||. isLecturer E.||. isCorrector E.||. isTutor
+
+ reason = E.case_
+ [ E.when_ isCurrent E.then_ . E.just $ E.val FavouriteCurrent
+ , E.when_ isAssociated E.then_ . E.just $ E.val FavouriteParticipant
+ ] (E.else_ $ courseFavourite E.?. CourseFavouriteReason)
+
+ E.where_ $ ((isFavourite E.||. isAssociated) E.&&. notBlacklist) E.||. isCurrent
+
+ return (course, reason)
+
+ return ( favCourses
+ , maybe userDefaultMaxFavouriteTerms userMaxFavouriteTerms $ view _2 <$> muid
+ , maybe userDefaultTheme userTheme $ view _2 <$> muid
+ )
+ favourites <- forM favourites' $ \(Entity _ c@Course{..}, E.Value mFavourite)
-> let courseRoute = CourseR courseTerm courseSchool courseShorthand CShowR
+ favouriteReason = fromMaybe FavouriteCurrent mFavourite
in do
items <- filterM menuItemAccessCallback (pageActions courseRoute)
items' <- forM items $ \i -> (i, ) <$> toTextUrl i
- return (c, courseRoute, items')
+ return (c, courseRoute, items', favouriteReason)
mmsgs <- if
| isModal -> getMessages
@@ -1658,9 +1756,11 @@ siteLayout' headingOverride widget = do
highR = find (`elem` navItems) . uncurry (++) $ partition (`elem` map (view _2) favourites) crumbs
in \r -> Just r == highR
favouriteTerms :: [TermIdentifier]
- favouriteTerms = Set.toDescList $ foldMap (\(Course{..}, _, _) -> Set.singleton $ unTermKey courseTerm) favourites
- favouriteTerm :: TermIdentifier -> [(Course, Route UniWorX, [(MenuItem, Text)])]
- favouriteTerm tid = filter (\(Course{..}, _, _) -> unTermKey courseTerm == tid) favourites
+ favouriteTerms = take maxFavouriteTerms . Set.toDescList $ foldMap (\(Course{..}, _, _, _) -> Set.singleton $ unTermKey courseTerm) favourites
+ favouriteTermReason :: TermIdentifier -> FavouriteReason -> [(Course, Route UniWorX, [(MenuItem, Text)], FavouriteReason)]
+ favouriteTermReason tid favReason' = favourites
+ & filter (\(Course{..}, _, _, favReason) -> unTermKey courseTerm == tid && favReason == favReason')
+ & sortOn (\(Course{..}, _, _, _) -> courseName)
-- We break up the default layout into two components:
-- default-layout is the contents of the body tag, and
@@ -1723,7 +1823,7 @@ siteLayout' headingOverride widget = do
applySystemMessages :: (MonadHandler m, HandlerSite m ~ UniWorX) => m ()
-applySystemMessages = liftHandlerT . runDB . runConduit $ selectSource [] [] .| C.mapM_ applyMessage
+applySystemMessages = liftHandler . runDB . runConduit $ selectSource [] [] .| C.mapM_ applyMessage
where
applyMessage (Entity smId SystemMessage{..}) = maybeT_ $ do
cID <- encrypt smId
@@ -1752,6 +1852,7 @@ instance YesodBreadcrumbs UniWorX where
breadcrumb (AuthR _) = return ("Login" , Just HomeR)
breadcrumb HomeR = return ("Uni2work" , Nothing)
breadcrumb UsersR = return ("Benutzer" , Just AdminR)
+ breadcrumb AdminUserAddR = return ("Benutzer anlegen", Just UsersR)
breadcrumb (AdminUserR _) = return ("Users" , Just UsersR)
breadcrumb AdminR = return ("Administration", Nothing)
breadcrumb AdminFeaturesR = return ("Test" , Just AdminR)
@@ -1762,6 +1863,10 @@ instance YesodBreadcrumbs UniWorX where
breadcrumb (SchoolR ssh SchoolEditR) = return (original (unSchoolKey ssh), Just SchoolListR)
breadcrumb SchoolNewR = return ("Neu" , Just SchoolListR)
+ breadcrumb (ExamOfficeR EOExamsR) = return ("Prüfungen", Nothing)
+ breadcrumb (ExamOfficeR EOFieldsR) = return ("Fächer" , Just $ ExamOfficeR EOExamsR)
+ breadcrumb (ExamOfficeR EOUsersR) = return ("Benutzer" , Just $ ExamOfficeR EOExamsR)
+
breadcrumb InfoR = return ("Information" , Nothing)
breadcrumb InfoLecturerR = return ("Veranstalter" , Just InfoR)
breadcrumb DataProtR = return ("Datenschutz" , Just InfoR)
@@ -1777,9 +1882,10 @@ instance YesodBreadcrumbs UniWorX where
breadcrumb InstanceR = return ("Identifikation", Nothing)
- breadcrumb ProfileR = return ("User" , Just HomeR)
- breadcrumb ProfileDataR = return ("Profile" , Just ProfileR)
- breadcrumb AuthPredsR = return ("Authentifizierung", Just ProfileR)
+ breadcrumb ProfileR = return ("Einstellungen" , Just HomeR)
+ breadcrumb SetDisplayEmailR = return ("Öffentliche E-Mail Adresse", Just ProfileR)
+ breadcrumb ProfileDataR = return ("Persönliche Daten", Just ProfileR)
+ breadcrumb AuthPredsR = return ("Authorisierung" , Just ProfileR)
breadcrumb TermShowR = return ("Semester" , Just HomeR)
breadcrumb TermCurrentR = return ("Aktuell" , Just TermShowR)
@@ -1803,6 +1909,7 @@ instance YesodBreadcrumbs UniWorX where
breadcrumb (CourseR tid ssh csh CUsersR) = return ("Anmeldungen", Just $ CourseR tid ssh csh CShowR)
breadcrumb (CourseR tid ssh csh CAddUserR) = return ("Kursteilnehmer hinzufügen", Just $ CourseR tid ssh csh CUsersR)
breadcrumb (CourseR tid ssh csh CInviteR) = return ("Einladung", Just $ CourseR tid ssh csh CShowR)
+ breadcrumb (CourseR tid ssh csh CExamOfficeR) = return ("Prüfungsamter", Just $ CourseR tid ssh csh CShowR)
breadcrumb (CourseR tid ssh csh (CUserR cID)) = do
uid <- decrypt cID
User{userDisplayName} <- runDB $ get404 uid
@@ -1817,6 +1924,11 @@ instance YesodBreadcrumbs UniWorX where
breadcrumb (CourseR tid ssh csh CTutorialListR) = return ("Tutorien", Just $ CourseR tid ssh csh CShowR)
breadcrumb (CourseR tid ssh csh CTutorialNewR) = return ("Anlegen", Just $ CourseR tid ssh csh CTutorialListR)
+ breadcrumb (CourseR tid ssh csh CNewsNewR) = return ("Neue Nachricht", Just $ CourseR tid ssh csh CShowR)
+ breadcrumb (CNewsR tid ssh csh _ CNShowR) = return ("Kursnachricht" , Just $ CourseR tid ssh csh CShowR)
+ breadcrumb (CNewsR tid ssh csh cID CNEditR) = return ("Bearbeiten" , Just $ CNewsR tid ssh csh cID CNShowR)
+ breadcrumb (CNewsR tid ssh csh cID CNDeleteR) = return ("Löschen" , Just $ CNewsR tid ssh csh cID CNShowR)
+
breadcrumb (CourseR tid ssh csh CExamListR) = return ("Prüfungen", Just $ CourseR tid ssh csh CShowR)
breadcrumb (CourseR tid ssh csh CExamNewR) = return ("Anlegen", Just $ CourseR tid ssh csh CExamListR)
@@ -1827,7 +1939,8 @@ instance YesodBreadcrumbs UniWorX where
breadcrumb (CExamR tid ssh csh examn EShowR) = return (original examn, Just $ CourseR tid ssh csh CExamListR)
breadcrumb (CExamR tid ssh csh examn EEditR) = return ("Bearbeiten", Just $ CExamR tid ssh csh examn EShowR)
breadcrumb (CExamR tid ssh csh examn EUsersR) = return ("Teilnehmer", Just $ CExamR tid ssh csh examn EShowR)
- breadcrumb (CExamR tid ssh csh examn EAddUserR) = return ("Prüfungsteilnehmer hinzufügen", Just $ CExamR tid ssh csh examn EUsersR)
+ breadcrumb (CExamR tid ssh csh examn EAddUserR) = return ("Prüfungsteilnehmer hinzufügen", Just $ CExamR tid ssh csh examn EUsersR)
+ breadcrumb (CExamR tid ssh csh examn EGradesR) = return ("Prüfungsleistungen", Just $ CExamR tid ssh csh examn EShowR)
breadcrumb (CTutorialR tid ssh csh tutn TUsersR) = return (original tutn, Just $ CourseR tid ssh csh CTutorialListR)
breadcrumb (CTutorialR tid ssh csh tutn TEditR) = return ("Bearbeiten", Just $ CTutorialR tid ssh csh tutn TUsersR)
@@ -1974,6 +2087,14 @@ defaultLinks = fmap catMaybes . mapM runMaybeT $ -- Define the menu items of the
, menuItemModal = False
, menuItemAccessCallback' = return True
}
+ , return MenuItem
+ { menuItemType = NavbarAside
+ , menuItemLabel = MsgMenuExamOfficeExams
+ , menuItemIcon = Just "poll-h"
+ , menuItemRoute = SomeRoute $ ExamOfficeR EOExamsR
+ , menuItemModal = False
+ , menuItemAccessCallback' = return True
+ }
, return MenuItem
{ menuItemType = NavbarAside
, menuItemLabel = MsgMenuUsers
@@ -2077,6 +2198,24 @@ pageActions (AdminR) =
, menuItemAccessCallback' = return True
}
]
+pageActions (ExamOfficeR EOExamsR) =
+ [ MenuItem
+ { menuItemType = PageActionPrime
+ , menuItemLabel = MsgMenuExamOfficeFields
+ , menuItemIcon = Nothing
+ , menuItemRoute = SomeRoute $ ExamOfficeR EOFieldsR
+ , menuItemModal = True
+ , menuItemAccessCallback' = return True
+ }
+ , MenuItem
+ { menuItemType = PageActionPrime
+ , menuItemLabel = MsgMenuExamOfficeUsers
+ , menuItemIcon = Nothing
+ , menuItemRoute = SomeRoute $ ExamOfficeR EOUsersR
+ , menuItemModal = True
+ , menuItemAccessCallback' = return True
+ }
+ ]
pageActions (SchoolListR) =
[ MenuItem
{ menuItemType = PageActionPrime
@@ -2096,6 +2235,14 @@ pageActions (UsersR) =
, menuItemModal = True
, menuItemAccessCallback' = return True
}
+ , MenuItem
+ { menuItemType = PageActionPrime
+ , menuItemLabel = MsgMenuUserAdd
+ , menuItemIcon = Nothing
+ , menuItemRoute = SomeRoute AdminUserAddR
+ , menuItemModal = True
+ , menuItemAccessCallback' = return True
+ }
]
pageActions (AdminUserR cID) =
[ MenuItem
@@ -2357,7 +2504,22 @@ pageActions (CourseR tid ssh csh CShowR) =
, menuItemIcon = Just "user-graduate"
, menuItemRoute = SomeRoute $ CourseR tid ssh csh CUsersR
, menuItemModal = False
- , menuItemAccessCallback' = return True
+ , menuItemAccessCallback' = do
+ now <- liftIO getCurrentTime
+ let courseWhere course = course <$ do
+ E.where_ $ course E.^. CourseTerm E.==. E.val tid
+ E.&&. course E.^. CourseSchool E.==. E.val ssh
+ E.&&. course E.^. CourseShorthand E.==. E.val csh
+ hasActiveAllocation = E.selectExists . E.from $ \(course `E.InnerJoin` allocationCourse `E.InnerJoin` allocation) -> do
+ E.on $ allocation E.^. AllocationId E.==. allocationCourse E.^. AllocationCourseAllocation
+ E.on $ allocationCourse E.^. AllocationCourseCourse E.==. course E.^. CourseId
+ void $ courseWhere course
+ E.where_ $ E.maybe E.false (E.<=. E.val now) (allocation E.^. AllocationRegisterByStaffFrom)
+ E.||. E.maybe E.false (E.<=. E.val now) (allocation E.^. AllocationRegisterByCourse)
+ hasParticipants = E.selectExists . E.from $ \(course `E.InnerJoin` courseParticipant) -> do
+ E.on $ course E.^. CourseId E.==. courseParticipant E.^. CourseParticipantCourse
+ void $ courseWhere course
+ runDB $ (not <$> hasActiveAllocation) `or2M` hasParticipants
}
, MenuItem
{ menuItemType = PageActionSecondary
@@ -2391,6 +2553,20 @@ pageActions (CourseR tid ssh csh CShowR) =
, menuItemModal = False
, menuItemAccessCallback' = return True
}
+ , MenuItem
+ { menuItemType = PageActionSecondary
+ , menuItemLabel = MsgMenuCourseExamOffice
+ , menuItemIcon = Nothing
+ , menuItemRoute = SomeRoute $ CourseR tid ssh csh CExamOfficeR
+ , menuItemModal = True
+ , menuItemAccessCallback' = do
+ uid <- requireAuthId
+ runDB $ do
+ cid <- getKeyBy404 $ TermSchoolCourseShort tid ssh csh
+ E.selectExists $ do
+ (_school, isForced) <- courseExamOfficeSchools (E.val uid) (E.val cid)
+ E.where_ $ E.not_ isForced
+ }
]
pageActions (CourseR tid ssh csh CCorrectionsR) =
[ MenuItem
@@ -2453,7 +2629,7 @@ pageActions (CourseR tid ssh csh SheetListR) =
case muid of
Nothing -> return False
(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 $ sheet E.^. SheetCourse E.==. course E.^. CourseId
E.where_ $ submission E.^. SubmissionRatingBy E.==. E.just (E.val uid)
@@ -2480,6 +2656,28 @@ pageActions (CourseR tid ssh csh CUsersR) =
, menuItemModal = True
, menuItemAccessCallback' = return True
}
+ , MenuItem
+ { menuItemType = PageActionPrime
+ , menuItemLabel = MsgMenuCourseApplications
+ , menuItemIcon = Nothing
+ , menuItemRoute = SomeRoute $ CourseR tid ssh csh CApplicationsR
+ , menuItemModal = False
+ , menuItemAccessCallback' =
+ let courseWhere course = course <$ do
+ E.where_ $ course E.^. CourseTerm E.==. E.val tid
+ E.&&. course E.^. CourseSchool E.==. E.val ssh
+ E.&&. course E.^. CourseShorthand E.==. E.val csh
+ existsApplications = E.selectExists . E.from $ \(course `E.InnerJoin` courseApplication) -> do
+ E.on $ course E.^. CourseId E.==. courseApplication E.^. CourseApplicationCourse
+ void $ courseWhere course
+ courseApplications = fmap (any E.unValue) . E.select . E.from $ \course -> do
+ void $ courseWhere course
+ return $ course E.^. CourseApplicationsRequired
+ courseAllocation = E.selectExists . E.from $ \(course `E.InnerJoin` allocationCourse) -> do
+ E.on $ course E.^. CourseId E.==. allocationCourse E.^. AllocationCourseCourse
+ void $ courseWhere course
+ in runDB $ courseAllocation `or2M` courseApplications `or2M` existsApplications
+ }
]
pageActions (CourseR tid ssh csh MaterialListR) =
[ MenuItem
@@ -2582,6 +2780,14 @@ pageActions (CExamR tid ssh csh examn EShowR) =
, menuItemModal = False
, 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 EUsersR) =
[ MenuItem
@@ -2592,6 +2798,24 @@ pageActions (CExamR tid ssh csh examn EUsersR) =
, menuItemModal = 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) =
[ MenuItem
@@ -2601,7 +2825,7 @@ pageActions (CSheetR tid ssh csh shn SShowR) =
, menuItemRoute = SomeRoute $ CSheetR tid ssh csh shn SubmissionNewR
, menuItemModal = True
, menuItemAccessCallback' = runDB . maybeT (return False) $ do
- uid <- MaybeT $ liftHandlerT maybeAuthId
+ uid <- MaybeT $ liftHandler maybeAuthId
submissions <- lift $ submissionList tid csh shn uid
guard $ null submissions
return True
@@ -2613,7 +2837,7 @@ pageActions (CSheetR tid ssh csh shn SShowR) =
, menuItemRoute = SomeRoute $ CSheetR tid ssh csh shn SubmissionOwnR
, menuItemModal = False
, menuItemAccessCallback' = runDB . maybeT (return False) $ do
- uid <- MaybeT $ liftHandlerT maybeAuthId
+ uid <- MaybeT $ liftHandler maybeAuthId
submissions <- lift $ submissionList tid csh shn uid
guard . not $ null submissions
return True
@@ -2788,6 +3012,16 @@ pageActions (CourseR tid ssh csh CApplicationsR) =
return $ courseApplication E.^. CourseApplicationId
in runDB . runConduit $ appSource .| anyMC appAccess
}
+ , MenuItem
+ { menuItemType = PageActionPrime
+ , menuItemLabel = MsgMenuCourseMembers
+ , menuItemIcon = Nothing
+ , menuItemRoute = SomeRoute $ CourseR tid ssh csh CUsersR
+ , menuItemModal = False
+ , menuItemAccessCallback' = runDB $ do
+ cid <- getKeyBy404 $ TermSchoolCourseShort tid ssh csh
+ exists [ CourseParticipantCourse ==. cid ]
+ }
]
pageActions (CorrectionsR) =
[ MenuItem
@@ -2813,7 +3047,7 @@ pageActions (CorrectionsR) =
, menuItemRoute = SomeRoute CorrectionsCreateR
, menuItemModal = False
, 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
E.on $ course E.^. CourseId E.==. sheet E.^. SheetCourse
let
@@ -2852,7 +3086,7 @@ pageActions (CorrectionsGradeR) =
, menuItemRoute = SomeRoute CorrectionsCreateR
, menuItemModal = False
, 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
E.on $ course E.^. CourseId E.==. sheet E.^. SheetCourse
let
@@ -2871,7 +3105,7 @@ pageActions _ = []
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!
pageHeading :: Route UniWorX -> Maybe Widget
@@ -2978,18 +3212,24 @@ pageHeading _
= Nothing
-routeNormalizers :: [Route UniWorX -> WriterT Any (ReaderT (YesodPersistBackend UniWorX) (HandlerT UniWorX IO)) (Route UniWorX)]
+routeNormalizers :: [Route UniWorX -> WriterT Any DB (Route UniWorX)]
routeNormalizers =
[ normalizeRender
, ncSchool
+ , ncAllocation
, ncCourse
, ncSheet
+ , ncMaterial
+ , ncTutorial
+ , ncExam
, verifySubmission
, verifyCourseApplication
+ , verifyCourseNews
]
where
+ normalizeRender :: Route UniWorX -> WriterT Any DB (Route UniWorX)
normalizeRender route = route <$ do
- YesodRequest{..} <- liftHandlerT getRequest
+ YesodRequest{..} <- liftHandler getRequest
let original = (W.pathInfo reqWaiRequest, reqGetParams)
rendered = renderRoute route
if
@@ -2998,37 +3238,64 @@ routeNormalizers =
| otherwise -> do
$logDebugS "normalizeRender" [st|Redirecting because #{tshow rendered} does not match #{tshow original}|]
tell $ Any True
+
+ maybeOrig :: (Route UniWorX -> MaybeT (WriterT Any DB) (Route UniWorX))
+ -> Route UniWorX -> WriterT Any DB (Route UniWorX)
maybeOrig f route = maybeT (return route) $ f route
- hasChanged a b
+
+ caseChanged :: (Eq a, Show a) => CI a -> CI a -> MaybeT (WriterT Any DB) ()
+ caseChanged a b
| ((/=) `on` original) a b = do
$logDebugS "routeNormalizers" [st|#{tshow a} /= #{tshow b}|]
tell $ Any True
| otherwise = return ()
- ncSchool = maybeOrig $ \route -> do
- TermSchoolCourseListR tid ssh <- return route
+
+ ncSchool = maybeOrig . typesUsing @RouteChildren @SchoolId $ \ssh -> $cachedHereBinary ssh $ do
let schoolShort :: SchoolShorthand
schoolShort = unSchoolKey ssh
Entity ssh' _ <- MaybeT . lift . getBy $ UniqueSchoolShorthand schoolShort
- (hasChanged `on` unSchoolKey)ssh ssh'
- return $ TermSchoolCourseListR tid ssh'
+ (caseChanged `on` unSchoolKey) ssh ssh'
+ return ssh'
+ ncAllocation = maybeOrig $ \route -> do
+ AllocationR tid ssh ash _ <- return route
+ Entity _ Allocation{..} <- MaybeT . $cachedHereBinary (tid, ssh, ash) . lift . getBy $ TermSchoolAllocationShort tid ssh ash
+ caseChanged ash allocationShorthand
+ return $ route & typesUsing @RouteChildren @AllocationShorthand . filtered (== ash) .~ allocationShorthand
ncCourse = maybeOrig $ \route -> do
- CourseR tid ssh csh subRoute <- return route
- Entity _ Course{..} <- MaybeT . lift . getBy $ TermSchoolCourseShort tid ssh csh
- hasChanged csh courseShorthand
- (hasChanged `on` unSchoolKey) ssh courseSchool
- return $ CourseR tid courseSchool courseShorthand subRoute
+ CourseR tid ssh csh _ <- return route
+ Entity _ Course{..} <- MaybeT . $cachedHereBinary (tid, ssh, csh) . lift . getBy $ TermSchoolCourseShort tid ssh csh
+ caseChanged csh courseShorthand
+ return $ route & typesUsing @RouteChildren @CourseShorthand . filtered (== csh) .~ courseShorthand
ncSheet = maybeOrig $ \route -> do
- CSheetR tid ssh csh shn subRoute <- return route
- Entity cid _ <- MaybeT . lift . getBy $ TermSchoolCourseShort tid ssh csh
- Entity _ Sheet{..} <- MaybeT . lift . getBy $ CourseSheet cid shn
- hasChanged shn sheetName
- return $ CSheetR tid ssh csh sheetName subRoute
+ CSheetR tid ssh csh shn _ <- return route
+ Entity cid Course{..} <- MaybeT . $cachedHereBinary (tid, ssh, csh) . lift . getBy $ TermSchoolCourseShort tid ssh csh
+ Entity _ Sheet{..} <- MaybeT . $cachedHereBinary (cid, shn) . lift . getBy $ CourseSheet cid shn
+ caseChanged shn sheetName
+ return $ route & typesUsing @RouteChildren @SheetName . filtered (== shn) .~ sheetName
+ ncMaterial = maybeOrig $ \route -> do
+ CMaterialR tid ssh csh mnm _ <- return route
+ Entity cid Course{..} <- MaybeT . $cachedHereBinary (tid, ssh, csh) . lift . getBy $ TermSchoolCourseShort tid ssh csh
+ Entity _ Material{..} <- MaybeT . $cachedHereBinary (cid, mnm) . lift . getBy $ UniqueMaterial cid mnm
+ caseChanged mnm materialName
+ return $ route & typesUsing @RouteChildren @MaterialName . filtered (== mnm) .~ materialName
+ ncTutorial = maybeOrig $ \route -> do
+ CTutorialR tid ssh csh tutn _ <- return route
+ Entity cid Course{..} <- MaybeT . $cachedHereBinary (tid, ssh, csh) . lift . getBy $ TermSchoolCourseShort tid ssh csh
+ Entity _ Tutorial{..} <- MaybeT . $cachedHereBinary (cid, tutn) . lift . getBy $ UniqueTutorial cid tutn
+ caseChanged tutn tutorialName
+ return $ route & typesUsing @RouteChildren @TutorialName . filtered (== tutn) .~ tutorialName
+ ncExam = maybeOrig $ \route -> do
+ CExamR tid ssh csh examn _ <- return route
+ Entity cid Course{..} <- MaybeT . $cachedHereBinary (tid, ssh, csh) . lift . getBy $ TermSchoolCourseShort tid ssh csh
+ Entity _ Exam{..} <- MaybeT . $cachedHereBinary (cid, examn) . lift . getBy $ UniqueExam cid examn
+ caseChanged examn examName
+ return $ route & typesUsing @RouteChildren @ExamName . filtered (== examn) .~ examName
verifySubmission = maybeOrig $ \route -> do
CSubmissionR _tid _ssh _csh _shn cID sr <- return route
- sId <- decrypt cID
- Submission{submissionSheet} <- lift . lift $ get404 sId
- Sheet{sheetCourse, sheetName} <- lift . lift $ get404 submissionSheet
- Course{courseTerm, courseSchool, courseShorthand} <- lift . lift $ get404 sheetCourse
+ sId <- $cachedHereBinary cID $ decrypt cID
+ Submission{submissionSheet} <- MaybeT . $cachedHereBinary cID . lift $ get sId
+ Sheet{sheetCourse, sheetName} <- MaybeT . $cachedHereBinary submissionSheet . lift $ get submissionSheet
+ Course{courseTerm, courseSchool, courseShorthand} <- MaybeT . $cachedHereBinary sheetCourse . lift $ get sheetCourse
let newRoute = CSubmissionR courseTerm courseSchool courseShorthand sheetName cID sr
tell . Any $ route /= newRoute
return newRoute
@@ -3040,6 +3307,14 @@ routeNormalizers =
let newRoute = CApplicationR courseTerm courseSchool courseShorthand cID sr
tell . Any $ route /= newRoute
return newRoute
+ verifyCourseNews = maybeOrig $ \route -> do
+ CNewsR _tid _ssh _csh cID sr <- return route
+ aId <- decrypt cID
+ CourseNews{courseNewsCourse} <- lift . lift $ get404 aId
+ Course{courseTerm, courseSchool, courseShorthand} <- lift . lift $ get404 courseNewsCourse
+ let newRoute = CNewsR courseTerm courseSchool courseShorthand cID sr
+ tell . Any $ route /= newRoute
+ return newRoute
-- How to run database actions.
@@ -3049,7 +3324,9 @@ instance YesodPersist UniWorX where
$logDebugS "YesodPersist" "runDB"
runSqlPool action =<< appConnPool <$> getYesod
instance YesodPersistRunner UniWorX where
- getDBRunner = defaultGetDBRunner appConnPool
+ getDBRunner = do
+ (DBRunner{..}, cleanup) <- defaultGetDBRunner appConnPool
+ return . (, cleanup) $ DBRunner (\act -> $logDebugS "YesodPersist" "runDBRunner" >> runDBRunner act)
data CampusUserConversionException
= CampusUserInvalidEmail
@@ -3073,7 +3350,7 @@ upsertCampusUser ldapData Creds{..} = do
let
userMatrikelnummer' = fold [ v | (k, v) <- ldapData, k == ldapUserMatriculation ]
userEmail' = fold [ v | (k, v) <- ldapData, k == ldapUserEmail ]
- userDisplayName' = fold [ v | (k, v) <- ldapData, k == ldapUserDisplayName ]
+ userDisplayName'' = fold [ v | (k, v) <- ldapData, k == ldapUserDisplayName ]
userFirstName' = fold [ v | (k, v) <- ldapData, k == ldapUserFirstName ]
userSurname' = fold [ v | (k, v) <- ldapData, k == ldapUserSurname ]
userTitle' = fold [ v | (k, v) <- ldapData, k == ldapUserTitle ]
@@ -3089,10 +3366,10 @@ upsertCampusUser ldapData Creds{..} = do
-> return $ mk userEmail
| otherwise
-> throwM CampusUserInvalidEmail
- userDisplayName <- if
- | [bs] <- userDisplayName'
- , Right userDisplayName <- Text.decodeUtf8' bs
- -> return userDisplayName
+ userDisplayName' <- if
+ | [bs] <- userDisplayName''
+ , Right userDisplayName' <- Text.decodeUtf8' bs
+ -> return userDisplayName'
| otherwise
-> throwM CampusUserInvalidDisplayName
userFirstName <- if
@@ -3128,6 +3405,7 @@ upsertCampusUser ldapData Creds{..} = do
newUser = User
{ userIdent = mk credsIdent
, userMaxFavourites = userDefaultMaxFavourites
+ , userMaxFavouriteTerms = userDefaultMaxFavouriteTerms
, userTheme = userDefaultTheme
, userDateTimeFormat = userDefaultDateTimeFormat
, userDateFormat = userDefaultDateFormat
@@ -3136,20 +3414,27 @@ upsertCampusUser ldapData Creds{..} = do
, userWarningDays = userDefaultWarningDays
, userNotificationSettings = def
, userMailLanguages = def
+ , userCsvOptions = def
, userTokensIssuedAfter = Nothing
, userCreated = now
, userLastLdapSynchronisation = Just now
+ , userDisplayName = userDisplayName'
+ , userDisplayEmail = userEmail
, ..
}
userUpdate = [ UserMatrikelnummer =. userMatrikelnummer
- , UserDisplayName =. userDisplayName
+ -- , UserDisplayName =. userDisplayName
+ , UserFirstName =. userFirstName
, UserSurname =. userSurname
+ , UserTitle =. userTitle
, UserEmail =. userEmail
, UserLastLdapSynchronisation =. Just now
] ++
[ UserLastAuthentication =. Just now | not isDummy ]
- user@(Entity userId _) <- upsertBy (UniqueAuthentication $ mk credsIdent) newUser userUpdate
+ user@(Entity userId userRec) <- upsertBy (UniqueAuthentication $ mk credsIdent) newUser userUpdate
+ unless (validDisplayName userTitle userFirstName userSurname $ userDisplayName userRec) $
+ update userId [ UserDisplayName =. userDisplayName' ]
let
userStudyFeatures = fmap concat . forM userStudyFeatures' $ parseStudyFeatures userId now
@@ -3179,10 +3464,10 @@ upsertCampusUser ldapData Creds{..} = do
. UUID.fromByteString
. fromStrict
. (convert :: Digest (SHAKE128 128) -> ByteString)
- . runIdentity
- $ sourceList (toStrict . Binary.encode <$> Set.toList studyTermCandidates) $$ sinkHash
+ . runConduitPure
+ $ 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
unless candidatesRecorded $ do
@@ -3260,14 +3545,14 @@ instance YesodAuth UniWorX where
loginHandler = do
toParent <- getRouteToParent
- lift . defaultLayout $ do
+ liftHandler . defaultLayout $ do
plugins <- getsYesod authPlugins
$logDebugS "Auth" $ "Enabled plugins: " <> Text.intercalate ", " (map apName plugins)
setTitleI MsgLoginTitle
$(widgetFile "login")
- authenticate Creds{..} = runDB $ do
+ authenticate Creds{..} = liftHandler . runDB $ do
now <- liftIO getCurrentTime
let
@@ -3336,7 +3621,7 @@ instance YesodAuth UniWorX where
, dummyLogin <$ guard appAuthDummyLogin
]
- authHttpManager = Yesod.getHttpManager
+ authHttpManager = getsYesod appHttpManager
onLogin = addMessageI Success Auth.NowLoggedIn
diff --git a/src/Handler/Admin.hs b/src/Handler/Admin.hs
index 9d8c03552..3b921bca3 100644
--- a/src/Handler/Admin.hs
+++ b/src/Handler/Admin.hs
@@ -21,7 +21,6 @@ import Database.Persist.Sql (fromSqlKey)
import qualified Database.Esqueleto as E
import Database.Esqueleto.Utils (mkExactFilter, mkContainsFilter)
-import Handler.Utils.Table.Cells
import qualified Handler.Utils.TermCandidates as Candidates
-- import Colonnade hiding (fromMaybe)
@@ -55,7 +54,7 @@ instance Button UniWorX ButtonCreate where
btnClasses CreateInf = [BCIsButton, BCPrimary]
-- END Button needed only here
-emailTestForm :: AForm (HandlerT UniWorX IO) (Email, MailContext)
+emailTestForm :: AForm (HandlerFor UniWorX) (Email, MailContext)
emailTestForm = (,)
<$> areq emailField (fslI MsgMailTestFormEmail) Nothing
<*> ( MailContext
@@ -113,7 +112,7 @@ postAdminTestR = do
jId <- queueJob $ JobSendTestEmail email ls
tell . pure $ Message Success [shamlet|Email-test gestartet (Job ##{tshow (fromSqlKey jId)})|] (Just IconEmail)
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`
let emailWidget' = wrapForm emailWidget def
@@ -147,7 +146,14 @@ postAdminTestR = do
$forall m <- msgs
$if isRegistered
@@ -86,6 +105,9 @@ getEShowR tid ssh csh examn = do
| fromMaybe False registered = Just [whamlet|_{MsgExamRegistered}|]
| otherwise = Nothing
+ showMaxPoints = any (has $ _entityVal . _examPartMaxPoints . _Just) examParts
+ showAchievedPoints = not $ null results
+
let heading = prependCourseTitle tid ssh csh $ CI.original examName
siteLayoutMsg heading $ do
diff --git a/src/Handler/Exam/Users.hs b/src/Handler/Exam/Users.hs
index 692a69c3c..a5f49fb46 100644
--- a/src/Handler/Exam/Users.hs
+++ b/src/Handler/Exam/Users.hs
@@ -4,25 +4,27 @@ module Handler.Exam.Users
( getEUsersR, postEUsersR
) where
-import Import
+import Import hiding ((<.), (.>))
import Handler.Utils
import Handler.Utils.Exam
-import Handler.Utils.Table.Columns
-import Handler.Utils.Table.Cells
import Handler.Utils.Csv
+import Handler.ExamOffice.Exam (examCloseWidget)
+
import qualified Database.Esqueleto as E
import qualified Database.Esqueleto.Utils as E
import Database.Esqueleto.Utils.TH
import qualified Data.Csv as Csv
-import Data.Map ((!))
+import Data.Map ((!), (!?))
import qualified Data.Map as Map
import qualified Data.Set as Set
+import qualified Data.HashMap.Lazy as HashMap
import qualified Data.Text as Text
+import qualified Data.Text.Encoding as Text
import qualified Data.Text.Lens as Text
import qualified Data.Conduit.List as C
@@ -33,9 +35,35 @@ import Numeric.Lens (integral)
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))
+import Jobs.Queue
+
+
+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
hasEntity = _dbrOutput . _2
@@ -47,28 +75,51 @@ _userTableOccurrence :: Lens' ExamUserTableData (Maybe (Entity ExamOccurrence))
_userTableOccurrence = _dbrOutput . _3
queryUser :: ExamUserTableExpr -> E.SqlExpr (Entity User)
-queryUser = $(sqlIJproj 2 2) . $(sqlLOJproj 5 1)
-
-queryStudyFeatures :: ExamUserTableExpr -> E.SqlExpr (Maybe (Entity StudyFeatures))
-queryStudyFeatures = $(sqlIJproj 3 1) . $(sqlLOJproj 2 2) . $(sqlLOJproj 5 3)
+queryUser = $(sqlIJproj 2 2) . $(sqlLOJproj 6 1)
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 = $(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 = $(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 = $(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 = $(sqlLOJproj 5 4)
+queryExamResult = $(sqlLOJproj 6 5)
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 = _dbrOutput . _1
@@ -88,11 +139,48 @@ resultStudyField = _dbrOutput . _6 . _Just
resultExamOccurrence :: Traversal' ExamUserTableData (Entity ExamOccurrence)
resultExamOccurrence = _dbrOutput . _3 . _Just
+resultExamBonus :: Traversal' ExamUserTableData (Entity ExamBonus)
+resultExamBonus = _dbrOutput . _7 . _Just
+
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 = _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
{ csvEUserSurname :: Maybe Text
@@ -103,24 +191,46 @@ data ExamUserTableCsv = ExamUserTableCsv
, csvEUserDegree :: Maybe Text
, csvEUserSemester :: Maybe Int
, csvEUserOccurrence :: Maybe (CI Text)
- , csvEUserExercisePoints :: Maybe Points
- , csvEUserExerciseNumPasses :: Maybe Int
- , csvEUserExercisePointsMax :: Maybe Points
- , csvEUserExerciseNumPassesMax :: Maybe Int
+ , csvEUserExercisePoints :: Maybe (Maybe Points)
+ , csvEUserExerciseNumPasses :: Maybe (Maybe Int)
+ , csvEUserExercisePointsMax :: Maybe (Maybe Points)
+ , csvEUserExerciseNumPassesMax :: Maybe (Maybe Int)
+ , csvEUserBonus :: Maybe (Maybe Points)
+ , csvEUserExamPartResults :: Map ExamPartNumber (Maybe ExamResultPoints)
, csvEUserExamResult :: Maybe ExamResultPassedGrade
, csvEUserCourseNote :: Maybe Html
}
deriving (Generic)
makeLenses_ ''ExamUserTableCsv
-examUserTableCsvOptions :: Csv.Options
-examUserTableCsvOptions = Csv.defaultOptions { Csv.fieldLabelModifier = camelToPathPiece' 3 }
-
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
- parseNamedRecord csv -- Manually defined awaiting issue #427
+ parseNamedRecord csv
= ExamUserTableCsv
<$> csv .:?? "surname"
<*> csv .:?? "first-name"
@@ -130,36 +240,66 @@ instance FromNamedRecord ExamUserTableCsv where
<*> csv .:?? "degree"
<*> csv .:?? "semester"
<*> csv .:?? "occurrence"
- <*> csv .:?? "exercise-points"
- <*> csv .:?? "exercise-num-passes"
- <*> csv .:?? "exercise-points-max"
- <*> csv .:?? "exercise-num-passes-max"
+ <*> fmap Just (csv .:?? "exercise-points")
+ <*> fmap Just (csv .:?? "exercise-num-passes")
+ <*> fmap Just (csv .:?? "exercise-points-max")
+ <*> fmap Just (csv .:?? "exercise-num-passes-max")
+ <*> fmap Just (csv .:?? "bonus")
+ <*> examPartResults
<*> csv .:?? "exam-result"
<*> csv .:?? "course-note"
-
-instance DefaultOrdered ExamUserTableCsv where
- headerOrder = Csv.genericHeaderOrder examUserTableCsvOptions
+ where
+ examPartResults = fmap fold . sequence . flip HashMap.mapMaybeWithKey csv $ \pNumber' _ -> do
+ pNumber <- pNumber' ^? csvExamPartHeader
+ return . fmap (singletonMap pNumber ) $ csv .:?? pNumber'
instance CsvColumnsExplained ExamUserTableCsv where
- csvColumnsExplanations = genericCsvColumnsExplanations examUserTableCsvOptions $ Map.fromList
- [ ('csvEUserSurname , MsgCsvColumnExamUserSurname )
- , ('csvEUserFirstName , MsgCsvColumnExamUserFirstName )
- , ('csvEUserName , MsgCsvColumnExamUserName )
- , ('csvEUserMatriculation , MsgCsvColumnExamUserMatriculation )
- , ('csvEUserField , MsgCsvColumnExamUserField )
- , ('csvEUserDegree , MsgCsvColumnExamUserDegree )
- , ('csvEUserSemester , MsgCsvColumnExamUserSemester )
- , ('csvEUserOccurrence , MsgCsvColumnExamUserOccurrence )
- , ('csvEUserExercisePoints , MsgCsvColumnExamUserExercisePoints )
- , ('csvEUserExerciseNumPasses , MsgCsvColumnExamUserExercisePasses )
- , ('csvEUserExercisePointsMax , MsgCsvColumnExamUserExercisePointsMax )
- , ('csvEUserExerciseNumPassesMax, MsgCsvColumnExamUserExercisePassesMax )
- , ('csvEUserExamResult , MsgCsvColumnExamUserResult )
- , ('csvEUserCourseNote , MsgCsvColumnExamUserCourseNote )
+ csvColumnsExplanations _ = mconcat
+ [ single "surname" MsgCsvColumnExamUserSurname
+ , single "first-name" MsgCsvColumnExamUserFirstName
+ , single "name" MsgCsvColumnExamUserName
+ , single "matriculation" MsgCsvColumnExamUserMatriculation
+ , single "field" MsgCsvColumnExamUserField
+ , single "degree" MsgCsvColumnExamUserDegree
+ , single "semester" MsgCsvColumnExamUserSemester
+ , single "occurrence" MsgCsvColumnExamUserOccurrence
+ , single "exercise-points" MsgCsvColumnExamUserExercisePoints
+ , single "exercise-num-passes" MsgCsvColumnExamUserExercisePasses
+ , single "exercise-points-max" MsgCsvColumnExamUserExercisePointsMax
+ , single "exercise-num-passes-max" MsgCsvColumnExamUserExercisePassesMax
+ , single "bonus" MsgCsvColumnExamUserBonus
+ , 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
| ExamUserAssignOccurrence
+ | ExamUserAcceptComputedResult
+ | ExamUserResetToComputedResult
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic, Typeable)
instance Universe ExamUserAction
@@ -169,13 +309,21 @@ embedRenderMessage ''UniWorX ''ExamUserAction id
data ExamUserActionData = ExamUserDeregisterData
| ExamUserAssignOccurrenceData (Maybe ExamOccurrenceId)
+ | ExamUserAcceptComputedResultData
+ | ExamUserResetToComputedResultData
+ { examUserResetBonus :: Bool
+ }
data ExamUserCsvActionClass
= ExamUserCsvCourseRegister
| ExamUserCsvRegister
| ExamUserCsvAssignOccurrence
| ExamUserCsvSetCourseField
+ | ExamUserCsvSetPartResult
+ | ExamUserCsvSetBonus
+ | ExamUserCsvOverrideBonus
| ExamUserCsvSetResult
+ | ExamUserCsvOverrideResult
| ExamUserCsvSetCourseNote
| ExamUserCsvDeregister
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable)
@@ -202,8 +350,19 @@ data ExamUserCsvAction
| ExamUserCsvDeregisterData
{ examUserCsvActRegistration :: ExamRegistrationId
}
- | ExamUserCsvSetResultData
+ | ExamUserCsvSetPartResultData
{ examUserCsvActUser :: UserId
+ , examUserCsvActExamPart :: ExamPartNumber
+ , examUserCsvActExamPartResult :: Maybe ExamResultPoints
+ }
+ | ExamUserCsvSetBonusData
+ { examUserCsvIsBonusOverride :: Bool
+ , examUserCsvActUser :: UserId
+ , examUserCsvActExamBonus :: Maybe Points
+ }
+ | ExamUserCsvSetResultData
+ { examUserCsvIsResultOverride :: Bool
+ , examUserCsvActUser :: UserId
, examUserCsvActExamResult :: Maybe ExamResultPassedGrade
}
| ExamUserCsvSetCourseNoteData
@@ -230,73 +389,148 @@ embedRenderMessage ''UniWorX ''ExamUserCsvException id
getEUsersR, postEUsersR :: TermId -> SchoolId -> CourseShorthand -> ExamName -> Handler Html
getEUsersR = postEUsersR
postEUsersR tid ssh csh examn = do
- (registrationResult, examUsersTable) <- runDB $ do
- exam@(Entity eid Exam{..}) <- fetchExam tid ssh csh examn
+ (((Any computedValues, registrationResult), examUsersTable), Entity eId examVal, bonus) <- runDB $ do
+ exam@(Entity eid examVal@Exam{..}) <- fetchExam tid ssh csh examn
+ examParts <- selectList [ExamPartExam ==. eid] [Asc ExamPartName]
bonus <- examBonus exam
let
+ allBoni :: SheetGradeSummary
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 = 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)
+
let
examUsersDBTable = DBTable{..}
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
- E.on $ courseUserNote E.?. CourseUserNoteUser E.==. E.just (user E.^. UserId)
- 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 $ 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, examResult, courseUserNote)
+ dbtSQLQuery = runReaderT $ do
+ examRegistration <- asks queryExamRegistration
+ user <- asks queryUser
+ occurrence <- asks queryExamOccurrence
+ courseParticipant <- asks queryCourseParticipant
+ studyFeatures <- asks queryStudyFeatures
+ studyDegree <- asks queryStudyDegree
+ studyField <- asks queryStudyField
+ examBonus' <- asks queryExamBonus
+ examResult <- asks queryExamResult
+ courseUserNote <- asks queryCourseNote
+
+ lift $ do
+ E.on $ courseUserNote E.?. CourseUserNoteUser E.==. E.just (user E.^. UserId)
+ 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)
- 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
- [ 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 colUserMatriclenr
, pure $ colField resultStudyField
, pure $ colDegreeShort resultStudyDegree
, pure $ colFeaturesSemester resultStudyFeatures
, pure $ sortable (Just "occurrence") (i18nCell MsgExamOccurrence) $ maybe mempty (anchorCell' (\n -> CExamR tid ssh csh examn EShowR :#: [st|exam-occurrence__#{n}|]) id . examOccurrenceName . entityVal) . view _userTableOccurrence
- , guardOn showPasses $ sortable Nothing (i18nCell MsgAchievedPasses) $ \(view $ resultUser . _entityKey -> uid) -> fromMaybe mempty $ do
- SheetGradeSummary{achievedPasses} <- examBonusAchieved uid bonus
- SheetGradeSummary{numSheetsPasses} <- examBonusPossible uid bonus
- return $ propCell (getSum achievedPasses) (getSum numSheetsPasses)
- , guardOn showPoints $ sortable Nothing (i18nCell MsgAchievedPoints) $ \(view $ resultUser . _entityKey -> uid) -> fromMaybe mempty $ do
- SheetGradeSummary{achievedPoints} <- examBonusAchieved uid bonus
- SheetGradeSummary{sumSheetsPoints} <- examBonusPossible uid bonus
- return $ propCell (getSum achievedPoints) (getSum sumSheetsPoints)
- , guardOn examShowGrades $ sortable (Just "result") (i18nCell MsgExamResult) $ maybe mempty i18nCell . preview (resultExamResult . _entityVal . _examResultResult)
- , guardOn (not examShowGrades) $ sortable (Just "result-bool") (i18nCell MsgExamResult) $ maybe mempty i18nCell . preview (resultExamResult . _entityVal . _examResultResult . to (over _examResult $ view passingGrade))
+ , guardOn showPasses $ sortable Nothing (i18nCell MsgAchievedPasses) $ \(view $ resultUser . _entityKey -> uid) ->
+ let SheetGradeSummary{achievedPasses} = examBonusAchieved uid bonus
+ SheetGradeSummary{numSheetsPasses} = examBonusPossible uid bonus
+ in propCell (getSum achievedPasses) (getSum numSheetsPasses)
+ , guardOn showPoints $ sortable Nothing (i18nCell MsgAchievedPoints) $ \(view $ resultUser . _entityKey -> uid) ->
+ let SheetGradeSummary{achievedPoints} = examBonusAchieved uid bonus
+ SheetGradeSummary{sumSheetsPoints} = examBonusPossible uid bonus
+ in propCell (getSum achievedPoints) (getSum sumSheetsPoints)
+ , guardOn doBonus $ sortable (Just "bonus") (i18nCell MsgExamBonusAchieved) . automaticCell $ resultExamBonus . _entityVal . _examBonusBonus . to Right <> resultAutomaticExamBonus' . to Left
+ , 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))
-> bool mempty (anchorCellM (CourseR tid ssh csh . CUserR <$> encrypt uid) $ hasComment True) hasNote
]
- dbtSorting = Map.fromList
- [ sortUserNameLink queryUser
- , sortUserMatriclenr queryUser
- , sortField queryStudyField
- , sortDegreeShort queryStudyDegree
- , sortFeaturesSemester queryStudyFeatures
- , ("occurrence", SortColumn $ queryExamOccurrence >>> (E.?. ExamOccurrenceName))
- , ("result", SortColumn $ queryExamResult >>> (E.?. ExamResultResult))
- , ("result-bool", SortColumn $ queryExamResult >>> (E.?. ExamResultResult) >>> E.orderByList [Just ExamVoided, Just ExamNoShow, Just $ ExamAttended Grade50])
- , ("note", SortColumn $ queryCourseNote >>> \note -> -- sort by last edit date
+ dbtSorting = mconcat
+ [ uncurry singletonMap $ sortUserNameLink queryUser
+ , uncurry singletonMap $ sortUserMatriclenr queryUser
+ , uncurry singletonMap $ sortField queryStudyField
+ , uncurry singletonMap $ sortDegreeShort queryStudyDegree
+ , uncurry singletonMap $ sortFeaturesSemester queryStudyFeatures
+ , mconcat
+ [ singletonMap (fromText [st|part-#{toPathPiece examPartNumber}|]) . SortColumn . queryExamPart epId $ \_ examPartResult -> return $ examPartResult E.?. ExamPartResultResult
+ | Entity epId ExamPart{..} <- examParts
+ ]
+ , 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.where_ $ note E.?. CourseUserNoteId E.==. E.just (edit E.^. CourseUserNoteEditNote)
return . E.max_ $ edit E.^. CourseUserNoteEditTime
- )
]
dbtFilter = Map.fromList
[ fltrUserNameEmail queryUser
@@ -339,40 +573,55 @@ postEUsersR tid ssh csh examn = do
, dbParamsFormAdditional = \csrf -> do
let
actionMap :: Map ExamUserAction (AForm Handler ExamUserActionData)
- actionMap = Map.fromList
- [ ( ExamUserDeregister
- , pure ExamUserDeregisterData
- )
- , ( ExamUserAssignOccurrence
- , ExamUserAssignOccurrenceData
+ actionMap = mconcat
+ [ singletonMap ExamUserDeregister $
+ pure ExamUserDeregisterData
+ , singletonMap ExamUserAssignOccurrence $
+ ExamUserAssignOccurrenceData
<$> 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
let formRes = (, mempty) . First . Just <$> res
return (formRes, formWgt)
- , dbParamsFormEvaluate = liftHandlerT . runFormPost
- , dbParamsFormResult = id
+ , dbParamsFormEvaluate = liftHandler . runFormPost
+ , dbParamsFormResult = _2
, dbParamsFormIdent = def
}
dbtIdent :: Text
dbtIdent = "exam-users"
- dbtCsvEncode :: DBTCsvEncode ExamUserTableData ExamUserTableCsv
- dbtCsvEncode = DictJust . C.map $ ExamUserTableCsv
- <$> view (resultUser . _entityVal . _userSurname . to Just)
- <*> view (resultUser . _entityVal . _userFirstName . to Just)
- <*> view (resultUser . _entityVal . _userDisplayName . to Just)
- <*> view (resultUser . _entityVal . _userMatrikelnummer)
- <*> preview (resultStudyField . _entityVal . to (\StudyTerms{..} -> studyTermsName <|> studyTermsShorthand <|> Just (tshow studyTermsKey)) . _Just)
- <*> preview (resultStudyDegree . _entityVal . to (\StudyDegree{..} -> studyDegreeName <|> studyDegreeShorthand <|> Just (tshow studyDegreeKey)) . _Just)
- <*> preview (resultStudyFeatures . _entityVal . _studyFeaturesSemester)
- <*> preview (resultExamOccurrence . _entityVal . _examOccurrenceName)
- <*> preview (resultUser . _entityKey . to (examBonusAchieved ?? bonus) . _Just . _achievedPoints . _Wrapped)
- <*> preview (resultUser . _entityKey . to (examBonusAchieved ?? bonus) . _Just . _achievedPasses . _Wrapped . integral)
- <*> preview (resultUser . _entityKey . to (examBonusPossible ?? bonus) . _Just . _sumSheetsPoints . _Wrapped)
- <*> preview (resultUser . _entityKey . to (examBonusPossible ?? bonus) . _Just . _numSheetsPasses . _Wrapped . integral)
- <*> preview (resultExamResult . _entityVal . _examResultResult . to resultView)
- <*> preview (resultCourseNote . _entityVal . _courseUserNoteNote)
+ dbtCsvEncode = Just DBTCsvEncode
+ { dbtCsvExportForm = pure ()
+ , dbtCsvDoEncode = \() -> C.map (doEncode' . view _2)
+ , dbtCsvName = unpack csvName
+ , dbtCsvNoExportData = Just id
+ , dbtCsvHeader = const . return . examUserTableCsvHeader allBoni doBonus $ examParts ^.. folded . _entityVal . _examPartNumber
+ }
+ where
+ doEncode' = ExamUserTableCsv
+ <$> view (resultUser . _entityVal . _userSurname . to Just)
+ <*> view (resultUser . _entityVal . _userFirstName . to Just)
+ <*> view (resultUser . _entityVal . _userDisplayName . to Just)
+ <*> view (resultUser . _entityVal . _userMatrikelnummer)
+ <*> preview (resultStudyField . _entityVal . to (\StudyTerms{..} -> studyTermsName <|> studyTermsShorthand <|> Just (tshow studyTermsKey)) . _Just)
+ <*> preview (resultStudyDegree . _entityVal . to (\StudyDegree{..} -> studyDegreeName <|> studyDegreeShorthand <|> Just (tshow studyDegreeKey)) . _Just)
+ <*> preview (resultStudyFeatures . _entityVal . _studyFeaturesSemester)
+ <*> preview (resultExamOccurrence . _entityVal . _examOccurrenceName)
+ <*> 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
{ dbtCsvRowKey = \csv -> do
uid <- lift $ view _2 <$> guessUser csv
@@ -381,20 +630,28 @@ postEUsersR tid ssh csh examn = do
DBCsvDiffMissing{dbCsvOldKey}
-> yield . ExamUserCsvDeregisterData $ E.unValue dbCsvOldKey
DBCsvDiffNew{dbCsvNewKey = Just _}
- -> fail "An UniqueExamRegistration could be found, but the ExamRegistrationKey is not among the existing keys"
+ -> error "An UniqueExamRegistration could be found, but the ExamRegistrationKey is not among the existing keys"
DBCsvDiffNew{dbCsvNewKey = Nothing, dbCsvNew} -> do
(isPart, uid) <- lift $ guessUser dbCsvNew
if
| isPart -> do
yieldM $ ExamUserCsvRegisterData <$> pure uid <*> lookupOccurrence dbCsvNew
newFeatures <- lift $ lookupStudyFeatures dbCsvNew
- Just (Entity cpId CourseParticipant{ courseParticipantField = oldFeatures }) <- lift . getBy $ UniqueParticipant uid examCourse
+ Entity cpId CourseParticipant{ courseParticipantField = oldFeatures } <- lift . getJustBy $ UniqueParticipant uid examCourse
when (newFeatures /= oldFeatures) $
yield $ ExamUserCsvSetCourseFieldData cpId newFeatures
| otherwise ->
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) $
- yield . ExamUserCsvSetResultData uid $ csvEUserExamResult dbCsvNew
+ yield . ExamUserCsvSetResultData False uid $ csvEUserExamResult dbCsvNew
note <- lift . getBy $ UniqueCourseUserNote uid examCourse
when (csvEUserCourseNote dbCsvNew /= note ^? _Just . _entityVal . _courseUserNoteNote) $
@@ -406,11 +663,56 @@ postEUsersR tid ssh csh examn = do
newFeatures <- lift $ lookupStudyFeatures dbCsvNew
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
- when (csvEUserExamResult dbCsvNew /= dbCsvOld ^? resultExamResult . _entityVal . _examResultResult . to resultView) $
- yield . ExamUserCsvSetResultData (dbCsvOld ^. resultUser . _entityKey) $ csvEUserExamResult dbCsvNew
+ let uid = dbCsvOld ^. resultUser . _entityKey
+
+ 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 ()
+ _ | Just ExamBonusManual{} <- examBonusRule
+ -> yield $ ExamUserCsvSetBonusData False uid newBonus
+ 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) $
yield . ExamUserCsvSetCourseNoteData (dbCsvOld ^. resultUser . _entityKey) $ csvEUserCourseNote dbCsvNew
@@ -420,7 +722,13 @@ postEUsersR tid ssh csh examn = do
ExamUserCsvDeregisterData{} -> ExamUserCsvDeregister
ExamUserCsvAssignOccurrenceData{} -> ExamUserCsvAssignOccurrence
ExamUserCsvSetCourseFieldData{} -> ExamUserCsvSetCourseField
- ExamUserCsvSetResultData{} -> ExamUserCsvSetResult
+ ExamUserCsvSetPartResultData{} -> ExamUserCsvSetPartResult
+ ExamUserCsvSetBonusData{..}
+ | examUserCsvIsBonusOverride -> ExamUserCsvOverrideBonus
+ | otherwise -> ExamUserCsvSetBonus
+ ExamUserCsvSetResultData{..}
+ | examUserCsvIsResultOverride -> ExamUserCsvOverrideResult
+ | otherwise -> ExamUserCsvSetResult
ExamUserCsvSetCourseNoteData{} -> ExamUserCsvSetCourseNote
, dbtCsvCoarsenActionClass = \case
ExamUserCsvCourseRegister -> DBCsvActionNew
@@ -436,8 +744,9 @@ postEUsersR tid ssh csh examn = do
, courseParticipantUser = examUserCsvActUser
, courseParticipantRegistration = now
, courseParticipantField = examUserCsvActCourseField
- , courseParticipantAllocated = False
+ , courseParticipantAllocated = Nothing
}
+ queueDBJob . JobQueueNotification $ NotificationCourseRegistered examUserCsvActUser examCourse
audit $ TransactionCourseParticipantEdit examCourse examUserCsvActUser
insert_ ExamRegistration
{ examRegistrationExam = eid
@@ -461,6 +770,34 @@ postEUsersR tid ssh csh examn = do
update examUserCsvActCourseParticipant [ CourseParticipantField =. examUserCsvActCourseField ]
CourseParticipant{..} <- getJust examUserCsvActCourseParticipant
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
Nothing -> do
deleteBy $ UniqueExamResult eid examUserCsvActUser
@@ -490,13 +827,13 @@ postEUsersR tid ssh csh examn = do
delete nid
ExamUserCsvSetCourseNoteData{ examUserCsvActCourseNote = Just note, .. } -> do
now <- liftIO getCurrentTime
- uid <- liftHandlerT requireAuthId
+ uid <- liftHandler requireAuthId
Entity nid _ <- upsert (CourseUserNote examCourse examUserCsvActUser note) [ CourseUserNoteNote =. note ]
insert_ $ CourseUserNoteEdit uid now nid
return $ CExamR tid ssh csh examn EUsersR
, dbtCsvRenderKey = \(registeredUserName -> registeredUserName') -> \case
ExamUserCsvCourseRegisterData{..} -> do
- (User{..}, occ) <- liftHandlerT . runDB $ (,) <$> getJust examUserCsvActUser <*> for examUserCsvActOccurrence getJust
+ (User{..}, occ) <- liftHandler . runDB $ (,) <$> getJust examUserCsvActUser <*> for examUserCsvActOccurrence getJust
[whamlet|
$newline never
^{nameWidget userDisplayName userSurname}
@@ -510,7 +847,7 @@ postEUsersR tid ssh csh examn = do
\ (_{MsgExamNoOccurrence})
|]
ExamUserCsvRegisterData{..} -> do
- (User{..}, occ) <- liftHandlerT . runDB $ (,) <$> getJust examUserCsvActUser <*> for examUserCsvActOccurrence getJust
+ (User{..}, occ) <- liftHandler . runDB $ (,) <$> getJust examUserCsvActUser <*> for examUserCsvActOccurrence getJust
[whamlet|
$newline never
^{nameWidget userDisplayName userSurname}
@@ -520,7 +857,7 @@ postEUsersR tid ssh csh examn = do
\ (_{MsgExamNoOccurrence})
|]
ExamUserCsvAssignOccurrenceData{..} -> do
- occ <- for examUserCsvActOccurrence $ liftHandlerT . runDB . getJust
+ occ <- for examUserCsvActOccurrence $ liftHandler . runDB . getJust
[whamlet|
$newline never
^{registeredUserName' examUserCsvActRegistration}
@@ -530,7 +867,7 @@ postEUsersR tid ssh csh examn = do
\ (_{MsgExamNoOccurrence})
|]
ExamUserCsvSetCourseFieldData{..} -> do
- User{..} <- liftHandlerT . runDB $ getJust . courseParticipantUser =<< getJust examUserCsvActCourseParticipant
+ User{..} <- liftHandler . runDB $ getJust . courseParticipantUser =<< getJust examUserCsvActCourseParticipant
[whamlet|
$newline never
^{nameWidget userDisplayName userSurname}
@@ -539,8 +876,34 @@ postEUsersR tid ssh csh examn = do
$nothing
, _{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
- User{..} <- liftHandlerT . runDB $ getJust examUserCsvActUser
+ User{..} <- liftHandler . runDB $ getJust examUserCsvActUser
[whamlet|
$newline never
^{nameWidget userDisplayName userSurname}
@@ -550,7 +913,7 @@ postEUsersR tid ssh csh examn = do
, _{MsgExamResultNone}
|]
ExamUserCsvSetCourseNoteData{..} -> do
- User{..} <- liftHandlerT . runDB $ getJust examUserCsvActUser
+ User{..} <- liftHandler . runDB $ getJust examUserCsvActUser
[whamlet|
$newline never
^{nameWidget userDisplayName userSurname}
@@ -571,22 +934,31 @@ postEUsersR tid ssh csh examn = do
guessUser :: ExamUserTableCsv -> DB (Bool, UserId)
guessUser ExamUserTableCsv{..} = $cachedHereBinary (csvEUserMatriculation, csvEUserName, csvEUserSurname) $ do
users <- E.select . E.from $ \user -> do
- E.where_ . E.and $ catMaybes
+ E.where_ . E.or $ catMaybes
[ (user E.^. UserMatrikelnummer E.==.) . E.val . Just <$> csvEUserMatriculation
- , (user E.^. UserDisplayName E.==.) . E.val <$> csvEUserName
- , (user E.^. UserSurname E.==.) . E.val <$> csvEUserSurname
- , (user E.^. UserFirstName E.==.) . E.val <$> csvEUserFirstName
+ , (user E.^. UserDisplayName `E.hasInfix`) . E.val <$> csvEUserName
+ , (user E.^. UserSurname `E.hasInfix`) . E.val <$> csvEUserSurname
+ , (user E.^. UserFirstName `E.hasInfix`) . E.val <$> csvEUserFirstName
]
let isCourseParticipant = E.exists . E.from $ \courseParticipant ->
E.where_ $ courseParticipant E.^. CourseParticipantCourse E.==. E.val examCourse
E.&&. courseParticipant E.^. CourseParticipantUser E.==. user E.^. UserId
- E.limit 2
- return (isCourseParticipant, user E.^. UserId)
- case users of
- (filter . view $ _1 . _Value -> [(E.Value isPart, E.Value uid)])
- -> return (isPart, uid)
- [(E.Value isPart, E.Value uid)]
- -> return (isPart, uid)
+ return (isCourseParticipant, user)
+ let users' = reverse $ sortBy closeness users
+ closeness :: (E.Value Bool, Entity User) -> (E.Value Bool, Entity User) -> Ordering
+ closeness = mconcat $ catMaybes
+ [ pure $ comparing (preview $ _2 . _entityVal . _userMatrikelnummer . only csvEUserMatriculation)
+ , pure $ comparing (view _1)
+ , csvEUserSurname <&> \surn -> comparing (preview $ _2 . _entityVal . _userSurname . to CI.mk . only (CI.mk surn))
+ , csvEUserFirstName <&> \firstn -> comparing (preview $ _2 . _entityVal . _userFirstName . to CI.mk . only (CI.mk firstn))
+ , csvEUserName <&> \dispn -> comparing (preview $ _2 . _entityVal . _userDisplayName . to CI.mk . only (CI.mk dispn))
+ ]
+ case users' of
+ [(E.Value isPart, Entity uid _)]
+ -> return (isPart, uid)
+ (x@(E.Value isPart, Entity uid _) : x' : _)
+ | GT <- x `closeness` x'
+ -> return (isPart, uid)
_other
-> throwM ExamUserCsvExceptionNoMatchingUser
@@ -650,21 +1022,21 @@ postEUsersR tid ssh csh examn = do
examUsersDBTableValidator = def & defaultSorting [SortAscBy "user-name"]
& 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
(First (Just act), regMap) <- inp
- let regSet = Map.keysSet . Map.filter id $ getDBFormResult (const False) regMap
- return (act, regSet)
- over _1 postprocess <$> dbTable examUsersDBTableValidator examUsersDBTable
+ let regMap' = Map.mapMaybe (uncurry guardOn) $ getDBFormResult (False,) regMap
+ return (act, regMap')
+ (, exam, bonus) . over (_1 . _2) postprocess <$> dbTable examUsersDBTableValidator examUsersDBTable
formResult registrationResult $ \case
- (ExamUserDeregisterData, selectedRegistrations) -> do
+ (ExamUserDeregisterData, Map.keysSet -> selectedRegistrations) -> do
nrDel <- runDB $ deleteWhereCount
[ ExamRegistrationId <-. Set.toList selectedRegistrations
]
addMessageI Success $ MsgExamUsersDeregistered nrDel
redirect $ CExamR tid ssh csh examn EUsersR
- (ExamUserAssignOccurrenceData occId, selectedRegistrations) -> do
+ (ExamUserAssignOccurrenceData occId, Map.keysSet -> selectedRegistrations) -> do
nrUpdated <- runDB $ updateWhereCount
[ ExamRegistrationId <-. Set.toList selectedRegistrations
]
@@ -672,7 +1044,67 @@ postEUsersR tid ssh csh examn = do
]
addMessageI Success $ MsgExamUsersOccurrenceUpdated nrUpdated
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
siteLayoutMsg (prependCourseTitle tid ssh csh MsgExamUsersHeading) $ do
setTitleI $ prependCourseTitle tid ssh csh MsgExamUsersHeading
+ let computedValuesTip = $(i18nWidgetFile "exam-users/computed-values-tip")
$(widgetFile "exam-users")
diff --git a/src/Handler/ExamOffice.hs b/src/Handler/ExamOffice.hs
new file mode 100644
index 000000000..5ad3a8bda
--- /dev/null
+++ b/src/Handler/ExamOffice.hs
@@ -0,0 +1,8 @@
+module Handler.ExamOffice
+ ( module Handler.ExamOffice
+ ) where
+
+import Handler.ExamOffice.Exams as Handler.ExamOffice
+import Handler.ExamOffice.Fields as Handler.ExamOffice
+import Handler.ExamOffice.Users as Handler.ExamOffice
+import Handler.ExamOffice.Exam as Handler.ExamOffice
diff --git a/src/Handler/ExamOffice/Course.hs b/src/Handler/ExamOffice/Course.hs
new file mode 100644
index 000000000..2db5ecf76
--- /dev/null
+++ b/src/Handler/ExamOffice/Course.hs
@@ -0,0 +1,75 @@
+module Handler.ExamOffice.Course
+ ( getCExamOfficeR, postCExamOfficeR
+ ) where
+
+import Import
+
+import qualified Data.Set as Set
+
+import qualified Database.Esqueleto as E
+import qualified Database.Esqueleto.Utils as E
+
+import Handler.Utils.ExamOffice.Course
+import Handler.Utils
+
+
+examOfficeOptOutForm :: UserId -> CourseId -> Maybe (Set SchoolId) -> Form (Set SchoolId)
+-- ^ Deals with sets of _opt outs_
+examOfficeOptOutForm uid cid (fromMaybe Set.empty -> template) = renderWForm FormStandard $ do
+ schools <- liftHandler . runDB . E.select $ courseExamOfficeSchools (E.val uid) (E.val cid)
+
+ 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)
+
+ return $ res <&> setOf (folded . filtered (not . view _2) . _1)
+
+getCExamOfficeR, postCExamOfficeR :: TermId -> SchoolId -> CourseShorthand -> Handler Html
+getCExamOfficeR = postCExamOfficeR
+postCExamOfficeR tid ssh csh = do
+ uid <- requireAuthId
+ isModal <- hasCustomHeader HeaderIsModal
+
+ (cid, optOuts, hasForced) <- runDB $ do
+ cid <- getKeyBy404 (TermSchoolCourseShort tid ssh csh)
+ optOuts <- selectList [ CourseUserExamOfficeOptOutCourse ==. cid, CourseUserExamOfficeOptOutUser ==. uid ] []
+ hasForced <- E.selectExists $ do
+ (_school, isForced) <- courseExamOfficeSchools (E.val uid) (E.val cid)
+ E.where_ isForced
+ return (cid, optOuts, hasForced)
+
+ ((optOutRes, optOutView), optOutEnc)
+ <- runFormPost $ examOfficeOptOutForm uid cid (Just $ setOf (folded . _entityVal . _courseUserExamOfficeOptOutSchool) optOuts )
+
+ formResultModal optOutRes (CourseR tid ssh csh CExamOfficeR) $ \optOuts' -> do
+ lift . runDB $ do
+ deleteWhere [ CourseUserExamOfficeOptOutCourse ==. cid
+ , CourseUserExamOfficeOptOutUser ==. uid
+ , CourseUserExamOfficeOptOutSchool /<-. Set.toList optOuts'
+ ]
+ forM_ optOuts' $ \ssh' ->
+ void $ insertUnique CourseUserExamOfficeOptOut
+ { courseUserExamOfficeOptOutCourse = cid
+ , courseUserExamOfficeOptOutUser = uid
+ , courseUserExamOfficeOptOutSchool = ssh'
+ }
+ tell . pure =<< messageI Success MsgExamOfficeOptOutsChanged
+
+
+ let optOutView' = wrapForm optOutView def
+ { formAction = Just . SomeRoute $ CourseR tid ssh csh CExamOfficeR
+ , formEncoding = optOutEnc
+ , formAttrs = [ asyncSubmitAttr | isModal ]
+ }
+
+ siteLayoutMsg MsgMenuCourseExamOffice $ do
+ setTitleI MsgMenuCourseExamOffice
+
+ let explanation = $(i18nWidgetFile "course-exam-office-explanation")
+
+ [whamlet|
+ $newline never
+
+ _{MsgExamOfficeSubscribedFieldsExplanation}
+ ^{fieldsView'}
+ |]
+
diff --git a/src/Handler/ExamOffice/Users.hs b/src/Handler/ExamOffice/Users.hs
new file mode 100644
index 000000000..0a5d3b9bd
--- /dev/null
+++ b/src/Handler/ExamOffice/Users.hs
@@ -0,0 +1,188 @@
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+
+module Handler.ExamOffice.Users
+ ( getEOUsersR, postEOUsersR
+ , getEOUsersInviteR, postEOUsersInviteR
+ ) where
+
+import Import
+import Utils.Form
+import Handler.Utils
+import Handler.Utils.Invitations
+
+import Text.Hamlet (ihamlet)
+import Data.Aeson hiding (Result(..))
+import Jobs.Queue
+
+import qualified Database.Esqueleto as E
+
+import qualified Data.Set as Set
+import qualified Data.Map as Map
+import Data.Map ((!), (!?))
+
+
+instance IsInvitableJunction ExamOfficeUser where
+ type InvitationFor ExamOfficeUser = User
+ data InvitableJunction ExamOfficeUser = JunctionExamOfficeUser
+ deriving (Eq, Ord, Read, Show, Generic, Typeable)
+ data InvitationDBData ExamOfficeUser = InvDBDataExamOfficeUser
+ deriving (Eq, Ord, Read, Show, Generic, Typeable)
+ data InvitationTokenData ExamOfficeUser = InvTokenDataExamOfficeUser
+ { invTokenExamOfficeUserOffice :: CryptoUUIDUser
+ }
+ deriving (Eq, Ord, Read, Show, Generic, Typeable)
+
+ _InvitableJunction = iso
+ (\ExamOfficeUser{..} -> (examOfficeUserUser, examOfficeUserOffice, JunctionExamOfficeUser))
+ (\(examOfficeUserUser, examOfficeUserOffice, JunctionExamOfficeUser) -> ExamOfficeUser{..})
+
+instance ToJSON (InvitableJunction ExamOfficeUser) where
+ toJSON = genericToJSON defaultOptions { fieldLabelModifier = camelToPathPiece' 1 }
+ toEncoding = genericToEncoding defaultOptions { fieldLabelModifier = camelToPathPiece' 1 }
+instance FromJSON (InvitableJunction ExamOfficeUser) where
+ parseJSON = genericParseJSON defaultOptions { fieldLabelModifier = camelToPathPiece' 1 }
+
+instance ToJSON (InvitationDBData ExamOfficeUser) where
+ toJSON = genericToJSON defaultOptions { constructorTagModifier = camelToPathPiece' 3 }
+ toEncoding = genericToEncoding defaultOptions { constructorTagModifier = camelToPathPiece' 3 }
+instance FromJSON (InvitationDBData ExamOfficeUser) where
+ parseJSON = genericParseJSON defaultOptions { constructorTagModifier = camelToPathPiece' 3 }
+
+instance ToJSON (InvitationTokenData ExamOfficeUser) where
+ toJSON = genericToJSON defaultOptions { fieldLabelModifier = camelToPathPiece' 5 }
+ toEncoding = genericToEncoding defaultOptions { fieldLabelModifier = camelToPathPiece' 5 }
+instance FromJSON (InvitationTokenData ExamOfficeUser) where
+ parseJSON = genericParseJSON defaultOptions { fieldLabelModifier = camelToPathPiece' 5 }
+
+examOfficeUserInvitationConfig :: InvitationConfig ExamOfficeUser
+examOfficeUserInvitationConfig = InvitationConfig{..}
+ where
+ invitationRoute _ _ = return $ ExamOfficeR EOUsersInviteR
+ invitationResolveFor InvTokenDataExamOfficeUser{..} = do
+ officeId <- decrypt invTokenExamOfficeUserOffice
+ bool notFound (return officeId) =<< existsKey officeId
+ invitationSubject (Entity _ User{..}) _ = do
+ return . SomeMessage $ MsgMailSubjectExamOfficeUserInvitation userDisplayName
+ invitationHeading (Entity _ User{..}) _ = do
+ return . SomeMessage $ MsgExamOfficeUserInviteHeading userDisplayName
+ invitationExplanation _ _ = return [ihamlet|_{SomeMessage MsgExamOfficeUserInviteExplanation}|]
+ invitationTokenConfig _ _ = do
+ itAuthority <- liftHandler requireAuthId
+ let itExpiresAt = Nothing
+ itStartsAt = Nothing
+ itAddAuth = Nothing
+ return InvitationTokenConfig{..}
+ invitationRestriction _ _ = return Authorized
+ invitationForm _ _ _ = pure (JunctionExamOfficeUser, ())
+ invitationInsertHook _ _ ExamOfficeUser{..} _ act = do
+ res <- act
+ audit $ TransactionExamOfficeUserAdd examOfficeUserOffice examOfficeUserUser
+ return res
+ invitationSuccessMsg _ _ =
+ return $ SomeMessage MsgExamOfficeUserInvitationAccepted
+ invitationUltDest _ _ = return $ SomeRoute HomeR
+
+
+makeExamOfficeUsersForm :: Maybe (Set (Either UserEmail UserId)) -> Form (Set (Either UserEmail UserId))
+makeExamOfficeUsersForm template = renderWForm FormStandard $ do
+ cRoute <- fromMaybe (error "makeExamOfficeUsersForm called from 404-handler") <$> getCurrentRoute
+
+ let
+ miAdd' :: (Text -> Text)
+ -> FieldView UniWorX
+ -> Form ([Either UserEmail UserId] -> FormResult [Either UserEmail UserId])
+ miAdd' nudge btn csrf = do
+ MsgRenderer mr <- getMsgRenderer
+ (addRes, addView) <- mpreq (multiUserField False Nothing) ("" & addName (nudge "users") & addPlaceholder (mr MsgLdapIdentificationOrEmail)) Nothing
+ let
+ res' :: FormResult ([Either UserEmail UserId] -> FormResult [Either UserEmail UserId])
+ res' = addRes <&> \newUsers oldUsers -> if
+ | null newUsers
+ -> pure oldUsers
+ | otherwise
+ -> pure . nub $ oldUsers ++ Set.toList newUsers
+ return (res', $(widgetFile "widgets/massinput/examOfficeUsers/add"))
+ miCell' :: Either UserEmail UserId -> Widget
+ miCell' (Left email) = do
+ invWarnMsg <- messageI Warning MsgEmailInvitationWarning
+ $(widgetFile "widgets/massinput/examOfficeUsers/cellInvitation")
+ miCell' (Right uid) = do
+ User{..} <- liftHandler . runDB $ getJust uid
+ $(widgetFile "widgets/massinput/examOfficeUsers/cellKnown")
+ miButtonAction' :: forall p. PathPiece p => p -> Maybe (SomeRoute UniWorX)
+ miButtonAction' frag = Just . SomeRoute $ cRoute :#: frag
+ miLayout' :: MassInputLayout ListLength _ ()
+ miLayout' lLength _ cellWdgts delButtons addWdgts = $(widgetFile "widgets/massinput/examOfficeUsers/layout")
+ miIdent' :: Text
+ miIdent' = "exam-office-users"
+ fSettings :: FieldSettings UniWorX
+ fSettings = fslI MsgExamOfficeSubscribedUsers
+ & setTooltip MsgExamOfficeSubscribedUsersTip
+ fRequired :: Bool
+ fRequired = False
+
+ template' <- for template $ \uids -> liftHandler . runDB $ do
+ let (invitations, knownUsers) = partitionEithers $ Set.toList uids
+ knownUsers' <- fmap (map E.unValue) . E.select . E.from $ \user -> do
+ E.where_ $ user E.^. UserId `E.in_` E.valList knownUsers
+ E.orderBy [E.asc $ user E.^. UserSurname, E.asc $ user E.^. UserDisplayName]
+ return $ user E.^. UserId
+ return $ map Left invitations ++ map Right knownUsers'
+
+ fmap Set.fromList <$> massInputAccumW miAdd' miCell' miButtonAction' miLayout' miIdent' fSettings fRequired template'
+
+
+-- | Manage the list of users this user (in her function as exam-office)
+-- has an interest in, i.e. that authorize her to view their grades
+getEOUsersR, postEOUsersR :: Handler Html
+getEOUsersR = postEOUsersR
+postEOUsersR = do
+ uid <- requireAuthId
+
+ oldUsers <- liftHandler . runDB $ do
+ users <- E.select . E.from $ \(user `E.InnerJoin` examOfficeUser) -> do
+ E.on $ user E.^. UserId E.==. examOfficeUser E.^. ExamOfficeUserUser
+ E.&&. examOfficeUser E.^. ExamOfficeUserOffice E.==. E.val uid
+ return $ user E.^. UserId
+ invites <- Map.keysSet <$> sourceInvitationsF @ExamOfficeUser uid
+ return $ setOf (folded . _Value . re _Right) users <> Set.mapMonotonic Left invites
+
+ ((usersRes, usersView), usersEnc) <- runFormPost . makeExamOfficeUsersForm $ Just oldUsers
+
+ formResult usersRes $ \(setSymmDiff oldUsers -> changes) -> do
+ liftHandler . runDBJobs . forM_ changes $ \change -> if
+ | change `Set.member` oldUsers -> case change of
+ Right change' -> do
+ deleteBy $ UniqueExamOfficeUser uid change'
+ audit $ TransactionExamOfficeUserDelete uid change'
+ Left change' ->
+ deleteInvitation @ExamOfficeUser uid change'
+ | otherwise -> case change of
+ Right change' -> do
+ insert_ $ ExamOfficeUser uid change'
+ audit $ TransactionExamOfficeUserAdd uid change'
+ Left change' -> do
+ cID <- encrypt uid
+ sinkInvitation examOfficeUserInvitationConfig (change', uid, (InvDBDataExamOfficeUser, InvTokenDataExamOfficeUser cID))
+ addMessageI Success $ MsgTransactionExamOfficeUsersUpdated (Set.size $ changes `Set.intersection` oldUsers) (Set.size $ changes `Set.difference` oldUsers)
+ redirect $ ExamOfficeR EOExamsR
+
+ let
+ usersView' = wrapForm usersView def
+ { formAction = Just . SomeRoute $ ExamOfficeR EOUsersR
+ , formEncoding = usersEnc
+ }
+
+ siteLayoutMsg MsgMenuExamOfficeUsers $ do
+ setTitleI MsgMenuExamOfficeUsers
+
+ [whamlet|
+ $newline never
+
+ _{MsgExamOfficeSubscribedUsersExplanation}
+ ^{usersView'}
+ |]
+
+getEOUsersInviteR, postEOUsersInviteR :: Handler Html
+getEOUsersInviteR = postEOUsersInviteR
+postEOUsersInviteR = invitationR examOfficeUserInvitationConfig
diff --git a/src/Handler/Health.hs b/src/Handler/Health.hs
index dad7ef747..7cb8aa83b 100644
--- a/src/Handler/Health.hs
+++ b/src/Handler/Health.hs
@@ -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)
case waitResult of
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
Just healthReports -> do
let (Max lastUpdated, Min status) = ofoldMap1 (Max *** Min . healthReportStatus) healthReports
diff --git a/src/Handler/Home.hs b/src/Handler/Home.hs
index 30beb116a..0382fab4a 100644
--- a/src/Handler/Home.hs
+++ b/src/Handler/Home.hs
@@ -3,8 +3,6 @@ module Handler.Home where
import Import
import Handler.Utils
-import Handler.Utils.Table.Cells
-
import qualified Data.Map as Map
import Database.Esqueleto.Utils.TH
@@ -60,7 +58,7 @@ homeUpcomingSheets uid = do
, E.Value UTCTime
, E.Value (Maybe SubmissionId)
))
- (DBCell (HandlerT UniWorX IO) ())
+ (DBCell Handler ())
colonnade = mconcat
[ -- dbRow
-- TOOD: sortable (Just "term") (textCell MsgTerm) $ \DBRow{ dbrOutput=(view _1 -> E.Value tid) } ->
@@ -84,7 +82,7 @@ homeUpcomingSheets uid = do
(hasTickmark True)
]
let validator = def & defaultSorting [SortDescBy "done", SortAscBy "deadline"]
- sheetTable <- liftHandlerT . runDB $ dbTableWidget' validator DBTable
+ sheetTable <- liftHandler . runDB $ dbTableWidget' validator DBTable
{ dbtSQLQuery = tableData
, dbtRowKey = \((_ `E.InnerJoin` _ `E.InnerJoin` sheet) `E.LeftOuterJoin` _) -> sheet E.^. SheetId
, dbtColonnade = colonnade
@@ -129,7 +127,7 @@ homeUpcomingSheets uid = do
homeUpcomingExams :: UserId -> Widget
homeUpcomingExams uid = do
now <- liftIO getCurrentTime
- ((Any hasExams, examTable), warningDays) <- liftHandlerT . runDB $ do
+ ((Any hasExams, examTable), warningDays) <- liftHandler . runDB $ do
User {userWarningDays} <- get404 uid
let fortnight = addUTCTime userWarningDays now
let -- code copied and slightly adapted from Handler.Course.getCShowR:
@@ -204,7 +202,7 @@ homeUpcomingExams uid = do
isRegistered <- existsBy $ UniqueExamRegistration eId uid
if
| mayRegister -> do
- (examRegisterForm, examRegisterEnctype) <- liftHandlerT . generateFormPost . buttonForm' $ bool [BtnRegister] [BtnDeregister] isRegistered
+ (examRegisterForm, examRegisterEnctype) <- liftHandler . generateFormPost . buttonForm' $ bool [BtnRegister] [BtnDeregister] isRegistered
return $ wrapForm examRegisterForm def
{ formAction = Just . SomeRoute $ CExamR courseTerm courseSchool courseShorthand examName ERegisterR
, formEncoding = examRegisterEnctype
diff --git a/src/Handler/Info.hs b/src/Handler/Info.hs
index 260f03357..aa98bdac1 100644
--- a/src/Handler/Info.hs
+++ b/src/Handler/Info.hs
@@ -47,4 +47,23 @@ getInfoLecturerR =
siteLayoutMsg' MsgInfoLecturerTitle $ do
setTitleI MsgInfoLecturerTitle
$(i18nWidgetFile "info-lecturer")
-
+ where
+ tooltipNew, tooltipProblem, tooltipPlanned, tooltipNewU2W :: WidgetFor UniWorX ()
+ tooltipNew = [whamlet| _{MsgLecturerInfoTooltipNew} |]
+ tooltipProblem = [whamlet| _{MsgLecturerInfoTooltipProblem} |]
+ tooltipPlanned = [whamlet| _{MsgLecturerInfoTooltipPlanned} |]
+ tooltipNewU2W = [whamlet| _{MsgLecturerInfoTooltipNewU2W} |]
+ newU2WFeat, probFeatInline, plannedFeat, plannedFeatInline :: WidgetFor UniWorX ()
+ newU2WFeat = [whamlet| ^{iconTooltip tooltipNew (Just IconAnnounce) True} |] -- to be used inside text blocks
+ probFeatInline = [whamlet| ^{iconTooltip tooltipProblem (Just IconProblem) True} |] -- to be used inside text blocks
+ plannedFeat = [whamlet| ^{iconTooltip tooltipPlanned (Just IconPlanned) False} |]
+ plannedFeatInline = [whamlet| ^{iconTooltip tooltipPlanned (Just IconPlanned) True} |] -- to be used inside text blocks
+
+ -- new feature with given introduction date
+ newFeat :: Integer -> Int -> Int -> WidgetFor UniWorX ()
+ newFeat year month day = do
+ currentTime <- liftIO getCurrentTime
+ let expiryTime = UTCTime (addGregorianMonthsRollOver 1 $ fromGregorian year month day) 0
+ if currentTime > expiryTime
+ then mempty
+ else toWidget [whamlet| ^{iconTooltip tooltipNewU2W (Just IconNew) False} |]
diff --git a/src/Handler/Material.hs b/src/Handler/Material.hs
index 791475e71..740ed670d 100644
--- a/src/Handler/Material.hs
+++ b/src/Handler/Material.hs
@@ -17,8 +17,6 @@ import Database.Esqueleto.Utils.TH
import Utils.Form
import Handler.Utils
import Handler.Utils.Delete
-import Handler.Utils.Table.Cells
-import Handler.Utils.Table.Columns
import Control.Monad.Writer (MonadWriter(..), execWriterT)
@@ -30,7 +28,7 @@ data MaterialForm = MaterialForm
, mfType :: Maybe (CI Text)
, mfDescription :: Maybe Html
, mfVisibleFrom :: Maybe UTCTime
- , mfFiles :: Maybe (Source Handler (Either FileId File))
+ , mfFiles :: Maybe (ConduitT () (Either FileId File) Handler ())
}
makeMaterialForm :: CourseId -> Maybe MaterialForm -> Form MaterialForm
@@ -42,7 +40,7 @@ makeMaterialForm cid template = identifyForm FIDmaterial $ \html -> do
| Just source <- template >>= mfFiles
= runConduit $ source .| C.foldMap setIds
| otherwise = return Set.empty
- typeOptions :: HandlerT UniWorX IO (OptionList (CI Text))
+ typeOptions :: HandlerFor UniWorX (OptionList (CI Text))
typeOptions = do
let defaults = Set.fromList $ map (CI.mk . mr) [MsgMaterialTypeSlides,MsgMaterialTypeCode,MsgMaterialTypeExample]
previouslyUsed <- runDB $
@@ -63,7 +61,7 @@ makeMaterialForm cid template = identifyForm FIDmaterial $ \html -> do
flip (renderAForm FormStandard) html $ MaterialForm
<$> areq (textField & cfStrip & cfCI) (fslI MsgMaterialName) (mfName <$> template)
- <*> aopt (textField & cfStrip & cfCI & addDatalist typeOptions)
+ <*> aopt (textField & cfStrip & guardField (not . null) & cfCI & addDatalist typeOptions)
(fslpI MsgMaterialType $ mr MsgMaterialTypePlaceholder)
(mfType <$> template)
<*> aopt htmlField (fslpI MsgMaterialDescription "Html")
@@ -79,8 +77,8 @@ getMaterialKeyBy404 tid ssh csh mnm = do
getKeyBy404 $ UniqueMaterial cid mnm
fetchMaterial :: TermId -> SchoolId -> CourseShorthand -> MaterialName -> DB (Entity Material)
-fetchMaterial tid ssh csh mnm = do
- [matEnt] <- E.select . E.from $ -- uniqueness guaranteed by DB constraints
+fetchMaterial tid ssh csh mnm =
+ maybe notFound return . listToMaybe <=< E.select . E.from $ -- uniqueness guaranteed by DB constraints
\(course `E.InnerJoin` material) -> do
E.on $ course E.^. CourseId E.==. material E.^. MaterialCourse
E.where_ $ course E.^. CourseTerm E.==. E.val tid
@@ -88,7 +86,6 @@ fetchMaterial tid ssh csh mnm = do
E.&&. course E.^. CourseShorthand E.==. E.val csh
E.&&. material E.^. MaterialName E.==. E.val mnm
return material
- return matEnt
getMaterialListR :: TermId -> SchoolId -> CourseShorthand -> Handler Html
@@ -247,7 +244,7 @@ postMEditR tid ssh csh mnm = do
E.on $ matFile E.^. MaterialFileFile E.==. file E.^. FileId
E.where_ $ matFile E.^. MaterialFileMaterial E.==. E.val (entityKey matEnt)
return $ file E.^. FileId
- return (matEnt, (Left . E.unValue) <$> fileIds)
+ return (matEnt, Left . E.unValue <$> fileIds)
-- let cid = materialCourse
let template = Just MaterialForm
{ mfName = materialName
@@ -310,14 +307,14 @@ handleMaterialEdit tid ssh csh cid template dbMaterial = do
when saveOk $ redirect -- redirect must happen outside of runDB
$ 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
oldFileIdVals <- E.select . E.from $ \(file `E.InnerJoin` materialFile) -> do
E.on $ materialFile E.^. MaterialFileFile E.==. file E.^. FileId
E.where_ $ materialFile E.^. MaterialFileMaterial E.==. E.val mid
return $ file E.^. FileId
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)
where
finsert (Left fileId) = tell $ singleton fileId
diff --git a/src/Handler/Profile.hs b/src/Handler/Profile.hs
index b4a38f4f3..9487e9b55 100644
--- a/src/Handler/Profile.hs
+++ b/src/Handler/Profile.hs
@@ -1,9 +1,17 @@
-module Handler.Profile where
+module Handler.Profile
+ ( getProfileR, postProfileR
+ , getProfileDataR, makeProfileData
+ , getAuthPredsR, postAuthPredsR
+ , getUserNotificationR, postUserNotificationR
+ , getSetDisplayEmailR, postSetDisplayEmailR
+ , getCsvOptionsR, postCsvOptionsR
+ ) where
import Import
import Handler.Utils
-import Handler.Utils.Table.Cells
+import Handler.Utils.Profile
+import Handler.Utils.Tokens
-- import Colonnade hiding (fromMaybe, singleton)
-- import Yesod.Colonnade
@@ -17,25 +25,33 @@ import qualified Database.Esqueleto.Utils as E
import qualified Data.CaseInsensitive as CI
+import Jobs
+
data SettingsForm = SettingsForm
- { stgMaxFavourties :: Int
- , stgTheme :: Theme
- , stgDateTime :: DateTimeFormat
- , stgDate :: DateTimeFormat
- , stgTime :: DateTimeFormat
- , stgDownloadFiles :: Bool
- , stgWarningDays :: NominalDiffTime
- , stgSchools :: Set SchoolId
+ { stgDisplayName :: UserDisplayName
+ , stgDisplayEmail :: UserEmail
+ , stgMaxFavourites :: Int
+ , stgMaxFavouriteTerms :: Int
+ , stgTheme :: Theme
+ , stgDateTime :: DateTimeFormat
+ , stgDate :: DateTimeFormat
+ , stgTime :: DateTimeFormat
+ , stgDownloadFiles :: Bool
+ , stgWarningDays :: NominalDiffTime
+ , stgSchools :: Set SchoolId
, stgNotificationSettings :: NotificationSettings
}
+makeLenses_ ''SettingsForm
data NotificationTriggerKind
= NTKAll
| NTKCourseParticipant
| NTKExamParticipant
| NTKCorrector
+ | NTKCourseLecturer
| NTKAllocationStaff
+ | NTKAllocationParticipant
| NTKFunctionary SchoolFunction
deriving (Eq, Ord, Generic, Typeable)
deriveFinite ''NotificationTriggerKind
@@ -46,7 +62,9 @@ instance RenderMessage UniWorX NotificationTriggerKind where
NTKCourseParticipant -> mr MsgNotificationTriggerKindCourseParticipant
NTKExamParticipant -> mr MsgNotificationTriggerKindExamParticipant
NTKCorrector -> mr MsgNotificationTriggerKindCorrector
+ NTKCourseLecturer -> mr MsgNotificationTriggerKindCourseLecturer
NTKAllocationStaff -> mr MsgNotificationTriggerKindAllocationStaff
+ NTKAllocationParticipant -> mr MsgNotificationTriggerKindAllocationParticipant
NTKFunctionary SchoolAdmin -> mr MsgNotificationTriggerKindAdmin
NTKFunctionary SchoolLecturer -> mr MsgNotificationTriggerKindLecturer
NTKFunctionary SchoolExamOffice -> mr MsgNotificationTriggerKindExamOffice
@@ -58,9 +76,14 @@ instance RenderMessage UniWorX NotificationTriggerKind where
makeSettingForm :: Maybe SettingsForm -> Form SettingsForm
makeSettingForm template html = do
(result, widget) <- flip (renderAForm FormStandard) html $ SettingsForm
- <$ aformSection MsgFormCosmetics
- <*> areq (natFieldI $ MsgNatField "Favoriten") -- TODO: natFieldI not working here
- (fslpI MsgFavoriten "Anzahl Favoriten") (stgMaxFavourties <$> template)
+ <$ aformSection MsgFormPersonalAppearance
+ <*> areq (textField & cfStrip) (fslI MsgUserDisplayName & setTooltip MsgUserDisplayNameRulesBelow) (stgDisplayName <$> template)
+ <*> areq (emailField & cfStrip & cfCI) (fslI MsgUserDisplayEmail & setTooltip MsgUserDisplayEmailTip) (stgDisplayEmail <$> template)
+ <* aformSection MsgFormCosmetics
+ <*> areq (natFieldI $ MsgNatField "Favoriten")
+ (fslpI MsgFavourites "Anzahl Favoriten" & setTooltip MsgFavouritesTip) (stgMaxFavourites <$> template)
+ <*> areq (natFieldI $ MsgNatField "Favoriten-Semester")
+ (fslpI MsgFavouriteSemesters "Anzahl Semester") (stgMaxFavouriteTerms <$> template)
<*> areq (selectField . return $ mkOptionList themeList)
(fslI MsgTheme) { fsId = Just "theme-select" } (stgTheme <$> template)
<*> areq (selectField $ dateTimeFormatOptions SelFormatDateTime) (fslI MsgDateTimeFormat) (stgDateTime <$> template)
@@ -85,7 +108,7 @@ schoolsForm template = formToAForm $ schoolsFormView =<< renderWForm FormStandar
where
schoolsForm' :: WForm Handler (FormResult (Set SchoolId))
schoolsForm' = do
- allSchools <- liftHandlerT . runDB $ selectList [] [Asc SchoolName]
+ allSchools <- liftHandler . runDB $ selectList [] [Asc SchoolName]
let
schoolForm (Entity ssh School{schoolName})
@@ -107,7 +130,7 @@ schoolsForm template = formToAForm $ schoolsFormView =<< renderWForm FormStandar
notificationForm :: Maybe NotificationSettings -> AForm Handler NotificationSettings
notificationForm template = wFormToAForm $ do
- mbUid <- liftHandlerT maybeAuthId
+ mbUid <- liftHandler maybeAuthId
isAdmin <- hasReadAccessTo AdminR
let
@@ -132,10 +155,14 @@ notificationForm template = wFormToAForm $ do
, NTKExamParticipant <- nt
= fmap not . E.selectExists . E.from $ \examRegistration ->
E.where_ $ examRegistration E.^. ExamRegistrationUser E.==. E.val uid
+ | Just uid <- mbUid
+ , NTKCourseLecturer <- nt
+ = fmap not . E.selectExists . E.from $ \lecturer ->
+ E.where_ $ lecturer E.^. LecturerUser E.==. E.val uid
| otherwise
= return False
- ntHidden <- liftHandlerT . runDB
+ ntHidden <- liftHandler . runDB
$ Set.fromList universeF
& Map.fromSet sectionIsHidden
& sequenceA
@@ -151,28 +178,43 @@ notificationForm template = wFormToAForm $ do
= apopt checkBoxField (fslI nt) (flip notificationAllowed nt <$> template)
ntSection = \case
- NTSubmissionRatedGraded -> Just NTKCourseParticipant
- NTSubmissionRated -> Just NTKCourseParticipant
- NTSheetActive -> Just NTKCourseParticipant
- NTSheetSoonInactive -> Just NTKCourseParticipant
- NTSheetInactive -> Just $ NTKFunctionary SchoolLecturer
- NTCorrectionsAssigned -> Just NTKCorrector
- NTCorrectionsNotDistributed -> Just $ NTKFunctionary SchoolLecturer
- NTUserRightsUpdate -> Just NTKAll
- NTUserAuthModeUpdate -> Just NTKAll
- NTExamResult -> Just NTKExamParticipant
- NTAllocationStaffRegister -> Just $ NTKFunctionary SchoolLecturer
- NTAllocationAllocation -> Just NTKAllocationStaff
- NTAllocationRegister -> Just NTKAll
- NTAllocationOutdatedRatings -> Just NTKAllocationStaff
- NTAllocationUnratedApplications -> Just NTKAllocationStaff
- -- _other -> Nothing
+ NTSubmissionRatedGraded -> Just NTKCourseParticipant
+ NTSubmissionRated -> Just NTKCourseParticipant
+ NTSheetActive -> Just NTKCourseParticipant
+ NTSheetSoonInactive -> Just NTKCourseParticipant
+ NTSheetInactive -> Just NTKCourseLecturer
+ NTCorrectionsAssigned -> Just NTKCorrector
+ NTCorrectionsNotDistributed -> Just NTKCourseLecturer
+ NTUserRightsUpdate -> Just NTKAll
+ NTUserAuthModeUpdate -> Just NTKAll
+ NTExamRegistrationActive -> Just NTKCourseParticipant
+ NTExamRegistrationSoonInactive -> Just NTKCourseParticipant
+ NTExamDeregistrationSoonInactive -> Just NTKCourseParticipant
+ NTExamResult -> Just NTKExamParticipant
+ NTAllocationStaffRegister -> Just $ NTKFunctionary SchoolLecturer
+ NTAllocationAllocation -> Just NTKAllocationStaff
+ NTAllocationRegister -> Just NTKAll
+ NTAllocationOutdatedRatings -> Just NTKAllocationStaff
+ NTAllocationUnratedApplications -> Just NTKAllocationStaff
+ NTAllocationResults -> Just NTKAllocationParticipant
+ NTExamOfficeExamResults -> Just $ NTKFunctionary SchoolExamOffice
+ NTExamOfficeExamResultsChanged -> Just $ NTKFunctionary SchoolExamOffice
+ NTCourseRegistered -> Just NTKAll
+ -- _other -> Nothing
forcedTriggers = [NTUserRightsUpdate, NTUserAuthModeUpdate]
aFormToWForm $ NotificationSettings <$> sectionedFuncForm ntSection nsForm (fslI MsgNotificationSettings) False
+validateSettings :: User -> FormValidator SettingsForm Handler ()
+validateSettings User{..} = do
+ userDisplayName' <- use _stgDisplayName
+
+ guardValidation MsgUserDisplayNameInvalid $
+ validDisplayName userTitle userFirstName userSurname userDisplayName'
+
+
data ButtonResetTokens = BtnResetTokens
deriving (Enum, Eq, Ord, Bounded, Read, Show, Generic, Typeable)
instance Universe ButtonResetTokens
@@ -195,7 +237,7 @@ nullaryPathPiece ''ProfileAnchor $ camelToPathPiece' 1
getProfileR, postProfileR :: Handler Html
getProfileR = postProfileR
postProfileR = do
- (uid, User{..}) <- requireAuthPair
+ (uid, user@User{..}) <- requireAuthPair
userSchools <- fmap (setOf $ folded . _Value) . runDB . E.select . E.from $ \school -> do
E.where_ . E.exists . E.from $ \userSchool ->
E.where_ $ E.not_ (userSchool E.^. UserSchoolIsOptOut)
@@ -203,36 +245,38 @@ postProfileR = do
E.&&. userSchool E.^. UserSchoolSchool E.==. school E.^. SchoolId
return $ school E.^. SchoolId
let settingsTemplate = Just SettingsForm
- { stgMaxFavourties = userMaxFavourites
- , stgTheme = userTheme
- , stgDateTime = userDateTimeFormat
- , stgDate = userDateFormat
- , stgTime = userTimeFormat
- , stgDownloadFiles = userDownloadFiles
- , stgSchools = userSchools
+ { stgDisplayName = userDisplayName
+ , stgDisplayEmail = userDisplayEmail
+ , stgMaxFavourites = userMaxFavourites
+ , stgMaxFavouriteTerms = userMaxFavouriteTerms
+ , stgTheme = userTheme
+ , stgDateTime = userDateTimeFormat
+ , stgDate = userDateFormat
+ , stgTime = userTimeFormat
+ , stgDownloadFiles = userDownloadFiles
+ , stgSchools = userSchools
, stgNotificationSettings = userNotificationSettings
- , stgWarningDays = userWarningDays
+ , stgWarningDays = userWarningDays
}
- ((res,formWidget), formEnctype) <- runFormPost . identifyForm ProfileSettings $ makeSettingForm settingsTemplate
+ ((res,formWidget), formEnctype) <- runFormPost . validateForm (validateSettings user) . identifyForm ProfileSettings $ makeSettingForm settingsTemplate
formResult res $ \SettingsForm{..} -> do
- runDB $ do
- update uid [ UserMaxFavourites =. stgMaxFavourties
- , UserTheme =. stgTheme
- , UserDateTimeFormat =. stgDateTime
- , UserDateFormat =. stgDate
- , UserTimeFormat =. stgTime
- , UserDownloadFiles =. stgDownloadFiles
- , UserWarningDays =. stgWarningDays
- , UserNotificationSettings =. stgNotificationSettings
- ]
- when (stgMaxFavourties < userMaxFavourites) $ do
- -- prune Favourites to user-defined size
- oldFavs <- selectKeysList [ CourseFavouriteUser ==. uid]
- [ Desc CourseFavouriteTime
- , OffsetBy stgMaxFavourties
- ]
- mapM_ delete oldFavs
+ runDBJobs $ do
+ update uid $
+ [ UserDisplayName =. stgDisplayName
+ , UserMaxFavourites =. stgMaxFavourites
+ , UserMaxFavouriteTerms =. stgMaxFavouriteTerms
+ , UserTheme =. stgTheme
+ , UserDateTimeFormat =. stgDateTime
+ , UserDateFormat =. stgDate
+ , UserTimeFormat =. stgTime
+ , UserDownloadFiles =. stgDownloadFiles
+ , UserWarningDays =. stgWarningDays
+ , UserNotificationSettings =. stgNotificationSettings
+ ] ++ [ UserDisplayEmail =. stgDisplayEmail | userDisplayEmail == stgDisplayEmail ]
+ when (stgDisplayEmail /= userDisplayEmail) $ do
+ queueDBJob $ JobChangeUserDisplayEmail uid stgDisplayEmail
+ addMessageI Info $ MsgUserDisplayEmailChangeSent stgDisplayEmail
let
symDiff = (stgSchools `Set.difference` userSchools) `Set.union` (userSchools `Set.difference` stgSchools)
forM_ symDiff $ \ssh -> if
@@ -252,7 +296,7 @@ postProfileR = do
}
[ UserSchoolIsOptOut =. True
]
- addMessageI Info MsgSettingsUpdate
+ addMessageI Success MsgSettingsUpdate
redirect $ ProfileR :#: ProfileSettings
((tokenRes, tokenFormWidget), tokenEnctype) <- runFormPost . identifyForm ProfileResetTokens $ buttonForm
@@ -286,6 +330,7 @@ postProfileR = do
, formAnchor = Just ProfileResetTokens
}
tokenExplanation = $(i18nWidgetFile "profile/tokenExplanation")
+ displayNameRules = $(i18nWidgetFile "profile/displayNameRules")
$(widgetFile "profile/profile")
@@ -726,3 +771,65 @@ postUserNotificationR cID = do
siteLayoutMsg (MsgNotificationSettingsHeading userDisplayName) $ do
setTitleI $ MsgNotificationSettingsHeading userDisplayName
formWidget
+
+
+data ButtonSetDisplayEmail = BtnSetDisplayEmail
+ deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable)
+instance Universe ButtonSetDisplayEmail
+instance Finite ButtonSetDisplayEmail
+
+nullaryPathPiece ''ButtonSetDisplayEmail $ camelToPathPiece' 1
+embedRenderMessage ''UniWorX ''ButtonSetDisplayEmail id
+
+instance Button UniWorX ButtonSetDisplayEmail where
+ btnClasses _ = [BCIsButton]
+
+
+getSetDisplayEmailR, postSetDisplayEmailR :: Handler Html
+getSetDisplayEmailR = postSetDisplayEmailR
+postSetDisplayEmailR = do
+ uid <- requireAuthId
+ mDisplayEmail <- requireCurrentTokenRestrictions
+
+ case mDisplayEmail of
+ Nothing -> invalidArgs ["Bearer token required"]
+ Just displayEmail -> do
+ ((btnRes, btnView), btnEnc) <- runFormPost $ formEmbedJwtPost buttonForm
+ let btnView' = wrapForm btnView def
+ { formSubmit = FormNoSubmit
+ , formAction = Just $ SomeRoute SetDisplayEmailR
+ , formEncoding = btnEnc
+ }
+
+ formResult btnRes $ \case
+ BtnSetDisplayEmail -> do
+ runDB $
+ update uid [UserDisplayEmail =. displayEmail]
+ addMessageI Success MsgUserDisplayEmailChanged
+ redirect ProfileR
+
+ siteLayoutMsg MsgTitleChangeUserDisplayEmail $ do
+ setTitleI MsgTitleChangeUserDisplayEmail
+ $(i18nWidgetFile "set-display-email")
+
+getCsvOptionsR, postCsvOptionsR :: Handler Html
+getCsvOptionsR = postCsvOptionsR
+postCsvOptionsR = do
+ Entity uid User{userCsvOptions} <- requireAuth
+
+ ((optionsRes, optionsWgt'), optionsEnctype) <- runFormPost . renderAForm FormStandard $
+ csvOptionsForm (fslI MsgCsvOptions & setTooltip MsgCsvOptionsTip) (Just userCsvOptions)
+
+ formResultModal optionsRes CsvOptionsR $ \opts -> do
+ lift . runDB $ update uid [ UserCsvOptions =. opts ]
+ tell . pure =<< messageI Success MsgCsvOptionsUpdated
+
+ siteLayoutMsg MsgCsvOptions $ do
+ setTitleI MsgCsvOptions
+
+ isModal <- hasCustomHeader HeaderIsModal
+ wrapForm optionsWgt' def
+ { formAction = Just $ SomeRoute CsvOptionsR
+ , formEncoding = optionsEnctype
+ , formAttrs = [ asyncSubmitAttr | isModal ]
+ }
diff --git a/src/Handler/School.hs b/src/Handler/School.hs
index f97130264..fa0ef7fe1 100644
--- a/src/Handler/School.hs
+++ b/src/Handler/School.hs
@@ -2,7 +2,6 @@ module Handler.School where
import Import
import Handler.Utils
-import Handler.Utils.Table.Columns
import qualified Database.Esqueleto as E
@@ -71,7 +70,7 @@ mkSchoolForm mSsh template = renderAForm FormStandard $ SchoolForm
<*> 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))
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 $
setOf (folded . _entityVal . _schoolLdapOrgUnit) <$> selectList [] []
diff --git a/src/Handler/Sheet.hs b/src/Handler/Sheet.hs
index 331922434..543865af2 100644
--- a/src/Handler/Sheet.hs
+++ b/src/Handler/Sheet.hs
@@ -2,7 +2,7 @@
module Handler.Sheet where
-import Import
+import Import hiding (link)
import Jobs.Queue
@@ -10,8 +10,6 @@ import Jobs.Queue
import Utils.Sheet
import Handler.Utils
-- import Handler.Utils.Zip
-import Handler.Utils.Table.Cells
--- import Handler.Utils.Table.Columns
import Handler.Utils.SheetType
import Handler.Utils.Delete
import Handler.Utils.Invitations
@@ -71,10 +69,7 @@ data SheetForm = SheetForm
, sfActiveTo :: UTCTime
, sfHintFrom :: Maybe UTCTime
, sfSolutionFrom :: Maybe UTCTime
- , sfSheetF :: Maybe (Source Handler (Either FileId File))
- , sfHintF :: Maybe (Source Handler (Either FileId File))
- , sfSolutionF :: Maybe (Source Handler (Either FileId File))
- , sfMarkingF :: Maybe (Source Handler (Either FileId File))
+ , sfSheetF, sfHintF, sfSolutionF, sfMarkingF :: Maybe (ConduitT () (Either FileId File) Handler ())
, sfType :: SheetType
, sfGrouping :: SheetGroup
, sfSubmissionMode :: SubmissionMode
@@ -95,7 +90,7 @@ makeSheetForm :: Maybe SheetId -> Maybe SheetForm -> Form SheetForm
makeSheetForm msId template = identifyForm FIDsheet $ \html -> do
oldFileIds <- (return.) <$> case msId of
Nothing -> return $ partitionFileType mempty
- (Just sId) -> liftHandlerT $ runDB $ getFtIdMap sId
+ (Just sId) -> liftHandler $ runDB $ getFtIdMap sId
mr <- getMsgRenderer
ctime <- ceilingQuarterHour <$> liftIO getCurrentTime
(result, widget) <- flip (renderAForm FormStandard) html $ SheetForm
@@ -427,11 +422,12 @@ getSShowR tid ssh csh shn = do
setTitleI $ prependCourseTitle tid ssh csh $ SomeMessage shn
let zipLink = CSheetR tid ssh csh shn SArchiveR
visibleFrom = visibleUTCTime SelFormatDateTime <$> sheetVisibleFrom sheet
- sheetFrom <- formatTime SelFormatDateTime $ sheetActiveFrom sheet
- sheetTo <- formatTime SelFormatDateTime $ sheetActiveTo sheet
- hintsFrom <- traverse (formatTime SelFormatDateTime) $ sheetHintFrom sheet
- solutionFrom <- traverse (formatTime SelFormatDateTime) $ sheetSolutionFrom sheet
- markingText <- runMaybeT $ assertM_ (Authorized ==) (evalAccessCorrector tid ssh csh) >> hoistMaybe (sheetMarkingText sheet)
+ sheetFrom <- formatTime SelFormatDateTime $ sheetActiveFrom sheet
+ sheetTo <- formatTime SelFormatDateTime $ sheetActiveTo sheet
+ hintsFrom <- traverse (formatTime SelFormatDateTime) $ sheetHintFrom sheet
+ solutionFrom <- traverse (formatTime SelFormatDateTime) $ sheetSolutionFrom sheet
+ markingText <- runMaybeT $ assertM_ (Authorized ==) (evalAccessCorrector tid ssh csh) >> hoistMaybe (sheetMarkingText sheet)
+ submissionTip <- messageI Info MsgSheetCorrectorSubmissionsTip
$(widgetFile "sheetShow")
getSArchiveR :: TermId -> SchoolId -> CourseShorthand -> SheetName -> Handler TypedContent
@@ -639,20 +635,20 @@ postSDelR tid ssh csh shn = do
insertSheetFile :: SheetId -> SheetFileType -> FileInfo -> YesodDB UniWorX ()
insertSheetFile sid ftype finfo = do
- runConduit $ (sourceFiles finfo) =$= C.mapM_ finsert
+ runConduit $ sourceFiles finfo .| C.mapM_ finsert
where
finsert file = do
fid <- insert file
void . insert $ SheetFile sid fid ftype -- cannot fail due to uniqueness, since we generated a fresh FileId in the previous step
-insertSheetFile' :: SheetId -> SheetFileType -> Source Handler (Either FileId File) -> YesodDB UniWorX ()
+insertSheetFile' :: SheetId -> SheetFileType -> ConduitT () (Either FileId File) Handler () -> YesodDB UniWorX ()
insertSheetFile' sid ftype fs = do
oldFileIds <- fmap setFromList . fmap (map E.unValue) . E.select . E.from $ \(file `E.InnerJoin` sheetFile) -> do
E.on $ file E.^. FileId E.==. sheetFile E.^. SheetFileFile
E.where_ $ sheetFile E.^. SheetFileSheet E.==. E.val sid
E.&&. sheetFile E.^. SheetFileType E.==. E.val ftype
return (file E.^. FileId)
- keep <- execWriterT . runConduit $ transPipe (lift . lift) fs =$= C.mapM_ finsert
+ keep <- execWriterT . runConduit $ transPipe (lift . lift) fs .| C.mapM_ finsert
mapM_ deleteCascade $ (oldFileIds \\ keep :: Set FileId)
where
finsert (Left fileId) = tell $ singleton fileId
@@ -691,13 +687,13 @@ defaultLoads shid = do
return (sheetCorrector E.^. SheetCorrectorUser, sheetCorrector E.^. SheetCorrectorLoad, sheetCorrector E.^. SheetCorrectorState)
where
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 shid = wFormToAForm $ do
- Just currentRoute <- liftHandlerT getCurrentRoute
- userId <- liftHandlerT requireAuthId
+ currentRoute <- fromMaybe (error "correctorForm called from 404-handler") <$> liftHandler getCurrentRoute
+ userId <- liftHandler requireAuthId
MsgRenderer mr <- getMsgRenderer
let
@@ -705,9 +701,9 @@ correctorForm shid = wFormToAForm $ do
currentLoads = Map.union
<$> fmap (foldMap $ \(Entity _ SheetCorrector{..}) -> Map.singleton (Right sheetCorrectorUser) (sheetCorrectorState, sheetCorrectorLoad)) (selectList [ SheetCorrectorSheet ==. 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
applyDefaultLoads = Map.null currentLoads' && not isWrite
@@ -768,8 +764,9 @@ correctorForm shid = wFormToAForm $ do
identWidget <- case userIdent of
Left email -> return . toWidget $ mailtoHtml email
Right uid -> do
- User{userEmail, userDisplayName, userSurname} <- liftHandlerT . runDB $ getJust uid
+ User{userEmail, userDisplayName, userSurname} <- liftHandler . runDB $ getJust uid
return $ nameEmailWidget userEmail userDisplayName userSurname
+ invWarnMsg <- messageI Warning MsgEmailInvitationWarning
return (res, $(widgetFile "sheetCorrectors/cell"))
@@ -814,7 +811,7 @@ correctorForm shid = wFormToAForm $ do
postProcess' :: (Either UserEmail UserId, (CorrectorState, Load)) -> Either (Invitation' SheetCorrector) 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 = Just . Map.fromList . zip [0..] $ Map.toList loads -- TODO orderBy Name?!
@@ -896,18 +893,22 @@ correctorInvitationConfig = InvitationConfig{..}
Course{..} <- get404 sheetCourse
return $ CSheetR courseTerm courseSchool courseShorthand sheetName SCorrInviteR
invitationResolveFor _ = do
- Just (CSheetR tid csh ssh shn SCorrInviteR) <- getCurrentRoute
- fetchSheetId tid csh ssh shn
+ cRoute <- getCurrentRoute
+ 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
Course{..} <- get404 sheetCourse
return . SomeMessage $ MsgMailSubjectCorrectorInvitation courseTerm courseSchool courseShorthand sheetName
invitationHeading (Entity _ Sheet{..}) _ = return . SomeMessage $ MsgSheetCorrInviteHeading sheetName
invitationExplanation _ _ = return [ihamlet|_{SomeMessage MsgSheetCorrInviteExplanation}|]
invitationTokenConfig _ _ = do
- itAuthority <- liftHandlerT requireAuthId
+ itAuthority <- liftHandler requireAuthId
return $ InvitationTokenConfig itAuthority Nothing Nothing Nothing
invitationRestriction _ _ = return Authorized
- invitationForm _ (InvDBDataSheetCorrector load state, _) _ = pure $ (JunctionSheetCorrector load state, ())
+ invitationForm _ (InvDBDataSheetCorrector cLoad cState, _) _ = pure $ (JunctionSheetCorrector cLoad cState, ())
invitationInsertHook _ _ _ _ = id
invitationSuccessMsg (Entity _ Sheet{..}) _ = return . SomeMessage $ MsgCorrectorInvitationAccepted sheetName
invitationUltDest (Entity _ Sheet{..}) _ = do
diff --git a/src/Handler/Submission.hs b/src/Handler/Submission.hs
index fe52740d4..a925753dd 100644
--- a/src/Handler/Submission.hs
+++ b/src/Handler/Submission.hs
@@ -11,7 +11,6 @@ import Jobs
import Handler.Utils
import Handler.Utils.Delete
import Handler.Utils.Submission
-import Handler.Utils.Table.Cells
import Handler.Utils.Invitations
-- import Control.Monad.Trans.Maybe
@@ -90,9 +89,13 @@ submissionUserInvitationConfig = InvitationConfig{..}
cID <- encrypt subId
return $ CSubmissionR courseTerm courseSchool courseShorthand sheetName cID SInviteR
invitationResolveFor _ = do
- Just (CSubmissionR _tid _ssh _csh _shn cID SInviteR) <- getCurrentRoute
- subId <- decrypt cID
- bool notFound (return subId) =<< existsKey subId
+ cRoute <- getCurrentRoute
+ case cRoute of
+ 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
Sheet{..} <- getJust submissionSheet
Course{..} <- getJust sheetCourse
@@ -104,7 +107,7 @@ submissionUserInvitationConfig = InvitationConfig{..}
invitationTokenConfig (Entity _ Submission{..}) _ = do
Sheet{..} <- getJust submissionSheet
Course{..} <- getJust sheetCourse
- itAuthority <- liftHandlerT requireAuthId
+ itAuthority <- liftHandler requireAuthId
itAddAuth <- either throwM (return . Just) $ routeAuthTags (CSheetR courseTerm courseSchool courseShorthand sheetName SubmissionNewR)
let itExpiresAt = Nothing
itStartsAt = Nothing
@@ -122,15 +125,17 @@ submissionUserInvitationConfig = InvitationConfig{..}
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 $ (,)
<$> fileUploadForm (not isLecturer && is _Nothing msmid) (fslI . bool MsgSubmissionFile MsgSubmissionArchive) uploadMode
<*> wFormToAForm submittorsForm
where
miCell' :: Markup -> Either UserEmail UserId -> Widget
- miCell' csrf (Left email) = $(widgetFile "widgets/massinput/submissionUsers/cellInvitation")
+ miCell' csrf (Left email) = do
+ invWarnMsg <- messageI Warning MsgEmailInvitationWarning
+ $(widgetFile "widgets/massinput/submissionUsers/cellInvitation")
miCell' csrf (Right uid) = do
- User{..} <- liftHandlerT . runDB $ getJust uid
+ User{..} <- liftHandler . runDB $ getJust uid
$(widgetFile "widgets/massinput/submissionUsers/cellKnown")
miLayout :: ListLength
@@ -192,7 +197,7 @@ makeSubmissionForm cid msmid uploadMode grouping isLecturer prefillUsers = ident
| null submittors' -> FormFailure [mr MsgSubmissionUsersEmpty]
| otherwise -> FormSuccess $ Set.fromList submittors'
| otherwise = do
- uid <- liftHandlerT requireAuthId
+ uid <- liftHandler requireAuthId
mRoute <- getCurrentRoute
let
@@ -276,7 +281,7 @@ submissionHelper :: TermId -> SchoolId -> CourseShorthand -> SheetName -> Maybe
submissionHelper tid ssh csh shn mcid = do
uid <- requireAuthId
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
csheet@(Entity shid Sheet{..}) <- fetchSheet tid ssh csh shn
@@ -437,7 +442,9 @@ submissionHelper tid ssh csh shn mcid = do
-- Determine old submission users
subUsersOld <- if
- | isJust msmid -> setOf (folded . _entityVal . _submissionUserUser . to Right) <$> selectList [SubmissionUserSubmission ==. smid] []
+ | isJust msmid -> Set.union
+ <$> (setOf (folded . _entityVal . _submissionUserUser . to Right) <$> selectList [SubmissionUserSubmission ==. smid] [])
+ <*> (sourceInvitationsF smid <&> Set.fromList . map (\(email, InvDBDataSubmissionUser) -> Left email))
| otherwise -> return Set.empty -- optimization (do not perform selection if submission was freshly created)
-- Since invitations carry no data we only need to consider changes to
@@ -477,7 +484,7 @@ submissionHelper tid ssh csh shn mcid = do
Nothing -> return ()
-- 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
[ sortable (Just "path") (i18nCell MsgFileTitle) $ \(coalesce -> (mOrig, mCorr)) -> let
Just fileTitle' = fileTitle . entityVal . snd <$> (mOrig <|> mCorr)
diff --git a/src/Handler/SystemMessage.hs b/src/Handler/SystemMessage.hs
index b2c7ef90f..c6a3c2214 100644
--- a/src/Handler/SystemMessage.hs
+++ b/src/Handler/SystemMessage.hs
@@ -9,7 +9,6 @@ import qualified Data.Set as Set
import qualified Data.List.NonEmpty as NonEmpty
import Handler.Utils
-import Handler.Utils.Table.Cells
import qualified Database.Esqueleto as E
@@ -180,7 +179,7 @@ postMessageListR = do
{ 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
{ dbtSQLQuery
, dbtRowKey = (E.^. SystemMessageId)
@@ -217,7 +216,7 @@ postMessageListR = do
]
(actionRes, action) <- multiActionM actions "" (Just SMActivate) mempty
return ((, mempty) . Last . Just <$> actionRes, toWidget frag <> action)
- , dbParamsFormEvaluate = liftHandlerT . runFormPost
+ , dbParamsFormEvaluate = liftHandler . runFormPost
, dbParamsFormResult = id
, dbParamsFormIdent = def
}
@@ -226,8 +225,8 @@ postMessageListR = do
, dbtCsvDecode = Nothing
}
- let tableRes = tableRes' & mapped._2 %~ Map.keysSet . Map.filter id . getDBFormResult (const False)
- & mapped._1 %~ fromMaybe (error "By construction the form should always return an action") . getLast
+ let tableRes = tableRes' <&> _2 %~ Map.keysSet . Map.filter id . getDBFormResult (const False)
+ <&> _1 %~ fromMaybe (error "By construction the form should always return an action") . getLast
case tableRes of
FormMissing -> return ()
diff --git a/src/Handler/Term.hs b/src/Handler/Term.hs
index ba4be993c..26c349c83 100644
--- a/src/Handler/Term.hs
+++ b/src/Handler/Term.hs
@@ -2,7 +2,6 @@ module Handler.Term where
import Import
import Handler.Utils
-import Handler.Utils.Table.Cells
import qualified Data.Map as Map
import qualified Database.Esqueleto as E
@@ -177,7 +176,7 @@ postTermEditExistR tid = do
termEditHandler :: TermFormTemplate -> Handler Html
termEditHandler term = do
- Just eHandler <- getCurrentRoute
+ eHandler <- fromMaybe (error "termEditHandler called from 404-handler") <$> getCurrentRoute
((result, formWidget), formEnctype) <- runFormPost $ newTermForm term
case result of
(FormSuccess res) -> do
diff --git a/src/Handler/Tutorial.hs b/src/Handler/Tutorial.hs
index babe6563f..04fc02220 100644
--- a/src/Handler/Tutorial.hs
+++ b/src/Handler/Tutorial.hs
@@ -1,476 +1,13 @@
-{-# OPTIONS_GHC -fno-warn-orphans #-}
-
module Handler.Tutorial
( module Handler.Tutorial
) where
-import Import
-import Handler.Utils
-import Handler.Utils.Tutorial
-import Handler.Utils.Table.Cells
-import Handler.Utils.Delete
-import Handler.Utils.Communication
-import Handler.Utils.Form.Occurrences
-import Handler.Utils.Invitations
-import Jobs.Queue
-
-import qualified Database.Esqueleto as E
-import Database.Esqueleto.Utils.TH
-
-import Data.Map ((!))
-import qualified Data.Map as Map
-import qualified Data.Set as Set
-
-import qualified Data.CaseInsensitive as CI
-
-import Data.Aeson hiding (Result(..))
-import Text.Hamlet (ihamlet)
-
-import Handler.Tutorial.Users as Handler.Tutorial
-
-{-# ANN module ("Hlint: ignore Redundant void" :: String) #-}
-
-
-getCTutorialListR :: TermId -> SchoolId -> CourseShorthand -> Handler Html
-getCTutorialListR tid ssh csh = do
- Entity cid Course{..} <- runDB . getBy404 $ TermSchoolCourseShort tid ssh csh
-
- let
- tutorialDBTable = DBTable{..}
- where
- dbtSQLQuery tutorial = do
- E.where_ $ tutorial E.^. TutorialCourse E.==. E.val cid
- let participants = E.sub_select . E.from $ \tutorialParticipant -> do
- E.where_ $ tutorialParticipant E.^. TutorialParticipantTutorial E.==. tutorial E.^. TutorialId
- return E.countRows :: E.SqlQuery (E.SqlExpr (E.Value Int))
- return (tutorial, participants)
- dbtRowKey = (E.^. TutorialId)
- dbtProj = return . over (_dbrOutput . _2) E.unValue
- dbtColonnade = dbColonnade $ mconcat
- [ sortable (Just "type") (i18nCell MsgTutorialType) $ \DBRow{ dbrOutput = (Entity _ Tutorial{..}, _) } -> textCell $ CI.original tutorialType
- , sortable (Just "name") (i18nCell MsgTutorialName) $ \DBRow{ dbrOutput = (Entity _ Tutorial{..}, _) } -> anchorCell (CTutorialR tid ssh csh tutorialName TUsersR) [whamlet|#{tutorialName}|]
- , sortable Nothing (i18nCell MsgTutorialTutors) $ \DBRow{ dbrOutput = (Entity tutid _, _) } -> sqlCell $ do
- tutors <- fmap (map $(unValueN 3)) . E.select . E.from $ \(tutor `E.InnerJoin` user) -> do
- E.on $ tutor E.^. TutorUser E.==. user E.^. UserId
- E.where_ $ tutor E.^. TutorTutorial E.==. E.val tutid
- return (user E.^. UserEmail, user E.^. UserDisplayName, user E.^. UserSurname)
- return [whamlet|
- $newline never
-
- $forall tutor <- tutors
-
+ $forall tutor <- tutors
+